package compose import ( "errors" "os" "time" ) // processLifetime owns a started Docker CLI process until it has been reaped and every process // it may have started has been released. Platform implementations must make close idempotent. type processLifetime interface { terminate(done <-chan error) error close() error } var startProcessForRunner = startProcess var terminateProcessForRunner = func(process processLifetime, done <-chan error) error { return process.terminate(done) } // terminateProcessTree terminates the owned tree before consulting the direct child's reaping // result. A direct child can be reaped while descendants survive, so a ready done channel is never // evidence that tree termination can be skipped. The direct kill is only a fallback when tree // ownership could not terminate the tree; an already-reaped direct child is benign in that case. func terminateProcessTree( done <-chan error, terminateTree func() error, terminateDirect func() error, bound time.Duration, ) error { treeErr := terminateTree() var directErr error if treeErr != nil { directErr = terminateDirect() if errors.Is(directErr, os.ErrProcessDone) { directErr = nil } } reapErr := waitForProcessReap(done, bound) if treeErr == nil && directErr == nil && reapErr == nil { return nil } return errors.Join(ErrProcessReap, treeErr, directErr, reapErr) } func waitForProcessReap(done <-chan error, bound time.Duration) error { timer := time.NewTimer(bound) defer timer.Stop() select { case <-done: return nil case <-timer.C: return ErrProcessReap } }