diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.WaitPid.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.WaitPid.cs index 2f4eae4584c0a9..0474a11b0bde9e 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.WaitPid.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.WaitPid.cs @@ -18,6 +18,6 @@ internal static partial class Sys /// 4) on error, -1 is returned. /// [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); } } diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs index 4fa2522868feae..9116d196197614 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -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 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 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 diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs index 2a290ea098724e..1b3c2e9918e3d6 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs @@ -28,22 +28,40 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali private readonly SafeWaitHandle? _handle; private readonly bool _releaseRef; + private readonly ProcessWaitState.Holder? _waitStateHolder; - 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) + /// + /// Gets the process ID. + /// + public int ProcessId + { + get + { + Validate(); + + if (_waitStateHolder is null) + { + throw new InvalidOperationException(SR.InvalidProcessHandle); + } + + return _waitStateHolder._state._processId; + } + } + + /// + /// Sets a value indicating whether the process has been terminated due to timeout or cancellation. + /// + private bool Canceled { - ProcessId = processId; - _handle = handle; - handle.DangerousAddRef(ref _releaseRef); + set => GetWaitState()._canceled = value; } protected override bool ReleaseHandle() @@ -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) @@ -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); + } + + 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) { @@ -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!); } } } diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs index 29e915a0803c0b..fe53e2c07de417 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.cs @@ -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; @@ -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 s_killOnParentExitJob = new(CreateKillOnParentExitJob); + /// + /// Gets the process ID. + /// + public int ProcessId + { + get + { + Validate(); + + if (field == -1) + { + field = Interop.Kernel32.GetProcessId(this); + } + + return field; + } + private set; + } = -1; + + /// + /// Gets or sets a value indicating whether the process has been terminated due to timeout or cancellation. + /// + private bool Canceled { get; set; } + protected override bool ReleaseHandle() { return Interop.Kernel32.CloseHandle(handle); @@ -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) { diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs index ca28ffe6f17fd5..fb5de007dbe4b5 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.cs @@ -13,10 +13,13 @@ using System; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.Win32.SafeHandles { @@ -29,25 +32,6 @@ public sealed partial class SafeProcessHandle : SafeHandleZeroOrMinusOneIsInvali internal static void EnsureShellExecuteFunc() => s_startWithShellExecute ??= StartWithShellExecute; - /// - /// Gets the process ID. - /// - public int ProcessId - { - get - { - Validate(); - - if (field == -1) - { - field = GetProcessIdCore(); - } - - return field; - } - private set; - } = -1; - /// /// Creates a . /// @@ -178,6 +162,214 @@ public bool Signal(PosixSignal signal) return SignalCore(signal); } + /// + /// Waits indefinitely for the process to exit. + /// + /// The exit status of the process. + /// + /// On Unix, it's impossible to obtain the exit status of a non-child process. + /// + /// The handle is invalid. + /// On Unix, the process is not a child process. + public ProcessExitStatus WaitForExit() + { + Validate(); + + return WaitForExitCore(); + } + + /// + /// Waits for the process to exit within the specified timeout. + /// + /// The maximum time to wait for the process to exit. + /// When this method returns , contains the exit status of the process. + /// if the process exited before the timeout; otherwise, . + /// + /// On Unix, it's impossible to obtain the exit status of a non-child process. + /// + /// The handle is invalid. + /// On Unix, the process is not a child process. + /// is negative and not equal to , + /// or is greater than milliseconds. + public bool TryWaitForExit(TimeSpan timeout, [NotNullWhen(true)] out ProcessExitStatus? exitStatus) + { + Validate(); + + return TryWaitForExitCore(ProcessUtils.ToTimeoutMilliseconds(timeout), out exitStatus); + } + + /// + /// Waits for the process to exit within the specified timeout. + /// If the process does not exit before the timeout, it is killed and then waited for exit. + /// + /// The maximum time to wait for the process to exit before killing it. + /// The exit status of the process. If the process was killed due to timeout, + /// will be . + /// + /// On Unix, it's impossible to obtain the exit status of a non-child process. + /// + /// The handle is invalid. + /// On Unix, the process is not a child process. + /// is negative and not equal to , + /// or is greater than milliseconds. + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public ProcessExitStatus WaitForExitOrKillOnTimeout(TimeSpan timeout) + { + Validate(); + + if (!ProcessUtils.PlatformSupportsProcessStartAndKill) + { + throw new PlatformNotSupportedException(); + } + + return WaitForExitOrKillOnTimeoutCore(ProcessUtils.ToTimeoutMilliseconds(timeout)); + } + + /// + /// Waits asynchronously for the process to exit. + /// + /// A cancellation token that can be used to cancel the wait operation. + /// A task that represents the asynchronous wait operation. The task result contains the exit status of the process. + /// The handle is invalid. + /// The cancellation token was canceled. + /// + /// + /// When the cancellation token is canceled, this method stops waiting and throws . + /// The process is NOT killed and continues running. If you want to kill the process on cancellation, + /// use instead. + /// + /// On Unix, it's impossible to obtain the exit status of a non-child process. + /// + /// On Unix, the process is not a child process. + public async Task WaitForExitAsync(CancellationToken cancellationToken = default) + { + Validate(); + + TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + RegisteredWaitHandle? registeredWaitHandle = null; + CancellationTokenRegistration ctr = default; + + var exitedEvent = GetWaitHandle(); + + try + { + registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject( + exitedEvent, + static (state, timedOut) => ((TaskCompletionSource)state!).TrySetResult(true), + tcs, + Timeout.Infinite, + executeOnlyOnce: true); + + if (cancellationToken.CanBeCanceled) + { + ctr = cancellationToken.UnsafeRegister( + static state => + { + var (taskSource, token) = ((TaskCompletionSource taskSource, CancellationToken token))state!; + taskSource.TrySetCanceled(token); + }, + (tcs, cancellationToken)); + } + + await tcs.Task.ConfigureAwait(false); + } + finally + { + ctr.Dispose(); + registeredWaitHandle?.Unregister(null); + + // On Unix, we don't own the ManualResetEvent. + if (OperatingSystem.IsWindows()) + { + exitedEvent.Dispose(); + } + } + + return GetExitStatus(); + } + + /// + /// Waits asynchronously for the process to exit. + /// When cancelled, kills the process and then waits for it to exit. + /// + /// A cancellation token that can be used to cancel the wait operation and kill the process. + /// A task that represents the asynchronous wait operation. The task result contains the exit status of the process. + /// If the process was killed due to cancellation, will be . + /// The handle is invalid. + /// + /// + /// When the cancellation token is canceled, this method kills the process and waits for it to exit. + /// The returned exit status will have the property set to if the process was killed. + /// If the cancellation token cannot be canceled (e.g., ), this method behaves identically + /// to and will wait indefinitely for the process to exit. + /// + /// On Unix, it's impossible to obtain the exit status of a non-child process. + /// + /// On Unix, the process is not a child process. + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public async Task WaitForExitOrKillOnCancellationAsync(CancellationToken cancellationToken) + { + Validate(); + + if (!ProcessUtils.PlatformSupportsProcessStartAndKill) + { + throw new PlatformNotSupportedException(); + } + + TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + RegisteredWaitHandle? registeredWaitHandle = null; + CancellationTokenRegistration ctr = default; + + var exitedEvent = GetWaitHandle(); + + try + { + registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject( + exitedEvent, + static (state, timedOut) => ((TaskCompletionSource)state!).TrySetResult(true), + tcs, + Timeout.Infinite, + executeOnlyOnce: true); + + if (cancellationToken.CanBeCanceled) + { + ctr = cancellationToken.UnsafeRegister( + static state => + { + var (handle, tcs) = ((SafeProcessHandle, TaskCompletionSource))state!; + try + { + handle.Canceled = handle.SignalCore(PosixSignal.SIGKILL); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + }, + (this, tcs)); + } + + await tcs.Task.ConfigureAwait(false); + } + finally + { + ctr.Dispose(); + registeredWaitHandle?.Unregister(null); + + // On Unix, we don't own the ManualResetEvent. + if (OperatingSystem.IsWindows()) + { + exitedEvent.Dispose(); + } + } + + return GetExitStatus(); + } + private void Validate() { if (IsInvalid) diff --git a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx index 8e504d19fcf803..da63478da67736 100644 --- a/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx @@ -366,4 +366,7 @@ UseShellExecute is not supported by StartAndForget. On Windows, shell execution may not create a new process, which would make it impossible to return a valid process ID. + + On Unix, it's impossible to obtain the exit status of a non-child process. + diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.cs index 8ff5443f4e2e16..5b11679d82c451 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.cs @@ -386,7 +386,7 @@ private void ReadPipesToBuffers( ref int errorBytesRead) { int timeoutMs = timeout.HasValue - ? ToTimeoutMilliseconds(timeout.Value) + ? ProcessUtils.ToTimeoutMilliseconds(timeout.Value) : Timeout.Infinite; var outputHandle = GetSafeHandleFromStreamReader(_standardOutput!); diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs index 6abe345f996f0c..bd7a9df16cb809 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs @@ -223,11 +223,11 @@ public ProcessModule? MainModule /// Checks whether the process has exited and updates state accordingly. private void UpdateHasExited() { - int? exitCode; - _exited = GetWaitState().GetExited(out exitCode, refresh: true); - if (_exited && exitCode != null) + ProcessExitStatus? exitStatus; + _exited = GetWaitState().GetExited(out exitStatus, refresh: true); + if (_exited && exitStatus is not null) { - _exitCode = exitCode.Value; + _exitCode = exitStatus.ExitCode; } } @@ -352,7 +352,10 @@ private SafeProcessHandle GetProcessHandle() } EnsureState(State.HaveNonExitedId | State.IsLocal); - return new SafeProcessHandle(_processId, GetSafeWaitHandle()); + // GetWaitState() ensures _waitStateHolder is initialized. + // IncrementRefCount() creates the Holder reference that is passed to SafeProcessHandle. + GetWaitState(); + return new SafeProcessHandle(_waitStateHolder!.IncrementRefCount()); } private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeFileHandle? stdoutHandle, SafeFileHandle? stderrHandle, SafeHandle[]? inheritedHandles) @@ -360,13 +363,13 @@ private bool StartCore(ProcessStartInfo startInfo, SafeFileHandle? stdinHandle, SafeProcessHandle startedProcess = SafeProcessHandle.StartCore(startInfo, stdinHandle, stdoutHandle, stderrHandle, inheritedHandles, out ProcessWaitState.Holder? waitStateHolder); Debug.Assert(!startedProcess.IsInvalid); - _waitStateHolder = waitStateHolder; + // SafeProcessHandle has its own copy of the wait state holder, so we need to increment the ref count for our copy. + _waitStateHolder = waitStateHolder!.IncrementRefCount(); SetProcessHandle(startedProcess); SetProcessId(startedProcess.ProcessId); return true; } - /// Finalizable holder for the underlying shared wait state object. private ProcessWaitState.Holder? _waitStateHolder; diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index 65b96e440b4061..8966e578389e92 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -749,7 +749,7 @@ public bool WaitForInputIdle(int milliseconds) /// This state is useful, for example, when your application needs to wait for a starting process /// to finish creating its main window before the application communicates with that window. /// - public bool WaitForInputIdle(TimeSpan timeout) => WaitForInputIdle(ToTimeoutMilliseconds(timeout)); + public bool WaitForInputIdle(TimeSpan timeout) => WaitForInputIdle(ProcessUtils.ToTimeoutMilliseconds(timeout)); public ISynchronizeInvoke? SynchronizingObject { get; set; } @@ -1469,17 +1469,7 @@ public bool WaitForExit(int milliseconds) /// Instructs the Process component to wait the specified number of milliseconds for /// the associated process to exit. /// - public bool WaitForExit(TimeSpan timeout) => WaitForExit(ToTimeoutMilliseconds(timeout)); - - private static int ToTimeoutMilliseconds(TimeSpan timeout) - { - long totalMilliseconds = (long)timeout.TotalMilliseconds; - - ArgumentOutOfRangeException.ThrowIfLessThan(totalMilliseconds, -1, nameof(timeout)); - ArgumentOutOfRangeException.ThrowIfGreaterThan(totalMilliseconds, int.MaxValue, nameof(timeout)); - - return (int)totalMilliseconds; - } + public bool WaitForExit(TimeSpan timeout) => WaitForExit(ProcessUtils.ToTimeoutMilliseconds(timeout)); /// /// Instructs the Process component to wait for the associated process to exit, or diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs index 24d53f7fc1391b..a6785d50f93550 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs @@ -46,5 +46,15 @@ internal static Win32Exception CreateExceptionForErrorStartingProcess(string err string msg = SR.Format(SR.ErrorStartingProcess, fileName, directoryForException, errorMessage); return new Win32Exception(errorCode, msg); } + + internal static int ToTimeoutMilliseconds(TimeSpan timeout) + { + long totalMilliseconds = (long)timeout.TotalMilliseconds; + + ArgumentOutOfRangeException.ThrowIfLessThan(totalMilliseconds, -1, nameof(timeout)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(totalMilliseconds, int.MaxValue, nameof(timeout)); + + return (int)totalMilliseconds; + } } } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs index ab1dd7e46bf2f6..90bda5bcf07f1c 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessWaitState.Unix.cs @@ -59,14 +59,18 @@ internal Holder(int processId, bool isNewChild = false, bool usesTerminal = fals _state = ProcessWaitState.AddRef(processId, isNewChild, usesTerminal); } + private Holder(ProcessWaitState source) => _state = source; + + /// Creates an additional holder for the same wait state, incrementing the ref count. + internal Holder IncrementRefCount() + { + _state.IncrementRefCount(); + return new(_state); + } + ~Holder() { - // Don't try to Dispose resources (like ManualResetEvents) if - // the process is shutting down. - if (_state != null && !Environment.HasShutdownStarted) - { - _state.ReleaseRef(); - } + _state?.ReleaseRef(); } public void Dispose() @@ -154,6 +158,16 @@ internal static ProcessWaitState AddRef(int processId, bool isNewChild, bool use } } + /// Increments the ref count for this wait state object. + internal void IncrementRefCount() + { + Dictionary waitStates = _isChild ? s_childProcessWaitStates : s_processWaitStates; + lock (waitStates) + { + _outstandingRefCount++; + } + } + /// /// Decrements the ref count on the wait state object, and if it's the last one, /// removes it from the table. @@ -165,7 +179,6 @@ internal void ReleaseRef() lock (waitStates) { bool foundState = waitStates.TryGetValue(_processId, out pws); - Debug.Assert(foundState); if (foundState) { --_outstandingRefCount; @@ -194,11 +207,13 @@ internal void ReleaseRef() /// private readonly object _gate = new object(); /// ID of the associated process. - private readonly int _processId; + internal readonly int _processId; /// Associated process is a child process. - private readonly bool _isChild; + internal readonly bool _isChild; /// Associated process is a child that can use the terminal. private readonly bool _usesTerminal; + /// A value indicating whether the process has been terminated due to timeout or cancellation. + internal volatile bool _canceled; /// An in-progress or completed wait operation. /// A completed task does not mean the process has exited. @@ -208,8 +223,8 @@ internal void ReleaseRef() /// Whether the associated process exited. private bool _exited; - /// If the process exited, it's exit code, or null if we were unable to determine one. - private int? _exitCode; + /// If the process exited, its exit status, or null if we were unable to determine one. + private ProcessExitStatus? _exitStatus; /// /// The approximate time the process exited. We do not have the ability to know exact time a process /// exited, so we approximate it by storing the time that we discovered it exited. @@ -310,14 +325,14 @@ internal bool HasExited } } - internal bool GetExited(out int? exitCode, bool refresh) + internal bool GetExited(out ProcessExitStatus? exitStatus, bool refresh) { lock (_gate) { // Have we already exited? If so, return the cached results. if (_exited) { - exitCode = _exitCode; + exitStatus = _exitStatus; return true; } @@ -325,7 +340,7 @@ internal bool GetExited(out int? exitCode, bool refresh) // and that task owns the right to call CheckForNonChildExit. if (!_waitInProgress.IsCompleted) { - exitCode = null; + exitStatus = null; return false; } @@ -337,8 +352,8 @@ internal bool GetExited(out int? exitCode, bool refresh) } // We now have an up-to-date snapshot for whether we've exited, - // and if we have, what the exit code is (if we were able to find out). - exitCode = _exitCode; + // and if we have, what the exit status is (if we were able to find out). + exitStatus = _exitStatus; return _exited; } } @@ -539,13 +554,14 @@ private async Task WaitForExitAsync(CancellationToken cancellationToken) } } - private void ChildReaped(int exitCode, bool configureConsole) + private void ChildReaped(int exitCode, int terminatingSignal, bool configureConsole) { lock (_gate) { Debug.Assert(!_exited); - _exitCode = exitCode; + PosixSignal? signal = terminatingSignal != 0 ? (PosixSignal)terminatingSignal : null; + _exitStatus = new ProcessExitStatus(exitCode, canceled: _canceled && signal is PosixSignal.SIGKILL, signal); if (_usesTerminal) { @@ -568,11 +584,12 @@ private bool TryReapChild(bool configureConsole) // Try to get the state of the child process int exitCode; - int waitResult = Interop.Sys.WaitPidExitedNoHang(_processId, out exitCode); + int terminatingSignal; + int waitResult = Interop.Sys.WaitPidExitedNoHang(_processId, out exitCode, out terminatingSignal); if (waitResult == _processId) { - ChildReaped(exitCode, configureConsole); + ChildReaped(exitCode, terminatingSignal, configureConsole); return true; } else if (waitResult == 0) @@ -673,7 +690,8 @@ internal static void CheckChildren(bool reapAll, bool configureConsole) do { int exitCode; - pid = Interop.Sys.WaitPidExitedNoHang(-1, out exitCode); + int terminatingSignal; + pid = Interop.Sys.WaitPidExitedNoHang(-1, out exitCode, out terminatingSignal); if (pid <= 0) { break; @@ -682,7 +700,7 @@ internal static void CheckChildren(bool reapAll, bool configureConsole) // Check if the process is a child that has just terminated. if (s_childProcessWaitStates.TryGetValue(pid, out ProcessWaitState? pws)) { - pws.ChildReaped(exitCode, configureConsole); + pws.ChildReaped(exitCode, terminatingSignal, configureConsole); pws.ReleaseRef(); } } while (true); diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs index 92d2c7bfde2fbe..c49b71627e0a0e 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Windows.cs @@ -108,7 +108,6 @@ public void ProcessStartedWithInvalidHandles_CanRedirectOutput(bool restrictHand private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) { const nint INVALID_HANDLE_VALUE = -1; - const int CREATE_SUSPENDED = 4; // RemoteExector has provided us with the right path and arguments, // we just need to add the terminating null character. @@ -135,7 +134,7 @@ private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) ref unused_SecAttrs, ref unused_SecAttrs, bInheritHandles: false, - CREATE_SUSPENDED | Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT, + Interop.Kernel32.EXTENDED_STARTUPINFO_PRESENT, null, null, &startupInfoEx, @@ -152,28 +151,14 @@ private unsafe int RunWithInvalidHandles(ProcessStartInfo startInfo) { using SafeProcessHandle safeProcessHandle = new(processInfo.hProcess, ownsHandle: true); - // We have started a suspended process, so we can get process by Id before it exits. - // As soon as SafeProcessHandle.WaitForExit* are implemented (#126293), we can use them instead. - using Process process = Process.GetProcessById(processInfo.dwProcessId); - - if (Interop.Kernel32.ResumeThread(processInfo.hThread) == 0xFFFFFFFF) - { - throw new Win32Exception(); - } - try { - process.WaitForExit(WaitInMS); - - // To avoid "Process was not started by this object, so requested information cannot be determined." - // we fetch the exit code directly here. - Assert.True(Interop.Kernel32.GetExitCodeProcess(safeProcessHandle, out int exitCode)); - - return exitCode; + ProcessExitStatus exitStatus = safeProcessHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMilliseconds(WaitInMS)); + return exitStatus.ExitCode; } finally { - process.Kill(); + safeProcessHandle.Kill(); } } finally diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs index 22db2706974389..a0a8ba46fb95c8 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.RemoteExecutor; +using Microsoft.Win32.SafeHandles; using Xunit; namespace System.Diagnostics.Tests @@ -671,5 +672,63 @@ public async Task WaitForExitAsync_NotDirected_ThrowsInvalidOperationException() var process = new Process(); await Assert.ThrowsAsync(() => process.WaitForExitAsync()); } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void ProcessSafeHandle_WaitForExit_ReturnsExitCode() + { + Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode); + process.Start(); + + ProcessExitStatus exitStatus = process.SafeHandle.WaitForExit(); + + Assert.Equal(RemoteExecutor.SuccessExitCode, exitStatus.ExitCode); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void ProcessSafeHandle_TryWaitForExit_ReturnsExitCode() + { + Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode); + process.Start(); + + bool exited = process.SafeHandle.TryWaitForExit(TimeSpan.FromMilliseconds(WaitInMS), out ProcessExitStatus? exitStatus); + + Assert.True(exited); + Assert.NotNull(exitStatus); + Assert.Equal(RemoteExecutor.SuccessExitCode, exitStatus.ExitCode); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public async Task ProcessSafeHandle_WaitForExitAsync_ReturnsExitCode() + { + Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode); + process.Start(); + + ProcessExitStatus exitStatus = await process.SafeHandle.WaitForExitAsync(); + + Assert.Equal(RemoteExecutor.SuccessExitCode, exitStatus.ExitCode); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void ProcessSafeHandle_WaitForExitOrKillOnTimeout_KillsOnTimeout() + { + Process process = CreateProcessLong(); + process.Start(); + + ProcessExitStatus exitStatus = process.SafeHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMilliseconds(0)); + + Assert.True(exitStatus.Canceled); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public async Task ProcessSafeHandle_WaitForExitOrKillOnCancellationAsync_KillsOnCancellation() + { + Process process = CreateProcessLong(); + process.Start(); + + using CancellationTokenSource cts = new CancellationTokenSource(0); + ProcessExitStatus exitStatus = await process.SafeHandle.WaitForExitOrKillOnCancellationAsync(cts.Token); + + Assert.True(exitStatus.Canceled); + } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs index 8a0e2394c03a6e..a9dacea578c48f 100644 --- a/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/SafeProcessHandleTests.cs @@ -5,6 +5,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; using Microsoft.DotNet.RemoteExecutor; using Microsoft.Win32.SafeHandles; using Xunit; @@ -44,25 +45,16 @@ public void CanStartProcess() using StreamReader streamReader = new(new FileStream(outputReadPipe, FileAccess.Read, bufferSize: 1, outputReadPipe.IsAsync)); Assert.Equal("ping", streamReader.ReadLine()); - // We can get the process by id only when it's still running, - // so we wait with "pong" until we obtain the process instance. - // When we introduce SafeProcessHandle.WaitForExit* APIs, it's not needed. - using Process fetchedProcess = Process.GetProcessById(processHandle.ProcessId); - using StreamWriter streamWriter = new(new FileStream(inputWritePipe, FileAccess.Write, bufferSize: 1, inputWritePipe.IsAsync)) { AutoFlush = true }; - try - { - streamWriter.WriteLine("pong"); - } - finally - { - fetchedProcess.Kill(); - fetchedProcess.WaitForExit(); - } + streamWriter.WriteLine("pong"); + + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMilliseconds(WaitInMS)); + Assert.Equal(RemoteExecutor.SuccessExitCode, exitStatus.ExitCode); + Assert.False(exitStatus.Canceled); } } @@ -119,6 +111,34 @@ public void Signal_InvalidHandle_ThrowsInvalidOperationException() Assert.Throws(() => invalidHandle.Signal(PosixSignal.SIGKILL)); } + [Fact] + public void WaitForExit_InvalidHandle_ThrowsInvalidOperationException() + { + using SafeProcessHandle invalidHandle = new SafeProcessHandle(); + Assert.Throws(() => invalidHandle.WaitForExit()); + } + + [Fact] + public void TryWaitForExit_InvalidHandle_ThrowsInvalidOperationException() + { + using SafeProcessHandle invalidHandle = new SafeProcessHandle(); + Assert.Throws(() => invalidHandle.TryWaitForExit(TimeSpan.Zero, out _)); + } + + [Fact] + public void WaitForExitOrKillOnTimeout_InvalidHandle_ThrowsInvalidOperationException() + { + using SafeProcessHandle invalidHandle = new SafeProcessHandle(); + Assert.Throws(() => invalidHandle.WaitForExitOrKillOnTimeout(TimeSpan.Zero)); + } + + [Fact] + public async Task WaitForExitAsync_InvalidHandle_ThrowsInvalidOperationException() + { + using SafeProcessHandle invalidHandle = new SafeProcessHandle(); + await Assert.ThrowsAsync(() => invalidHandle.WaitForExitAsync()); + } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public void Kill_RunningProcess_Terminates() { @@ -129,11 +149,11 @@ public void Kill_RunningProcess_Terminates() }); using SafeProcessHandle processHandle = SafeProcessHandle.Start(process.StartInfo); - using Process fetchedProcess = Process.GetProcessById(processHandle.ProcessId); processHandle.Kill(); - Assert.True(fetchedProcess.WaitForExit(WaitInMS)); + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(30)); + Assert.NotEqual(0, exitStatus.ExitCode); } [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] @@ -249,5 +269,248 @@ public void Kill_HandleWithoutTerminatePermission_ThrowsWin32Exception() process.WaitForExit(); } } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public async Task WaitForExit_ProcessExitsNormally_ReturnsExitCode(bool useAsync) + { + Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode); + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(process.StartInfo); + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + + ProcessExitStatus exitStatus = useAsync + ? await processHandle.WaitForExitAsync(cts.Token) + : processHandle.WaitForExit(); + + Assert.Equal(RemoteExecutor.SuccessExitCode, exitStatus.ExitCode); + Assert.False(exitStatus.Canceled); + Assert.Null(exitStatus.Signal); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void TryWaitForExit_ProcessExitsBeforeTimeout_ReturnsTrue() + { + Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode); + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(process.StartInfo); + + bool exited = processHandle.TryWaitForExit(TimeSpan.FromSeconds(30), out ProcessExitStatus? exitStatus); + + Assert.True(exited); + Assert.NotNull(exitStatus); + Assert.Equal(RemoteExecutor.SuccessExitCode, exitStatus.ExitCode); + Assert.False(exitStatus.Canceled); + Assert.Null(exitStatus.Signal); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void TryWaitForExit_ProcessDoesNotExitBeforeTimeout_ReturnsFalse() + { + Process process = CreateProcess(static () => + { + Thread.Sleep(Timeout.Infinite); + return RemoteExecutor.SuccessExitCode; + }); + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(process.StartInfo); + + try + { + Stopwatch stopwatch = Stopwatch.StartNew(); + bool exited = processHandle.TryWaitForExit(TimeSpan.FromMilliseconds(300), out ProcessExitStatus? exitStatus); + stopwatch.Stop(); + + Assert.False(exited); + Assert.Null(exitStatus); + Assert.InRange(stopwatch.Elapsed, TimeSpan.FromMilliseconds(200), TimeSpan.FromMilliseconds(5000)); + } + finally + { + processHandle.Kill(); + processHandle.WaitForExit(); + } + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public async Task WaitForExitOrKill_ProcessExitsBeforeTimeout_DoesNotKill(bool useAsync) + { + Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode); + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(process.StartInfo); + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); + + ProcessExitStatus exitStatus = useAsync + ? await processHandle.WaitForExitOrKillOnCancellationAsync(cts.Token) + : processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(30)); + + Assert.Equal(RemoteExecutor.SuccessExitCode, exitStatus.ExitCode); + Assert.False(exitStatus.Canceled); + Assert.Null(exitStatus.Signal); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public async Task WaitForExitOrKill_ProcessDoesNotExit_KillsAndReturns(bool useAsync) + { + Process process = CreateProcess(static () => + { + Thread.Sleep(Timeout.Infinite); + return RemoteExecutor.SuccessExitCode; + }); + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(process.StartInfo); + + Stopwatch stopwatch = Stopwatch.StartNew(); + using CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(300)); + + ProcessExitStatus exitStatus = useAsync + ? await processHandle.WaitForExitOrKillOnCancellationAsync(cts.Token) + : processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromMilliseconds(300)); + stopwatch.Stop(); + + Assert.InRange(stopwatch.Elapsed, TimeSpan.FromMilliseconds(200), TimeSpan.FromSeconds(10)); + Assert.True(exitStatus.Canceled); + Assert.NotEqual(0, exitStatus.ExitCode); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public async Task WaitForExitAsync_WithoutCancellationToken_CompletesNormally() + { + Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode); + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(process.StartInfo); + + ProcessExitStatus exitStatus = await processHandle.WaitForExitAsync(); + + Assert.Equal(RemoteExecutor.SuccessExitCode, exitStatus.ExitCode); + Assert.False(exitStatus.Canceled); + Assert.Null(exitStatus.Signal); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public async Task WaitForExitAsync_CancellationRequested_ThrowsOperationCanceledException() + { + Process process = CreateProcess(static () => + { + Thread.Sleep(Timeout.Infinite); + return RemoteExecutor.SuccessExitCode; + }); + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(process.StartInfo); + + try + { + using CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(300)); + + await Assert.ThrowsAnyAsync( + async () => await processHandle.WaitForExitAsync(cts.Token)); + } + finally + { + processHandle.Kill(); + processHandle.WaitForExit(); + } + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void WaitForExit_CalledAfterKill_ReturnsImmediately() + { + Process process = CreateProcess(static () => + { + Thread.Sleep(Timeout.Infinite); + return RemoteExecutor.SuccessExitCode; + }); + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(process.StartInfo); + processHandle.Kill(); + + Stopwatch stopwatch = Stopwatch.StartNew(); + ProcessExitStatus exitStatus = processHandle.WaitForExitOrKillOnTimeout(TimeSpan.FromSeconds(5)); + stopwatch.Stop(); + + Assert.InRange(stopwatch.Elapsed, TimeSpan.Zero, TimeSpan.FromSeconds(5)); + Assert.NotEqual(RemoteExecutor.SuccessExitCode, exitStatus.ExitCode); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [SkipOnPlatform(TestPlatforms.Windows, "Signal property is Unix-specific")] + [InlineData(PosixSignal.SIGKILL)] + [InlineData(PosixSignal.SIGTERM)] + public void WaitForExit_ProcessKilledBySignal_ReportsSignal(PosixSignal signal) + { + Process process = CreateProcess(static () => + { + Thread.Sleep(Timeout.Infinite); + return RemoteExecutor.SuccessExitCode; + }); + + using SafeProcessHandle processHandle = SafeProcessHandle.Start(process.StartInfo); + processHandle.Signal(signal); + + ProcessExitStatus exitStatus = processHandle.WaitForExit(); + + Assert.NotNull(exitStatus.Signal); + Assert.Equal(signal, exitStatus.Signal); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public async Task WaitForExit_NonChildProcess_NotSupportedOnUnix() + { + RemoteInvokeOptions remoteInvokeOptions = new() { CheckExitCode = false }; + remoteInvokeOptions.StartInfo.RedirectStandardOutput = true; + remoteInvokeOptions.StartInfo.RedirectStandardInput = true; + + using RemoteInvokeHandle childHandle = RemoteExecutor.Invoke( + () => + { + using Process grandChild = CreateProcessLong(); + grandChild.Start(); + + Console.WriteLine(grandChild.Id); + + // Keep it alive to avoid any kind of re-parenting on Unix + _ = Console.ReadLine(); + + return RemoteExecutor.SuccessExitCode; + }, remoteInvokeOptions); + + int grandChildPid = int.Parse(childHandle.Process.StandardOutput.ReadLine()); + + // Obtain a Process instance before the child exits to avoid PID reuse issues. + using Process grandchild = Process.GetProcessById(grandChildPid); + + try + { + await Verify(grandchild.SafeHandle); + } + finally + { + grandchild.Kill(); + childHandle.Process.Kill(); + } + + static async Task Verify(SafeProcessHandle handle) + { + if (OperatingSystem.IsWindows()) + { + Assert.False(handle.TryWaitForExit(TimeSpan.Zero, out _)); + + handle.Kill(); + ProcessExitStatus processExitStatus = await handle.WaitForExitAsync(); + Assert.Equal(-1, processExitStatus.ExitCode); + } + else + { + Assert.Throws(() => handle.WaitForExit()); + Assert.Throws(() => handle.TryWaitForExit(TimeSpan.FromSeconds(1), out _)); + await Assert.ThrowsAsync(async () => await handle.WaitForExitAsync()); + } + } + } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj index ee26045c6cbf20..a9332ad98c999f 100644 --- a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj +++ b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj @@ -74,8 +74,6 @@ Link="Common\Interop\Windows\Kernel32\Interop.SetConsoleCtrlHandler.cs" /> - diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 82c00925143113..bc2a713849c1b7 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -3,6 +3,7 @@ #include "pal_config.h" #include "pal_process.h" +#include "pal_signal.h" #include "pal_io.h" #include "pal_utilities.h" @@ -929,10 +930,13 @@ int32_t SystemNative_WaitIdAnyExitedNoHangNoWait(void) return result; } -int32_t SystemNative_WaitPidExitedNoHang(int32_t pid, int32_t* exitCode) +int32_t SystemNative_WaitPidExitedNoHang(int32_t pid, int32_t* exitCode, int32_t* terminatingSignal) { assert(exitCode != NULL); + assert(terminatingSignal != NULL); + *exitCode = 0; + *terminatingSignal = 0; int32_t result; int status; while (CheckInterrupted(result = waitpid(pid, &status, WNOHANG))); @@ -942,11 +946,16 @@ int32_t SystemNative_WaitPidExitedNoHang(int32_t pid, int32_t* exitCode) { // the child terminated normally. *exitCode = WEXITSTATUS(status); + *terminatingSignal = 0; } else if (WIFSIGNALED(status)) { // child process was terminated by a signal. - *exitCode = 128 + WTERMSIG(status); + int sig = WTERMSIG(status); + *exitCode = 128 + sig; + PosixSignal posixSignal = PosixSignalInvalid; + TryConvertSignalCodeToPosixSignal(sig, &posixSignal); + *terminatingSignal = (int32_t)posixSignal; } else { diff --git a/src/native/libs/System.Native/pal_process.h b/src/native/libs/System.Native/pal_process.h index 00bddcd17d78f4..abcc7d87439cd7 100644 --- a/src/native/libs/System.Native/pal_process.h +++ b/src/native/libs/System.Native/pal_process.h @@ -186,8 +186,12 @@ PALEXPORT int32_t SystemNative_WaitIdAnyExitedNoHangNoWait(void); * 2) if pid is not a child or there are no unwaited-for children, -1 is returned (errno=ECHILD) * 3) if the child has not yet terminated, 0 is returned * 4) on error, -1 is returned. + * + * exitCode: set to WEXITSTATUS on normal exit, or 128 + signal number on signal termination. + * terminatingSignal: set to 0 on normal exit, or the PosixSignal value on signal termination. + * For signals not in the known PosixSignal set, this is the raw signal number. */ -PALEXPORT int32_t SystemNative_WaitPidExitedNoHang(int32_t pid, int32_t* exitCode); +PALEXPORT int32_t SystemNative_WaitPidExitedNoHang(int32_t pid, int32_t* exitCode, int32_t* terminatingSignal); /** * Gets the configurable limit or variable for system path or file descriptor options. diff --git a/src/native/libs/System.Native/pal_process_wasi.c b/src/native/libs/System.Native/pal_process_wasi.c index ff32ba0a39bba5..1ad9f888bbc04c 100644 --- a/src/native/libs/System.Native/pal_process_wasi.c +++ b/src/native/libs/System.Native/pal_process_wasi.c @@ -76,7 +76,7 @@ int32_t SystemNative_WaitIdAnyExitedNoHangNoWait(void) return -1; } -int32_t SystemNative_WaitPidExitedNoHang(int32_t pid, int32_t* exitCode) +int32_t SystemNative_WaitPidExitedNoHang(int32_t pid, int32_t* exitCode, int32_t* terminatingSignal) { return -1; } diff --git a/src/native/libs/System.Native/pal_signal.c b/src/native/libs/System.Native/pal_signal.c index 18254d590d9fe9..3e02b2466fc65d 100644 --- a/src/native/libs/System.Native/pal_signal.c +++ b/src/native/libs/System.Native/pal_signal.c @@ -83,7 +83,7 @@ static bool IsSigIgn(struct sigaction* action) action->sa_handler == SIG_IGN; } -static bool TryConvertSignalCodeToPosixSignal(int signalCode, PosixSignal* posixSignal) +bool TryConvertSignalCodeToPosixSignal(int signalCode, PosixSignal* posixSignal) { assert(posixSignal != NULL); diff --git a/src/native/libs/System.Native/pal_signal.h b/src/native/libs/System.Native/pal_signal.h index 074c77315f5edc..801251746031c9 100644 --- a/src/native/libs/System.Native/pal_signal.h +++ b/src/native/libs/System.Native/pal_signal.h @@ -83,6 +83,14 @@ PALEXPORT void SystemNative_DisablePosixSignalHandling(int signalCode); */ PALEXPORT void SystemNative_HandleNonCanceledPosixSignal(int32_t signalCode); +/** + * Converts a native signal code to PosixSignal. + * + * Returns true if the signal code was mapped to a known PosixSignal. + * Returns false if the signal is not in the known set (posixSignal is set to the raw signal code). + */ +bool TryConvertSignalCodeToPosixSignal(int signalCode, PosixSignal* posixSignal); + typedef void (*ConsoleSigTtouHandler)(void); /**