741fdcee24
Process spawn/wait/kill functions for subprocess management with output capture, timeout, and process group cleanup. DagRun and DagStepResult types for SQLite execution persistence. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
75 lines
2.0 KiB
Go
75 lines
2.0 KiB
Go
package infra
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// ProcessSpawn launches a subprocess using the given shell.
|
|
// If shell is empty, "sh" is used. If command contains newlines it is treated
|
|
// as a multi-line script: the content is written to a temp file and executed
|
|
// with `shell <tempfile>`. Otherwise it is executed with `shell -c <command>`.
|
|
// dir sets the working directory (empty = inherit). env sets the environment
|
|
// (nil = inherit parent env). The process group is created with Setpgid so
|
|
// that ProcessKill can target the whole group.
|
|
func ProcessSpawn(command string, dir string, env []string, shell string) (*ProcessHandle, error) {
|
|
if shell == "" {
|
|
shell = "sh"
|
|
}
|
|
|
|
var cmd *exec.Cmd
|
|
|
|
if strings.Contains(command, "\n") {
|
|
// Multi-line script: write to a temp file and execute it.
|
|
tmp, err := os.CreateTemp("", "fn-proc-*.sh")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("process_spawn: create temp file: %w", err)
|
|
}
|
|
if _, err := tmp.WriteString(command); err != nil {
|
|
_ = os.Remove(tmp.Name())
|
|
return nil, fmt.Errorf("process_spawn: write temp file: %w", err)
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
_ = os.Remove(tmp.Name())
|
|
return nil, fmt.Errorf("process_spawn: close temp file: %w", err)
|
|
}
|
|
cmd = exec.Command(shell, tmp.Name())
|
|
} else {
|
|
cmd = exec.Command(shell, "-c", command)
|
|
}
|
|
|
|
if dir != "" {
|
|
cmd.Dir = dir
|
|
}
|
|
if len(env) > 0 {
|
|
cmd.Env = env
|
|
}
|
|
|
|
// New process group so we can kill all children as a group.
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
|
|
|
// Use buffers instead of pipes to avoid race between Wait() and ReadAll().
|
|
var stdoutBuf, stderrBuf bytes.Buffer
|
|
cmd.Stdout = &stdoutBuf
|
|
cmd.Stderr = &stderrBuf
|
|
|
|
start := time.Now()
|
|
if err := cmd.Start(); err != nil {
|
|
return nil, fmt.Errorf("process_spawn: start: %w", err)
|
|
}
|
|
|
|
return &ProcessHandle{
|
|
Cmd: cmd,
|
|
Pid: cmd.Process.Pid,
|
|
StartTime: start,
|
|
Dir: dir,
|
|
stdout: &stdoutBuf,
|
|
stderr: &stderrBuf,
|
|
}, nil
|
|
}
|