Files
talk/internal/daemon/spawn.go
T

52 lines
1.7 KiB
Go
Raw Normal View History

package daemon
import (
"fmt"
"os"
"os/exec"
)
// Spawn starts `exe daemon run` as an independent background process (own
// session or own process group, detached from the terminal), with
// stdout/stderr going to the log file logPath (created fresh per start;
// empty = discard). env is the child's environment (keys, TALK_*
// overrides).
//
// The return value is a channel that delivers the Wait result exactly once
// when the child exits; the caller uses it to detect a failed start (e.g.
// port in use) without waiting for a timeout. If the caller exits first,
// the daemon keeps running (that is the point).
func Spawn(exe, logPath string, env []string) (<-chan error, error) {
if logPath == "" {
logPath = os.DevNull
}
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
return nil, fmt.Errorf("daemon log %s: %w", logPath, err)
}
defer logFile.Close() // the child holds its own handle
// First attempt: fully detached, including a breakaway from the parent's
// job object (Windows). Shells such as WinRM, SSH servers or CI runners
// put children into a job that is terminated with the session; the
// breakaway lets the daemon outlive it. If the job forbids breakaway,
// CreateProcess fails, and the second attempt starts without it.
var cmd *exec.Cmd
for _, breakaway := range []bool{true, false} {
cmd = exec.Command(exe, "daemon", "run")
cmd.Env = env
cmd.Stdin = nil
cmd.Stdout = logFile
cmd.Stderr = logFile
detach(cmd, breakaway)
if err = cmd.Start(); err == nil {
break
}
}
if err != nil {
return nil, fmt.Errorf("starting daemon (%s daemon run): %w", exe, err)
}
exited := make(chan error, 1)
go func() { exited <- cmd.Wait() }()
return exited, nil
}