66
77namespace Microsoft . DotNet . Watch
88{
9- internal sealed class ProcessRunner
9+ internal sealed class ProcessRunner (
10+ TimeSpan processCleanupTimeout ,
11+ CancellationToken shutdownCancellationToken )
1012 {
1113 private const int SIGKILL = 9 ;
1214 private const int SIGTERM = 15 ;
@@ -15,14 +17,13 @@ private sealed class ProcessState
1517 {
1618 public int ProcessId ;
1719 public bool HasExited ;
18- public bool ForceExit ;
1920 }
2021
2122 /// <summary>
2223 /// Launches a process.
2324 /// </summary>
2425 /// <param name="isUserApplication">True if the process is a user application, false if it is a helper process (e.g. msbuild).</param>
25- public static async Task < int > RunAsync ( ProcessSpec processSpec , IReporter reporter , bool isUserApplication , ProcessLaunchResult ? launchResult , CancellationToken processTerminationToken )
26+ public async Task < int > RunAsync ( ProcessSpec processSpec , IReporter reporter , bool isUserApplication , ProcessLaunchResult ? launchResult , CancellationToken processTerminationToken )
2627 {
2728 Ensure . NotNull ( processSpec , nameof ( processSpec ) ) ;
2829
@@ -38,8 +39,6 @@ public static async Task<int> RunAsync(ProcessSpec processSpec, IReporter report
3839
3940 using var process = CreateProcess ( processSpec , onOutput , state , reporter ) ;
4041
41- processTerminationToken . Register ( ( ) => TerminateProcess ( process , state , reporter ) ) ;
42-
4342 stopwatch . Start ( ) ;
4443
4544 Exception ? launchException = null ;
@@ -85,45 +84,15 @@ public static async Task<int> RunAsync(ProcessSpec processSpec, IReporter report
8584 {
8685 try
8786 {
88- await process . WaitForExitAsync ( processTerminationToken ) ;
89-
90- // ensures that all process output has been reported:
91- try
92- {
93- process . WaitForExit ( ) ;
94- }
95- catch
96- {
97- }
87+ _ = await WaitForExitAsync ( process , timeout : null , processTerminationToken ) ;
9888 }
9989 catch ( OperationCanceledException )
10090 {
10191 // Process termination requested via cancellation token.
102- // Wait for the actual process exit.
103- while ( true )
104- {
105- try
106- {
107- // non-cancellable to not leave orphaned processes around blocking resources:
108- await process . WaitForExitAsync ( CancellationToken . None ) . WaitAsync ( TimeSpan . FromSeconds ( 5 ) , CancellationToken . None ) ;
109- break ;
110- }
111- catch ( TimeoutException )
112- {
113- // nop
114- }
115-
116- if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) || state . ForceExit )
117- {
118- reporter . Output ( $ "Waiting for process { state . ProcessId } to exit ...") ;
119- }
120- else
121- {
122- reporter . Output ( $ "Forcing process { state . ProcessId } to exit ...") ;
123- }
92+ // Either Ctrl+C was pressed or the process is being restarted.
12493
125- state . ForceExit = true ;
126- }
94+ // Non-cancellable to not leave orphaned processes around blocking resources:
95+ await TerminateProcessAsync ( process , state , reporter , CancellationToken . None ) ;
12796 }
12897 }
12998 catch ( Exception e )
@@ -243,24 +212,82 @@ private static Process CreateProcess(ProcessSpec processSpec, Action<OutputLine>
243212 return process ;
244213 }
245214
246- private static void TerminateProcess ( Process process , ProcessState state , IReporter reporter )
215+ private async ValueTask TerminateProcessAsync ( Process process , ProcessState state , IReporter reporter , CancellationToken cancellationToken )
216+ {
217+ if ( ! shutdownCancellationToken . IsCancellationRequested )
218+ {
219+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
220+ {
221+ // Ctrl+C hasn't been sent, force termination.
222+ // We don't have means to terminate gracefully on Windows (https://github.com/dotnet/runtime/issues/109432)
223+ TerminateProcess ( process , state , reporter , force : true ) ;
224+ _ = await WaitForExitAsync ( process , timeout : null , cancellationToken ) ;
225+
226+ return ;
227+ }
228+ else
229+ {
230+ // Ctrl+C hasn't been sent, send SIGTERM now:
231+ TerminateProcess ( process , state , reporter , force : false ) ;
232+ }
233+ }
234+
235+ // Ctlr+C/SIGTERM has been sent, wait for the process to exit gracefully.
236+ if ( ! await WaitForExitAsync ( process , processCleanupTimeout , cancellationToken ) )
237+ {
238+ // Force termination if the process is still running after the timeout.
239+ TerminateProcess ( process , state , reporter , force : true ) ;
240+
241+ _ = await WaitForExitAsync ( process , timeout : null , cancellationToken ) ;
242+ }
243+ }
244+
245+ private static async ValueTask < bool > WaitForExitAsync ( Process process , TimeSpan ? timeout , CancellationToken cancellationToken )
246+ {
247+ var task = process . WaitForExitAsync ( cancellationToken ) ;
248+
249+ if ( timeout . HasValue )
250+ {
251+ try
252+ {
253+ await task . WaitAsync ( timeout . Value , cancellationToken ) ;
254+ }
255+ catch ( TimeoutException )
256+ {
257+ return false ;
258+ }
259+ }
260+ else
261+ {
262+ await task ;
263+ }
264+
265+ // ensures that all process output has been reported:
266+ try
267+ {
268+ process . WaitForExit ( ) ;
269+ }
270+ catch
271+ {
272+ }
273+
274+ return true ;
275+ }
276+
277+ private static void TerminateProcess ( Process process , ProcessState state , IReporter reporter , bool force )
247278 {
248279 try
249280 {
250281 if ( ! state . HasExited && ! process . HasExited )
251282 {
252- reporter . Report ( MessageDescriptor . KillingProcess , state . ProcessId . ToString ( ) ) ;
253-
254283 if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
255284 {
256- TerminateWindowsProcess ( process , state , reporter ) ;
285+ TerminateWindowsProcess ( process , state , reporter , force ) ;
257286 }
258287 else
259288 {
260- TerminateUnixProcess ( state , reporter ) ;
289+ TerminateUnixProcess ( state , reporter , force ) ;
261290 }
262-
263- reporter . Verbose ( $ "Process { state . ProcessId } killed.") ;
264291 }
265292 }
266293 catch ( Exception ex )
@@ -272,12 +299,19 @@ private static void TerminateProcess(Process process, ProcessState state, IRepor
272299 }
273300 }
274301
275- private static void TerminateWindowsProcess ( Process process , ProcessState state , IReporter reporter )
302+ private static void TerminateWindowsProcess ( Process process , ProcessState state , IReporter reporter , bool force )
276303 {
277304 // Needs API: https://github.com/dotnet/runtime/issues/109432
278305 // Code below does not work because the process creation needs CREATE_NEW_PROCESS_GROUP flag.
279- #if TODO
280- if ( ! state . ForceExit )
306+
307+ reporter . Verbose ( $ "Terminating process { state . ProcessId } .") ;
308+
309+ if ( force )
310+ {
311+ process . Kill ( ) ;
312+ }
313+ #if TODO
314+ else
281315 {
282316 const uint CTRL_C_EVENT = 0 ;
283317
@@ -301,16 +335,16 @@ private static void TerminateWindowsProcess(Process process, ProcessState state,
301335 reporter . Verbose ( $ "Failed to send Ctrl+C to process { state . ProcessId } : { Marshal . GetPInvokeErrorMessage ( error ) } (code { error } )") ;
302336 }
303337#endif
304-
305- process . Kill ( ) ;
306338 }
307339
308- private static void TerminateUnixProcess ( ProcessState state , IReporter reporter )
340+ private static void TerminateUnixProcess ( ProcessState state , IReporter reporter , bool force )
309341 {
310342 [ DllImport ( "libc" , SetLastError = true , EntryPoint = "kill" ) ]
311343 static extern int sys_kill ( int pid , int sig ) ;
312344
313- var result = sys_kill ( state . ProcessId , state . ForceExit ? SIGKILL : SIGTERM ) ;
345+ reporter . Verbose ( $ "Terminating process { state . ProcessId } ({ ( force ? "SIGKILL" : "SIGTERM" ) } ).") ;
346+
347+ var result = sys_kill ( state . ProcessId , force ? SIGKILL : SIGTERM ) ;
314348 if ( result != 0 )
315349 {
316350 var error = Marshal . GetLastPInvokeError ( ) ;
0 commit comments