Files
summercms/cmd/summer/runtime.go
Jakub Zych 2237a640d2 feat(11-02): add schedule:run as a scheduler process and a cron --once mode
- schedule:run runs a worker on the scheduled queue with every plugin's periodic jobs
- schedule:run --once runs entries due in the current app.timezone minute without River,
  warns and skips unregistered commands, and returns the first command error
- summer schedule:run delegate forwards --once
- tests for --once minute matching, forged-entry skipping (T-11-09) and ByPeriod dedupe
2026-09-29 22:34:21 +02:00

140 lines
3.5 KiB
Go

package main
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"git.golem15.com/golem15/summercms/internal/build"
"git.golem15.com/golem15/summercms/modules/bonfire"
)
func delegateCommand(name, description string) bonfire.Command {
return bonfire.Command{
Name: name,
Description: description,
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
argv := []string{name}
argv = append(argv, in.Args()...)
return runAppBinary(ctx, argv, out)
},
}
}
func delegateRollbackCommand() bonfire.Command {
return bonfire.Command{
Name: "migrate:rollback",
Description: "Roll back the last migration of a plugin in the app binary",
Flags: []bonfire.Flag{{
Name: "plugin",
Description: "Plugin ID whose last migration to roll back",
}},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
argv := []string{"migrate:rollback"}
if plugin, ok := in.Flag("plugin"); ok && plugin != "" {
argv = append(argv, "--plugin", plugin)
}
argv = append(argv, in.Args()...)
return runAppBinary(ctx, argv, out)
},
}
}
func delegateQueueWorkCommand() bonfire.Command {
return bonfire.Command{
Name: "queue:work",
Description: "Run background job workers in the app binary",
Flags: []bonfire.Flag{{
Name: "queue",
Description: "Queue to work (repeatable; default all known queues)",
Repeatable: true,
}},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
argv := []string{"queue:work"}
for _, q := range in.Flags("queue") {
argv = append(argv, "--queue", q)
}
argv = append(argv, in.Args()...)
return runAppBinary(ctx, argv, out)
},
}
}
func delegateScheduleRunCommand() bonfire.Command {
return bonfire.Command{
Name: "schedule:run",
Description: "Run the scheduler in the app binary (or once for system cron)",
Flags: []bonfire.Flag{{
Name: "once",
Description: "Run the entries due this minute and exit (for system cron)",
Bare: true,
}},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
argv := []string{"schedule:run"}
if v, ok := in.Flag("once"); ok {
if once, _ := strconv.ParseBool(v); once {
argv = append(argv, "--once")
}
}
argv = append(argv, in.Args()...)
return runAppBinary(ctx, argv, out)
},
}
}
func runAppBinary(ctx context.Context, argv []string, out bonfire.Output) error {
if len(argv) == 0 {
return fmt.Errorf("summer: missing app command")
}
dir, err := os.Getwd()
if err != nil {
return err
}
appDir, err := build.FindAppDir(dir)
if err != nil {
return err
}
m, err := build.LoadManifest(filepath.Join(appDir, "summer.yaml"))
if err != nil {
return err
}
bin := filepath.Join(appDir, "bin", m.Binary)
if _, err := os.Stat(bin); err != nil {
if err := build.App(ctx, appDir, out); err != nil {
return err
}
}
self, err := os.Executable()
if err == nil {
if same, _ := sameFile(self, bin); same {
return fmt.Errorf("summer: refusing to recurse into %s", bin)
}
}
cmd := exec.CommandContext(ctx, bin, argv...)
cmd.Dir = appDir
cmd.Stdout = out
cmd.Stderr = out
cmd.Stdin = os.Stdin
cmd.Env = os.Environ()
if err := cmd.Run(); err != nil {
return fmt.Errorf("summer: %s: %w", strings.Join(argv, " "), err)
}
return nil
}
func sameFile(a, b string) (bool, error) {
ai, err := os.Stat(a)
if err != nil {
return false, err
}
bi, err := os.Stat(b)
if err != nil {
return false, err
}
return os.SameFile(ai, bi), nil
}