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