66
77namespace Microsoft . DotNet . Watch
88{
9- internal sealed class ProcessRunner (
10- TimeSpan processCleanupTimeout ,
11- CancellationToken shutdownCancellationToken )
9+ internal sealed class ProcessRunner ( TimeSpan processCleanupTimeout )
1210 {
13- private const int SIGKILL = 9 ;
14- private const int SIGTERM = 15 ;
15-
1611 private sealed class ProcessState
1712 {
1813 public int ProcessId ;
1914 public bool HasExited ;
2015 }
2116
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+
2228 /// <summary>
2329 /// Launches a process.
2430 /// </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 )
2732 {
2833 var state = new ProcessState ( ) ;
2934 var stopwatch = new Stopwatch ( ) ;
@@ -49,6 +54,14 @@ public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, boo
4954
5055 state . ProcessId = process . Id ;
5156
57+ if ( processSpec . IsUserApplication )
58+ {
59+ lock ( s_runningApplicationProcesses )
60+ {
61+ s_runningApplicationProcesses . Add ( state . ProcessId ) ;
62+ }
63+ }
64+
5265 if ( onOutput != null )
5366 {
5467 process . BeginOutputReadLine ( ) ;
@@ -90,12 +103,12 @@ public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, boo
90103 // Either Ctrl+C was pressed or the process is being restarted.
91104
92105 // 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 ) ;
94107 }
95108 }
96109 catch ( Exception e )
97110 {
98- if ( isUserApplication )
111+ if ( processSpec . IsUserApplication )
99112 {
100113 reporter . Error ( $ "Application failed: { e . Message } ") ;
101114 }
@@ -104,6 +117,14 @@ public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, boo
104117 {
105118 stopwatch . Stop ( ) ;
106119
120+ if ( processSpec . IsUserApplication )
121+ {
122+ lock ( s_runningApplicationProcesses )
123+ {
124+ s_runningApplicationProcesses . Remove ( state . ProcessId ) ;
125+ }
126+ }
127+
107128 state . HasExited = true ;
108129
109130 try
@@ -117,7 +138,7 @@ public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, boo
117138
118139 reporter . Verbose ( $ "Process id { process . Id } ran for { stopwatch . ElapsedMilliseconds } ms and exited with exit code { exitCode } .") ;
119140
120- if ( isUserApplication )
141+ if ( processSpec . IsUserApplication )
121142 {
122143 if ( exitCode == 0 )
123144 {
@@ -157,6 +178,11 @@ private static Process CreateProcess(ProcessSpec processSpec, Action<OutputLine>
157178 }
158179 } ;
159180
181+ if ( processSpec . IsUserApplication && RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
182+ {
183+ process . StartInfo . CreateNewProcessGroup = true ;
184+ }
185+
160186 if ( processSpec . EscapedArguments is not null )
161187 {
162188 process . StartInfo . Arguments = processSpec . EscapedArguments ;
@@ -210,28 +236,21 @@ private static Process CreateProcess(ProcessSpec processSpec, Action<OutputLine>
210236 return process ;
211237 }
212238
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 )
214240 {
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 ;
223242
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 ;
231250 }
232251
233252 // Ctlr+C/SIGTERM has been sent, wait for the process to exit gracefully.
234- if ( processCleanupTimeout . Milliseconds == 0 ||
253+ if ( processCleanupTimeout . TotalMilliseconds == 0 ||
235254 ! await WaitForExitAsync ( process , state , processCleanupTimeout , reporter , cancellationToken ) )
236255 {
237256 // Force termination if the process is still running after the timeout.
@@ -327,55 +346,28 @@ private static void TerminateProcess(Process process, ProcessState state, IRepor
327346
328347 private static void TerminateWindowsProcess ( Process process , ProcessState state , IReporter reporter , bool force )
329348 {
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 ;
332350
333- reporter . Verbose ( $ "Terminating process { state . ProcessId } .") ;
351+ reporter . Verbose ( $ "Terminating process { processId } ( { ( force ? "Kill" : "Ctrl+C" ) } ) .") ;
334352
335353 if ( force )
336354 {
337355 process . Kill ( ) ;
338356 }
339- #if TODO
340357 else
341358 {
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 ) ) ;
362360 }
363- #endif
364361 }
365362
366363 private static void TerminateUnixProcess ( ProcessState state , IReporter reporter , bool force )
367364 {
368- [ DllImport ( "libc" , SetLastError = true , EntryPoint = "kill" ) ]
369- static extern int sys_kill ( int pid , int sig ) ;
370-
371365 reporter . Verbose ( $ "Terminating process { state . ProcessId } ({ ( force ? "SIGKILL" : "SIGTERM" ) } ).") ;
372366
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 ) ) ;
379371 }
380372 }
381373}
0 commit comments