2026-03-06 12:50:09 +00:00
package process
import (
2026-04-24 20:11:00 +01:00
// Note: banned-imports exception: go-process is THE implementation of core.Process and cannot depend on itself; core.* helpers are downstream and unavailable at this layer.
2026-03-06 12:50:09 +00:00
"context"
2026-04-24 20:11:00 +01:00
// Note: banned-imports exception: core.* string/format helpers are downstream from this core.Process primitive and unavailable here.
2026-03-09 08:26:00 +00:00
"fmt"
2026-04-24 20:11:00 +01:00
// Note: banned-imports exception: go-process is THE implementation of core.Process and cannot depend on itself; OS handles and signals are intrinsic to process management.
2026-03-09 08:26:00 +00:00
"os"
2026-04-24 20:11:00 +01:00
// Note: banned-imports exception: os/exec is intrinsic to process management in THE implementation of core.Process, which cannot depend on itself.
2026-03-06 12:50:09 +00:00
"os/exec"
2026-04-25 08:45:04 +01:00
// Note: AX-6 — internal concurrency primitive; structural per RFC §2
2026-03-06 12:50:09 +00:00
"sync"
2026-04-24 20:11:00 +01:00
// Note: banned-imports exception: syscall is intrinsic to process management in THE implementation of core.Process, which cannot depend on itself.
2026-03-17 17:44:28 +00:00
"syscall"
2026-04-24 20:11:00 +01:00
// Note: banned-imports exception: process lifecycle timing is intrinsic here; core.* helpers are downstream and unavailable at this layer.
2026-03-06 12:50:09 +00:00
"time"
2026-03-16 20:34:23 +00:00
fix(go-process): update stale coreerr alias to dappco.re/go/log (AX-6)
Updated `coreerr "dappco.re/go/core/log"` → `coreerr "dappco.re/go/log"`
across 12 files (actions.go, daemon.go, errors.go, exec/exec.go,
health.go, pkg/api/provider.go, process.go, process_global.go,
program.go, registry.go, runner.go, service.go). No stale path
remains in .go.
Pre-existing blocker (out of ticket scope): `dappco.re/go/io@v0.4.2`
is 404 from module proxy — affects `go test ./...` but is unrelated
to this import rename.
Closes tasks.lthn.sh/view.php?id=718
Co-authored-by: Codex <noreply@openai.com>
2026-04-24 21:45:29 +01:00
coreerr "dappco.re/go/log"
2026-04-24 20:11:00 +01:00
// Note: banned-imports exception: stdlib io is intrinsic for process pipes; go-process is THE core.Process implementation and cannot self-depend.
2026-04-04 03:17:30 +00:00
goio "io"
2026-03-06 12:50:09 +00:00
)
2026-04-04 03:14:25 +00:00
// ManagedProcess represents a managed external process.
2026-04-04 00:39:27 +00:00
//
// Example:
//
// proc, err := svc.Start(ctx, "echo", "hello")
2026-04-04 03:14:25 +00:00
type ManagedProcess struct {
2026-03-06 12:50:09 +00:00
ID string
Command string
Args [ ] string
Dir string
Env [ ] string
StartedAt time . Time
Status Status
ExitCode int
Duration time . Duration
2026-04-04 01:00:27 +00:00
cmd * exec . Cmd
ctx context . Context
cancel context . CancelFunc
output * RingBuffer
2026-04-04 03:17:30 +00:00
stdin goio . WriteCloser
2026-04-04 01:00:27 +00:00
done chan struct { }
mu sync . RWMutex
gracePeriod time . Duration
killGroup bool
killNotified bool
killSignal string
2026-03-06 12:50:09 +00:00
}
2026-04-04 03:14:25 +00:00
// Process is kept as an alias for ManagedProcess for compatibility.
type Process = ManagedProcess
2026-03-06 12:50:09 +00:00
// Info returns a snapshot of process state.
2026-04-04 00:39:27 +00:00
//
// Example:
//
// info := proc.Info()
2026-04-04 03:14:25 +00:00
func ( p * ManagedProcess ) Info ( ) Info {
2026-03-06 12:50:09 +00:00
p . mu . RLock ( )
defer p . mu . RUnlock ( )
pid := 0
if p . cmd != nil && p . cmd . Process != nil {
pid = p . cmd . Process . Pid
}
2026-04-04 02:53:12 +00:00
duration := p . Duration
if p . Status == StatusRunning {
duration = time . Since ( p . StartedAt )
}
2026-03-06 12:50:09 +00:00
return Info {
ID : p . ID ,
Command : p . Command ,
2026-04-04 00:04:54 +00:00
Args : append ( [ ] string ( nil ) , p . Args ... ) ,
2026-03-06 12:50:09 +00:00
Dir : p . Dir ,
StartedAt : p . StartedAt ,
2026-04-04 00:01:22 +00:00
Running : p . Status == StatusRunning ,
2026-03-06 12:50:09 +00:00
Status : p . Status ,
ExitCode : p . ExitCode ,
2026-04-04 02:53:12 +00:00
Duration : duration ,
2026-03-06 12:50:09 +00:00
PID : pid ,
}
}
// Output returns the captured output as a string.
2026-04-04 00:39:27 +00:00
//
// Example:
//
// fmt.Println(proc.Output())
2026-04-04 03:14:25 +00:00
func ( p * ManagedProcess ) Output ( ) string {
2026-03-06 12:50:09 +00:00
p . mu . RLock ( )
defer p . mu . RUnlock ( )
if p . output == nil {
return ""
}
return p . output . String ( )
}
// OutputBytes returns the captured output as bytes.
2026-04-04 00:39:27 +00:00
//
// Example:
//
// data := proc.OutputBytes()
2026-04-04 03:14:25 +00:00
func ( p * ManagedProcess ) OutputBytes ( ) [ ] byte {
2026-03-06 12:50:09 +00:00
p . mu . RLock ( )
defer p . mu . RUnlock ( )
if p . output == nil {
return nil
}
return p . output . Bytes ( )
}
// IsRunning returns true if the process is still executing.
2026-04-04 03:14:25 +00:00
func ( p * ManagedProcess ) IsRunning ( ) bool {
2026-03-06 12:50:09 +00:00
p . mu . RLock ( )
defer p . mu . RUnlock ( )
return p . Status == StatusRunning
}
// Wait blocks until the process exits.
2026-04-04 00:39:27 +00:00
//
// Example:
//
// if err := proc.Wait(); err != nil { return err }
2026-04-04 03:14:25 +00:00
func ( p * ManagedProcess ) Wait ( ) error {
2026-03-06 12:50:09 +00:00
<- p . done
p . mu . RLock ( )
defer p . mu . RUnlock ( )
2026-03-09 08:26:00 +00:00
if p . Status == StatusFailed {
2026-03-16 20:34:23 +00:00
return coreerr . E ( "Process.Wait" , fmt . Sprintf ( "process failed to start: %s" , p . ID ) , nil )
2026-03-09 08:26:00 +00:00
}
if p . Status == StatusKilled {
2026-03-16 20:34:23 +00:00
return coreerr . E ( "Process.Wait" , fmt . Sprintf ( "process was killed: %s" , p . ID ) , nil )
2026-03-06 12:50:09 +00:00
}
if p . ExitCode != 0 {
2026-03-16 20:34:23 +00:00
return coreerr . E ( "Process.Wait" , fmt . Sprintf ( "process exited with code %d" , p . ExitCode ) , nil )
2026-03-06 12:50:09 +00:00
}
return nil
}
// Done returns a channel that closes when the process exits.
2026-04-04 00:39:27 +00:00
//
// Example:
//
// <-proc.Done()
2026-04-04 03:14:25 +00:00
func ( p * ManagedProcess ) Done ( ) <- chan struct { } {
2026-03-06 12:50:09 +00:00
return p . done
}
// Kill forcefully terminates the process.
2026-03-17 17:44:28 +00:00
// If KillGroup is set, kills the entire process group.
2026-04-04 00:39:27 +00:00
//
// Example:
//
// _ = proc.Kill()
2026-04-04 03:14:25 +00:00
func ( p * ManagedProcess ) Kill ( ) error {
2026-04-04 01:00:27 +00:00
_ , err := p . kill ( )
return err
}
// kill terminates the process and reports whether a signal was actually sent.
2026-04-04 03:14:25 +00:00
func ( p * ManagedProcess ) kill ( ) ( bool , error ) {
2026-03-06 12:50:09 +00:00
p . mu . Lock ( )
defer p . mu . Unlock ( )
if p . Status != StatusRunning {
2026-04-04 01:00:27 +00:00
return false , nil
2026-03-06 12:50:09 +00:00
}
if p . cmd == nil || p . cmd . Process == nil {
2026-04-04 01:00:27 +00:00
return false , nil
2026-03-06 12:50:09 +00:00
}
2026-03-17 17:44:28 +00:00
if p . killGroup {
// Kill entire process group (negative PID)
2026-04-04 01:00:27 +00:00
return true , syscall . Kill ( - p . cmd . Process . Pid , syscall . SIGKILL )
2026-03-17 17:44:28 +00:00
}
2026-04-04 01:00:27 +00:00
return true , p . cmd . Process . Kill ( )
2026-03-06 12:50:09 +00:00
}
2026-04-04 02:10:35 +00:00
// killTree forcefully terminates the process group when one exists.
2026-04-04 03:14:25 +00:00
func ( p * ManagedProcess ) killTree ( ) ( bool , error ) {
2026-04-04 02:10:35 +00:00
p . mu . Lock ( )
defer p . mu . Unlock ( )
if p . Status != StatusRunning {
return false , nil
}
if p . cmd == nil || p . cmd . Process == nil {
return false , nil
}
return true , syscall . Kill ( - p . cmd . Process . Pid , syscall . SIGKILL )
}
2026-03-17 17:44:28 +00:00
// Shutdown gracefully stops the process: SIGTERM, then SIGKILL after grace period.
// If GracePeriod was not set (zero), falls back to immediate Kill().
// If KillGroup is set, signals are sent to the entire process group.
2026-04-04 00:39:27 +00:00
//
// Example:
//
// _ = proc.Shutdown()
2026-04-04 03:14:25 +00:00
func ( p * ManagedProcess ) Shutdown ( ) error {
2026-03-17 17:44:28 +00:00
p . mu . RLock ( )
grace := p . gracePeriod
p . mu . RUnlock ( )
if grace <= 0 {
return p . Kill ( )
}
// Send SIGTERM
if err := p . terminate ( ) ; err != nil {
return p . Kill ( )
}
// Wait for exit or grace period
select {
case <- p . done :
return nil
case <- time . After ( grace ) :
return p . Kill ( )
}
}
// terminate sends SIGTERM to the process (or process group if KillGroup is set).
2026-04-04 03:14:25 +00:00
func ( p * ManagedProcess ) terminate ( ) error {
2026-03-17 17:44:28 +00:00
p . mu . Lock ( )
defer p . mu . Unlock ( )
if p . Status != StatusRunning {
return nil
}
if p . cmd == nil || p . cmd . Process == nil {
return nil
}
pid := p . cmd . Process . Pid
if p . killGroup {
pid = - pid
}
return syscall . Kill ( pid , syscall . SIGTERM )
}
2026-03-06 12:50:09 +00:00
// Signal sends a signal to the process.
2026-04-04 00:39:27 +00:00
//
// Example:
//
// _ = proc.Signal(os.Interrupt)
2026-04-04 03:14:25 +00:00
func ( p * ManagedProcess ) Signal ( sig os . Signal ) error {
2026-04-04 00:42:46 +00:00
p . mu . RLock ( )
status := p . Status
cmd := p . cmd
killGroup := p . killGroup
p . mu . RUnlock ( )
2026-03-06 12:50:09 +00:00
2026-04-04 00:42:46 +00:00
if status != StatusRunning {
2026-03-09 08:26:00 +00:00
return ErrProcessNotRunning
2026-03-06 12:50:09 +00:00
}
2026-04-04 00:42:46 +00:00
if cmd == nil || cmd . Process == nil {
2026-03-06 12:50:09 +00:00
return nil
}
2026-04-04 00:42:46 +00:00
if ! killGroup {
return cmd . Process . Signal ( sig )
2026-04-03 23:53:19 +00:00
}
2026-04-04 00:42:46 +00:00
sysSig , ok := sig . ( syscall . Signal )
if ! ok {
return cmd . Process . Signal ( sig )
}
2026-04-04 07:29:00 +00:00
if sysSig == 0 {
return syscall . Kill ( - cmd . Process . Pid , 0 )
}
2026-04-04 00:42:46 +00:00
if err := syscall . Kill ( - cmd . Process . Pid , sysSig ) ; err != nil {
return err
}
// Some shells briefly ignore or defer the signal while they are still
2026-04-04 01:03:42 +00:00
// initialising child jobs. Retry a few times after short delays so the
2026-04-04 01:32:43 +00:00
// whole process group is more reliably terminated. If the requested signal
// still does not stop the group, escalate to SIGKILL so callers do not hang.
2026-04-04 00:42:46 +00:00
go func ( pid int , sig syscall . Signal , done <- chan struct { } ) {
2026-04-04 01:03:42 +00:00
ticker := time . NewTicker ( 100 * time . Millisecond )
defer ticker . Stop ( )
for i := 0 ; i < 5 ; i ++ {
select {
case <- done :
return
case <- ticker . C :
_ = syscall . Kill ( - pid , sig )
}
2026-04-04 00:42:46 +00:00
}
2026-04-04 01:32:43 +00:00
select {
case <- done :
return
default :
_ = syscall . Kill ( - pid , syscall . SIGKILL )
}
2026-04-04 00:42:46 +00:00
} ( cmd . Process . Pid , sysSig , p . done )
return nil
2026-03-06 12:50:09 +00:00
}
// SendInput writes to the process stdin.
2026-04-04 00:39:27 +00:00
//
// Example:
//
// _ = proc.SendInput("hello\n")
2026-04-04 03:14:25 +00:00
func ( p * ManagedProcess ) SendInput ( input string ) error {
2026-03-06 12:50:09 +00:00
p . mu . RLock ( )
defer p . mu . RUnlock ( )
if p . Status != StatusRunning {
return ErrProcessNotRunning
}
if p . stdin == nil {
return ErrStdinNotAvailable
}
_ , err := p . stdin . Write ( [ ] byte ( input ) )
return err
}
// CloseStdin closes the process stdin pipe.
2026-04-04 00:39:27 +00:00
//
// Example:
//
// _ = proc.CloseStdin()
2026-04-04 03:14:25 +00:00
func ( p * ManagedProcess ) CloseStdin ( ) error {
2026-03-06 12:50:09 +00:00
p . mu . Lock ( )
defer p . mu . Unlock ( )
if p . stdin == nil {
return nil
}
err := p . stdin . Close ( )
p . stdin = nil
return err
}