6
6
7
7
namespace Microsoft . DotNet . Watch
8
8
{
9
- internal sealed class ProcessRunner
9
+ internal sealed class ProcessRunner (
10
+ TimeSpan processCleanupTimeout ,
11
+ CancellationToken shutdownCancellationToken )
10
12
{
11
13
private const int SIGKILL = 9 ;
12
14
private const int SIGTERM = 15 ;
@@ -15,14 +17,13 @@ private sealed class ProcessState
15
17
{
16
18
public int ProcessId ;
17
19
public bool HasExited ;
18
- public bool ForceExit ;
19
20
}
20
21
21
22
/// <summary>
22
23
/// Launches a process.
23
24
/// </summary>
24
25
/// <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 )
26
27
{
27
28
Ensure . NotNull ( processSpec , nameof ( processSpec ) ) ;
28
29
@@ -38,8 +39,6 @@ public static async Task<int> RunAsync(ProcessSpec processSpec, IReporter report
38
39
39
40
using var process = CreateProcess ( processSpec , onOutput , state , reporter ) ;
40
41
41
- processTerminationToken . Register ( ( ) => TerminateProcess ( process , state , reporter ) ) ;
42
-
43
42
stopwatch . Start ( ) ;
44
43
45
44
Exception ? launchException = null ;
@@ -85,45 +84,15 @@ public static async Task<int> RunAsync(ProcessSpec processSpec, IReporter report
85
84
{
86
85
try
87
86
{
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 ) ;
98
88
}
99
89
catch ( OperationCanceledException )
100
90
{
101
91
// 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.
124
93
125
- state . ForceExit = true ;
126
- }
94
+ // Non-cancellable to not leave orphaned processes around blocking resources:
95
+ await TerminateProcessAsync ( process , state , reporter , CancellationToken . None ) ;
127
96
}
128
97
}
129
98
catch ( Exception e )
@@ -243,24 +212,82 @@ private static Process CreateProcess(ProcessSpec processSpec, Action<OutputLine>
243
212
return process ;
244
213
}
245
214
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 )
247
278
{
248
279
try
249
280
{
250
281
if ( ! state . HasExited && ! process . HasExited )
251
282
{
252
- reporter . Report ( MessageDescriptor . KillingProcess , state . ProcessId . ToString ( ) ) ;
253
-
254
283
if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
255
284
{
256
- TerminateWindowsProcess ( process , state , reporter ) ;
285
+ TerminateWindowsProcess ( process , state , reporter , force ) ;
257
286
}
258
287
else
259
288
{
260
- TerminateUnixProcess ( state , reporter ) ;
289
+ TerminateUnixProcess ( state , reporter , force ) ;
261
290
}
262
-
263
- reporter . Verbose ( $ "Process { state . ProcessId } killed.") ;
264
291
}
265
292
}
266
293
catch ( Exception ex )
@@ -272,12 +299,19 @@ private static void TerminateProcess(Process process, ProcessState state, IRepor
272
299
}
273
300
}
274
301
275
- private static void TerminateWindowsProcess ( Process process , ProcessState state , IReporter reporter )
302
+ private static void TerminateWindowsProcess ( Process process , ProcessState state , IReporter reporter , bool force )
276
303
{
277
304
// Needs API: https://github.com/dotnet/runtime/issues/109432
278
305
// 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
281
315
{
282
316
const uint CTRL_C_EVENT = 0 ;
283
317
@@ -301,16 +335,16 @@ private static void TerminateWindowsProcess(Process process, ProcessState state,
301
335
reporter . Verbose ( $ "Failed to send Ctrl+C to process { state . ProcessId } : { Marshal . GetPInvokeErrorMessage ( error ) } (code { error } )") ;
302
336
}
303
337
#endif
304
-
305
- process . Kill ( ) ;
306
338
}
307
339
308
- private static void TerminateUnixProcess ( ProcessState state , IReporter reporter )
340
+ private static void TerminateUnixProcess ( ProcessState state , IReporter reporter , bool force )
309
341
{
310
342
[ DllImport ( "libc" , SetLastError = true , EntryPoint = "kill" ) ]
311
343
static extern int sys_kill ( int pid , int sig ) ;
312
344
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 ) ;
314
348
if ( result != 0 )
315
349
{
316
350
var error = Marshal . GetLastPInvokeError ( ) ;
0 commit comments