Merge pull request #20936 from michaelnebel/csharp/nocrashdotnetinfo

C#: Retry logic for `dotnet --info` when it fails with exit code 143.
This commit is contained in:
Michael Nebel
2025-12-04 09:13:12 +01:00
committed by GitHub
6 changed files with 78 additions and 21 deletions

View File

@@ -48,7 +48,7 @@ namespace Semmle.Autobuild.CSharp
{
// When a custom .NET CLI has been installed, `dotnet --info` has already been executed
// to verify the installation.
var ret = dotNetPath is null ? GetInfoCommand(builder.Actions, dotNetPath, environment) : BuildScript.Success;
var ret = dotNetPath is null ? DotNet.InfoScript(builder.Actions, DotNetCommand(builder.Actions, dotNetPath), environment, builder.Logger) : BuildScript.Success;
foreach (var projectOrSolution in builder.ProjectsOrSolutionsToBuild)
{
var cleanCommand = GetCleanCommand(builder.Actions, dotNetPath, environment);
@@ -111,14 +111,6 @@ namespace Semmle.Autobuild.CSharp
private static string DotNetCommand(IBuildActions actions, string? dotNetPath) =>
dotNetPath is not null ? actions.PathCombine(dotNetPath, "dotnet") : "dotnet";
private static BuildScript GetInfoCommand(IBuildActions actions, string? dotNetPath, IDictionary<string, string>? environment)
{
var info = new CommandBuilder(actions, null, environment).
RunCommand(DotNetCommand(actions, dotNetPath)).
Argument("--info");
return info.Script;
}
private static CommandBuilder GetCleanCommand(IBuildActions actions, string? dotNetPath, IDictionary<string, string>? environment)
{
var clean = new CommandBuilder(actions, null, environment).

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using Newtonsoft.Json.Linq;
using Semmle.Util;
@@ -36,12 +37,29 @@ namespace Semmle.Extraction.CSharp.DependencyFetching
public static IDotNet Make(ILogger logger, string? dotNetPath, TemporaryDirectory tempWorkingDirectory, DependabotProxy? dependabotProxy) => new DotNet(logger, dotNetPath, tempWorkingDirectory, dependabotProxy);
private static void HandleRetryExitCode143(string dotnet, int attempt, ILogger logger)
{
logger.LogWarning($"Running '{dotnet} --info' failed with exit code 143. Retrying...");
var sleep = Math.Pow(2, attempt) * 1000;
Thread.Sleep((int)sleep);
}
private void Info()
{
var res = dotnetCliInvoker.RunCommand("--info", silent: false);
if (!res)
// Allow up to four attempts (with up to three retries) to run `dotnet --info`, to mitigate transient issues
for (int attempt = 0; attempt < 4; attempt++)
{
throw new Exception($"{dotnetCliInvoker.Exec} --info failed.");
var exitCode = dotnetCliInvoker.RunCommandExitCode("--info", silent: false);
switch (exitCode)
{
case 0:
return;
case 143 when attempt < 3:
HandleRetryExitCode143(dotnetCliInvoker.Exec, attempt, logger);
continue;
default:
throw new Exception($"{dotnetCliInvoker.Exec} --info failed with exit code {exitCode}.");
}
}
}
@@ -193,6 +211,35 @@ namespace Semmle.Extraction.CSharp.DependencyFetching
return BuildScript.Failure;
}
/// <summary>
/// Returns a script for running `dotnet --info`, with retries on exit code 143.
/// </summary>
public static BuildScript InfoScript(IBuildActions actions, string dotnet, IDictionary<string, string>? environment, ILogger logger)
{
var info = new CommandBuilder(actions, null, environment).
RunCommand(dotnet).
Argument("--info");
var script = info.Script;
for (var attempt = 0; attempt < 4; attempt++)
{
var attemptCopy = attempt; // Capture in local variable
script = BuildScript.Bind(script, ret =>
{
switch (ret)
{
case 0:
return BuildScript.Success;
case 143 when attemptCopy < 3:
HandleRetryExitCode143(dotnet, attemptCopy, logger);
return info.Script;
default:
return BuildScript.Failure;
}
});
}
return script;
}
/// <summary>
/// Returns a script for downloading specific .NET SDK versions, if the
/// versions are not already installed.
@@ -292,9 +339,7 @@ namespace Semmle.Extraction.CSharp.DependencyFetching
};
}
var dotnetInfo = new CommandBuilder(actions, environment: MinimalEnvironment).
RunCommand(actions.PathCombine(path, "dotnet")).
Argument("--info").Script;
var dotnetInfo = InfoScript(actions, actions.PathCombine(path, "dotnet"), MinimalEnvironment.ToDictionary(), logger);
Func<string, BuildScript> getInstallAndVerify = version =>
// run `dotnet --info` after install, to check that it executes successfully

