Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
33ea555
Implement SafeProcessHandle.WaitForExit* methods
Copilot Apr 16, 2026
d2f68c9
Address PR feedback: store ProcessExitStatus in ProcessWaitState, mov…
Copilot Apr 16, 2026
33f89fc
Add comment explaining ERROR_ACCESS_DENIED handling in TerminateProce…
Copilot Apr 16, 2026
7c723d5
Address PR feedback: remove TerminateProcessCore, use SignalCore with…
Copilot Apr 16, 2026
df26b6b
address my own feedback:
adamsitnik Apr 17, 2026
5454880
Merge branch 'main' into copilot/implement-issue-126293
adamsitnik Apr 17, 2026
1fd5785
Apply suggestions from code review
adamsitnik Apr 17, 2026
fedd0fb
reduce code duplication
adamsitnik Apr 17, 2026
ef41f82
Fix _waitState lazy initialization and add signal/Process.SafeHandle …
Copilot Apr 17, 2026
d1c541c
Address code review: fix race condition in EnsureWaitStateInitialized…
Copilot Apr 17, 2026
77e5143
Merge duplicate tests into theories with bool useAsync parameter
Copilot Apr 18, 2026
77a6493
Simplify EnsureWaitStateInitialized to use ProcessWaitState.Holder(pr…
Copilot Apr 19, 2026
d09130b
Simplify wait state holder ownership: make _waitState readonly, remov…
Copilot Apr 20, 2026
d4815a0
address code review feedback and fix the implementation
adamsitnik Apr 20, 2026
7add4f0
address code review feedback: don't use an extra field to store PID o…
adamsitnik Apr 20, 2026
c0cd053
Apply suggestions from code review
adamsitnik Apr 20, 2026
5cf7e58
address code review feedback: don't use an extra field to store PID o…
adamsitnik Apr 20, 2026
ee3eeec
Change NotSupportedException to PlatformNotSupportedException for non…
Copilot Apr 20, 2026
5998a74
Merge branch 'main' into copilot/implement-issue-126293
adamsitnik Apr 20, 2026
8907972
Fix resource string grammar, update XML docs with PNSE, fix comments
Copilot Apr 20, 2026
cfdd414
Rename ProcessId/IsChild fields back to _processId/_isChild, remove t…
Copilot Apr 21, 2026
e414dc5
Change ProcessId to throw InvalidOperationException when _waitStateHo…
Copilot Apr 21, 2026
e790bae
address code review feedback
adamsitnik Apr 21, 2026
6143eba
Apply suggestions from code review
adamsitnik Apr 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ internal static partial class Sys
/// 4) on error, -1 is returned.
/// </returns>
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_WaitPidExitedNoHang", SetLastError = true)]
internal static partial int WaitPidExitedNoHang(int pid, out int exitCode);
internal static partial int WaitPidExitedNoHang(int pid, out int exitCode, out int terminatingSignal);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ public void Kill() { }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]
[System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")]
public static Microsoft.Win32.SafeHandles.SafeProcessHandle Start(System.Diagnostics.ProcessStartInfo startInfo) { throw null; }
public bool TryWaitForExit(System.TimeSpan timeout, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Diagnostics.ProcessExitStatus? exitStatus) { throw null; }
public System.Diagnostics.ProcessExitStatus WaitForExit() { throw null; }
public System.Threading.Tasks.Task<System.Diagnostics.ProcessExitStatus> WaitForExitAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]
[System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")]
public System.Threading.Tasks.Task<System.Diagnostics.ProcessExitStatus> WaitForExitOrKillOnCancellationAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]
[System.Runtime.Versioning.SupportedOSPlatformAttribute("maccatalyst")]
public System.Diagnostics.ProcessExitStatus WaitForExitOrKillOnTimeout(System.TimeSpan timeout) { throw null; }
}
}
namespace System.Diagnostics
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,40 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali

private readonly SafeWaitHandle? _handle;
private readonly bool _releaseRef;
private readonly ProcessWaitState.Holder? _waitStateHolder;
Comment thread
adamsitnik marked this conversation as resolved.

private SafeProcessHandle(int processId, ProcessWaitState.Holder waitStateHolder) : base(ownsHandle: true)
internal SafeProcessHandle(ProcessWaitState.Holder waitStateHolder) : base(ownsHandle: true)
{
ProcessId = processId;

_handle = waitStateHolder._state.EnsureExitedEvent().GetSafeWaitHandle();
_waitStateHolder = waitStateHolder;
_handle = _waitStateHolder._state.EnsureExitedEvent().GetSafeWaitHandle();
_handle.DangerousAddRef(ref _releaseRef);
SetHandle(_handle.DangerousGetHandle());
}

internal SafeProcessHandle(int processId, SafeWaitHandle handle) :
this(handle.DangerousGetHandle(), ownsHandle: true)
/// <summary>
/// Gets the process ID.
/// </summary>
public int ProcessId
{
get
{
Validate();

if (_waitStateHolder is null)
{
throw new InvalidOperationException(SR.InvalidProcessHandle);
}

return _waitStateHolder._state._processId;
}
}

/// <summary>
/// Sets a value indicating whether the process has been terminated due to timeout or cancellation.
/// </summary>
private bool Canceled
{
ProcessId = processId;
_handle = handle;
handle.DangerousAddRef(ref _releaseRef);
set => GetWaitState()._canceled = value;
}

protected override bool ReleaseHandle()
Expand All @@ -53,12 +71,10 @@ protected override bool ReleaseHandle()
Debug.Assert(_handle != null);
_handle.DangerousRelease();
}
_waitStateHolder?.Dispose();
return true;
}

// On Unix, we don't use process descriptors yet, so we can't get PID.
private static int GetProcessIdCore() => throw new PlatformNotSupportedException();

