diff --git a/csharp/autobuilder/Semmle.Autobuild.CSharp/DotNetRule.cs b/csharp/autobuilder/Semmle.Autobuild.CSharp/DotNetRule.cs index a396ab751eaf..e07f75928872 100644 --- a/csharp/autobuilder/Semmle.Autobuild.CSharp/DotNetRule.cs +++ b/csharp/autobuilder/Semmle.Autobuild.CSharp/DotNetRule.cs @@ -48,7 +48,7 @@ public BuildScript Analyse(IAutobuilder builder, bool au { // 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 @@ public static BuildScript WithDotNet(IAutobuilder builde 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? 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? environment) { var clean = new CommandBuilder(actions, null, environment). diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs index 7bc792384154..635b901397b1 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; +using System.Threading; using Newtonsoft.Json.Linq; using Semmle.Util; @@ -36,12 +37,29 @@ private DotNet(ILogger logger, string? dotNetPath, TemporaryDirectory tempWorkin 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 @@ private static BuildScript DownloadDotNet(IBuildActions actions, ILogger logger, return BuildScript.Failure; } + /// + /// Returns a script for running `dotnet --info`, with retries on exit code 143. + /// + public static BuildScript InfoScript(IBuildActions actions, string dotnet, IDictionary? 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; + } + /// /// Returns a script for downloading specific .NET SDK versions, if the /// versions are not already installed. @@ -292,9 +339,7 @@ BuildScript GetInstall(string pwsh) => }; } - 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 getInstallAndVerify = version => // run `dotnet --info` after install, to check that it executes successfully diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNetCliInvoker.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNetCliInvoker.cs index 45f69a1fdfcd..4c4e789973ca 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNetCliInvoker.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNetCliInvoker.cs @@ -57,15 +57,21 @@ private ProcessStartInfo MakeDotnetStartInfo(string args, string? workingDirecto return startInfo; } - private bool RunCommandAux(string args, string? workingDirectory, out IList output, bool silent) + private int RunCommandExitCodeAux(string args, string? workingDirectory, out IList 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 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 @@ private bool RunCommandAux(string args, string? workingDirectory, out IList 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 output, bool silent = true) => RunCommandAux(args, null, out output, silent); diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDotNetCliInvoker.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDotNetCliInvoker.cs index 3a599afe96d7..61d0ea4260db 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDotNetCliInvoker.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDotNetCliInvoker.cs @@ -30,6 +30,12 @@ internal interface IDotNetCliInvoker /// bool RunCommand(string args, bool silent = true); + /// + /// Execute `dotnet ` and return the exit code. + /// If `silent` is true the output of the command is logged as `debug` otherwise as `info`. + /// + int RunCommandExitCode(string args, bool silent = true); + /// /// Execute `dotnet ` and return true if the command succeeded, otherwise false. /// The output of the command is returned in `output`. diff --git a/csharp/extractor/Semmle.Extraction.Tests/DotNet.cs b/csharp/extractor/Semmle.Extraction.Tests/DotNet.cs index 904ad04ce82f..a2996497e005 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/DotNet.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/DotNet.cs @@ -12,6 +12,7 @@ internal class DotNetCliInvokerStub : IDotNetCliInvoker private string lastArgs = ""; public string WorkingDirectory { get; private set; } = ""; public bool Success { get; set; } = true; + public int ExitCode { get; set; } = 0; public DotNetCliInvokerStub(IList output) { @@ -26,6 +27,12 @@ public bool RunCommand(string args, bool silent) return Success; } + public int RunCommandExitCode(string args, bool silent) + { + lastArgs = args; + return ExitCode; + } + public bool RunCommand(string args, out IList output, bool silent) { lastArgs = args; @@ -83,7 +90,7 @@ public void TestDotnetInfo() public void TestDotnetInfoFailure() { // Setup - var dotnetCliInvoker = new DotNetCliInvokerStub(new List()) { Success = false }; + var dotnetCliInvoker = new DotNetCliInvokerStub(new List()) { ExitCode = 1 }; // Execute try @@ -94,7 +101,7 @@ public void TestDotnetInfoFailure() // 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"); diff --git a/csharp/ql/integration-tests/all-platforms/dotnet_10/test.py b/csharp/ql/integration-tests/all-platforms/dotnet_10/test.py index d34be2b8b506..1f1fe52a4e5e 100644 --- a/csharp/ql/integration-tests/all-platforms/dotnet_10/test.py +++ b/csharp/ql/integration-tests/all-platforms/dotnet_10/test.py @@ -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")