View File

@@ -57,15 +57,21 @@ namespace Semmle.Extraction.CSharp.DependencyFetching
return startInfo;
}
private bool RunCommandAux(string args, string? workingDirectory, out IList<string> output, bool silent)
private int RunCommandExitCodeAux(string args, string? workingDirectory, out IList<string> output, out string dirLog, bool silent)
{
var dirLog = string.IsNullOrWhiteSpace(workingDirectory) ? "" : $" in {workingDirectory}";
dirLog = string.IsNullOrWhiteSpace(workingDirectory) ? "" : $" in {workingDirectory}";
var pi = MakeDotnetStartInfo(args, workingDirectory);
var threadId = Environment.CurrentManagedThreadId;
void onOut(string s) => logger.Log(silent ? Severity.Debug : Severity.Info, s, threadId);
void onError(string s) => logger.LogError(s, threadId);
logger.LogInfo($"Running '{Exec} {args}'{dirLog}");
var exitCode = pi.ReadOutput(out output, onOut, onError);
return exitCode;
}
private bool RunCommandAux(string args, string? workingDirectory, out IList<string> output, bool silent)
{
var exitCode = RunCommandExitCodeAux(args, workingDirectory, out output, out var dirLog, silent);
if (exitCode != 0)
{
logger.LogError($"Command '{Exec} {args}'{dirLog} failed with exit code {exitCode}");
@@ -77,6 +83,9 @@ namespace Semmle.Extraction.CSharp.DependencyFetching
public bool RunCommand(string args, bool silent = true) =>
RunCommandAux(args, null, out _, silent);
public int RunCommandExitCode(string args, bool silent = true) =>
RunCommandExitCodeAux(args, null, out _, out _, silent);
public bool RunCommand(string args, out IList<string> output, bool silent = true) =>
RunCommandAux(args, null, out output, silent);

View File

@@ -30,6 +30,12 @@ namespace Semmle.Extraction.CSharp.DependencyFetching
/// </summary>
bool RunCommand(string args, bool silent = true);
/// <summary>
/// Execute `dotnet <paramref name="args"/>` and return the exit code.
/// If `silent` is true the output of the command is logged as `debug` otherwise as `info`.
/// </summary>
int RunCommandExitCode(string args, bool silent = true);
/// <summary>
/// Execute `dotnet <paramref name="args"/>` and return true if the command succeeded, otherwise false.
/// The output of the command is returned in `output`.

View File

@@ -12,6 +12,7 @@ namespace Semmle.Extraction.Tests
private string lastArgs = "";
public string WorkingDirectory { get; private set; } = "";
public bool Success { get; set; } = true;
public int ExitCode { get; set; } = 0;
public DotNetCliInvokerStub(IList<string> output)
{
@@ -26,6 +27,12 @@ namespace Semmle.Extraction.Tests
return Success;
}
public int RunCommandExitCode(string args, bool silent)
{
lastArgs = args;
return ExitCode;
}
public bool RunCommand(string args, out IList<string> output, bool silent)
{
lastArgs = args;
@@ -83,7 +90,7 @@ namespace Semmle.Extraction.Tests
public void TestDotnetInfoFailure()
{
// Setup
var dotnetCliInvoker = new DotNetCliInvokerStub(new List<string>()) { Success = false };
var dotnetCliInvoker = new DotNetCliInvokerStub(new List<string>()) { ExitCode = 1 };
// Execute
try
@@ -94,7 +101,7 @@ namespace Semmle.Extraction.Tests
// Verify
catch (Exception e)
{
Assert.Equal("dotnet --info failed.", e.Message);
Assert.Equal("dotnet --info failed with exit code 1.", e.Message);
return;
}
Assert.Fail("Expected exception");

View File

@@ -1,9 +1,7 @@
import pytest
@pytest.mark.skip(reason=".NET 10 info command crashes")
def test1(codeql, csharp):
codeql.database.create()
@pytest.mark.skip(reason=".NET 10 info command crashes")
def test2(codeql, csharp):
codeql.database.create(build_mode="none")