private bool SignalCore(PosixSignal signal)
{
if (!ProcessUtils.PlatformSupportsProcessStartAndKill)
Expand Down Expand Up @@ -89,20 +105,74 @@ private bool SignalCore(PosixSignal signal)
return true;
}

private delegate SafeProcessHandle StartWithShellExecuteDelegate(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, out ProcessWaitState.Holder? waitStateHolder);
private static StartWithShellExecuteDelegate? s_startWithShellExecute;
private ProcessExitStatus WaitForExitCore()
{
ProcessWaitState waitState = GetWaitState();
waitState.WaitForExit(Timeout.Infinite);

private static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandlesSnapshot = null)
return GetExitStatus(waitState);
}

private bool TryWaitForExitCore(int milliseconds, [NotNullWhen(true)] out ProcessExitStatus? exitStatus)
{
SafeProcessHandle startedProcess = StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandlesSnapshot, out ProcessWaitState.Holder? waitStateHolder);
ProcessWaitState waitState = GetWaitState();
if (!waitState.WaitForExit(milliseconds))
{
exitStatus = null;
return false;
}

// For standalone SafeProcessHandle.Start, we dispose the wait state holder immediately.
// The DangerousAddRef on the SafeWaitHandle (Unix) keeps the handle alive.
waitStateHolder?.Dispose();
exitStatus = GetExitStatus(waitState);
return true;
}

private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds)
{
ProcessWaitState waitState = GetWaitState();

return startedProcess;
if (!waitState.WaitForExit(milliseconds))
{
waitState._canceled = SignalCore(PosixSignal.SIGKILL);
waitState.WaitForExit(Timeout.Infinite);
}

return GetExitStatus(waitState);
}

private ManualResetEvent GetWaitHandle() => GetWaitState().EnsureExitedEvent();

private ProcessWaitState GetWaitState()
{
if (_waitStateHolder is null)
{
throw new InvalidOperationException(SR.InvalidProcessHandle);
Comment thread
adamsitnik marked this conversation as resolved.
}

if (!_waitStateHolder._state._isChild)
{
throw new PlatformNotSupportedException(SR.NotSupportedForNonChildProcess);
}

return _waitStateHolder._state;
}

private ProcessExitStatus GetExitStatus() => GetExitStatus(GetWaitState());

private static ProcessExitStatus GetExitStatus(ProcessWaitState waitState)
{
// GetWaitState ensures the process is a child process, so obtaining the exit status should never fail.
bool exited = waitState.GetExited(out ProcessExitStatus? exitStatus, refresh: false);
Debug.Assert(exited);
Debug.Assert(exitStatus is not null);
return exitStatus ?? throw new InvalidOperationException();
}

private delegate SafeProcessHandle StartWithShellExecuteDelegate(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, out ProcessWaitState.Holder? waitStateHolder);
private static StartWithShellExecuteDelegate? s_startWithShellExecute;

private static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandlesSnapshot = null)
=> StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandlesSnapshot, out _);

internal static SafeProcessHandle StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle,
SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles, out ProcessWaitState.Holder? waitStateHolder)
{
Expand Down Expand Up @@ -292,7 +362,7 @@ private static SafeProcessHandle ForkAndExecProcess(
throw ProcessUtils.CreateExceptionForErrorStartingProcess(new Interop.ErrorInfo(errno).GetErrorMessage(), errno, resolvedFilename, cwd);
}

return new SafeProcessHandle(childPid, waitStateHolder!);
return new SafeProcessHandle(waitStateHolder!);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
Expand All @@ -21,6 +22,30 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali
// by the OS, which terminates all child processes in the job.
private static readonly Lazy<Interop.Kernel32.SafeJobHandle> s_killOnParentExitJob = new(CreateKillOnParentExitJob);

/// <summary>
/// Gets the process ID.
/// </summary>
public int ProcessId
{
get
{
Validate();

if (field == -1)
{
field = Interop.Kernel32.GetProcessId(this);
}

return field;
}
private set;
} = -1;

/// <summary>
/// Gets or sets a value indicating whether the process has been terminated due to timeout or cancellation.
/// </summary>
private bool Canceled { get; set; }

protected override bool ReleaseHandle()
{
return Interop.Kernel32.CloseHandle(handle);
Expand Down Expand Up @@ -622,7 +647,52 @@ private static void AssignJobAndResumeThread(IntPtr hThread, SafeProcessHandle p
}
}

private int GetProcessIdCore() => Interop.Kernel32.GetProcessId(this);
private ProcessExitStatus WaitForExitCore()
{
using Interop.Kernel32.ProcessWaitHandle processWaitHandle = new(this);
processWaitHandle.WaitOne(Timeout.Infinite);

return GetExitStatus();
}

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 = GetExitStatus();
return true;
}

private ProcessExitStatus WaitForExitOrKillOnTimeoutCore(int milliseconds)
{
using Interop.Kernel32.ProcessWaitHandle processWaitHandle = new(this);
if (!processWaitHandle.WaitOne(milliseconds))
{
#pragma warning disable CA1416 // PosixSignal.SIGKILL is supported on Windows via SignalCore
Canceled = SignalCore(PosixSignal.SIGKILL);
#pragma warning restore CA1416
processWaitHandle.WaitOne(Timeout.Infinite);
}

return GetExitStatus();
}

private Interop.Kernel32.ProcessWaitHandle GetWaitHandle() => new(this);

private ProcessExitStatus GetExitStatus()
{
if (!Interop.Kernel32.GetExitCodeProcess(this, out int exitCode))
{
throw new Win32Exception();
}

return new ProcessExitStatus(exitCode, Canceled);
}

private bool SignalCore(PosixSignal signal)
{
Expand Down
Loading
Loading