docs(ax): align process docs with AX action/result contract

This commit is contained in:
Virgil 2026-03-30 05:29:51 +00:00
parent aa3602fbb0
commit b0dd22fc5e
4 changed files with 49 additions and 56 deletions

View file

@ -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)

View file

@ -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", "./...").

View file

@ -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

View file

@ -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