Skip to content

Commit 52f341d

Browse files
committed
C#: Gracefully handle non-zero exitcodes for dotnet --info.
1 parent 865095b commit 52f341d

File tree

5 files changed

+77
-19
lines changed

5 files changed

+77
-19
lines changed

csharp/autobuilder/Semmle.Autobuild.CSharp/DotNetRule.cs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public BuildScript Analyse(IAutobuilder<CSharpAutobuildOptions> builder, bool au
4848
{
4949
// When a custom .NET CLI has been installed, `dotnet --info` has already been executed
5050
// to verify the installation.
51-
var ret = dotNetPath is null ? GetInfoCommand(builder.Actions, dotNetPath, environment) : BuildScript.Success;
51+
var ret = dotNetPath is null ? DotNet.InfoScript(builder.Actions, DotNetCommand(builder.Actions, dotNetPath), environment, builder.Logger) : BuildScript.Success;
5252
foreach (var projectOrSolution in builder.ProjectsOrSolutionsToBuild)
5353
{
5454
var cleanCommand = GetCleanCommand(builder.Actions, dotNetPath, environment);
@@ -111,14 +111,6 @@ public static BuildScript WithDotNet(IAutobuilder<AutobuildOptionsShared> builde
111111
private static string DotNetCommand(IBuildActions actions, string? dotNetPath) =>
112112
dotNetPath is not null ? actions.PathCombine(dotNetPath, "dotnet") : "dotnet";
113113

114-
private static BuildScript GetInfoCommand(IBuildActions actions, string? dotNetPath, IDictionary<string, string>? environment)
115-
{
116-
var info = new CommandBuilder(actions, null, environment).
117-
RunCommand(DotNetCommand(actions, dotNetPath)).
118-
Argument("--info");
119-
return info.Script;
120-
}
121-
122114
private static CommandBuilder GetCleanCommand(IBuildActions actions, string? dotNetPath, IDictionary<string, string>? environment)
123115
{
124116
var clean = new CommandBuilder(actions, null, environment).

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections.ObjectModel;
44
using System.IO;
55
using System.Linq;
6+
using System.Threading;
67
using Newtonsoft.Json.Linq;
78

89
using Semmle.Util;
@@ -36,12 +37,29 @@ private DotNet(ILogger logger, string? dotNetPath, TemporaryDirectory tempWorkin
3637

3738
public static IDotNet Make(ILogger logger, string? dotNetPath, TemporaryDirectory tempWorkingDirectory, DependabotProxy? dependabotProxy) => new DotNet(logger, dotNetPath, tempWorkingDirectory, dependabotProxy);
3839

40+
private static void HandleRetryExitCode143(string dotnet, int attempt, ILogger logger)
41+
{
42+
logger.LogWarning($"Running '{dotnet} --info' failed with exit code 143. Retrying...");
43+
var sleep = Math.Pow(2, attempt) * 1000;
44+
Thread.Sleep((int)sleep);
45+
}
46+
3947
private void Info()
4048
{
41-
var res = dotnetCliInvoker.RunCommand("--info", silent: false);
42-
if (!res)
49+
// Allow up to three retry attempts to run `dotnet --info`, to mitigate transient issues
50+
for (int attempt = 0; attempt < 4; attempt++)
4351
{
44-
throw new Exception($"{dotnetCliInvoker.Exec} --info failed.");
52+
var exitCode = dotnetCliInvoker.RunCommandExitCode("--info", silent: false);
53+
switch (exitCode)
54+
{
55+
case 0:
56+
return;
57+
case 143 when attempt < 3:
58+
HandleRetryExitCode143(dotnetCliInvoker.Exec, attempt, logger);
59+
break;
60+
default:
61+
throw new Exception($"{dotnetCliInvoker.Exec} --info failed with exit code {exitCode}.");
62+
}
4563
}
4664
}
4765

@@ -193,6 +211,34 @@ private static BuildScript DownloadDotNet(IBuildActions actions, ILogger logger,
193211
return BuildScript.Failure;
194212
}
195213

214+
/// <summary>
215+
/// Returns a script for running `dotnet --info`, with retries on exit code 143.
216+
/// </summary>
217+
public static BuildScript InfoScript(IBuildActions actions, string dotnet, IDictionary<string, string>? environment, ILogger logger)
218+
{
219+
var info = new CommandBuilder(actions, null, environment).
220+
RunCommand(dotnet).
221+
Argument("--info");
222+
var script = info.Script;
223+
for (int attempt = 0; attempt < 4; attempt++)
224+
{
225+
script = BuildScript.Bind(script, ret =>
226+
{
227+
switch (ret)
228+
{
229+
case 0:
230+
return BuildScript.Success;
231+
case 143 when attempt < 3:
232+
HandleRetryExitCode143(dotnet, attempt, logger);
233+
return info.Script;
234+
default:
235+
return BuildScript.Failure;
236+
}
237+
});
238+
}
239+
return script;
240+
}
241+
196242
/// <summary>
197243
/// Returns a script for downloading specific .NET SDK versions, if the
198244
/// versions are not already installed.
@@ -292,9 +338,7 @@ BuildScript GetInstall(string pwsh) =>
292338
};
293339
}
294340

295-
var dotnetInfo = new CommandBuilder(actions, environment: MinimalEnvironment).
296-
RunCommand(actions.PathCombine(path, "dotnet")).
297-
Argument("--info").Script;
341+
var dotnetInfo = InfoScript(actions, actions.PathCombine(path, "dotnet"), MinimalEnvironment.ToDictionary(), logger);
298342

299343
Func<string, BuildScript> getInstallAndVerify = version =>
300344
// run `dotnet --info` after install, to check that it executes successfully

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNetCliInvoker.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,21 @@ private ProcessStartInfo MakeDotnetStartInfo(string args, string? workingDirecto
5757
return startInfo;
5858
}
5959

60-
private bool RunCommandAux(string args, string? workingDirectory, out IList<string> output, bool silent)
60+
private int RunCommandExitCodeAux(string args, string? workingDirectory, out IList<string> output, out string dirLog, bool silent)
6161
{
62-
var dirLog = string.IsNullOrWhiteSpace(workingDirectory) ? "" : $" in {workingDirectory}";
62+
dirLog = string.IsNullOrWhiteSpace(workingDirectory) ? "" : $" in {workingDirectory}";
6363
var pi = MakeDotnetStartInfo(args, workingDirectory);
6464
var threadId = Environment.CurrentManagedThreadId;
6565
void onOut(string s) => logger.Log(silent ? Severity.Debug : Severity.Info, s, threadId);
6666
void onError(string s) => logger.LogError(s, threadId);
6767
logger.LogInfo($"Running '{Exec} {args}'{dirLog}");
6868
var exitCode = pi.ReadOutput(out output, onOut, onError);
69+
return exitCode;
70+
}
71+
72+
private bool RunCommandAux(string args, string? workingDirectory, out IList<string> output, bool silent)
73+
{
74+
var exitCode = RunCommandExitCodeAux(args, workingDirectory, out output, out var dirLog, silent);
6975
if (exitCode != 0)
7076
{
7177
logger.LogError($"Command '{Exec} {args}'{dirLog} failed with exit code {exitCode}");
@@ -77,6 +83,9 @@ private bool RunCommandAux(string args, string? workingDirectory, out IList<stri
7783
public bool RunCommand(string args, bool silent = true) =>
7884
RunCommandAux(args, null, out _, silent);
7985

86+
public int RunCommandExitCode(string args, bool silent = true) =>
87+
RunCommandExitCodeAux(args, null, out _, out _, silent);
88+
8089
public bool RunCommand(string args, out IList<string> output, bool silent = true) =>
8190
RunCommandAux(args, null, out output, silent);
8291

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDotNetCliInvoker.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ internal interface IDotNetCliInvoker
3030
/// </summary>
3131
bool RunCommand(string args, bool silent = true);
3232

33+
/// <summary>
34+
/// Execute `dotnet <paramref name="args"/>` and return the exit code.
35+
/// If `silent` is true the output of the command is logged as `debug` otherwise as `info`.
36+
/// </summary>
37+
int RunCommandExitCode(string args, bool silent = true);
38+
3339
/// <summary>
3440
/// Execute `dotnet <paramref name="args"/>` and return true if the command succeeded, otherwise false.
3541
/// The output of the command is returned in `output`.

csharp/extractor/Semmle.Extraction.Tests/DotNet.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ internal class DotNetCliInvokerStub : IDotNetCliInvoker
1212
private string lastArgs = "";
1313
public string WorkingDirectory { get; private set; } = "";
1414
public bool Success { get; set; } = true;
15+
public int ExitCode { get; set; } = 0;
1516

1617
public DotNetCliInvokerStub(IList<string> output)
1718
{
@@ -26,6 +27,12 @@ public bool RunCommand(string args, bool silent)
2627
return Success;
2728
}
2829

30+
public int RunCommandExitCode(string args, bool silent)
31+
{
32+
lastArgs = args;
33+
return ExitCode;
34+
}
35+
2936
public bool RunCommand(string args, out IList<string> output, bool silent)
3037
{
3138
lastArgs = args;
@@ -83,7 +90,7 @@ public void TestDotnetInfo()
8390
public void TestDotnetInfoFailure()
8491
{
8592
// Setup
86-
var dotnetCliInvoker = new DotNetCliInvokerStub(new List<string>()) { Success = false };
93+
var dotnetCliInvoker = new DotNetCliInvokerStub(new List<string>()) { ExitCode = 1 };
8794

8895
// Execute
8996
try
@@ -94,7 +101,7 @@ public void TestDotnetInfoFailure()
94101
// Verify
95102
catch (Exception e)
96103
{
97-
Assert.Equal("dotnet --info failed.", e.Message);
104+
Assert.Equal("dotnet --info failed with exit code 1.", e.Message);
98105
return;
99106
}
100107
Assert.Fail("Expected exception");

0 commit comments

Comments
 (0)