@@ -10,20 +10,27 @@ internal sealed class ProcessRunner(
1010 TimeSpan processCleanupTimeout ,
1111 CancellationToken shutdownCancellationToken )
1212 {
13- private const int SIGKILL = 9 ;
14- private const int SIGTERM = 15 ;
15-
1613 private sealed class ProcessState
1714 {
1815 public int ProcessId ;
1916 public bool HasExited ;
2017 }
2118
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+
2230 /// <summary>
2331 /// Launches a process.
2432 /// </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 )
2734 {
2835 var state = new ProcessState ( ) ;
2936 var stopwatch = new Stopwatch ( ) ;
@@ -49,6 +56,14 @@ public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, boo
4956
5057 state . ProcessId = process . Id ;
5158
59+ if ( processSpec . IsUserApplication )
60+ {
61+ lock ( s_runningApplicationProcesses )
62+ {
63+ s_runningApplicationProcesses . Add ( state . ProcessId ) ;
64+ }
65+ }
66+
5267 if ( onOutput != null )
5368 {
5469 process . BeginOutputReadLine ( ) ;
@@ -90,12 +105,12 @@ public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, boo
90105 // Either Ctrl+C was pressed or the process is being restarted.
91106
92107 // 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 ) ;
94109 }
95110 }
96111 catch ( Exception e )
97112 {
98- if ( isUserApplication )
113+ if ( processSpec . IsUserApplication )
99114 {
100115 reporter . Error ( $ "Application failed: { e . Message } ") ;
101116 }
@@ -104,6 +119,14 @@ public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, boo
104119 {
105120 stopwatch . Stop ( ) ;
106121
122+ if ( processSpec . IsUserApplication )
123+ {
124+ lock ( s_runningApplicationProcesses )
125+ {
126+ s_runningApplicationProcesses . Remove ( state . ProcessId ) ;
127+ }
128+ }
129+
107130 state . HasExited = true ;
108131
109132 try
@@ -117,7 +140,7 @@ public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, boo
117140
118141 reporter . Verbose ( $ "Process id { process . Id } ran for { stopwatch . ElapsedMilliseconds } ms and exited with exit code { exitCode } .") ;
119142
120- if ( isUserApplication )
143+ if ( processSpec . IsUserApplication )
121144 {
122145 if ( exitCode == 0 )
123146 {
@@ -157,6 +180,11 @@ private static Process CreateProcess(ProcessSpec processSpec, Action<OutputLine>
157180 }
158181 } ;
159182
183+ if ( processSpec . IsUserApplication && RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
184+ {
185+ process . StartInfo . CreateNewProcessGroup = true ;
186+ }
187+
160188 if ( processSpec . EscapedArguments is not null )
161189 {
162190 process . StartInfo . Arguments = processSpec . EscapedArguments ;
@@ -210,28 +238,27 @@ private static Process CreateProcess(ProcessSpec processSpec, Action<OutputLine>
210238 return process ;
211239 }
212240
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 )
214242 {
215243 if ( ! shutdownCancellationToken . IsCancellationRequested )
216244 {
217- if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
245+ }
246+
247+ //{
248+ var forceOnly = RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) && ! processSpec . IsUserApplication ;
249+
250+ // Ctrl+C hasn't been sent.
251+ TerminateProcess ( process , state , reporter , forceOnly ) ;
252+
253+ if ( forceOnly )
218254 {
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 ) ;
222255 _ = await WaitForExitAsync ( process , state , timeout : null , reporter , cancellationToken ) ;
223-
224256 return ;
225257 }
226- else
227- {
228- // Ctrl+C hasn't been sent, send SIGTERM now:
229- TerminateProcess ( process , state , reporter , force : false ) ;
230- }
231- }
258+ //}
232259
233260 // Ctlr+C/SIGTERM has been sent, wait for the process to exit gracefully.
234- if ( processCleanupTimeout . Milliseconds == 0 ||
261+ if ( processCleanupTimeout . TotalMilliseconds == 0 ||
235262 ! await WaitForExitAsync ( process , state , processCleanupTimeout , reporter , cancellationToken ) )
236263 {
237264 // Force termination if the process is still running after the timeout.
@@ -327,55 +354,28 @@ private static void TerminateProcess(Process process, ProcessState state, IRepor
327354
328355 private static void TerminateWindowsProcess ( Process process , ProcessState state , IReporter reporter , bool force )
329356 {
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.
357+ var processId = state . ProcessId ;
332358
333- reporter . Verbose ( $ "Terminating process { state . ProcessId } .") ;
359+ reporter . Verbose ( $ "Terminating process { processId } ( { ( force ? "Kill" : "Ctrl+C" ) } ) .") ;
334360
335361 if ( force )
336362 {
337363 process . Kill ( ) ;
338364 }
339- #if TODO
340365 else
341366 {
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 } )") ;
367+ ProcessUtilities . SendWindowsCtrlCEvent ( processId , m => reporter . Verbose ( m ) ) ;
362368 }
363- #endif
364369 }
365370
366371 private static void TerminateUnixProcess ( ProcessState state , IReporter reporter , bool force )
367372 {
368- [ DllImport ( "libc" , SetLastError = true , EntryPoint = "kill" ) ]
369- static extern int sys_kill ( int pid , int sig ) ;
370-
371373 reporter . Verbose ( $ "Terminating process { state . ProcessId } ({ ( force ? "SIGKILL" : "SIGTERM" ) } ).") ;
372374
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- }
375+ ProcessUtilities . SendPosixSignal (
376+ state . ProcessId ,
377+ signal : force ? ProcessUtilities . SIGKILL : ProcessUtilities . SIGTERM ,
378+ log : m => reporter . Verbose ( m ) ) ;
379379 }
380380 }
381381}
0 commit comments