From 8a6c253ea2d2527fca5e1f3c837c4432eb1bd3b7 Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 13:43:00 +0000 Subject: [PATCH] fix(ax): align action handlers and exec errors --- actions.go | 124 +++++++++++++++++++++++++++++++++++++++++++++- docs/RFC.md | 6 +-- exec/exec.go | 8 +-- service.go | 116 ------------------------------------------- specs/exec/RFC.md | 8 +-- 5 files changed, 134 insertions(+), 128 deletions(-) diff --git a/actions.go b/actions.go index 7f33cf8..14d66eb 100644 --- a/actions.go +++ b/actions.go @@ -1,6 +1,12 @@ package process -import "time" +import ( + "context" + "syscall" + "time" + + "dappco.re/go/core" +) // --- ACTION messages (broadcast via Core.ACTION) --- @@ -35,3 +41,119 @@ type ActionProcessKilled struct { ID string Signal string } + +// --- Core Action Handlers --------------------------------------------------- + +func (s *Service) handleRun(ctx context.Context, opts core.Options) core.Result { + command := opts.String("command") + if command == "" { + return core.Result{Value: core.E("process.run", "command is required", nil), OK: false} + } + + runOpts := RunOptions{ + Command: command, + Dir: opts.String("dir"), + } + if r := opts.Get("args"); r.OK { + runOpts.Args = optionStrings(r.Value) + } + if r := opts.Get("env"); r.OK { + runOpts.Env = optionStrings(r.Value) + } + + return s.runCommand(ctx, runOpts) +} + +func (s *Service) handleStart(ctx context.Context, opts core.Options) core.Result { + command := opts.String("command") + if command == "" { + return core.Result{Value: core.E("process.start", "command is required", nil), OK: false} + } + + detach := true + if opts.Has("detach") { + detach = opts.Bool("detach") + } + + runOpts := RunOptions{ + Command: command, + Dir: opts.String("dir"), + Detach: detach, + } + if r := opts.Get("args"); r.OK { + runOpts.Args = optionStrings(r.Value) + } + if r := opts.Get("env"); r.OK { + runOpts.Env = optionStrings(r.Value) + } + + r := s.StartWithOptions(ctx, runOpts) + if !r.OK { + return r + } + return core.Result{Value: r.Value.(*ManagedProcess).ID, OK: true} +} + +func (s *Service) handleKill(_ context.Context, opts core.Options) core.Result { + id := opts.String("id") + if id != "" { + if err := s.Kill(id); err != nil { + if core.Is(err, ErrProcessNotFound) { + return core.Result{Value: core.E("process.kill", core.Concat("not found: ", id), nil), OK: false} + } + return core.Result{Value: core.E("process.kill", core.Concat("kill failed: ", id), err), OK: false} + } + return core.Result{OK: true} + } + + pid := opts.Int("pid") + if pid > 0 { + proc, err := processHandle(pid) + if err != nil { + return core.Result{Value: core.E("process.kill", core.Concat("find pid failed: ", core.Sprintf("%d", pid)), err), OK: false} + } + if err := proc.Signal(syscall.SIGTERM); err != nil { + return core.Result{Value: core.E("process.kill", core.Concat("signal failed: ", core.Sprintf("%d", pid)), err), OK: false} + } + return core.Result{OK: true} + } + + return core.Result{Value: core.E("process.kill", "need id or pid", nil), OK: false} +} + +func (s *Service) handleList(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: s.managed.Names(), OK: true} +} + +func (s *Service) handleGet(_ context.Context, opts core.Options) core.Result { + id := opts.String("id") + if id == "" { + return core.Result{Value: core.E("process.get", "id is required", nil), OK: false} + } + proc, err := s.Get(id) + if err != nil { + return core.Result{Value: core.E("process.get", core.Concat("not found: ", id), err), OK: false} + } + return core.Result{Value: proc.Info(), OK: true} +} + +func optionStrings(value any) []string { + switch typed := value.(type) { + case nil: + return nil + case []string: + return append([]string(nil), typed...) + case []any: + result := make([]string, 0, len(typed)) + for _, item := range typed { + text, ok := item.(string) + if !ok { + return nil + } + result = append(result, text) + } + return result + default: + return nil + } +} diff --git a/docs/RFC.md b/docs/RFC.md index 15ff2da..a6af897 100644 --- a/docs/RFC.md +++ b/docs/RFC.md @@ -24,9 +24,9 @@ go-process provides: c.Action("process.run", s.handleRun) Without go-process registered, `c.Process().Run()` returns `Result{OK: false}`. Permission-by-registration. -### Current State (2026-03-25) +### Current State (2026-03-30) -The codebase is PRE-migration. The RFC describes the v0.8.0 target. What exists today: +The codebase now matches the v0.8.0 target. The bullets below are the historical migration delta that was closed out: - `service.go` — `NewService(opts) func(*Core) (any, error)` — **old factory signature**. Change to `Register(c *Core) core.Result` - `OnStartup() error` / `OnShutdown() error` — **Change** to return `core.Result` @@ -44,7 +44,7 @@ daemon.go — DaemonEntry, managed daemon lifecycle health.go — health check endpoints pidfile.go — PID file management buffer.go — output buffering -actions.go — WILL CONTAIN Action handlers after migration +actions.go — Action payloads and Core action handlers global.go — global Default() singleton — DELETE after migration ``` diff --git a/exec/exec.go b/exec/exec.go index c097618..368979c 100644 --- a/exec/exec.go +++ b/exec/exec.go @@ -86,7 +86,7 @@ func (c *Cmd) Run() error { c.logDebug("executing command") if err := c.cmd.Run(); err != nil { - wrapped := wrapError("Cmd.Run", err, c.name, c.args) + wrapped := wrapError("exec.cmd.run", err, c.name, c.args) c.logError("command failed", wrapped) return wrapped } @@ -100,7 +100,7 @@ func (c *Cmd) Output() ([]byte, error) { out, err := c.cmd.Output() if err != nil { - wrapped := wrapError("Cmd.Output", err, c.name, c.args) + wrapped := wrapError("exec.cmd.output", err, c.name, c.args) c.logError("command failed", wrapped) return nil, wrapped } @@ -114,7 +114,7 @@ func (c *Cmd) CombinedOutput() ([]byte, error) { out, err := c.cmd.CombinedOutput() if err != nil { - wrapped := wrapError("Cmd.CombinedOutput", err, c.name, c.args) + wrapped := wrapError("exec.cmd.combined_output", err, c.name, c.args) c.logError("command failed", wrapped) return out, wrapped } @@ -147,7 +147,7 @@ func RunQuiet(ctx context.Context, name string, args ...string) error { var stderr bytes.Buffer cmd := Command(ctx, name, args...).WithStderr(&stderr) if err := cmd.Run(); err != nil { - return core.E("RunQuiet", core.Trim(stderr.String()), err) + return core.E("exec.run_quiet", core.Trim(stderr.String()), err) } return nil } diff --git a/service.go b/service.go index 67bfd16..79a7a52 100644 --- a/service.go +++ b/service.go @@ -370,89 +370,6 @@ func (s *Service) RunWithOptions(ctx context.Context, opts RunOptions) core.Resu return s.runCommand(ctx, opts) } -// --- Internal Request Helpers --- - -func (s *Service) handleRun(ctx context.Context, opts core.Options) core.Result { - command := opts.String("command") - if command == "" { - return core.Result{Value: core.E("process.run", "command is required", nil), OK: false} - } - - runOpts := RunOptions{ - Command: command, - Dir: opts.String("dir"), - } - if r := opts.Get("args"); r.OK { - runOpts.Args = optionStrings(r.Value) - } - if r := opts.Get("env"); r.OK { - runOpts.Env = optionStrings(r.Value) - } - - return s.runCommand(ctx, runOpts) -} - -func (s *Service) handleStart(ctx context.Context, opts core.Options) core.Result { - command := opts.String("command") - if command == "" { - return core.Result{Value: core.E("process.start", "command is required", nil), OK: false} - } - - detach := true - if opts.Has("detach") { - detach = opts.Bool("detach") - } - - runOpts := RunOptions{ - Command: command, - Dir: opts.String("dir"), - Detach: detach, - } - if r := opts.Get("args"); r.OK { - runOpts.Args = optionStrings(r.Value) - } - if r := opts.Get("env"); r.OK { - runOpts.Env = optionStrings(r.Value) - } - - r := s.StartWithOptions(ctx, runOpts) - if !r.OK { - return r - } - return core.Result{Value: r.Value.(*ManagedProcess).ID, OK: true} -} - -func (s *Service) handleKill(ctx context.Context, opts core.Options) core.Result { - id := opts.String("id") - if id != "" { - if err := s.Kill(id); err != nil { - if core.Is(err, ErrProcessNotFound) { - return core.Result{Value: core.E("process.kill", core.Concat("not found: ", id), nil), OK: false} - } - return core.Result{Value: err, OK: false} - } - return core.Result{OK: true} - } - - pid := opts.Int("pid") - if pid > 0 { - proc, err := processHandle(pid) - if err != nil { - return core.Result{Value: core.E("process.kill", core.Concat("find pid failed: ", core.Sprintf("%d", pid)), err), OK: false} - } - if err := proc.Signal(syscall.SIGTERM); err != nil { - return core.Result{Value: core.E("process.kill", core.Concat("signal failed: ", core.Sprintf("%d", pid)), err), OK: false} - } - return core.Result{OK: true} - } - - return core.Result{Value: core.E("process.kill", "need id or pid", nil), OK: false} -} - -func (s *Service) handleList(ctx context.Context, opts core.Options) core.Result { - return core.Result{Value: s.managed.Names(), OK: true} -} - func (s *Service) runCommand(ctx context.Context, opts RunOptions) core.Result { if opts.Command == "" { return core.Result{Value: core.E("process.run", "command is required", nil), OK: false} @@ -523,39 +440,6 @@ func isNotExist(err error) bool { return os.IsNotExist(err) } -func (s *Service) handleGet(ctx context.Context, opts core.Options) core.Result { - id := opts.String("id") - if id == "" { - return core.Result{Value: core.E("process.get", "id is required", nil), OK: false} - } - proc, err := s.Get(id) - if err != nil { - return core.Result{Value: core.E("process.get", core.Concat("not found: ", id), err), OK: false} - } - return core.Result{Value: proc.Info(), OK: true} -} - -func optionStrings(value any) []string { - switch typed := value.(type) { - case nil: - return nil - case []string: - return append([]string(nil), typed...) - case []any: - result := make([]string, 0, len(typed)) - for _, item := range typed { - text, ok := item.(string) - if !ok { - return nil - } - result = append(result, text) - } - return result - default: - return nil - } -} - func classifyProcessExit(proc *ManagedProcess, err error) (Status, int, error, string) { if err == nil { return StatusExited, 0, nil, "" diff --git a/specs/exec/RFC.md b/specs/exec/RFC.md index 5a43ad8..4dbbc09 100644 --- a/specs/exec/RFC.md +++ b/specs/exec/RFC.md @@ -46,7 +46,7 @@ Exported fields: ### Package Functions - `func Command(ctx context.Context, name string, args ...string) *Cmd`: Returns a `Cmd` for the supplied context, executable name, and arguments. -- `func RunQuiet(ctx context.Context, name string, args ...string) error`: Runs a command with stderr captured into a buffer and returns `core.E("RunQuiet", core.Trim(stderr.String()), err)` on failure. +- `func RunQuiet(ctx context.Context, name string, args ...string) error`: Runs a command with stderr captured into a buffer and returns `core.E("exec.run_quiet", core.Trim(stderr.String()), err)` on failure. - `func SetDefaultLogger(l Logger)`: Sets the package-level default logger. Passing `nil` replaces it with `NopLogger`. - `func DefaultLogger() Logger`: Returns the package-level default logger. @@ -58,9 +58,9 @@ Exported fields: - `func (c *Cmd) WithStdout(w io.Writer) *Cmd`: Sets `Options.Stdout` and returns the same command. - `func (c *Cmd) WithStderr(w io.Writer) *Cmd`: Sets `Options.Stderr` and returns the same command. - `func (c *Cmd) WithLogger(l Logger) *Cmd`: Sets a command-specific logger and returns the same command. -- `func (c *Cmd) Run() error`: Prepares the underlying `exec.Cmd`, logs `"executing command"`, runs it, and wraps failures with `wrapError("Cmd.Run", ...)`. -- `func (c *Cmd) Output() ([]byte, error)`: Prepares the underlying `exec.Cmd`, logs `"executing command"`, returns stdout bytes, and wraps failures with `wrapError("Cmd.Output", ...)`. -- `func (c *Cmd) CombinedOutput() ([]byte, error)`: Prepares the underlying `exec.Cmd`, logs `"executing command"`, returns combined stdout and stderr, and wraps failures with `wrapError("Cmd.CombinedOutput", ...)`. +- `func (c *Cmd) Run() error`: Prepares the underlying `exec.Cmd`, logs `"executing command"`, runs it, and wraps failures with `wrapError("exec.cmd.run", ...)`. +- `func (c *Cmd) Output() ([]byte, error)`: Prepares the underlying `exec.Cmd`, logs `"executing command"`, returns stdout bytes, and wraps failures with `wrapError("exec.cmd.output", ...)`. +- `func (c *Cmd) CombinedOutput() ([]byte, error)`: Prepares the underlying `exec.Cmd`, logs `"executing command"`, returns combined stdout and stderr, and wraps failures with `wrapError("exec.cmd.combined_output", ...)`. ### `NopLogger` Methods