go-process/process.go
2026-04-04 03:17:30 +00:00

341 lines
6.6 KiB
Go

package process
import (
"context"
"fmt"
"os"
"os/exec"
"sync"
"syscall"
"time"
coreerr "dappco.re/go/core/log"
goio "io"
)
// ManagedProcess represents a managed external process.
//
// Example:
//
// proc, err := svc.Start(ctx, "echo", "hello")
type ManagedProcess struct {
ID string
Command string
Args []string
Dir string
Env []string
StartedAt time.Time
Status Status
ExitCode int
Duration time.Duration
cmd *exec.Cmd
ctx context.Context
cancel context.CancelFunc
output *RingBuffer
stdin goio.WriteCloser
done chan struct{}
mu sync.RWMutex
gracePeriod time.Duration
killGroup bool
killNotified bool
killSignal string
}
// Process is kept as an alias for ManagedProcess for compatibility.
type Process = ManagedProcess
// Info returns a snapshot of process state.
//
// Example:
//
// info := proc.Info()
func (p *ManagedProcess) Info() Info {
p.mu.RLock()
defer p.mu.RUnlock()
pid := 0
if p.cmd != nil && p.cmd.Process != nil {
pid = p.cmd.Process.Pid
}
duration := p.Duration
if p.Status == StatusRunning {
duration = time.Since(p.StartedAt)
}
return Info{
ID: p.ID,
Command: p.Command,
Args: append([]string(nil), p.Args...),
Dir: p.Dir,
StartedAt: p.StartedAt,
Running: p.Status == StatusRunning,
Status: p.Status,
ExitCode: p.ExitCode,
Duration: duration,
PID: pid,
}
}
// Output returns the captured output as a string.
//
// Example:
//
// fmt.Println(proc.Output())
func (p *ManagedProcess) Output() string {
p.mu.RLock()
defer p.mu.RUnlock()
if p.output == nil {
return ""
}
return p.output.String()
}
// OutputBytes returns the captured output as bytes.
//
// Example:
//
// data := proc.OutputBytes()
func (p *ManagedProcess) OutputBytes() []byte {
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.
func (p *ManagedProcess) IsRunning() bool {
p.mu.RLock()
defer p.mu.RUnlock()
return p.Status == StatusRunning
}
// Wait blocks until the process exits.
//
// Example:
//
// if err := proc.Wait(); err != nil { return err }
func (p *ManagedProcess) Wait() error {
<-p.done
p.mu.RLock()
defer p.mu.RUnlock()
if p.Status == StatusFailed {
return coreerr.E("Process.Wait", fmt.Sprintf("process failed to start: %s", p.ID), nil)
}
if p.Status == StatusKilled {
return coreerr.E("Process.Wait", fmt.Sprintf("process was killed: %s", p.ID), nil)
}
if p.ExitCode != 0 {
return coreerr.E("Process.Wait", fmt.Sprintf("process exited with code %d", p.ExitCode), nil)
}
return nil
}
// Done returns a channel that closes when the process exits.
//
// Example:
//
// <-proc.Done()
func (p *ManagedProcess) Done() <-chan struct{} {
return p.done
}
// Kill forcefully terminates the process.
// If KillGroup is set, kills the entire process group.
//
// Example:
//
// _ = proc.Kill()
func (p *ManagedProcess) Kill() error {
_, err := p.kill()
return err
}
// kill terminates the process and reports whether a signal was actually sent.
func (p *ManagedProcess) kill() (bool, error) {
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
}
if p.killGroup {
// Kill entire process group (negative PID)
return true, syscall.Kill(-p.cmd.Process.Pid, syscall.SIGKILL)
}
return true, p.cmd.Process.Kill()
}
// killTree forcefully terminates the process group when one exists.
func (p *ManagedProcess) killTree() (bool, error) {
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)
}
// 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.
//
// Example:
//
// _ = proc.Shutdown()
func (p *ManagedProcess) Shutdown() error {
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).
func (p *ManagedProcess) terminate() error {
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)
}
// Signal sends a signal to the process.
//
// Example:
//
// _ = proc.Signal(os.Interrupt)
func (p *ManagedProcess) Signal(sig os.Signal) error {
p.mu.RLock()
status := p.Status
cmd := p.cmd
killGroup := p.killGroup
p.mu.RUnlock()
if status != StatusRunning {
return ErrProcessNotRunning
}
if cmd == nil || cmd.Process == nil {
return nil
}
if !killGroup {
return cmd.Process.Signal(sig)
}
sysSig, ok := sig.(syscall.Signal)
if !ok {
return cmd.Process.Signal(sig)
}
if err := syscall.Kill(-cmd.Process.Pid, sysSig); err != nil {
return err
}
// Some shells briefly ignore or defer the signal while they are still
// initialising child jobs. Retry a few times after short delays so the
// 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.
go func(pid int, sig syscall.Signal, done <-chan struct{}) {
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)
}
}
select {
case <-done:
return
default:
_ = syscall.Kill(-pid, syscall.SIGKILL)
}
}(cmd.Process.Pid, sysSig, p.done)
return nil
}
// SendInput writes to the process stdin.
//
// Example:
//
// _ = proc.SendInput("hello\n")
func (p *ManagedProcess) SendInput(input string) error {
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.
//
// Example:
//
// _ = proc.CloseStdin()
func (p *ManagedProcess) CloseStdin() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.stdin == nil {
return nil
}
err := p.stdin.Close()
p.stdin = nil
return err
}