@@ -24,8 +24,13 @@ public abstract class BaseCommand<T> : Command<T>
2424 {
2525 public sealed override int Execute ( CommandContext context , T settings )
2626 {
27+ // We use two CancellationTokenSources: one for timeout, the second for manual Ctrl+C, so that we can react differently
28+ // according to the cancellation reason.
2729 CancellationTokenSource ? timeoutCancellation = null ;
2830
31+ var mainCancellation = new CancellationTokenSource ( ) ;
32+ Console . CancelKeyPress += ( _ , _ ) => mainCancellation . Cancel ( ) ;
33+
2934 try
3035 {
3136 var stopwatch = Stopwatch . StartNew ( ) ;
@@ -35,7 +40,7 @@ public sealed override int Execute( CommandContext context, T settings )
3540 Debugger . Launch ( ) ;
3641 }
3742
38- if ( ! BuildContext . TryCreate ( context , settings , out var buildContext ) )
43+ if ( ! BuildContext . TryCreate ( context , settings , mainCancellation . Token , out var buildContext ) )
3944 {
4045 return 1 ;
4146 }
@@ -49,7 +54,7 @@ public sealed override int Execute( CommandContext context, T settings )
4954 if ( settings . Timeout != null )
5055 {
5156 timeoutCancellation = new CancellationTokenSource ( TimeSpan . FromMinutes ( settings . Timeout . Value ) ) ;
52- timeoutCancellation . Token . Register ( ( ) => OnTimeout ( buildContext , stopwatch ) ) ;
57+ timeoutCancellation . Token . Register ( ( ) => OnTimeout ( buildContext , stopwatch , mainCancellation ) ) ;
5358 }
5459
5560 MSBuildHelper . InitializeLocator ( ) ;
@@ -149,18 +154,23 @@ public sealed override int Execute( CommandContext context, T settings )
149154 // Execute the command itself.
150155 var success = this . ExecuteCore ( buildContext , settings ) ;
151156
157+ if ( buildContext . CancellationToken . IsCancellationRequested )
158+ {
159+ return ( int ) ExitCodes . Cancelled ;
160+ }
161+
152162 if ( ! settings . NoLogo )
153163 {
154164 buildContext . Console . WriteMessage ( $ "Finished at { DateTime . Now } after { stopwatch . Elapsed } ." ) ;
155165 }
156166
157- return success ? 0 : 1 ;
167+ return ( int ) ( success ? ExitCodes . Success : ExitCodes . Error ) ;
158168 }
159169 catch ( Exception ex )
160170 {
161171 AnsiConsole . WriteException ( ex ) ;
162172
163- return 10 ;
173+ return ( int ) ExitCodes . Exception ;
164174 }
165175 finally
166176 {
@@ -170,15 +180,42 @@ public sealed override int Execute( CommandContext context, T settings )
170180
171181 protected abstract bool ExecuteCore ( BuildContext context , T settings ) ;
172182
173- private static void OnTimeout ( BuildContext buildContext , Stopwatch stopwatch )
183+ private static void OnTimeout ( BuildContext buildContext , Stopwatch stopwatch , CancellationTokenSource mainCancellation )
174184 {
175- // ReSharper disable AccessToModifiedClosure
185+ var console = buildContext . Console ;
186+
187+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
188+ {
189+ console . WriteError ( $ "The process timed out after { stopwatch . Elapsed } . Dumping and killing the process tree." ) ;
190+ var directory = Path . Combine ( buildContext . RepoDirectory , buildContext . Product . DumpDirectory ) ;
191+
192+ // List all child processes.
193+ var processes = ProcessHelper . GetProcessTree ( console , Process . GetCurrentProcess ( ) . Id ) ;
194+
195+ console . WriteMessage ( "Process tree:" ) ;
176196
177- buildContext . Console . WriteError ( $ "The process timed out after { stopwatch . Elapsed } . Dumping and killing the process tree." ) ;
178- var directory = Path . Combine ( buildContext . RepoDirectory , buildContext . Product . DumpDirectory ) ;
179- ProcessHelper . DumpAndKillProcessTree ( buildContext . Console , Process . GetCurrentProcess ( ) , directory ) ;
197+ foreach ( var node in processes )
198+ {
199+ var indent = new string ( '-' , ( node . NestingLevel + 1 ) * 3 ) ;
200+ console . WriteMessage ( $ "+{ indent } { node . Process . Id } { ProcessHelper . GetCommandLine ( node . Process ) } " ) ;
201+ }
202+
203+ // Dump these processes.
204+ ProcessHelper . DumpProcesses ( console , processes . Select ( p => p . Process ) , directory ) ;
205+
206+ // Signal the main cancellation source.
207+ mainCancellation . Cancel ( ) ;
208+
209+ // Kill all processes (except the current one) in reverse order.
210+ ProcessHelper . KillProcesses ( console , processes . Reverse ( ) . Select ( x => x . Process ) ) ;
211+ }
212+ else
213+ {
214+ console . WriteError ( $ "The process timed out after { stopwatch . Elapsed } . Exiting." ) ;
215+ }
180216
181- // ReSharper restore AccessToModifiedClosure
217+ console . WriteWarning ( "Terminating the current process." ) ;
218+ Environment . FailFast ( $ "The process timed out after { stopwatch . Elapsed } ." ) ;
182219 }
183220 }
184221}
0 commit comments