Skip to content
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

Open
wants to merge 6 commits into
base: release/8.0.2xx
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Cli/dotnet/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.DotNet.Tools;
using Microsoft.DotNet.Tools.Format;
using Microsoft.DotNet.Tools.Help;
using Microsoft.DotNet.Tools.Info;
using Microsoft.DotNet.Tools.MSBuild;
using Microsoft.DotNet.Tools.NuGet;
using Microsoft.TemplateEngine.Cli;
Expand Down Expand Up @@ -51,7 +52,8 @@ public static class Parser
HelpCommandParser.GetCommand(),
SdkCommandParser.GetCommand(),
InstallSuccessCommand,
WorkloadCommandParser.GetCommand()
WorkloadCommandParser.GetCommand(),
InfoCommandParser.GetCommand()
};

public static readonly CliOption<bool> DiagOption = CommonOptionsFactory.CreateDiagnosticsOption(recursive: false);
Expand Down
197 changes: 197 additions & 0 deletions src/Cli/dotnet/commands/dotnet-info/InfoCommand.cs
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();
Copy link
Member

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 current dotnet --info command that's the

  • Host,
  • .NET SDKs installed,
  • .NET runtimes installed,
  • Other architectures found,
  • Environment Variables, and
  • global.json file

sections. Especially the SDKs/Runtimes sections in a parseable format would be useful for folks.

Copy link
Contributor Author

@Markliniubility Markliniubility Dec 10, 2023

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 in dotnet/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 within dotnet/sdk. Therefore, A new standalone info command may need to make modifications to the dotnet/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 within dotnet/sdk to get those information, it would be really helpful. Making changes to dotnet/runtime, which involves complex and low-level C++ code, is a significant undertaking and difficult to debug with.

Copy link
Member

@am11 am11 Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Options:

  1. Expose the missing pieces of information from corehost so SDK can utilize it via interop for this new command.
  2. We are currently using RapidJSON in corehost to parse runtime json files. We can use it to emit info in JSON format when -f json arg (or any of its variants; --format json, --format=json) is set in a separate function like print_muxer_info_json, which will inject the host-specific parts (not very clean solution but doable..)
  3. Without changing the corehost; spawn a process with 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. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^

Copy link
Member

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:

  • It produces partially localized output - the host parts are not localized (ever), while the SDK parts are.
  • It's complex to maintain (and confusing to work on).

As to what mechanism to use to expose the necessary information to managed code:

Personally - this feels more aligned with the hostfxr_dotnet_environment_info. But I don't feel strongly either way.

}

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
Copy link
Member

Choose a reason for hiding this comment

The 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; }
Copy link
Member

Choose a reason for hiding this comment

The 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 });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this relatively simple JSON content personally I would go with just Utf8JsonWriter. Maintaining the classes just for serialization feels like an overkill (and it will need lot of attributes to change the property names to camel case and so on).

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);
}
}
}
41 changes: 41 additions & 0 deletions src/Cli/dotnet/commands/dotnet-info/InfoCommandParser.cs
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;
}
}
}

38 changes: 38 additions & 0 deletions src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
using CommonStrings = Microsoft.DotNet.Workloads.Workload.LocalizableStrings;
using IReporter = Microsoft.DotNet.Cli.Utils.IReporter;

using Workload = Microsoft.DotNet.Tools.Info.InfoCommand.Workload;
using WorkloadManifest = Microsoft.DotNet.Tools.Info.InfoCommand.WorkloadManifest;

namespace Microsoft.DotNet.Cli
{
internal static class WorkloadCommandParser
Expand Down Expand Up @@ -47,6 +50,41 @@ internal static string GetWorkloadsVersion(WorkloadInfoHelper workloadInfoHelper
return workloadInfoHelper.ManifestProvider.GetWorkloadVersion();
}

internal static List<Workload> GetWorkloadsInfo(ParseResult parseResult = null, WorkloadInfoHelper workloadInfoHelper = null, string dotnetDir = null)
{
var workloads = new List<Workload>();
workloadInfoHelper ??= new WorkloadInfoHelper(parseResult != null ? parseResult.HasOption(SharedOptions.InteractiveOption) : false);
IEnumerable<WorkloadId> installedList = workloadInfoHelper.InstalledSdkWorkloadIds;
InstalledWorkloadsCollection installedWorkloads = workloadInfoHelper.AddInstalledVsWorkloads(installedList);

if (installedWorkloads.Count == 0)
{
return workloads;
}

var manifestInfoDict = workloadInfoHelper.WorkloadResolver.GetInstalledManifests().ToDictionary(info => info.Id, StringComparer.OrdinalIgnoreCase);

foreach (var workloadEntry in installedWorkloads.AsEnumerable())
{
var workloadManifest = workloadInfoHelper.WorkloadResolver.GetManifestFromWorkload(new WorkloadId(workloadEntry.Key));
var workloadFeatureBand = manifestInfoDict[workloadManifest.Id].ManifestFeatureBand;

var workload = new Workload
{
Name = workloadEntry.Key,
Manifest = new WorkloadManifest
{
InstallType = WorkloadInstallType.GetWorkloadInstallType(new SdkFeatureBand(workloadFeatureBand), dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath)).ToString(),
Path = workloadManifest.ManifestPath,
Version = workloadManifest.Version
}
};

workloads.Add(workload);
}

return workloads;
}
internal static void ShowWorkloadsInfo(ParseResult parseResult = null, WorkloadInfoHelper workloadInfoHelper = null, IReporter reporter = null, string dotnetDir = null)
{
workloadInfoHelper ??= new WorkloadInfoHelper(parseResult != null ? parseResult.HasOption(SharedOptions.InteractiveOption) : false);
Expand Down
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
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public void GivenOnlyDotnetItSuggestsTopLevelCommandsAndOptions()
"sdk",
"fsi",
"help",
"info",
"list",
"msbuild",
"new",
Expand Down
1 change: 1 addition & 0 deletions src/Tests/dotnet.Tests/dotnet.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<Compile Include="..\dotnet-format.Tests\**\*.cs" LinkBase="dotnet-format" />
<Compile Include="..\dotnet-fsi.Tests\**\*.cs" LinkBase="dotnet-fsi" />
<Compile Include="..\dotnet-help.Tests\**\*.cs" LinkBase="dotnet-help" />
<Compile Include="..\dotnet-info.Tests\**\*.cs" LinkBase="dotnet-info" />
<Compile Include="..\dotnet-install-tool.Tests\**\*.cs" LinkBase="dotnet-install-tool" />
<Compile Include="..\dotnet-list-package.Tests\**\*.cs" LinkBase="dotnet-list-package" />
<Compile Include="..\dotnet-list-reference.Tests\**\*.cs" LinkBase="dotnet-list-reference" />
Expand Down