Files
narratio/internal/adapters/subprocess/process_tree.go

67 lines
1.8 KiB
Go
Raw Normal View History

2026-08-10 18:44:10 +00:00
package subprocess
import (
"context"
"errors"
"fmt"
"os/exec"
"time"
)
const (
gracefulTerminationWait = 2 * time.Second
forcefulTerminationWait = 2 * time.Second
)
// ownedProcessTree owns every process started by a command invocation.
// Implementations must tolerate a leader that has already exited.
type ownedProcessTree interface {
Start(*exec.Cmd) error
TerminateGracefully() error
TerminateForcefully() error
Close() error
}
2026-08-10 18:55:49 +00:00
func waitForOwnedCommand(ctx context.Context, tree ownedProcessTree, waitCh <-chan error, captureLimits <-chan *captureLimitError) (waitErr, ctxErr error, captureLimit *captureLimitError, cleanupErr error) {
2026-08-10 18:44:10 +00:00
select {
case waitErr = <-waitCh:
2026-08-10 18:55:49 +00:00
return waitErr, nil, nil, nil
2026-08-10 18:44:10 +00:00
case <-ctx.Done():
ctxErr = ctx.Err()
2026-08-10 18:55:49 +00:00
case captureLimit = <-captureLimits:
2026-08-10 18:44:10 +00:00
}
cleanupErr = tree.TerminateGracefully()
gracefulTimer := time.NewTimer(gracefulTerminationWait)
defer gracefulTimer.Stop()
select {
case waitErr = <-waitCh:
// The leader may exit before descendants finish graceful shutdown.
cleanupErr = joinErrors(cleanupErr, tree.TerminateForcefully())
2026-08-10 18:55:49 +00:00
return waitErr, ctxErr, captureLimit, cleanupErr
2026-08-10 18:44:10 +00:00
case <-gracefulTimer.C:
}
cleanupErr = joinErrors(cleanupErr, tree.TerminateForcefully())
forcefulTimer := time.NewTimer(forcefulTerminationWait)
defer forcefulTimer.Stop()
select {
case waitErr = <-waitCh:
2026-08-10 18:55:49 +00:00
return waitErr, ctxErr, captureLimit, cleanupErr
2026-08-10 18:44:10 +00:00
case <-forcefulTimer.C:
2026-08-10 18:55:49 +00:00
return nil, ctxErr, captureLimit, joinErrors(cleanupErr, fmt.Errorf("owned subprocess did not reap within %s after forceful termination", forcefulTerminationWait))
2026-08-10 18:44:10 +00:00
}
}
func joinErrors(errs ...error) error {
filtered := make([]error, 0, len(errs))
for _, err := range errs {
if err != nil {
filtered = append(filtered, err)
}
}
return errors.Join(filtered...)
}