-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Issue #33697] New standalone info
command
#36943
base: release/8.0.2xx
Are you sure you want to change the base?
Changes from all commits
25e732a
d9521c0
07dee66
fff2401
c4820be
f2d7cce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.CommandLine; | ||
using Microsoft.DotNet.Cli; | ||
using System.Text.Json; | ||
using Microsoft.DotNet.Cli.Utils; | ||
|
||
using LocalizableStrings = Microsoft.DotNet.Cli.Utils.LocalizableStrings; | ||
using RuntimeEnvironment = Microsoft.DotNet.Cli.Utils.RuntimeEnvironment; | ||
|
||
namespace Microsoft.DotNet.Tools.Info | ||
{ | ||
public class InfoCommand | ||
{ | ||
private readonly ParseResult _parseResult; | ||
|
||
public InfoCommand(ParseResult parseResult) | ||
{ | ||
_parseResult = parseResult; | ||
} | ||
|
||
public static int Run(ParseResult result) | ||
{ | ||
result.HandleDebugSwitch(); | ||
var format = result.GetValue(InfoCommandParser.FormatOption); | ||
if (format != InfoCommandParser.FormatOptions.json) { | ||
PrintInfo(); | ||
} else { | ||
// To be implemented | ||
PrintJsonInfo(); | ||
} | ||
return 0; | ||
} | ||
|
||
public static void PrintVersion() | ||
{ | ||
Reporter.Output.WriteLine(Product.Version); | ||
} | ||
|
||
public static void PrintInfo() | ||
{ | ||
DotnetVersionFile versionFile = DotnetFiles.VersionFileObject; | ||
var commitSha = versionFile.CommitSha ?? "N/A"; | ||
Reporter.Output.WriteLine($"{LocalizableStrings.DotNetSdkInfoLabel}"); | ||
Reporter.Output.WriteLine($" Version: {Product.Version}"); | ||
Reporter.Output.WriteLine($" Commit: {commitSha}"); | ||
Reporter.Output.WriteLine($" Workload version: {WorkloadCommandParser.GetWorkloadsVersion()}"); | ||
Reporter.Output.WriteLine(); | ||
Reporter.Output.WriteLine($"{LocalizableStrings.DotNetRuntimeInfoLabel}"); | ||
Reporter.Output.WriteLine($" OS Name: {RuntimeEnvironment.OperatingSystem}"); | ||
Reporter.Output.WriteLine($" OS Version: {RuntimeEnvironment.OperatingSystemVersion}"); | ||
Reporter.Output.WriteLine($" OS Platform: {RuntimeEnvironment.OperatingSystemPlatform}"); | ||
Reporter.Output.WriteLine($" RID: {GetDisplayRid(versionFile)}"); | ||
Reporter.Output.WriteLine($" Base Path: {AppContext.BaseDirectory}"); | ||
PrintWorkloadsInfo(); | ||
} | ||
|
||
private static void PrintWorkloadsInfo() | ||
{ | ||
Reporter.Output.WriteLine(); | ||
Reporter.Output.WriteLine($"{LocalizableStrings.DotnetWorkloadInfoLabel}"); | ||
WorkloadCommandParser.ShowWorkloadsInfo(); | ||
} | ||
|
||
private static string GetDisplayRid(DotnetVersionFile versionFile) | ||
{ | ||
FrameworkDependencyFile fxDepsFile = new(); | ||
|
||
string currentRid = RuntimeInformation.RuntimeIdentifier; | ||
|
||
// if the current RID isn't supported by the shared framework, display the RID the CLI was | ||
// built with instead, so the user knows which RID they should put in their "runtimes" section. | ||
return fxDepsFile.IsRuntimeSupported(currentRid) ? | ||
currentRid : | ||
versionFile.BuildRid; | ||
} | ||
|
||
public class RuntimeEnvironmentInfo | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd put these classes that define the JSON model in a separate file. |
||
{ | ||
public string Name { get; set; } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use JsonPropertyName to set the camelCase name style defined in baronfel's original issue. |
||
public string Version { get; set; } | ||
public string Platform { get; set; } | ||
public string Rid { get; set; } | ||
} | ||
|
||
public class SdkInfo | ||
{ | ||
public string Commit { get; set; } | ||
public string Version { get; set; } | ||
} | ||
public class HostInfo | ||
{ | ||
public string Arch { get; set; } | ||
public string Commit { get; set; } | ||
public string Version { get; set; } | ||
} | ||
|
||
public class InstalledRuntime | ||
{ | ||
public string Name { get; set; } | ||
public string Path { get; set; } | ||
public string Version { get; set; } | ||
} | ||
|
||
public class OtherArchInfo | ||
{ | ||
public string Arch { get; set; } | ||
public string BasePath { get; set; } | ||
} | ||
|
||
public class WorkloadManifest | ||
{ | ||
public string InstallType { get; set; } | ||
public string Path { get; set; } | ||
public string Version { get; set; } | ||
} | ||
|
||
public class Workload | ||
{ | ||
public List<InstallSource> InstallSources { get; set; } | ||
public WorkloadManifest Manifest { get; set; } | ||
public string Name { get; set; } | ||
} | ||
|
||
public class InstallSource | ||
{ | ||
public string Name { get; set; } | ||
public string Version { get; set; } | ||
} | ||
|
||
public class DotNetInfo | ||
{ | ||
public string BasePath { get; set; } | ||
public Dictionary<string, string> EnvVars { get; set; } | ||
public string GlobalJson { get; set; } | ||
public HostInfo Host { get; set; } | ||
public List<InstalledRuntime> InstalledRuntimes { get; set; } | ||
public List<OtherArchInfo> OtherArch { get; set; } | ||
public RuntimeEnvironmentInfo RuntimeEnv { get; set; } | ||
public SdkInfo Sdk { get; set; } | ||
public List<Workload> Workloads { get; set; } | ||
|
||
public string ToJson() | ||
{ | ||
return JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this relatively simple JSON content personally I would go with just There's also a performance consideration. Currently this uses reflection based serialization which brings in a lot of dependencies on the reflection stack which is pretty expensive. Since we're writing this as new code, we should try to make it better than that. If you decide to still go with object based serialization, then this should use the source generator. If you go with the direct JSON writer approach the problem goes away as well. |
||
} | ||
} | ||
|
||
public static void PrintJsonInfo() | ||
{ | ||
DotnetVersionFile versionFile = DotnetFiles.VersionFileObject; | ||
var commitSha = versionFile.CommitSha ?? "N/A"; | ||
var basePath = AppContext.BaseDirectory; | ||
|
||
var dotNetInfo = new DotNetInfo | ||
{ | ||
BasePath = basePath, | ||
EnvVars = new Dictionary<string, string> | ||
{ | ||
// Populate with environment variables | ||
}, | ||
GlobalJson = Environment.CurrentDirectory, | ||
Host = new HostInfo | ||
{ | ||
Arch = RuntimeInformation.ProcessArchitecture.ToString(), | ||
Commit = commitSha, | ||
Version = Product.Version | ||
}, | ||
InstalledRuntimes = new List<InstalledRuntime> | ||
{ | ||
// Populate with installed runtime information | ||
}, | ||
OtherArch = new List<OtherArchInfo> | ||
{ | ||
// Populate with other architecture information | ||
}, | ||
RuntimeEnv = new RuntimeEnvironmentInfo | ||
{ | ||
Name = RuntimeEnvironment.OperatingSystem, | ||
Platform = RuntimeEnvironment.OperatingSystemPlatform.ToString(), | ||
Rid = GetDisplayRid(versionFile), | ||
Version = RuntimeEnvironment.OperatingSystemVersion | ||
}, | ||
Sdk = new SdkInfo | ||
{ | ||
Commit = commitSha, | ||
Version = Product.Version | ||
}, | ||
Workloads = WorkloadCommandParser.GetWorkloadsInfo() | ||
}; | ||
|
||
string jsonOutput = dotNetInfo.ToJson(); | ||
Reporter.Output.WriteLine(jsonOutput); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.CommandLine; | ||
using Microsoft.DotNet.Cli; | ||
|
||
namespace Microsoft.DotNet.Tools.Info | ||
{ | ||
internal static class InfoCommandParser | ||
{ | ||
public static readonly string DocsLink = "TODO"; | ||
|
||
public enum FormatOptions | ||
{ | ||
text, | ||
json | ||
} | ||
|
||
public static readonly CliOption<FormatOptions> FormatOption = new("--format", "-f") | ||
{ | ||
Description = "" | ||
}; | ||
|
||
private static readonly CliCommand Command = ConstructCommand(); | ||
|
||
public static CliCommand GetCommand() | ||
{ | ||
return Command; | ||
} | ||
|
||
private static CliCommand ConstructCommand() | ||
{ | ||
DocumentedCommand command = new("info", DocsLink); | ||
command.Options.Add(FormatOption); | ||
command.SetAction(InfoCommand.Run); | ||
|
||
return command; | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Microsoft.DotNet.Tools.Info; | ||
using Newtonsoft.Json; | ||
using Newtonsoft.Json.Linq; | ||
using Newtonsoft.Json.Schema; | ||
|
||
namespace Microsoft.DotNet.Info.Tests | ||
{ | ||
public class GivenThatIWantToShowInfoForDotnetCommand : SdkTest | ||
{ | ||
private const string InfoTextRegex = | ||
@"\.NET SDK:\s*(?:.*\n?)+?Runtime Environment:\s*(?:.*\n?)+?\.NET workloads installed:\s*(?:.*\n?)+?"; | ||
private const string RuntimeEnv = "RuntimeEnv"; | ||
private const string Sdk = "Sdk"; | ||
private const string Workloads = "Workloads"; | ||
|
||
public GivenThatIWantToShowInfoForDotnetCommand(ITestOutputHelper log) : base(log) | ||
{ | ||
} | ||
|
||
[Fact] | ||
public void WhenInfoCommandIsPassedToDotnetItPrintsInfo() | ||
{ | ||
var cmd = new DotnetCommand(Log, "info") | ||
.Execute(); | ||
cmd.Should().Pass(); | ||
cmd.StdOut.Should().MatchRegex(InfoTextRegex); | ||
} | ||
|
||
[Fact] | ||
public void WhenInfoCommandWithTextOptionIsPassedToDotnetItPrintsInfo() | ||
{ | ||
var cmd = new DotnetCommand(Log, "info") | ||
.Execute("--format", "text"); | ||
|
||
cmd.Should().Pass(); | ||
cmd.StdOut.Should().MatchRegex(InfoTextRegex); | ||
Comment on lines
+38
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. consider using Verify for these tests so you can ensure that the entire format doesn't change without having to keep big literal strings in source code. |
||
} | ||
|
||
[Fact] | ||
public void WhenInfoCommandWithJsonOptionIsPassedToDotnetItPrintsJsonInfo() | ||
{ | ||
var cmd = new DotnetCommand(Log, "info") | ||
.Execute("--format", "json"); | ||
|
||
cmd.Should().Pass(); | ||
|
||
JToken parsedJson = JToken.Parse(cmd.StdOut); | ||
|
||
var expectedKeys = new List<string> { Sdk, RuntimeEnv, Workloads }; | ||
foreach (var key in expectedKeys) | ||
{ | ||
Assert.True(parsedJson[key] != null, $"Expected key '{key}' not found in JSON output."); | ||
} | ||
|
||
Assert.True(parsedJson[Sdk].Type == JTokenType.Object, "SDK should be an object."); | ||
Assert.True(parsedJson[RuntimeEnv].Type == JTokenType.Object, "RuntimeEnvironment should be an object."); | ||
Assert.True(parsedJson[Workloads].Type == JTokenType.Array, "Workloads should be an array."); | ||
} | ||
Comment on lines
+58
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here - let Verify handle JSON diffing for you |
||
|
||
[Fact] | ||
public void WhenInvalidCommandIsPassedToDotnetInfoItPrintsError() | ||
{ | ||
var cmd = new DotnetCommand(Log, "info") | ||
.Execute("--invalid"); | ||
|
||
cmd.Should().Fail(); | ||
cmd.StdErr.Should().Contain("Unrecognized command or argument"); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great start! I would love to see the information that the
dotnet
host provides here as well. In the currentdotnet --info
command that's thesections. Especially the SDKs/Runtimes sections in a parseable format would be useful for folks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the feedback and apologies for the late response! I was exploring how to integrate the information into the new info command. I realized that the existing
--info
command have these information hard coded for printing in some C++ files indotnet/runtime
.Though we have ways to acquire information about SDKs installed and runtimes installed from the dotnet/runtime C++ files:
Interop.cs
, as currently, I didn't find ways to acquire Host and Other architectures file information withindotnet/sdk
. Therefore, A new standalone info command may need to make modifications to thedotnet/runtime
project in this file, by adding get_environmental_variable, get_host_info, get_global_json functions.If you happen to know any other way to get host information without making modifications to
dotnet/runtime
, or any existing methods withindotnet/sdk
to get those information, it would be really helpful. Making changes todotnet/runtime
, which involves complex and low-level C++ code, is a significant undertaking and difficult to debug with.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Options:
print_muxer_info_json
, which will inject the host-specific parts (not very clean solution but doable..)unset unset DOTNET_ROOT; dotnet --info
command, capture and parse result, convert to JSON and print. (worse than#2
because process spawning is expensive and not provided by all supported platforms)I think the first option is preferred, but I will defer to @elinor-fung and @vitek-karas. Lets hear their thoughts. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^^
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to do everything in managed code - we've discussed this in the past and the consensus was that the current design where part of the output is produced by the managed code and part of it is from native code is not what we want. Some of the downsides:
As to what mechanism to use to expose the necessary information to managed code:
hostfxr
API - or rather modify the existing. Effectively modify this struct to include the necessary info: https://github.com/dotnet/runtime/blob/c28bec4d3d63849c9e60dee1e7174b9a180a7e55/src/native/corehost/hostfxr.h#L311-L323.AppContext.GetData
, so that would have to be added as well.Personally - this feels more aligned with the
hostfxr_dotnet_environment_info
. But I don't feel strongly either way.