Skip to content

Commit 19aac2c

Browse files
committed
Improved timeout handling.
1 parent f08c5ab commit 19aac2c

File tree

15 files changed

+193
-92
lines changed

15 files changed

+193
-92
lines changed

src/PostSharp.Engineering.BuildTools/AppExtensions.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,15 @@ internal static void AddCommands( this CommandApp app, Product product )
219219
{
220220
tools.AddCommand<KillCommand>( "kill" )
221221
.WithData( data )
222-
.WithDescription( "Kill all compiler processes" );
222+
.WithDescription( "Kill all compiler processes." );
223223

224-
tools.AddCommand<DumpAndKillCommand>( "dump-and-kill" )
224+
tools.AddCommand<DumpCommand>( "dump" )
225225
.WithData( data )
226-
.WithDescription( "Dump and kill a given process and all its descendants" );
226+
.WithDescription( "Dump a given process and all its descendants." );
227+
228+
tools.AddCommand<WaitCommand>( "wait" )
229+
.WithData( data )
230+
.WithDescription( "Wait a given number of seconds. When used to test the behavior of the the --timeout argument." );
227231

228232
tools.AddBranch(
229233
"csproj",

src/PostSharp.Engineering.BuildTools/BaseCommand.cs

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

src/PostSharp.Engineering.BuildTools/Build/BuildContext.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System;
1010
using System.Diagnostics.CodeAnalysis;
1111
using System.IO;
12+
using System.Threading;
1213

1314
namespace PostSharp.Engineering.BuildTools.Build
1415
{
@@ -72,7 +73,8 @@ private BuildContext(
7273
string branch,
7374
CommandContext commandContext,
7475
bool useProjectDirectoryAsWorkingDirectory,
75-
CommonCommandSettings settings )
76+
CommonCommandSettings settings,
77+
CancellationToken cancellationToken )
7678
{
7779
this.Console = console;
7880
this.RepoDirectory = repoDirectory;
@@ -81,6 +83,7 @@ private BuildContext(
8183
this.CommandContext = commandContext;
8284
this.UseProjectDirectoryAsWorkingDirectory = useProjectDirectoryAsWorkingDirectory;
8385
this.Settings = settings;
86+
this.CancellationToken = cancellationToken;
8487
}
8588

8689
/// <summary>
@@ -89,6 +92,7 @@ private BuildContext(
8992
public static bool TryCreate(
9093
CommandContext commandContext,
9194
CommonCommandSettings settings,
95+
CancellationToken cancellationToken,
9296
[NotNullWhen( true )] out BuildContext? buildContext )
9397
{
9498
buildContext = null;
@@ -114,7 +118,8 @@ public static bool TryCreate(
114118
currentBranch,
115119
commandContext,
116120
useProjectDirectoryAsWorkingDirectory: false,
117-
settings );
121+
settings,
122+
cancellationToken );
118123

119124
return true;
120125
}
@@ -158,7 +163,8 @@ public BuildContext WithConsoleHelper( ConsoleHelper consoleHelper )
158163
this.Branch,
159164
this.CommandContext,
160165
this.UseProjectDirectoryAsWorkingDirectory,
161-
this.Settings );
166+
this.Settings,
167+
this.CancellationToken );
162168

163169
public BuildContext WithUseProjectDirectoryAsWorkingDirectory( bool useProjectDirectoryAsWorkingDirectory )
164170
=> new(
@@ -168,7 +174,8 @@ public BuildContext WithUseProjectDirectoryAsWorkingDirectory( bool useProjectDi
168174
this.Branch,
169175
this.CommandContext,
170176
useProjectDirectoryAsWorkingDirectory,
171-
this.Settings );
177+
this.Settings,
178+
this.CancellationToken );
172179

173180
[PublicAPI]
174181
#pragma warning disable CA1822
@@ -180,6 +187,8 @@ public BuildContext WithUseProjectDirectoryAsWorkingDirectory( bool useProjectDi
180187
[PublicAPI]
181188
public CommonCommandSettings Settings { get; }
182189

190+
public CancellationToken CancellationToken { get; }
191+
183192
internal TimeSpan BuildTimeout
184193
{
185194
get

src/PostSharp.Engineering.BuildTools/CodeStyle/BaseCodeStyleCommand.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,22 @@ internal abstract class BaseCodeStyleCommand<T> : BaseCommand<T>
1515
protected static string? GetCodeStyleRepo( BuildContext context, CodeStyleSettings settings )
1616
{
1717
var sharedRepo = Path.GetFullPath( Path.Combine( context.RepoDirectory, "..", "PostSharp.Engineering.CodeStyle" ) );
18+
var console = context.Console;
1819

1920
// Check if the repo exists.
2021
if ( !Directory.Exists( sharedRepo ) )
2122
{
2223
if ( !settings.Create )
2324
{
24-
context.Console.WriteError( $"The directory '{sharedRepo} does not exist. Use --create'" );
25+
console.WriteError( $"The directory '{sharedRepo} does not exist. Use --create'" );
2526

2627
return null;
2728
}
2829
else
2930
{
3031
var baseDir = Path.GetDirectoryName( sharedRepo )!;
3132

32-
ToolInvocationHelper.InvokeTool( context.Console, "git", $"clone {settings.Url}", baseDir );
33+
ToolInvocationHelper.InvokeTool( console, "git", $"clone {settings.Url}", baseDir );
3334
}
3435
}
3536

src/PostSharp.Engineering.BuildTools/CommonCommandSettings.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ protected set
118118

119119
[Description( "Overrides the build timeout." )]
120120
[CommandOption( "--timeout" )]
121-
public int? Timeout { get; set; }
121+
public double? Timeout { get; set; }
122122

123123
public ImmutableDictionary<string, string> Properties { get; protected set; } =
124124
ImmutableDictionary.Create<string, string>( StringComparer.OrdinalIgnoreCase );

src/PostSharp.Engineering.BuildTools/EngineeringApp.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ public void Configure( Action<IConfigurator> configure )
2727
this._app.Configure( configure );
2828
}
2929

30-
public int Run( IEnumerable<string> args ) => this._app.Run( args );
30+
public int Run( IEnumerable<string> args ) { return this._app.Run( args ); }
3131
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) SharpCrafters s.r.o. See the LICENSE.md file in the root directory of this repository root for details.
2+
3+
namespace PostSharp.Engineering.BuildTools;
4+
5+
public enum ExitCodes
6+
{
7+
Success,
8+
Error,
9+
Exception = 100,
10+
Cancelled = 200,
11+
Timeout = 300
12+
}

src/PostSharp.Engineering.BuildTools/Tools/Processes/DumpAndKillCommand.cs

Lines changed: 0 additions & 34 deletions
This file was deleted.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) SharpCrafters s.r.o. See the LICENSE.md file in the root directory of this repository root for details.
2+
3+
using JetBrains.Annotations;
4+
using PostSharp.Engineering.BuildTools.Build;
5+
using PostSharp.Engineering.BuildTools.Utilities;
6+
using System;
7+
using System.Diagnostics;
8+
using System.IO;
9+
using System.Linq;
10+
11+
namespace PostSharp.Engineering.BuildTools.Tools.Processes;
12+
13+
[UsedImplicitly]
14+
internal class DumpCommand : BaseCommand<DumpCommandSettings>
15+
{
16+
protected override bool ExecuteCore( BuildContext context, DumpCommandSettings settings )
17+
{
18+
var console = context.Console;
19+
20+
// List all child processes.
21+
var processes = ProcessHelper.GetProcessTree( console, Process.GetCurrentProcess().Id );
22+
23+
console.WriteMessage( "Process tree:" );
24+
25+
foreach ( var node in processes )
26+
{
27+
var indent = new string( '-', (node.NestingLevel + 1) * 3 );
28+
console.WriteMessage( $"+{indent} {node.Process.Id} {ProcessHelper.GetCommandLine( node.Process )}" );
29+
}
30+
31+
// Dump these processes.
32+
ProcessHelper.DumpProcesses( console, processes.Select( p => p.Process ), Path.GetTempPath() );
33+
34+
return true;
35+
}
36+
}

src/PostSharp.Engineering.BuildTools/Tools/Processes/DumpAndKillCommandSettings.cs renamed to src/PostSharp.Engineering.BuildTools/Tools/Processes/DumpCommandSettings.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
namespace PostSharp.Engineering.BuildTools.Tools.Processes;
77

88
[UsedImplicitly]
9-
internal sealed class DumpAndKillCommandSettings : CommonCommandSettings
9+
internal sealed class DumpCommandSettings : CommonCommandSettings
1010
{
1111
[CommandArgument( 0, "<process-id>" )]
1212
public int ProcessId { get; init; }

0 commit comments

Comments
 (0)