@@ -10,20 +10,27 @@ internal sealed class ProcessRunner(
10
10
TimeSpan processCleanupTimeout ,
11
11
CancellationToken shutdownCancellationToken )
12
12
{
13
- private const int SIGKILL = 9 ;
14
- private const int SIGTERM = 15 ;
15
-
16
13
private sealed class ProcessState
17
14
{
18
15
public int ProcessId ;
19
16
public bool HasExited ;
20
17
}
21
18
19
+ // For testing purposes only, lock on access.
20
+ private static readonly HashSet < int > s_runningApplicationProcesses = [ ] ;
21
+
22
+ public static IReadOnlyCollection < int > GetRunningApplicationProcesses ( )
23
+ {
24
+ lock ( s_runningApplicationProcesses )
25
+ {
26
+ return [ .. s_runningApplicationProcesses ] ;
27
+ }
28
+ }
29
+
22
30
/// <summary>
23
31
/// Launches a process.
24
32
/// </summary>
25
- /// <param name="isUserApplication">True if the process is a user application, false if it is a helper process (e.g. msbuild).</param>
26
- public async Task < int > RunAsync ( ProcessSpec processSpec , IReporter reporter , bool isUserApplication , ProcessLaunchResult ? launchResult , CancellationToken processTerminationToken )
33
+ public async Task < int > RunAsync ( ProcessSpec processSpec , IReporter reporter , ProcessLaunchResult ? launchResult , CancellationToken processTerminationToken )
27
34
{
28
35
var state = new ProcessState ( ) ;
29
36
var stopwatch = new Stopwatch ( ) ;
@@ -49,6 +56,14 @@ public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, boo
49
56
50
57
state . ProcessId = process . Id ;
51
58
59
+ if ( processSpec . IsUserApplication )
60
+ {
61
+ lock ( s_runningApplicationProcesses )
62
+ {
63
+ s_runningApplicationProcesses . Add ( state . ProcessId ) ;
64
+ }
65
+ }
66
+
52
67
if ( onOutput != null )
53
68
{
54
69
process . BeginOutputReadLine ( ) ;
@@ -90,12 +105,12 @@ public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, boo
90
105
// Either Ctrl+C was pressed or the process is being restarted.
91
106
92
107
// Non-cancellable to not leave orphaned processes around blocking resources:
93
- await TerminateProcessAsync ( process , state , reporter , CancellationToken . None ) ;
108
+ await TerminateProcessAsync ( process , processSpec , state , reporter , CancellationToken . None ) ;
94
109
}
95
110
}
96
111
catch ( Exception e )
97
112
{
98
- if ( isUserApplication )
113
+ if ( processSpec . IsUserApplication )
99
114
{
100
115
reporter . Error ( $ "Application failed: { e . Message } ") ;
101
116
}
@@ -104,6 +119,14 @@ public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, boo
104
119
{
105
120
stopwatch . Stop ( ) ;
106
121
122
+ if ( processSpec . IsUserApplication )
123
+ {
124
+ lock ( s_runningApplicationProcesses )
125
+ {
126
+ s_runningApplicationProcesses . Remove ( state . ProcessId ) ;
127
+ }
128
+ }
129
+
107
130
state . HasExited = true ;
108
131
109
132
try
@@ -117,7 +140,7 @@ public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, boo
117
140
118
141
reporter . Verbose ( $ "Process id { process . Id } ran for { stopwatch . ElapsedMilliseconds } ms and exited with exit code { exitCode } .") ;
119
142
120
- if ( isUserApplication )
143
+ if ( processSpec . IsUserApplication )
121
144
{
122
145
if ( exitCode == 0 )
123
146
{
@@ -157,6 +180,11 @@ private static Process CreateProcess(ProcessSpec processSpec, Action<OutputLine>
157
180
}
158
181
} ;
159
182
183
+ if ( processSpec . IsUserApplication && RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
184
+ {
185
+ process . StartInfo . CreateNewProcessGroup = true ;
186
+ }
187
+
160
188
if ( processSpec . EscapedArguments is not null )
161
189
{
162
190
process . StartInfo . Arguments = processSpec . EscapedArguments ;
@@ -210,28 +238,24 @@ private static Process CreateProcess(ProcessSpec processSpec, Action<OutputLine>
210
238
return process ;
211
239
}
212
240
213
- private async ValueTask TerminateProcessAsync ( Process process , ProcessState state , IReporter reporter , CancellationToken cancellationToken )
241
+ private async ValueTask TerminateProcessAsync ( Process process , ProcessSpec processSpec , ProcessState state , IReporter reporter , CancellationToken cancellationToken )
214
242
{
215
243
if ( ! shutdownCancellationToken . IsCancellationRequested )
216
244
{
217
- if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
245
+ var forceOnly = RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) && ! processSpec . IsUserApplication ;
246
+
247
+ // Ctrl+C hasn't been sent.
248
+ TerminateProcess ( process , state , reporter , forceOnly ) ;
249
+
250
+ if ( forceOnly )
218
251
{
219
- // Ctrl+C hasn't been sent, force termination.
220
- // We don't have means to terminate gracefully on Windows (https://github.com/dotnet/runtime/issues/109432)
221
- TerminateProcess ( process , state , reporter , force : true ) ;
222
252
_ = await WaitForExitAsync ( process , state , timeout : null , reporter , cancellationToken ) ;
223
-
224
253
return ;
225
254
}
226
- else
227
- {
228
- // Ctrl+C hasn't been sent, send SIGTERM now:
229
- TerminateProcess ( process , state , reporter , force : false ) ;
230
- }
231
255
}
232
256
233
257
// Ctlr+C/SIGTERM has been sent, wait for the process to exit gracefully.
234
- if ( processCleanupTimeout . Milliseconds == 0 ||
258
+ if ( processCleanupTimeout . TotalMilliseconds == 0 ||
235
259
! await WaitForExitAsync ( process , state , processCleanupTimeout , reporter , cancellationToken ) )
236
260
{
237
261
// Force termination if the process is still running after the timeout.
@@ -327,55 +351,28 @@ private static void TerminateProcess(Process process, ProcessState state, IRepor
327
351
328
352
private static void TerminateWindowsProcess ( Process process , ProcessState state , IReporter reporter , bool force )
329
353
{
330
- // Needs API: https://github.com/dotnet/runtime/issues/109432
331
- // Code below does not work because the process creation needs CREATE_NEW_PROCESS_GROUP flag.
354
+ var processId = state . ProcessId ;
332
355
333
- reporter . Verbose ( $ "Terminating process { state . ProcessId } .") ;
356
+ reporter . Verbose ( $ "Terminating process { processId } ( { ( force ? "Kill" : "Ctrl+C" ) } ) .") ;
334
357
335
358
if ( force )
336
359
{
337
360
process . Kill ( ) ;
338
361
}
339
- #if TODO
340
362
else
341
363
{
342
- const uint CTRL_C_EVENT = 0 ;
343
-
344
- [ DllImport ( "kernel32.dll" , SetLastError = true ) ]
345
- static extern bool GenerateConsoleCtrlEvent ( uint dwCtrlEvent , uint dwProcessGroupId ) ;
346
-
347
- [ DllImport ( "kernel32.dll" , SetLastError = true ) ]
348
- static extern bool AttachConsole ( uint dwProcessId ) ;
349
-
350
- [ DllImport ( "kernel32.dll" , SetLastError = true ) ]
351
- static extern bool FreeConsole ( ) ;
352
-
353
- if ( AttachConsole ( ( uint ) state . ProcessId ) &&
354
- GenerateConsoleCtrlEvent ( CTRL_C_EVENT , 0 ) &&
355
- FreeConsole ( ) )
356
- {
357
- return ;
358
- }
359
-
360
- var error = Marshal . GetLastPInvokeError ( ) ;
361
- reporter . Verbose ( $ "Failed to send Ctrl+C to process { state . ProcessId } : { Marshal . GetPInvokeErrorMessage ( error ) } (code { error } )") ;
364
+ ProcessUtilities . SendWindowsCtrlCEvent ( processId , m => reporter . Verbose ( m ) ) ;
362
365
}
363
- #endif
364
366
}
365
367
366
368
private static void TerminateUnixProcess ( ProcessState state , IReporter reporter , bool force )
367
369
{
368
- [ DllImport ( "libc" , SetLastError = true , EntryPoint = "kill" ) ]
369
- static extern int sys_kill ( int pid , int sig ) ;
370
-
371
370
reporter . Verbose ( $ "Terminating process { state . ProcessId } ({ ( force ? "SIGKILL" : "SIGTERM" ) } ).") ;
372
371
373
- var result = sys_kill ( state . ProcessId , force ? SIGKILL : SIGTERM ) ;
374
- if ( result != 0 )
375
- {
376
- var error = Marshal . GetLastPInvokeError ( ) ;
377
- reporter . Verbose ( $ "Error while sending SIGTERM to process { state . ProcessId } : { Marshal . GetPInvokeErrorMessage ( error ) } (code { error } ).") ;
378
- }
372
+ ProcessUtilities . SendPosixSignal (
373
+ state . ProcessId ,
374
+ signal : force ? ProcessUtilities . SIGKILL : ProcessUtilities . SIGTERM ,
375
+ log : m => reporter . Verbose ( m ) ) ;
379
376
}
380
377
}
381
378
}
0 commit comments