1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System.Collections.Generic;
6 using System.Runtime.InteropServices;
7 using System.Threading;
8 using System.Threading.Tasks;
10 namespace System.Diagnostics
14 // We have a few constraints we're working under here:
15 // - waitid is used on Unix to get the exit status (including exit code) of a child process, but once a child
16 // process is reaped, it is no longer possible to get the status.
17 // - The Process design allows for multiple independent Process objects to be handed out, and each of those
18 // objects may be used concurrently with each other, even if they refer to the same underlying process.
19 // Same with ProcessWaitHandle objects. This is based on the Windows design where anyone with a handle to the
20 // process can retrieve completion information about that process.
21 // - There is no good Unix equivalent to asynchronously be notified of a non-child process' exit, which means such
22 // support needs to be layered on top of kill.
24 // As a result, we have the following scheme:
25 // - We maintain a static/shared table that maps process ID to ProcessWaitState objects.
26 // Access to this table requires taking a global lock, so we try to minimize the number of
27 // times we need to access the table, primarily just the first time a Process object needs
28 // access to process exit/wait information and subsequently when that Process object gets GC'd.
29 // - Each process holds a ProcessWaitState.Holder object; when that object is constructed,
30 // it ensures there's an appropriate entry in the mapping table and increments that entry's ref count.
31 // - When a Process object is dropped and its ProcessWaitState.Holder is finalized, it'll
32 // decrement the ref count, and when no more process objects exist for a particular process ID,
33 // that entry in the table will be cleaned up.
34 // - This approach effectively allows for multiple independent Process objects for the same process ID to all
35 // share the same ProcessWaitState. And since they are sharing the same wait state object,
36 // the wait state object uses its own lock to protect the per-process state. This includes
37 // caching exit / exit code / exit time information so that a Process object for a process that's already
38 // had waitpid called for it can get at its exit information.
39 // - When we detect a recycled pid, we remove that ProcessWaitState from the table and replace it with a new one
40 // that represents the new process. For child processes we know a pid is recycled when we see the pid of a new
41 // child is already in the table. For non-child processes, we assume that a pid may be recycled as soon as
42 // we've observed it has exited.
44 /// <summary>Exit information and waiting capabilities for a process.</summary>
45 internal sealed class ProcessWaitState : IDisposable
48 /// Finalizable holder for a process wait state. Instantiating one
49 /// will ensure that a wait state object exists for a process, will
50 /// grab it, and will increment its ref count. Dropping or disposing
51 /// one will decrement the ref count and clean up after it if the ref
54 internal sealed class Holder : IDisposable
56 internal ProcessWaitState _state;
58 internal Holder(int processId, bool isNewChild = false, bool usesTerminal = false)
60 _state = ProcessWaitState.AddRef(processId, isNewChild, usesTerminal);
65 // Don't try to Dispose resources (like ManualResetEvents) if
66 // the process is shutting down.
67 if (_state != null && !Environment.HasShutdownStarted)
77 GC.SuppressFinalize(this);
85 /// Global table that maps process IDs of non-child Processes to the associated shared wait state information.
87 private static readonly Dictionary<int, ProcessWaitState> s_processWaitStates =
88 new Dictionary<int, ProcessWaitState>();
91 /// Global table that maps process IDs of child Processes to the associated shared wait state information.
93 private static readonly Dictionary<int, ProcessWaitState> s_childProcessWaitStates =
94 new Dictionary<int, ProcessWaitState>();
97 /// Ensures that the mapping table contains an entry for the process ID,
98 /// increments its ref count, and returns it.
100 /// <param name="processId">The process ID for which we need wait state.</param>
101 /// <returns>The wait state object.</returns>
102 internal static ProcessWaitState AddRef(int processId, bool isNewChild, bool usesTerminal)
104 lock (s_childProcessWaitStates)
106 ProcessWaitState pws;
109 // When the PID is recycled for a new child, we remove the old child.
110 s_childProcessWaitStates.Remove(processId);
112 pws = new ProcessWaitState(processId, isChild: true, usesTerminal);
113 s_childProcessWaitStates.Add(processId, pws);
114 pws._outstandingRefCount++; // For Holder
115 pws._outstandingRefCount++; // Decremented in CheckChildren
119 lock (s_processWaitStates)
121 DateTime exitTime = default;
122 // We are referencing an existing process.
123 // This may be a child process, so we check s_childProcessWaitStates too.
124 if (s_childProcessWaitStates.TryGetValue(processId, out pws))
128 else if (s_processWaitStates.TryGetValue(processId, out pws))
130 // This is best effort for dealing with recycled pids for non-child processes.
131 // As long as we haven't observed process exit, it's safe to share the ProcessWaitState.
132 // Once we've observed the exit, we'll create a new ProcessWaitState just in case
133 // this may be a recycled pid.
134 // If it wasn't, that ProcessWaitState will observe too that the process has exited.
135 // We pass the ExitTime so it can be the same, but we'll clear it when we see there
136 // is a live process with that pid.
137 if (pws.GetExited(out _, refresh: false))
139 s_processWaitStates.Remove(processId);
140 exitTime = pws.ExitTime;
146 pws = new ProcessWaitState(processId, isChild: false, usesTerminal: false, exitTime);
147 s_processWaitStates.Add(processId, pws);
149 pws._outstandingRefCount++;
157 /// Decrements the ref count on the wait state object, and if it's the last one,
158 /// removes it from the table.
160 internal void ReleaseRef()
162 ProcessWaitState pws;
163 Dictionary<int, ProcessWaitState> waitStates = _isChild ? s_childProcessWaitStates : s_processWaitStates;
166 bool foundState = waitStates.TryGetValue(_processId, out pws);
167 Debug.Assert(foundState);
170 --_outstandingRefCount;
171 if (_outstandingRefCount == 0)
173 // The dictionary may contain a different ProcessWaitState if the pid was recycled.
176 waitStates.Remove(_processId);
190 /// Synchronization object used to protect all instance state. Any number of
191 /// Process and ProcessWaitHandle objects may be using a ProcessWaitState
192 /// instance concurrently.
194 private readonly object _gate = new object();
195 /// <summary>ID of the associated process.</summary>
196 private readonly int _processId;
197 /// <summary>Associated process is a child process.</summary>
198 private readonly bool _isChild;
199 /// <summary>Associated process is a child that can use the terminal.</summary>
200 private readonly bool _usesTerminal;
202 /// <summary>If a wait operation is in progress, the Task that represents it; otherwise, null.</summary>
203 private Task _waitInProgress;
204 /// <summary>The number of alive users of this object.</summary>
205 private int _outstandingRefCount;
207 /// <summary>Whether the associated process exited.</summary>
208 private bool _exited;
209 /// <summary>If the process exited, it's exit code, or null if we were unable to determine one.</summary>
210 private int? _exitCode;
212 /// The approximate time the process exited. We do not have the ability to know exact time a process
213 /// exited, so we approximate it by storing the time that we discovered it exited.
215 private DateTime _exitTime;
216 /// <summary>A lazily-initialized event set when the process exits.</summary>
217 private ManualResetEvent _exitedEvent;
219 /// <summary>Initialize the wait state object.</summary>
220 /// <param name="processId">The associated process' ID.</param>
221 private ProcessWaitState(int processId, bool isChild, bool usesTerminal, DateTime exitTime = default)
223 Debug.Assert(processId >= 0);
224 _processId = processId;
226 _usesTerminal = usesTerminal;
227 _exitTime = exitTime;
230 /// <summary>Releases managed resources used by the ProcessWaitState.</summary>
231 public void Dispose()
233 Debug.Assert(!Monitor.IsEntered(_gate));
237 if (_exitedEvent != null)
239 _exitedEvent.Dispose();
245 /// <summary>Notes that the process has exited.</summary>
246 private void SetExited()
248 Debug.Assert(Monitor.IsEntered(_gate));
251 if (_exitTime == default)
253 _exitTime = DateTime.Now;
258 /// <summary>Ensures an exited event has been initialized and returns it.</summary>
259 /// <returns></returns>
260 internal ManualResetEvent EnsureExitedEvent()
262 Debug.Assert(!Monitor.IsEntered(_gate));
266 // If we already have an initialized event, just return it.
267 if (_exitedEvent == null)
269 // If we don't, create one, and if the process hasn't yet exited,
270 // make sure we have a task that's actively monitoring the completion state.
271 _exitedEvent = new ManualResetEvent(initialState: _exited);
276 // If we haven't exited, we need to spin up an asynchronous operation that
277 // will completed the exitedEvent when the other process exits. If there's already
278 // another operation underway, then we'll just tack ours onto the end of it.
279 _waitInProgress = _waitInProgress == null ?
281 _waitInProgress.ContinueWith((_, state) => ((ProcessWaitState)state).WaitForExitAsync(),
282 this, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default).Unwrap();
290 internal DateTime ExitTime
296 Debug.Assert(_exited);
302 internal bool HasExited
307 return GetExited(out ignored, refresh: true);
311 internal bool GetExited(out int? exitCode, bool refresh)
315 // Have we already exited? If so, return the cached results.
318 exitCode = _exitCode;
322 // Is another wait operation in progress? If so, then we haven't exited,
323 // and that task owns the right to call CheckForNonChildExit.
324 if (_waitInProgress != null)
332 // We don't know if we've exited, but no one else is currently
333 // checking, so check.
334 CheckForNonChildExit();
337 // We now have an up-to-date snapshot for whether we've exited,
338 // and if we have, what the exit code is (if we were able to find out).
339 exitCode = _exitCode;
344 private void CheckForNonChildExit()
346 Debug.Assert(Monitor.IsEntered(_gate));
350 // We won't be able to get an exit code, but we'll at least be able to determine if the process is
352 int killResult = Interop.Sys.Kill(_processId, Interop.Sys.Signals.None); // None means don't send a signal
355 // Process is still running. This could also be a defunct process that has completed
356 // its work but still has an entry in the processes table due to its parent not yet
357 // having waited on it to clean it up.
360 else // error from kill
362 Interop.Error errno = Interop.Sys.GetLastError();
363 if (errno == Interop.Error.ESRCH)
365 // Couldn't find the process; assume it's exited
368 else if (errno == Interop.Error.EPERM)
370 // Don't have permissions to the process; assume it's alive
375 Debug.Fail("Unexpected errno value from kill");
390 /// <summary>Waits for the associated process to exit.</summary>
391 /// <param name="millisecondsTimeout">The amount of time to wait, or -1 to wait indefinitely.</param>
392 /// <returns>true if the process exited; false if the timeout occurred.</returns>
393 internal bool WaitForExit(int millisecondsTimeout)
395 Debug.Assert(!Monitor.IsEntered(_gate));
401 // If we already know that the process exited, we're done.
407 ManualResetEvent exitEvent = EnsureExitedEvent();
408 return exitEvent.WaitOne(millisecondsTimeout);
412 // Track the time the we start waiting.
413 long startTime = Stopwatch.GetTimestamp();
418 bool createdTask = false;
419 CancellationTokenSource cts = null;
422 // We're in a polling loop... determine how much time remains
423 int remainingTimeout = millisecondsTimeout == Timeout.Infinite ?
425 (int)Math.Max(millisecondsTimeout - ((Stopwatch.GetTimestamp() - startTime) / (double)Stopwatch.Frequency * 1000), 0);
429 // If we already know that the process exited, we're done.
435 // If a timeout of 0 was supplied, then we simply need to poll
436 // to see if the process has already exited.
437 if (remainingTimeout == 0)
439 // If there's currently a wait-in-progress, then we know the other process
440 // hasn't exited (barring races and the polling interval).
441 if (_waitInProgress != null)
446 // No one else is checking for the process' exit... so check.
447 // We're currently holding the _gate lock, so we don't want to
448 // allow CheckForNonChildExit to block indefinitely.
449 CheckForNonChildExit();
453 // The process has not yet exited (or at least we don't know it yet)
454 // so we need to wait for it to exit, outside of the lock.
455 // If there's already a wait in progress, we'll do so later
456 // by waiting on that existing task. Otherwise, we'll spin up
458 if (_waitInProgress != null)
460 waitTask = _waitInProgress;
465 CancellationToken token = remainingTimeout == Timeout.Infinite ?
466 CancellationToken.None :
467 (cts = new CancellationTokenSource(remainingTimeout)).Token;
468 waitTask = WaitForExitAsync(token);
474 // We created this task, and it'll get canceled automatically after our timeout.
475 // This Wait should only wake up when either the process has exited or the timeout
476 // has expired. Either way, we'll loop around again; if the process exited, that'll
477 // be caught first thing in the loop where we check _exited, and if it didn't exit,
478 // our remaining time will be zero, so we'll do a quick remaining check and bail.
484 // It's someone else's task. We'll wait for it to complete. This could complete
485 // either because our remainingTimeout expired or because the task completed,
486 // which could happen because the process exited or because whoever created
487 // that task gave it a timeout. In any case, we'll loop around again, and the loop
488 // will catch these cases, potentially issuing another wait to make up any
490 waitTask.Wait(remainingTimeout);
496 /// <summary>Spawns an asynchronous polling loop for process completion.</summary>
497 /// <param name="cancellationToken">A token to monitor to exit the polling loop.</param>
498 /// <returns>The task representing the loop.</returns>
499 private Task WaitForExitAsync(CancellationToken cancellationToken = default(CancellationToken))
501 Debug.Assert(Monitor.IsEntered(_gate));
502 Debug.Assert(_waitInProgress == null);
503 Debug.Assert(!_isChild);
505 return _waitInProgress = Task.Run(async delegate // Task.Run used because of potential blocking in CheckForNonChildExit
507 // Arbitrary values chosen to balance delays with polling overhead. Start with fast polling
508 // to handle quickly completing processes, but fall back to longer polling to minimize
509 // overhead for those that take longer to complete.
510 const int StartingPollingIntervalMs = 1, MaxPollingIntervalMs = 100;
511 int pollingIntervalMs = StartingPollingIntervalMs;
515 // While we're not canceled
516 while (!cancellationToken.IsCancellationRequested)
523 CheckForNonChildExit();
525 if (_exited) // may have been updated by CheckForNonChildExit
534 await Task.Delay(pollingIntervalMs, cancellationToken); // no need for ConfigureAwait(false) as we're in a Task.Run
535 pollingIntervalMs = Math.Min(pollingIntervalMs * 2, MaxPollingIntervalMs);
537 catch (OperationCanceledException) { }
542 // Task is no longer active
545 _waitInProgress = null;
551 private bool TryReapChild()
560 // Try to get the state of the child process
562 int waitResult = Interop.Sys.WaitPidExitedNoHang(_processId, out exitCode);
564 if (waitResult == _processId)
566 _exitCode = exitCode;
570 // Update terminal settings before calling SetExited.
571 Process.ConfigureTerminalForChildProcesses(-1);
578 else if (waitResult == 0)
580 // Process is still running
585 int errorCode = Marshal.GetLastWin32Error();
586 Environment.FailFast("Error while reaping child. errno = " + errorCode);
592 internal static void CheckChildren(bool reapAll)
594 // This is called on SIGCHLD from a native thread.
595 // A lock in Process ensures no new processes are spawned while we are checking.
596 lock (s_childProcessWaitStates)
598 bool checkAll = false;
600 // Check terminated processes.
604 // Find a process that terminated without reaping it yet.
605 pid = Interop.Sys.WaitIdAnyExitedNoHangNoWait();
608 if (s_childProcessWaitStates.TryGetValue(pid, out ProcessWaitState pws))
611 if (pws.TryReapChild())
618 // unlikely: This is not a managed Process, so we are not responsible for reaping.
619 // Fall back to checking all Processes.
626 // No more terminated children.
631 int errorCode = Marshal.GetLastWin32Error();
632 Environment.FailFast("Error while checking for terminated children. errno = " + errorCode);
638 // We track things to unref so we don't invalidate our iterator by changing s_childProcessWaitStates.
639 ProcessWaitState firstToRemove = null;
640 List<ProcessWaitState> additionalToRemove = null;
641 foreach (KeyValuePair<int, ProcessWaitState> kv in s_childProcessWaitStates)
643 ProcessWaitState pws = kv.Value;
644 if (pws.TryReapChild())
646 if (firstToRemove == null)
652 if (additionalToRemove == null)
654 additionalToRemove = new List<ProcessWaitState>();
656 additionalToRemove.Add(pws);
661 if (firstToRemove != null)
663 firstToRemove.ReleaseRef();
664 if (additionalToRemove != null)
666 foreach (ProcessWaitState pws in additionalToRemove)
679 pid = Interop.Sys.WaitPidExitedNoHang(-1, out exitCode);