From b0dd22fc5e080d87e92357ae8db525ce636f148b Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 05:29:51 +0000 Subject: [PATCH] docs(ax): align process docs with AX action/result contract --- CLAUDE.md | 11 ++++---- docs/architecture.md | 27 +++++++++---------- docs/development.md | 4 --- docs/index.md | 63 ++++++++++++++++++++++---------------------- 4 files changed, 49 insertions(+), 56 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5be5192..217a6fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,11 +20,12 @@ core go vet # Vet The package has three layers, all in the root `process` package (plus a `exec` subpackage): -### Layer 1: Process Execution (service.go, process.go, process_global.go) +### Layer 1: Process Execution (service.go, process.go) `Service` is a Core service (`*core.ServiceRuntime[Options]`) that manages all `Process` instances. It spawns subprocesses, pipes stdout/stderr through goroutines, captures output to a `RingBuffer`, and broadcasts IPC actions (`ActionProcessStarted`, `ActionProcessOutput`, `ActionProcessExited`, `ActionProcessKilled` — defined in actions.go). -`process_global.go` provides package-level convenience functions (`Start`, `Run`, `Kill`, `List`) that delegate to a global `Service` singleton initialized via `Init(core)`. Follows the same pattern as Go's `i18n` package. +The legacy global singleton API (`process_global.go`) was removed in favor of +explicit Core service registration. ### Layer 2: Daemon Lifecycle (daemon.go, pidfile.go, health.go, registry.go) @@ -45,19 +46,19 @@ Builder-pattern wrapper around `os/exec` with structured logging via a pluggable ## Key Patterns -- **Core integration**: `Service` embeds `*core.ServiceRuntime[Options]` and uses `s.Core().ACTION(...)` to broadcast typed action messages. Tests create a Core instance via `framework.New(framework.WithName("process", NewService(...)))`. +- **Core integration**: `Service` embeds `*core.ServiceRuntime[Options]` and uses `s.Core().ACTION(...)` to broadcast typed action messages. Tests create a Core instance via `framework.New(framework.WithService(Register))`. - **Output capture**: All process output goes through a fixed-size `RingBuffer` (default 1MB). Oldest data is silently overwritten when full. Set `RunOptions.DisableCapture` to skip buffering for long-running processes where output is only streamed via IPC. - **Process lifecycle**: Status transitions are `StatusPending → StatusRunning → StatusExited|StatusFailed|StatusKilled`. The `done` channel closes on exit; use `<-proc.Done()` or `proc.Wait()`. - **Detach / process group isolation**: Set `RunOptions.Detach = true` to run the subprocess in its own process group (`Setpgid`). Detached processes use `context.Background()` so they survive parent context cancellation and parent death. - **Graceful shutdown**: `Service.OnShutdown` kills all running processes. `Daemon.Stop()` performs ordered teardown: sets health to not-ready → shuts down health server → releases PID file → unregisters from registry. `DaemonOptions.ShutdownTimeout` (default 30 s) bounds the shutdown context. - **Auto-registration**: Pass a `Registry` and `RegistryEntry` in `DaemonOptions` to automatically register the daemon on `Start()` and unregister on `Stop()`. - **PID liveness checks**: Both `PIDFile` and `Registry` use `proc.Signal(syscall.Signal(0))` to check if a PID is alive before trusting stored state. -- **Error handling**: All errors MUST use `coreerr.E()` from `go-log` (imported as `coreerr`), never `fmt.Errorf` or `errors.New`. Sentinel errors are package-level vars created with `coreerr.E("", "message", nil)`. +- **Error handling**: All errors MUST use `core.E()`, never `fmt.Errorf` or + `errors.New`. Sentinel errors are package-level vars created with `core.E("", "message", nil)`. ## Dependencies - `dappco.re/go/core` — Core DI framework, IPC actions, `ServiceRuntime` -- `dappco.re/go/core/log` — Structured error constructor (`coreerr.E()`) - `dappco.re/go/core/io` — Filesystem abstraction (`coreio.Local`) used by PIDFile and Registry - `github.com/stretchr/testify` — test assertions (require/assert) diff --git a/docs/architecture.md b/docs/architecture.md index 4da33a4..9bb13cc 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -60,32 +60,28 @@ participate in the Core DI container and implements both `Startable` and ```go type Service struct { *core.ServiceRuntime[Options] - processes map[string]*Process - mu sync.RWMutex + managed *core.Registry[*ManagedProcess] bufSize int - idCounter atomic.Uint64 } ``` Key behaviours: -- **OnStartup** — currently a no-op; reserved for future initialisation. +- **OnStartup** — registers the named Core actions `process.run`, `process.start`, `process.kill`, `process.list`, and `process.get`. - **OnShutdown** — iterates all running processes and calls `Kill()` on each, ensuring no orphaned child processes when the application exits. -- Process IDs are generated as `proc-N` using an atomic counter, guaranteeing - uniqueness without locks. +- Process IDs are generated with `core.ID()` and stored in a Core registry. #### Registration The service is registered with Core via a factory function: ```go -process.NewService(process.Options{BufferSize: 2 * 1024 * 1024}) +core.New(core.WithService(process.Register)) ``` -`NewService` returns a `func(*core.Core) (any, error)` closure — the standard -Core service factory signature. The `Options` struct is captured by the closure -and applied when Core instantiates the service. +`Register` returns `core.Result{Value: *Service, OK: true}` — the standard +Core `WithService` factory signature used by the v0.8.0 contract. ### Process @@ -163,12 +159,12 @@ const ( When `Service.StartWithOptions()` is called: ``` -1. Generate unique ID (atomic counter) +1. Generate a unique ID with `core.ID()` 2. Create context with cancel 3. Build os/exec.Cmd with dir, env, pipes 4. Create RingBuffer (unless DisableCapture is set) 5. cmd.Start() -6. Store process in map +6. Store process in the Core registry 7. Broadcast ActionProcessStarted via Core.ACTION 8. Spawn 2 goroutines to stream stdout and stderr - Each line is written to the RingBuffer @@ -176,8 +172,9 @@ When `Service.StartWithOptions()` is called: 9. Spawn 1 goroutine to wait for process exit - Waits for output goroutines to finish first - Calls cmd.Wait() - - Updates process status and exit code + - Classifies the exit as exited, failed, or killed - Closes the done channel + - Broadcasts ActionProcessKilled when the process died from a signal - Broadcasts ActionProcessExited ``` @@ -296,12 +293,12 @@ File naming convention: `{code}-{daemon}.json` (slashes replaced with dashes). ## exec Sub-Package -The `exec` package (`forge.lthn.ai/core/go-process/exec`) provides a fluent +The `exec` package (`dappco.re/go/core/process/exec`) provides a fluent wrapper around `os/exec` for simple, one-shot commands that do not need Core integration: ```go -import "forge.lthn.ai/core/go-process/exec" +import "dappco.re/go/core/process/exec" // Fluent API err := exec.Command(ctx, "go", "build", "./..."). diff --git a/docs/development.md b/docs/development.md index d11384f..954bbd0 100644 --- a/docs/development.md +++ b/docs/development.md @@ -101,9 +101,7 @@ go-process/ pidfile.go # PID file single-instance lock pidfile_test.go # PID file tests process.go # Process type and methods - process_global.go # Global singleton and convenience API process_test.go # Process tests - global_test.go # Global API tests (concurrency) registry.go # Daemon registry (JSON file store) registry_test.go # Registry tests runner.go # Pipeline runner (sequential, parallel, DAG) @@ -142,8 +140,6 @@ go-process/ | `ErrProcessNotFound` | No process with the given ID exists in the service | | `ErrProcessNotRunning` | Operation requires a running process (e.g. SendInput, Signal) | | `ErrStdinNotAvailable` | Stdin pipe is nil (already closed or never created) | -| `ErrServiceNotInitialized` | Global convenience function called before `process.Init()` | -| `ServiceError` | Wraps service-level errors with a message string | ## Build Configuration diff --git a/docs/index.md b/docs/index.md index ddc0a5c..333a7ea 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,10 +5,10 @@ description: Process management with Core IPC integration for Go applications. # go-process -`forge.lthn.ai/core/go-process` is a process management library that provides +`dappco.re/go/core/process` is a process management library that provides spawning, monitoring, and controlling external processes with real-time output streaming via the Core ACTION (IPC) system. It integrates directly with the -[Core DI framework](https://forge.lthn.ai/core/go) as a first-class service. +[Core DI framework](https://dappco.re/go/core) as a first-class service. ## Features @@ -28,22 +28,17 @@ streaming via the Core ACTION (IPC) system. It integrates directly with the ```go import ( "context" - framework "forge.lthn.ai/core/go/pkg/core" - "forge.lthn.ai/core/go-process" + "dappco.re/go/core" + "dappco.re/go/core/process" ) -// Create a Core instance with the process service -c, err := framework.New( - framework.WithName("process", process.NewService(process.Options{})), -) -if err != nil { - log.Fatal(err) -} +// Create a Core instance with the process service registered. +c := core.New(core.WithService(process.Register)) // Retrieve the typed service -svc, err := framework.ServiceFor[*process.Service](c, "process") -if err != nil { - log.Fatal(err) +svc, ok := core.ServiceFor[*process.Service](c, "process") +if !ok { + panic("process service not registered") } ``` @@ -51,15 +46,19 @@ if err != nil { ```go // Fire-and-forget (async) -proc, err := svc.Start(ctx, "go", "test", "./...") -if err != nil { - return err +start := svc.Start(ctx, "go", "test", "./...") +if !start.OK { + return start.Value.(error) } +proc := start.Value.(*process.Process) <-proc.Done() fmt.Println(proc.Output()) // Synchronous convenience -output, err := svc.Run(ctx, "echo", "hello world") +run := svc.Run(ctx, "echo", "hello world") +if run.OK { + fmt.Println(run.Value.(string)) +} ``` ### Listen for Events @@ -67,7 +66,7 @@ output, err := svc.Run(ctx, "echo", "hello world") Process lifecycle events are broadcast through Core's ACTION system: ```go -c.RegisterAction(func(c *framework.Core, msg framework.Message) error { +c.RegisterAction(func(c *core.Core, msg core.Message) core.Result { switch m := msg.(type) { case process.ActionProcessStarted: fmt.Printf("Started: %s (PID %d)\n", m.Command, m.PID) @@ -78,24 +77,24 @@ c.RegisterAction(func(c *framework.Core, msg framework.Message) error { case process.ActionProcessKilled: fmt.Printf("Killed with %s\n", m.Signal) } - return nil + return core.Result{OK: true} }) ``` -### Global Convenience API +### Permission Model -For applications that only need a single process service, a global singleton -is available: +Core's process primitive delegates to named actions registered by this module. +Without `process.Register`, `c.Process().Run(...)` fails with `OK: false`. ```go -// Initialise once at startup -process.Init(coreInstance) +c := core.New() +r := c.Process().Run(ctx, "echo", "blocked") +fmt.Println(r.OK) // false -// Then use package-level functions anywhere -proc, _ := process.Start(ctx, "ls", "-la") -output, _ := process.Run(ctx, "date") -procs := process.List() -running := process.Running() +c = core.New(core.WithService(process.Register)) +_ = c.ServiceStartup(ctx, nil) +r = c.Process().Run(ctx, "echo", "allowed") +fmt.Println(r.OK) // true ``` ## Package Layout @@ -109,7 +108,7 @@ running := process.Running() | Field | Value | |-------|-------| -| Module path | `forge.lthn.ai/core/go-process` | +| Module path | `dappco.re/go/core/process` | | Go version | 1.26.0 | | Licence | EUPL-1.2 | @@ -117,7 +116,7 @@ running := process.Running() | Module | Purpose | |--------|---------| -| `forge.lthn.ai/core/go` | Core DI framework (`ServiceRuntime`, `Core.ACTION`, lifecycle interfaces) | +| `dappco.re/go/core` | Core DI framework (`ServiceRuntime`, `Core.ACTION`, lifecycle interfaces) | | `github.com/stretchr/testify` | Test assertions (test-only) | The package has no other runtime dependencies beyond the Go standard library