docs(ax): align process docs with AX action/result contract
This commit is contained in:
parent
aa3602fbb0
commit
b0dd22fc5e
4 changed files with 49 additions and 56 deletions
11
CLAUDE.md
11
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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", "./...").
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue