diff --git a/service.go b/service.go index 44ccfc6..661dac9 100644 --- a/service.go +++ b/service.go @@ -9,6 +9,7 @@ import ( "os/exec" "sync" "sync/atomic" + "syscall" "time" "forge.lthn.ai/core/go/pkg/core" @@ -96,7 +97,12 @@ func (s *Service) Start(ctx context.Context, command string, args ...string) (*P func (s *Service) StartWithOptions(ctx context.Context, opts RunOptions) (*Process, error) { id := fmt.Sprintf("proc-%d", s.idCounter.Add(1)) - procCtx, cancel := context.WithCancel(ctx) + // Detached processes use Background context so they survive parent death + parentCtx := ctx + if opts.Detach { + parentCtx = context.Background() + } + procCtx, cancel := context.WithCancel(parentCtx) cmd := exec.CommandContext(procCtx, opts.Command, opts.Args...) if opts.Dir != "" { @@ -106,6 +112,11 @@ func (s *Service) StartWithOptions(ctx context.Context, opts RunOptions) (*Proce cmd.Env = append(cmd.Environ(), opts.Env...) } + // Detached processes get their own process group + if opts.Detach { + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + } + // Set up pipes stdout, err := cmd.StdoutPipe() if err != nil { diff --git a/types.go b/types.go index 4489af7..8589122 100644 --- a/types.go +++ b/types.go @@ -73,6 +73,10 @@ type RunOptions struct { // DisableCapture disables output buffering. // By default, output is captured to a ring buffer. DisableCapture bool + // Detach creates the process in its own process group (Setpgid). + // Detached processes survive parent death and context cancellation. + // The context is replaced with context.Background() when Detach is true. + Detach bool } // Info provides a snapshot of process state without internal fields.