Console: toggle terminal echo based on presence of interactive child processes (dotne...
[platform/upstream/dotnet/runtime.git] / src / libraries / System.Diagnostics.Process / src / System / Diagnostics / ProcessWaitState.Unix.cs
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.
4
5 using System.Collections.Generic;
6 using System.Runtime.InteropServices;
7 using System.Threading;
8 using System.Threading.Tasks;
9
10 namespace System.Diagnostics
11 {
12     // Overview
13     // --------
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.
23     // 
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.
43
44     /// <summary>Exit information and waiting capabilities for a process.</summary>
45     internal sealed class ProcessWaitState : IDisposable
46     {
47         /// <summary>
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
52         /// count hits zero.
53         /// </summary>
54         internal sealed class Holder : IDisposable
55         {
56             internal ProcessWaitState _state;
57
58             internal Holder(int processId, bool isNewChild = false, bool usesTerminal = false)
59             {
60                 _state = ProcessWaitState.AddRef(processId, isNewChild, usesTerminal);
61             }
62
63             ~Holder()
64             {
65                 // Don't try to Dispose resources (like ManualResetEvents) if 
66                 // the process is shutting down.
67                 if (_state != null && !Environment.HasShutdownStarted)
68                 {
69                     _state.ReleaseRef();
70                 }
71             }
72
73             public void Dispose()
74             {
75                 if (_state != null)
76                 {
77                     GC.SuppressFinalize(this);
78                     _state.ReleaseRef();
79                     _state = null;
80                 }
81             }
82         }
83
84         /// <summary>
85         /// Global table that maps process IDs of non-child Processes to the associated shared wait state information.
86         /// </summary>
87         private static readonly Dictionary<int, ProcessWaitState> s_processWaitStates =
88             new Dictionary<int, ProcessWaitState>();
89
90         /// <summary>
91         /// Global table that maps process IDs of child Processes to the associated shared wait state information.
92         /// </summary>
93         private static readonly Dictionary<int, ProcessWaitState> s_childProcessWaitStates =
94             new Dictionary<int, ProcessWaitState>();
95
96         /// <summary>
97         /// Ensures that the mapping table contains an entry for the process ID,
98         /// increments its ref count, and returns it.
99         /// </summary>
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)
103         {
104             lock (s_childProcessWaitStates)
105             {
106                 ProcessWaitState pws;
107                 if (isNewChild)
108                 {
109                     // When the PID is recycled for a new child, we remove the old child.
110                     s_childProcessWaitStates.Remove(processId);
111
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
116                 }
117                 else
118                 {
119                     lock (s_processWaitStates)
120                     {
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))
125                         {
126                             // child process
127                         }
128                         else if (s_processWaitStates.TryGetValue(processId, out pws))
129                         {
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))
138                             {
139                                 s_processWaitStates.Remove(processId);
140                                 exitTime = pws.ExitTime;
141                                 pws = null;
142                             }
143                         }
144                         if (pws == null)
145                         {
146                             pws = new ProcessWaitState(processId, isChild: false, usesTerminal: false, exitTime);
147                             s_processWaitStates.Add(processId, pws);
148                         }
149                         pws._outstandingRefCount++;
150                     }
151                 }
152                 return pws;
153             }
154         }
155
156         /// <summary>
157         /// Decrements the ref count on the wait state object, and if it's the last one,
158         /// removes it from the table.
159         /// </summary>
160         internal void ReleaseRef()
161         {
162             ProcessWaitState pws;
163             Dictionary<int, ProcessWaitState> waitStates = _isChild ? s_childProcessWaitStates : s_processWaitStates;
164             lock (waitStates)
165             {
166                 bool foundState = waitStates.TryGetValue(_processId, out pws);
167                 Debug.Assert(foundState);
168                 if (foundState)
169                 {
170                     --_outstandingRefCount;
171                     if (_outstandingRefCount == 0)
172                     {
173                         // The dictionary may contain a different ProcessWaitState if the pid was recycled.
174                         if (pws == this)
175                         {
176                             waitStates.Remove(_processId);
177                         }
178                         pws = this;
179                     }
180                     else
181                     {
182                         pws = null;
183                     }
184                 }
185             }
186             pws?.Dispose();
187         }
188
189         /// <summary>
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.
193         /// </summary>
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;
201
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;
206
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;
211         /// <summary>
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.
214         /// </summary>
215         private DateTime _exitTime;
216         /// <summary>A lazily-initialized event set when the process exits.</summary>
217         private ManualResetEvent _exitedEvent;
218
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)
222         {
223             Debug.Assert(processId >= 0);
224             _processId = processId;
225             _isChild = isChild;
226             _usesTerminal = usesTerminal;
227             _exitTime = exitTime;
228         }
229
230         /// <summary>Releases managed resources used by the ProcessWaitState.</summary>
231         public void Dispose()
232         {
233             Debug.Assert(!Monitor.IsEntered(_gate));
234
235             lock (_gate)
236             {
237                 if (_exitedEvent != null)
238                 {
239                     _exitedEvent.Dispose();
240                     _exitedEvent = null;
241                 }
242             }
243         }
244
245         /// <summary>Notes that the process has exited.</summary>
246         private void SetExited()
247         {
248             Debug.Assert(Monitor.IsEntered(_gate));
249
250             _exited = true;
251             if (_exitTime == default)
252             {
253                 _exitTime = DateTime.Now;
254             }
255             _exitedEvent?.Set();
256         }
257
258         /// <summary>Ensures an exited event has been initialized and returns it.</summary>
259         /// <returns></returns>
260         internal ManualResetEvent EnsureExitedEvent()
261         {
262             Debug.Assert(!Monitor.IsEntered(_gate));
263
264             lock (_gate)
265             {
266                 // If we already have an initialized event, just return it.
267                 if (_exitedEvent == null)
268                 {
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);
272                     if (!_exited)
273                     {
274                         if (!_isChild)
275                         {
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 ?
280                                 WaitForExitAsync() :
281                                 _waitInProgress.ContinueWith((_, state) => ((ProcessWaitState)state).WaitForExitAsync(),
282                                     this, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default).Unwrap();
283                         }
284                     }
285                 }
286                 return _exitedEvent;
287             }
288         }
289
290         internal DateTime ExitTime
291         {
292             get
293             {
294                 lock (_gate)
295                 {
296                     Debug.Assert(_exited);
297                     return _exitTime;
298                 }
299             }
300         }
301
302         internal bool HasExited
303         {
304             get
305             {
306                 int? ignored;
307                 return GetExited(out ignored, refresh: true);
308             }
309         }
310
311         internal bool GetExited(out int? exitCode, bool refresh)
312         {
313             lock (_gate)
314             {
315                 // Have we already exited?  If so, return the cached results.
316                 if (_exited)
317                 {
318                     exitCode = _exitCode;
319                     return true;
320                 }
321
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)
325                 {
326                     exitCode = null;
327                     return false;
328                 }
329
330                 if (refresh)
331                 {
332                     // We don't know if we've exited, but no one else is currently
333                     // checking, so check.
334                     CheckForNonChildExit();
335                 }
336
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;
340                 return _exited;
341             }
342         }
343
344         private void CheckForNonChildExit()
345         {
346             Debug.Assert(Monitor.IsEntered(_gate));
347             if (!_isChild)
348             {
349                 bool exited;
350                 // We won't be able to get an exit code, but we'll at least be able to determine if the process is
351                 // still running.
352                 int killResult = Interop.Sys.Kill(_processId, Interop.Sys.Signals.None); // None means don't send a signal
353                 if (killResult == 0)
354                 {
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.
358                     exited = false;
359                 }
360                 else // error from kill
361                 {
362                     Interop.Error errno = Interop.Sys.GetLastError();
363                     if (errno == Interop.Error.ESRCH)
364                     {
365                         // Couldn't find the process; assume it's exited
366                         exited = true;
367                     }
368                     else if (errno == Interop.Error.EPERM)
369                     {
370                         // Don't have permissions to the process; assume it's alive
371                         exited = false;
372                     }
373                     else
374                     {
375                         Debug.Fail("Unexpected errno value from kill");
376                         exited = true;
377                     }
378                 }
379                 if (exited)
380                 {
381                     SetExited();
382                 }
383                 else
384                 {
385                     _exitTime = default;
386                 }
387             }
388         }
389
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)
394         {
395             Debug.Assert(!Monitor.IsEntered(_gate));
396
397             if (_isChild)
398             {
399                 lock (_gate)
400                 {
401                     // If we already know that the process exited, we're done.
402                     if (_exited)
403                     {
404                         return true;
405                     }
406                 }
407                 ManualResetEvent exitEvent = EnsureExitedEvent();
408                 return exitEvent.WaitOne(millisecondsTimeout);
409             }
410             else
411             {
412                 // Track the time the we start waiting.
413                 long startTime = Stopwatch.GetTimestamp();
414
415                 // Polling loop
416                 while (true)
417                 {
418                     bool createdTask = false;
419                     CancellationTokenSource cts = null;
420                     Task waitTask;
421
422                     // We're in a polling loop... determine how much time remains
423                     int remainingTimeout = millisecondsTimeout == Timeout.Infinite ?
424                         Timeout.Infinite :
425                         (int)Math.Max(millisecondsTimeout - ((Stopwatch.GetTimestamp() - startTime) / (double)Stopwatch.Frequency * 1000), 0);
426
427                     lock (_gate)
428                     {
429                         // If we already know that the process exited, we're done.
430                         if (_exited)
431                         {
432                             return true;
433                         }
434
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)
438                         {
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)
442                             {
443                                 return false;
444                             }
445
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();
450                             return _exited;
451                         }
452
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
457                         // such a task.
458                         if (_waitInProgress != null)
459                         {
460                             waitTask = _waitInProgress;
461                         }
462                         else
463                         {
464                             createdTask = true;
465                             CancellationToken token = remainingTimeout == Timeout.Infinite ?
466                                 CancellationToken.None :
467                                 (cts = new CancellationTokenSource(remainingTimeout)).Token;
468                             waitTask = WaitForExitAsync(token);
469                         }
470                     } // lock(_gate)
471
472                     if (createdTask)
473                     {
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.
479                         waitTask.Wait();
480                         cts?.Dispose();
481                     }
482                     else
483                     {
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
489                         // remaining time.
490                         waitTask.Wait(remainingTimeout);
491                     }
492                 }
493             }
494         }
495
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))
500         {
501             Debug.Assert(Monitor.IsEntered(_gate));
502             Debug.Assert(_waitInProgress == null);
503             Debug.Assert(!_isChild);
504
505             return _waitInProgress = Task.Run(async delegate // Task.Run used because of potential blocking in CheckForNonChildExit
506             {
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;
512
513                 try
514                 {
515                     // While we're not canceled
516                     while (!cancellationToken.IsCancellationRequested)
517                     {
518                         // Poll
519                         lock (_gate)
520                         {
521                             if (!_exited)
522                             {
523                                 CheckForNonChildExit();
524                             }
525                             if (_exited) // may have been updated by CheckForNonChildExit
526                             {
527                                 return;
528                             }
529                         }
530
531                         // Wait
532                         try
533                         {
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);
536                         }
537                         catch (OperationCanceledException) { }
538                     }
539                 }
540                 finally
541                 {
542                     // Task is no longer active
543                     lock (_gate)
544                     {
545                         _waitInProgress = null;
546                     }
547                 }
548             });
549         }
550
551         private bool TryReapChild()
552         {
553             lock (_gate)
554             {
555                 if (_exited)
556                 {
557                     return false;
558                 }
559
560                 // Try to get the state of the child process
561                 int exitCode;
562                 int waitResult = Interop.Sys.WaitPidExitedNoHang(_processId, out exitCode);
563
564                 if (waitResult == _processId)
565                 {
566                     _exitCode = exitCode;
567
568                     if (_usesTerminal)
569                     {
570                         // Update terminal settings before calling SetExited.
571                         Process.ConfigureTerminalForChildProcesses(-1);
572                     }
573
574                     SetExited();
575
576                     return true;
577                 }
578                 else if (waitResult == 0)
579                 {
580                     // Process is still running
581                 }
582                 else
583                 {
584                     // Unexpected.
585                     int errorCode = Marshal.GetLastWin32Error();
586                     Environment.FailFast("Error while reaping child. errno = " + errorCode);
587                 }
588                 return false;
589             }
590         }
591
592         internal static void CheckChildren(bool reapAll)
593         {
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)
597             {
598                 bool checkAll = false;
599
600                 // Check terminated processes.
601                 int pid;
602                 do
603                 {
604                     // Find a process that terminated without reaping it yet.
605                     pid = Interop.Sys.WaitIdAnyExitedNoHangNoWait();
606                     if (pid > 0)
607                     {
608                         if (s_childProcessWaitStates.TryGetValue(pid, out ProcessWaitState pws))
609                         {
610                             // Known Process.
611                             if (pws.TryReapChild())
612                             {
613                                 pws.ReleaseRef();
614                             }
615                         }
616                         else
617                         {
618                             // unlikely: This is not a managed Process, so we are not responsible for reaping.
619                             // Fall back to checking all Processes.
620                             checkAll = true;
621                             break;
622                         }
623                     }
624                     else if (pid == 0)
625                     {
626                         // No more terminated children.
627                     }
628                     else
629                     {
630                         // Unexpected.
631                         int errorCode = Marshal.GetLastWin32Error();
632                         Environment.FailFast("Error while checking for terminated children. errno = " + errorCode);
633                     }
634                 } while (pid > 0);
635
636                 if (checkAll)
637                 {
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)
642                     {
643                         ProcessWaitState pws = kv.Value;
644                         if (pws.TryReapChild())
645                         {
646                             if (firstToRemove == null)
647                             {
648                                 firstToRemove = pws;
649                             }
650                             else
651                             {
652                                 if (additionalToRemove == null)
653                                 {
654                                     additionalToRemove = new List<ProcessWaitState>();
655                                 }
656                                 additionalToRemove.Add(pws);
657                             }
658                         }
659                     }
660
661                     if (firstToRemove != null)
662                     {
663                         firstToRemove.ReleaseRef();
664                         if (additionalToRemove != null)
665                         {
666                             foreach (ProcessWaitState pws in additionalToRemove)
667                             {
668                                 pws.ReleaseRef();
669                             }
670                         }
671                     }
672                 }
673
674                 if (reapAll)
675                 {
676                     do
677                     {
678                         int exitCode;
679                         pid = Interop.Sys.WaitPidExitedNoHang(-1, out exitCode);
680                     } while (pid > 0);
681                 }
682             }
683         }
684     }
685 }