From 6a87755ff1bec8d539d66448db1d80fd105d37fd Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 5 Jul 2023 13:36:44 +0200 Subject: [PATCH] C#: Use dotnet --list-runtimes to find runtime locations. --- .../BuildAnalysis.cs | 2 +- .../DotNet.cs | 36 ++++- .../Runtime.cs | 145 +++++++++++++----- 3 files changed, 139 insertions(+), 44 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.Standalone/BuildAnalysis.cs b/csharp/extractor/Semmle.Extraction.CSharp.Standalone/BuildAnalysis.cs index ad501c9e758..8491f9f8c81 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.Standalone/BuildAnalysis.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.Standalone/BuildAnalysis.cs @@ -66,7 +66,7 @@ namespace Semmle.BuildAnalyser // Find DLLs in the .Net Framework if (options.ScanNetFrameworkDlls) { - var runtimeLocation = Runtime.GetRuntime(options.UseSelfContainedDotnet); + var runtimeLocation = new Runtime(dotnet).GetRuntime(options.UseSelfContainedDotnet); progressMonitor.Log(Util.Logging.Severity.Debug, $"Runtime location selected: {runtimeLocation}"); dllDirNames.Add(runtimeLocation); } diff --git a/csharp/extractor/Semmle.Extraction.CSharp.Standalone/DotNet.cs b/csharp/extractor/Semmle.Extraction.CSharp.Standalone/DotNet.cs index dbe3b2c4a1e..5bfa0fc759c 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.Standalone/DotNet.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.Standalone/DotNet.cs @@ -1,5 +1,7 @@ -using System; +using System; +using System.Collections.Generic; using System.Diagnostics; +using Semmle.Util; namespace Semmle.BuildAnalyser { @@ -8,6 +10,7 @@ namespace Semmle.BuildAnalyser /// internal class DotNet { + private const string dotnet = "dotnet"; private readonly ProgressMonitor progressMonitor; public DotNet(ProgressMonitor progressMonitor) @@ -19,26 +22,26 @@ namespace Semmle.BuildAnalyser private void Info() { // TODO: make sure the below `dotnet` version is matching the one specified in global.json - progressMonitor.RunningProcess("dotnet --info"); - using var proc = Process.Start("dotnet", "--info"); + progressMonitor.RunningProcess($"{dotnet} --info"); + using var proc = Process.Start(dotnet, "--info"); proc.WaitForExit(); var ret = proc.ExitCode; if (ret != 0) { - progressMonitor.CommandFailed("dotnet", "--info", ret); - throw new Exception($"dotnet --info failed with exit code {ret}."); + progressMonitor.CommandFailed(dotnet, "--info", ret); + throw new Exception($"{dotnet} --info failed with exit code {ret}."); } } private bool RunCommand(string args) { - progressMonitor.RunningProcess($"dotnet {args}"); - using var proc = Process.Start("dotnet", args); + progressMonitor.RunningProcess($"{dotnet} {args}"); + using var proc = Process.Start(dotnet, args); proc.WaitForExit(); if (proc.ExitCode != 0) { - progressMonitor.CommandFailed("dotnet", args, proc.ExitCode); + progressMonitor.CommandFailed(dotnet, args, proc.ExitCode); return false; } @@ -62,5 +65,22 @@ namespace Semmle.BuildAnalyser var args = $"add \"{folder}\" package \"{package}\" --no-restore"; return RunCommand(args); } + + public IList GetListedRuntimes() + { + var args = "--list-runtimes"; + var pi = new ProcessStartInfo(dotnet, args) + { + RedirectStandardOutput = true, + UseShellExecute = false + }; + var exitCode = pi.ReadOutput(out var runtimes); + if (exitCode != 0) + { + progressMonitor.CommandFailed(dotnet, args, exitCode); + return new List(); + } + return runtimes; + } } } diff --git a/csharp/extractor/Semmle.Extraction.CSharp.Standalone/Runtime.cs b/csharp/extractor/Semmle.Extraction.CSharp.Standalone/Runtime.cs index 4b76efc3d65..d491a55e8ec 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.Standalone/Runtime.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.Standalone/Runtime.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Runtime.InteropServices; using System.IO; using System.Linq; +using System.Text.RegularExpressions; +using Semmle.BuildAnalyser; using Semmle.Util; namespace Semmle.Extraction.CSharp.Standalone @@ -10,33 +12,97 @@ namespace Semmle.Extraction.CSharp.Standalone /// /// Locates .NET Runtimes. /// - internal static class Runtime + internal partial class Runtime { + private readonly DotNet dotNet; + public Runtime(DotNet dotNet) => this.dotNet = dotNet; + + private sealed class Version : IComparable + { + private readonly string Dir; + public int Major { get; } + public int Minor { get; } + public int Patch { get; } + + + public string FullPath => Path.Combine(Dir, this.ToString()); + + + public Version(string version, string dir) + { + var parts = version.Split('.'); + Major = int.Parse(parts[0]); + Minor = int.Parse(parts[1]); + Patch = int.Parse(parts[2]); + Dir = dir; + } + + public int CompareTo(Version? other) => + other is null ? 1 : GetHashCode().CompareTo(other.GetHashCode()); + + public override bool Equals(object? obj) => + obj is not null && obj is Version other && other.FullPath == FullPath; + + public override int GetHashCode() => + (Major * 1000 + Minor) * 1000 + Patch; + + public override string ToString() => + $"{Major}.{Minor}.{Patch}"; + } + private static string ExecutingRuntime => RuntimeEnvironment.GetRuntimeDirectory(); - /// - /// Locates .NET Core Runtimes. - /// - private static IEnumerable CoreRuntimes + private static readonly string NetCoreApp = "Microsoft.NETCore.App"; + private static readonly string AspNetCoreApp = "Microsoft.AspNetCore.App"; + + private static void AddOrUpdate(Dictionary dict, string framework, Version version) { - get + if (!dict.TryGetValue(framework, out var existing) || existing.CompareTo(version) < 0) { - var dotnetPath = FileUtils.FindProgramOnPath(Win32.IsWindows() ? "dotnet.exe" : "dotnet"); - var dotnetDirs = dotnetPath is not null - ? new[] { dotnetPath } - : new[] { "/usr/share/dotnet", @"C:\Program Files\dotnet" }; - var coreDirs = dotnetDirs.Select(d => Path.Combine(d, "shared", "Microsoft.NETCore.App")); - - var dir = coreDirs.FirstOrDefault(Directory.Exists); - if (dir is not null) - { - return Directory.EnumerateDirectories(dir).OrderByDescending(Path.GetFileName); - } - - return Enumerable.Empty(); + dict[framework] = version; } } + [GeneratedRegex(@"^(\S+)\s(\d+\.\d+\.\d+)\s\[(\S+)\]$")] + private static partial Regex RuntimeRegex(); + + /// + /// Parses the output of `dotnet --list-runtimes` to get a map from a runtime to the location of + /// the newest version of the runtime. + /// It is assume that the format of a listed runtime is something like: + /// Microsoft.NETCore.App 7.0.2 [/usr/share/dotnet/shared/Microsoft.NETCore.App] + /// + private static Dictionary ParseRuntimes(IList listed) + { + // Parse listed runtimes. + var runtimes = new Dictionary(); + listed.ForEach(r => + { + var match = RuntimeRegex().Match(r); + if (match.Success) + { + AddOrUpdate(runtimes, match.Groups[1].Value, new Version(match.Groups[2].Value, match.Groups[3].Value)); + } + }); + + return runtimes; + } + + private Dictionary GetNewestRuntimes() + { + try + { + var listed = dotNet.GetListedRuntimes(); + return ParseRuntimes(listed); + } + catch (Exception ex) + when (ex is System.ComponentModel.Win32Exception || ex is FileNotFoundException) + { + return new Dictionary(); + } + } + + /// /// Locates .NET Desktop Runtimes. /// This includes Mono and Microsoft.NET. @@ -69,24 +135,33 @@ namespace Semmle.Extraction.CSharp.Standalone } } + private IEnumerable GetRuntimes() + { + // Gets the newest version of the installed runtimes. + var newestRuntimes = GetNewestRuntimes(); + + // Location of the newest .NET Core Runtime. + if (newestRuntimes.TryGetValue(NetCoreApp, out var netCoreApp)) + { + yield return netCoreApp.FullPath; + } + + // Location of the newest ASP.NET Core Runtime. + if (newestRuntimes.TryGetValue(AspNetCoreApp, out var aspNetCoreApp)) + { + yield return aspNetCoreApp.FullPath; + } + + foreach (var r in DesktopRuntimes) + yield return r; + + // A bad choice if it's the self-contained runtime distributed in codeql dist. + yield return ExecutingRuntime; + } + /// /// Gets the .NET runtime location to use for extraction /// - public static string GetRuntime(bool useSelfContained) => useSelfContained ? ExecutingRuntime : Runtimes.First(); - - private static IEnumerable Runtimes - { - get - { - foreach (var r in CoreRuntimes) - yield return r; - - foreach (var r in DesktopRuntimes) - yield return r; - - // A bad choice if it's the self-contained runtime distributed in codeql dist. - yield return ExecutingRuntime; - } - } + public string GetRuntime(bool useSelfContained) => useSelfContained ? ExecutingRuntime : GetRuntimes().First(); } }