@@ -29,7 +29,6 @@ public sealed override int Execute( CommandContext context, T settings )
2929 CancellationTokenSource ? timeoutCancellation = null ;
3030
3131 var mainCancellation = new CancellationTokenSource ( ) ;
32- Console . CancelKeyPress += ( _ , _ ) => mainCancellation . Cancel ( ) ;
3332
3433 try
3534 {
@@ -50,6 +49,8 @@ public sealed override int Execute( CommandContext context, T settings )
5049 buildContext = buildContext . WithUseProjectDirectoryAsWorkingDirectory ( true ) ;
5150 }
5251
52+ Console . CancelKeyPress += ( _ , _ ) => OnCancel ( buildContext , mainCancellation ) ;
53+
5354 // Sets up a timeout. The Timer class does not support long periods, so we use CancellationTokenSource.
5455 if ( settings . Timeout != null )
5556 {
@@ -156,6 +157,8 @@ public sealed override int Execute( CommandContext context, T settings )
156157
157158 if ( buildContext . CancellationToken . IsCancellationRequested )
158159 {
160+ buildContext . Console . WriteError ( "The build was cancelled." ) ;
161+
159162 return ( int ) ExitCode . Cancelled ;
160163 }
161164
@@ -180,6 +183,34 @@ public sealed override int Execute( CommandContext context, T settings )
180183
181184 protected abstract bool ExecuteCore ( BuildContext context , T settings ) ;
182185
186+ private static void OnCancel ( BuildContext buildContext , CancellationTokenSource mainCancellation )
187+ {
188+ var console = buildContext . Console ;
189+
190+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
191+ {
192+ console . WriteError ( $ "Cancelling and killing all child processes." ) ;
193+
194+ // List all child processes.
195+ var processes = ProcessHelper . GetProcessTree ( console , Process . GetCurrentProcess ( ) . Id ) ;
196+
197+ console . WriteMessage ( "Process tree:" ) ;
198+
199+ foreach ( var node in processes )
200+ {
201+ var indent = new string ( '-' , ( node . NestingLevel + 1 ) * 3 ) ;
202+ console . WriteMessage ( $ "+{ indent } { node . Process . Id } { ProcessHelper . GetCommandLine ( node . Process ) } " ) ;
203+ }
204+
205+ // Kill all processes (except the current one) in reverse order.
206+ ProcessHelper . KillProcesses ( console , processes . Reverse ( ) . Select ( x => x . Process ) ) ;
207+ }
208+
209+ // Signal the main cancellation source.
210+ // We don't exit the process so exception handlers and finally blocks can run.
211+ mainCancellation . Cancel ( ) ;
212+ }
213+
183214 private static void OnTimeout ( BuildContext buildContext , Stopwatch stopwatch , CancellationTokenSource mainCancellation )
184215 {
185216 var console = buildContext . Console ;
@@ -212,8 +243,13 @@ private static void OnTimeout( BuildContext buildContext, Stopwatch stopwatch, C
212243 else
213244 {
214245 console . WriteError ( $ "The process timed out after { stopwatch . Elapsed } . Exiting." ) ;
246+ mainCancellation . Cancel ( ) ;
215247 }
216248
249+ // Give the normal cancellation workflow a chance to complete.
250+ Thread . Sleep ( 10 ) ;
251+
252+ // If we're still here, we're abruptly terminating the process.
217253 console . WriteWarning ( "Terminating the current process." ) ;
218254 Environment . FailFast ( $ "The process timed out after { stopwatch . Elapsed } ." ) ;
219255 }
0 commit comments