From 4a6ae216a4a9228a7eab41c2a158555f24bf504c Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 26 Nov 2025 13:31:45 +0100 Subject: [PATCH 1/4] C#: Gracefully handle non-zero exitcodes for dotnet --info. --- .../Semmle.Autobuild.CSharp/DotNetRule.cs | 10 +--- .../DotNet.cs | 56 +++++++++++++++++-- .../DotNetCliInvoker.cs | 13 ++++- .../IDotNetCliInvoker.cs | 6 ++ .../Semmle.Extraction.Tests/DotNet.cs | 11 +++- 5 files changed, 77 insertions(+), 19 deletions(-) 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..aea0aeffa672 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 three retry attempts 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); + break; + default: + throw new Exception($"{dotnetCliInvoker.Exec} --info failed with exit code {exitCode}."); + } } } @@ -193,6 +211,34 @@ 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 (int attempt = 0; attempt < 4; attempt++) + { + script = BuildScript.Bind(script, ret => + { + switch (ret) + { + case 0: + return BuildScript.Success; + case 143 when attempt < 3: + HandleRetryExitCode143(dotnet, attempt, 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 +338,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"); From 1d9b88de8b49c108838f7c0556d848c12951e77b Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Tue, 2 Dec 2025 14:59:45 +0100 Subject: [PATCH 2/4] C#: Comment back in the .NET 10 tests. --- csharp/ql/integration-tests/all-platforms/dotnet_10/test.py | 2 -- 1 file changed, 2 deletions(-) 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") From 3197b50da7eb708dc99f88c9a7bc7a1a63e8aad0 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Tue, 2 Dec 2025 16:16:29 +0100 Subject: [PATCH 3/4] C#: Address review comments. --- .../DotNet.cs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs index aea0aeffa672..3bfc9a8dc883 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs @@ -46,7 +46,7 @@ private static void HandleRetryExitCode143(string dotnet, int attempt, ILogger l private void Info() { - // Allow up to three retry attempts to run `dotnet --info`, to mitigate transient issues + // 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++) { var exitCode = dotnetCliInvoker.RunCommandExitCode("--info", silent: false); @@ -220,21 +220,22 @@ public static BuildScript InfoScript(IBuildActions actions, string dotnet, IDict RunCommand(dotnet). Argument("--info"); var script = info.Script; - for (int attempt = 0; attempt < 4; attempt++) + 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 attempt < 3: - HandleRetryExitCode143(dotnet, attempt, logger); - return info.Script; - default: - return BuildScript.Failure; - } - }); + 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; } From c1793ab529d46fdeebec767f8880b383fb73a66b Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 3 Dec 2025 11:48:32 +0100 Subject: [PATCH 4/4] C#: Code quality improvement. --- .../Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs index 3bfc9a8dc883..635b901397b1 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs @@ -56,7 +56,7 @@ private void Info() return; case 143 when attempt < 3: HandleRetryExitCode143(dotnetCliInvoker.Exec, attempt, logger); - break; + continue; default: throw new Exception($"{dotnetCliInvoker.Exec} --info failed with exit code {exitCode}."); }