diff --git a/internal/adapters/subprocess/process_tree_windows.go b/internal/adapters/subprocess/process_tree_windows.go index 2ddad2c..384b4e8 100644 --- a/internal/adapters/subprocess/process_tree_windows.go +++ b/internal/adapters/subprocess/process_tree_windows.go @@ -43,25 +43,44 @@ func (tree *windowsProcessTree) Start(cmd *exec.Cmd) error { withHandleErr := cmd.Process.WithHandle(func(handle uintptr) { assignErr = windows.AssignProcessToJobObject(job, windows.Handle(handle)) }) - if withHandleErr == nil && assignErr == nil { - assignErr = resumeInitialThread(uint32(cmd.Process.Pid)) + if withHandleErr != nil { + return failWindowsProcessStart(cmd, job, fmt.Errorf("acquire stable process handle for job assignment: %w", withHandleErr)) } - if withHandleErr != nil || assignErr != nil { - killErr := cmd.Process.Kill() - waitErr := cmd.Wait() - _ = windows.CloseHandle(job) - return joinErrors( - fmt.Errorf("acquire process handle for job assignment: %w", withHandleErr), - fmt.Errorf("assign process to job object or resume initial thread: %w", assignErr), - killErr, - waitErr, - ) + if assignErr != nil { + return failWindowsProcessStart(cmd, job, fmt.Errorf("assign process to job object: %w", assignErr)) + } + if err := resumeInitialThread(uint32(cmd.Process.Pid)); err != nil { + return failWindowsProcessStart(cmd, job, fmt.Errorf("resume initial process thread: %w", err)) } tree.job = job return nil } +func failWindowsProcessStart(cmd *exec.Cmd, job windows.Handle, setupErr error) error { + killErr := cmd.Process.Kill() + waitErr := cmd.Wait() + var exitErr *exec.ExitError + if killErr == nil && errors.As(waitErr, &exitErr) { + // A killed suspended child normally reports a non-zero exit from Wait; + // successful reaping is not an additional setup failure. + waitErr = nil + } + return joinErrors( + setupErr, + wrapWindowsProcessCleanupError("kill suspended child", killErr), + wrapWindowsProcessCleanupError("reap suspended child", waitErr), + wrapWindowsProcessCleanupError("close unowned job object", windows.CloseHandle(job)), + ) +} + +func wrapWindowsProcessCleanupError(operation string, err error) error { + if err == nil { + return nil + } + return fmt.Errorf("%s: %w", operation, err) +} + func resumeInitialThread(processID uint32) error { snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPTHREAD, 0) if err != nil {