Skip to content

Implement SafeProcessHandle.WaitForExit* methods #126293

Closed

Description

@adamsitnik
namespace Microsoft.Win32.SafeHandles
{
    public partial class SafeProcessHandle
    {
        public ProcessExitStatus WaitForExit();
        public bool TryWaitForExit(TimeSpan timeout, [NotNullWhen(true)] out ProcessExitStatus? exitStatus);
        public ProcessExitStatus WaitForExitOrKillOnTimeout(TimeSpan timeout);

        public Task<ProcessExitStatus> WaitForExitAsync(CancellationToken cancellationToken = default);
        public Task<ProcessExitStatus> WaitForExitOrKillOnCancellationAsync(CancellationToken cancellationToken);
    }
}
  • The WaitForExitOrKillOnTimeout and WaitForExitOrKillOnCancellationAsync methods must not throw on timeout/cancellation, just kill the process (if it has not already exited) and return ProcessExitStatus with Canceled=true (if the process was actually killed)
    /// <summary>
    /// Gets a value indicating whether the process has been terminated due to timeout or cancellation.
    /// </summary>
    public bool Canceled { get; }
  • Working Windows implementation was already provided in Add new process management APIs to SafeProcessHandle #124375:
    private ProcessExitStatus WaitForExitCore()
    {
    using Interop.Kernel32.ProcessWaitHandle processWaitHandle = new(this);
    processWaitHandle.WaitOne(Timeout.Infinite);
    return new ProcessExitStatus(GetExitCode(), false);
    }
    private bool TryWaitForExitCore(int milliseconds, [NotNullWhen(true)] out ProcessExitStatus? exitStatus)
    {
    using Interop.Kernel32.ProcessWaitHandle processWaitHandle = new(this);
    if (!processWaitHandle.WaitOne(milliseconds))
    {
    exitStatus = null;
    return false;
    }
    exitStatus = new ProcessExitStatus(GetExitCode(), false);
    return true;
    }
    private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds)
    {
    bool wasKilledOnTimeout = false;
    using Interop.Kernel32.ProcessWaitHandle processWaitHandle = new(this);
    if (!processWaitHandle.WaitOne(milliseconds))
    {
    wasKilledOnTimeout = KillCore();
    processWaitHandle.WaitOne(Timeout.Infinite);
    }
    return new ProcessExitStatus(GetExitCode(), wasKilledOnTimeout);
    }
    private async Task<ProcessExitStatus> WaitForExitAsyncCore(CancellationToken cancellationToken)
    {
    using Interop.Kernel32.ProcessWaitHandle processWaitHandle = new(this);
    TaskCompletionSource<bool> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
    RegisteredWaitHandle? registeredWaitHandle = null;
    CancellationTokenRegistration ctr = default;
    try
    {
    registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(
    processWaitHandle,
    (state, timedOut) => ((TaskCompletionSource<bool>)state!).TrySetResult(true),
    tcs,
    Timeout.Infinite,
    executeOnlyOnce: true);
    if (cancellationToken.CanBeCanceled)
    {
    ctr = cancellationToken.UnsafeRegister(
    static state =>
    {
    var (taskSource, token) = ((TaskCompletionSource<bool> taskSource, CancellationToken token))state!;
    taskSource.TrySetCanceled(token);
    },
    (tcs, cancellationToken));
    }
    await tcs.Task.ConfigureAwait(false);
    }
    finally
    {
    ctr.Dispose();
    registeredWaitHandle?.Unregister(null);
    }
    return new ProcessExitStatus(GetExitCode(), false);
    }
    private async Task<ProcessExitStatus> WaitForExitOrKillOnCancellationAsyncCore(CancellationToken cancellationToken)
    {
    using Interop.Kernel32.ProcessWaitHandle processWaitHandle = new(this);
    TaskCompletionSource<bool> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
    RegisteredWaitHandle? registeredWaitHandle = null;
    CancellationTokenRegistration ctr = default;
    StrongBox<bool> wasKilledBox = new(false);
    try
    {
    registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(
    processWaitHandle,
    (state, timedOut) => ((TaskCompletionSource<bool>)state!).TrySetResult(true),
    tcs,
    Timeout.Infinite,
    executeOnlyOnce: true);
    if (cancellationToken.CanBeCanceled)
    {
    ctr = cancellationToken.UnsafeRegister(
    static state =>
    {
    var (handle, wasCancelled) = ((SafeProcessHandle, StrongBox<bool>))state!;
    wasCancelled.Value = handle.KillCore();
    },
    (this, wasKilledBox));
    }
    await tcs.Task.ConfigureAwait(false);
    }
    finally
    {
    ctr.Dispose();
    registeredWaitHandle?.Unregister(null);
    }
    return new ProcessExitStatus(GetExitCode(), wasKilledBox.Value);
    }
  • Working macOS implementation was already provided in Implement SafeProcessHandle APIs for macOS #124923. It needs to be used for FreeBSD as well:
    private ProcessExitStatus WaitForExitCore()
    {
    switch (Interop.Sys.WaitForExitAndReap(this, ProcessId, out int exitCode, out int rawSignal))
    {
    case -1:
    throw new Win32Exception();
    default:
    ProcessExitStatus status = new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null);
    OnProcessExited();
    return status;
    }
    }
    private bool TryWaitForExitCore(int milliseconds, [NotNullWhen(true)] out ProcessExitStatus? exitStatus)
    {
    switch (Interop.Sys.TryWaitForExit(this, ProcessId, milliseconds, out int exitCode, out int rawSignal))
    {
    case -1:
    throw new Win32Exception();
    case 1: // timeout
    exitStatus = null;
    return false;
    default:
    exitStatus = new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null);
    OnProcessExited();
    return true;
    }
    }
    private static int GetProcessIdCore() => throw new PlatformNotSupportedException();
    private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds)
    {
    switch (Interop.Sys.WaitForExitOrKillOnTimeout(this, ProcessId, _isGroupLeader, milliseconds, out int exitCode, out int rawSignal, out int hasTimedout))
    {
    case -1:
    throw new Win32Exception();
    default:
    ProcessExitStatus status = new(exitCode, hasTimedout == 1, rawSignal != 0 ? (PosixSignal)rawSignal : null);
    OnProcessExited();
    return status;
    }
    }
    private async Task<ProcessExitStatus> WaitForExitAsyncCore(CancellationToken cancellationToken)
    {
    if (!cancellationToken.CanBeCanceled)
    {
    return await Task.Run(WaitForExitCore, cancellationToken).ConfigureAwait(false);
    }
    CreatePipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle);
    using (readHandle)
    using (writeHandle)
    {
    using CancellationTokenRegistration registration = cancellationToken.Register(static state =>
    {
    ((SafeFileHandle)state!).Close(); // Close the write end of the pipe to signal cancellation
    }, writeHandle);
    return await Task.Run(() =>
    {
    switch (Interop.Sys.TryWaitForExitCancellable(this, ProcessId, (int)readHandle.DangerousGetHandle(), out int exitCode, out int rawSignal))
    {
    case -1:
    throw new Win32Exception();
    case 1: // canceled
    throw new OperationCanceledException(cancellationToken);
    default:
    ProcessExitStatus status = new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null);
    OnProcessExited();
    return status;
    }
    }, cancellationToken).ConfigureAwait(false);
    }
    }
    private async Task<ProcessExitStatus> WaitForExitOrKillOnCancellationAsyncCore(CancellationToken cancellationToken)
    {
    if (!cancellationToken.CanBeCanceled)
    {
    return await Task.Run(WaitForExitCore, cancellationToken).ConfigureAwait(false);
    }
    CreatePipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle);
    using (readHandle)
    using (writeHandle)
    {
    using CancellationTokenRegistration registration = cancellationToken.Register(static state =>
    {
    ((SafeFileHandle)state!).Close(); // Close the write end of the pipe to signal cancellation
    }, writeHandle);
    return await Task.Run(() =>
    {
    switch (Interop.Sys.TryWaitForExitCancellable(this, ProcessId, (int)readHandle.DangerousGetHandle(), out int exitCode, out int rawSignal))
    {
    case -1:
    throw new Win32Exception();
    case 1: // canceled
    bool wasKilled = KillCore(throwOnError: false, entireProcessGroup: _isGroupLeader);
    ProcessExitStatus status = WaitForExitCore();
    return new ProcessExitStatus(status.ExitCode, wasKilled, status.Signal);
    default:
    ProcessExitStatus exitStatus = new(exitCode, false, rawSignal != 0 ? (PosixSignal)rawSignal : null);
    OnProcessExited();
    return exitStatus;
    }
    }, cancellationToken).ConfigureAwait(false);
    }
    }
  • A draft on Linux implementation that uses pidfd and poll to wait for process exit was provided in Implement SafeProcessHandle APIs for Linux and other Unixes #124979
  • The Unix implementation needs to use SafeFileHandle.CreateAnonymousPipe API for creating pipe that is used for monitoring for cancellation
  • On Unix, we need to integrate with existing logic for reaping processes
    • the new Wait methods need to be able to reap the process, but once they do they need to update ProcessWaitState
    • if ProcessWaitState reaps the process first, the Wait* methods should obtain the exit status from there
  • the tests should go to src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs introduced by Introduce SafeProcessHandle.Start and ProcessId #126192 and reuse tests provided in Add new process management APIs to SafeProcessHandle #124375 and Implement SafeProcessHandle APIs for macOS #124923

Activity

  1. added this to the 11.0.0 milestone on Mar 30, 2026
  2. self-assigned this
    on Mar 30, 2026
  3. dotnet-policy-service commented on Mar 30, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
    See info in area-owners.md if you want to be subscribed.

  4. locked and limited conversation to collaborators on May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions