From b591882e8e227e00af2c947037185e014dbaff1a Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Thu, 17 Apr 2025 15:50:35 -0700 Subject: [PATCH 1/2] wip --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 65a59229b8..2828eefb26 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -85,7 +85,7 @@ - + From f3ee69b892f5ee6e2436c7bf12ee29de3fcbf185 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Fri, 30 May 2025 11:44:56 -0700 Subject: [PATCH 2/2] update System.CommandLine --- Directory.Packages.props | 3 +- NuGet.config | 4 + .../(Recipes)/MarkupTestFileFacts.cs | 3 +- ...t.Interactive.Http.Tests.v3.ncrunchproject | 10 +- .../StartupTelemetryEventBuilder.cs | 25 +- .../TelemetrySender.cs | 1 + .../KernelHost.cs | 2 +- .../CommandLine/CommandExtensions.cs | 76 +++ ...CommandLineConfigurationValidationTests.cs | 243 ++++++++++ .../CommandLine/CommandLineParserTests.cs | 272 +++++------ .../CommandLine/FirstTimeUseSentinelTests.cs | 25 +- .../CommandLine/StartupTelemetryTests.cs | 105 ++--- .../Extensions}/EnumerableExtensions.cs | 2 +- .../InProcessTestServer.cs | 18 +- .../JupyterInstallCommandTests.cs | 20 +- .../JupyterKernelSpecTests.cs | 17 +- ...dotnet-interactive.Tests.v3.ncrunchproject | 10 +- .../CommandLine/CommandLineParser.cs | 445 ++++++++++-------- .../CommandLine/HttpCommand.cs | 8 +- .../CommandLine/JupyterCommand.cs | 8 +- .../CommandLine/StartupOptions.cs | 47 +- .../CommandLine/StdIOOptions.cs | 14 - .../CommandLine/StdIoMode.cs | 3 +- src/dotnet-interactive/Http/HttpPortRange.cs | 2 +- .../JupyterKernelSpecInstaller.cs | 32 +- src/dotnet-interactive/KernelBuilder.cs | 9 +- src/dotnet-interactive/KernelExtensions.cs | 5 + src/dotnet-interactive/Program.cs | 6 +- .../dotnet-interactive.csproj | 3 +- src/interface-generator/Program.cs | 17 +- 30 files changed, 882 insertions(+), 553 deletions(-) create mode 100644 src/dotnet-interactive.Tests/CommandLine/CommandExtensions.cs create mode 100644 src/dotnet-interactive.Tests/CommandLine/CommandLineConfigurationValidationTests.cs rename src/{Microsoft.DotNet.Interactive.Tests/Utility => dotnet-interactive.Tests/Extensions}/EnumerableExtensions.cs (92%) delete mode 100644 src/dotnet-interactive/CommandLine/StdIOOptions.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 2828eefb26..251a45ffc0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -84,8 +84,7 @@ - - + diff --git a/NuGet.config b/NuGet.config index 2caf14f5cd..234e0f5679 100644 --- a/NuGet.config +++ b/NuGet.config @@ -7,6 +7,7 @@ + @@ -22,6 +23,9 @@ + + + diff --git a/src/Microsoft.DotNet.Interactive.CSharpProject.Tests/(Recipes)/MarkupTestFileFacts.cs b/src/Microsoft.DotNet.Interactive.CSharpProject.Tests/(Recipes)/MarkupTestFileFacts.cs index 1d84139fe9..ec13b7f94d 100644 --- a/src/Microsoft.DotNet.Interactive.CSharpProject.Tests/(Recipes)/MarkupTestFileFacts.cs +++ b/src/Microsoft.DotNet.Interactive.CSharpProject.Tests/(Recipes)/MarkupTestFileFacts.cs @@ -24,8 +24,7 @@ public void GetPosition_Should_Return_String_Without_Dollar_Signs() public void GetPosition_Should_Return_Correct_Position() { var input = "some$$string"; - string output; - MarkupTestFile.GetPosition(input, out output, out var position); + MarkupTestFile.GetPosition(input, out var output, out var position); Assert.Equal(4, position); } diff --git a/src/Microsoft.DotNet.Interactive.Http.Tests/Microsoft.DotNet.Interactive.Http.Tests.v3.ncrunchproject b/src/Microsoft.DotNet.Interactive.Http.Tests/Microsoft.DotNet.Interactive.Http.Tests.v3.ncrunchproject index a993d86c17..270a9e19b4 100644 --- a/src/Microsoft.DotNet.Interactive.Http.Tests/Microsoft.DotNet.Interactive.Http.Tests.v3.ncrunchproject +++ b/src/Microsoft.DotNet.Interactive.Http.Tests/Microsoft.DotNet.Interactive.Http.Tests.v3.ncrunchproject @@ -1,5 +1,13 @@ - + 20000 + + + Microsoft.DotNet.Interactive.Http.Tests.HttpKernelTests+NamedRequest.body_content_produces_the_entirety_of_the_body_content("example.response.body.*") + + + Microsoft.DotNet.Interactive.Http.Tests.HttpKernelTests.traceparent_header_has_a_new_top_level_value_for_each_request + + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.Telemetry/StartupTelemetryEventBuilder.cs b/src/Microsoft.DotNet.Interactive.Telemetry/StartupTelemetryEventBuilder.cs index afe90df028..fc2762a910 100644 --- a/src/Microsoft.DotNet.Interactive.Telemetry/StartupTelemetryEventBuilder.cs +++ b/src/Microsoft.DotNet.Interactive.Telemetry/StartupTelemetryEventBuilder.cs @@ -13,7 +13,7 @@ namespace Microsoft.DotNet.Interactive.Telemetry; public sealed class StartupTelemetryEventBuilder { private readonly Func _hash; - private readonly HashSet _clearTextProperties = new(new[] { "frontend" }); + private readonly HashSet _clearTextProperties = ["frontend"]; public StartupTelemetryEventBuilder(Func hash) { @@ -80,7 +80,7 @@ public IEnumerable GetTelemetryEventsFrom(ParseResult parseResul var commandResult = parseResult.CommandResult; - var frontendName = GetFrontendName(parseResult.Directives, parseResult.CommandResult); + var frontendName = GetFrontendName(parseResult, parseResult.CommandResult); entryItems.Add(new KeyValuePair("frontend", frontendName)); foreach (var item in rule.Items) @@ -94,7 +94,10 @@ public IEnumerable GetTelemetryEventsFrom(ParseResult parseResul switch (item) { case OptionItem optItem: - var optionValue = commandResult.Children.OfType().FirstOrDefault(o => o.Option.HasAlias(optItem.Option))?.GetValueOrDefault()?.ToString(); + var optionResult = commandResult.Children.OfType().FirstOrDefault(o => o.Option.Name == optItem.Option); + + var optionValue = optionResult?.GetValue(optItem.Option)?.ToString(); + if (optionValue is not null && optItem.Values.Contains(optionValue)) { entryItems.Add(new KeyValuePair(optItem.EntryKey, optionValue)); @@ -259,7 +262,7 @@ private static CommandRuleItem Ignore(TokenType type, bool isOptional) }; private static string GetFrontendName( - DirectiveCollection directives, + ParseResult parseResult, CommandResult commandResult) { if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CODESPACES"))) @@ -267,15 +270,15 @@ private static string GetFrontendName( return "gitHubCodeSpaces"; } - foreach (var directive in directives) + foreach (var directive in parseResult.Tokens.Where(t => t is { Type: TokenType.Directive })) { - switch (directive.Key) + switch (directive.Value) { - case "jupyter": - case "synapse": - case "vscode": - case "vs": - return directive.Key; + case "[jupyter]": + case "[synapse]": + case "[vscode]": + case "[vs]": + return directive.Value.Trim('[', ']'); } } diff --git a/src/Microsoft.DotNet.Interactive.Telemetry/TelemetrySender.cs b/src/Microsoft.DotNet.Interactive.Telemetry/TelemetrySender.cs index 596a932559..2c1279721f 100644 --- a/src/Microsoft.DotNet.Interactive.Telemetry/TelemetrySender.cs +++ b/src/Microsoft.DotNet.Interactive.Telemetry/TelemetrySender.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.CommandLine; using System.CommandLine.Parsing; using System.Diagnostics; using System.Reflection; diff --git a/src/Microsoft.DotNet.Interactive/KernelHost.cs b/src/Microsoft.DotNet.Interactive/KernelHost.cs index 3b381dfec0..d77eda62f1 100644 --- a/src/Microsoft.DotNet.Interactive/KernelHost.cs +++ b/src/Microsoft.DotNet.Interactive/KernelHost.cs @@ -42,7 +42,7 @@ internal KernelHost( _kernel = kernel; _defaultSender = sender; _receiver = receiver; - _defaultConnector = async (name) => + _defaultConnector = async name => { var connector = new DefaultKernelConnector( _defaultSender, diff --git a/src/dotnet-interactive.Tests/CommandLine/CommandExtensions.cs b/src/dotnet-interactive.Tests/CommandLine/CommandExtensions.cs new file mode 100644 index 0000000000..7caa1909ab --- /dev/null +++ b/src/dotnet-interactive.Tests/CommandLine/CommandExtensions.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.Linq; +using Microsoft.DotNet.Interactive.App.Tests.Extensions; + +namespace Microsoft.DotNet.Interactive.App.Tests.CommandLine; + +public static class CommandExtensions +{ + /// + /// Throws an exception if the parser configuration is ambiguous or otherwise not valid. + /// + /// Due to the performance cost of this method, it is recommended to be used in unit testing or in scenarios where the parser is configured dynamically at runtime. + /// Thrown if the configuration is found to be invalid. + public static void ThrowIfInvalid(this Command command) + { + if (command.Parents.FlattenBreadthFirst(c => c.Parents).Any(ancestor => ancestor == command)) + { + throw new InvalidOperationException($"Cycle detected in command tree. Command '{command.Name}' is its own ancestor."); + } + + int count = command.Subcommands.Count + command.Options.Count; + for (var i = 0; i < count; i++) + { + Symbol symbol1 = GetChild(i, command, out HashSet aliases1); + + for (var j = i + 1; j < count; j++) + { + Symbol symbol2 = GetChild(j, command, out HashSet aliases2); + + if (symbol1.Name.Equals(symbol2.Name, StringComparison.Ordinal) + || aliases1 is not null && aliases1.Contains(symbol2.Name)) + { + throw new InvalidOperationException($"Duplicate alias '{symbol2.Name}' found on command '{command.Name}'."); + } + else if (aliases2 is not null && aliases2.Contains(symbol1.Name)) + { + throw new InvalidOperationException($"Duplicate alias '{symbol1.Name}' found on command '{command.Name}'."); + } + + if (aliases1 is not null && aliases2 is not null) + { + // take advantage of the fact that we are dealing with two hash sets + if (aliases1.Overlaps(aliases2)) + { + foreach (string symbol2Alias in aliases2) + { + if (aliases1.Contains(symbol2Alias)) + { + throw new InvalidOperationException($"Duplicate alias '{symbol2Alias}' found on command '{command.Name}'."); + } + } + } + } + } + + if (symbol1 is Command childCommand) + { + childCommand.ThrowIfInvalid(); + } + } + + static Symbol GetChild(int index, Command command, out HashSet aliases) + { + if (index < command.Subcommands.Count) + { + aliases = command.Subcommands[index].Aliases.ToHashSet(); + return command.Subcommands[index]; + } + + aliases = command.Options[index - command.Subcommands.Count].Aliases.ToHashSet(); + return command.Options[index - command.Subcommands.Count]; + } + } +} \ No newline at end of file diff --git a/src/dotnet-interactive.Tests/CommandLine/CommandLineConfigurationValidationTests.cs b/src/dotnet-interactive.Tests/CommandLine/CommandLineConfigurationValidationTests.cs new file mode 100644 index 0000000000..8bdc20a8a1 --- /dev/null +++ b/src/dotnet-interactive.Tests/CommandLine/CommandLineConfigurationValidationTests.cs @@ -0,0 +1,243 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.CommandLine; +using FluentAssertions; +using Xunit; + +namespace Microsoft.DotNet.Interactive.App.Tests.CommandLine; + +public class CommandLineConfigurationValidationTests +{ + [Fact] + public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_on_the_root_command() + { + var option1 = new Option("--dupe"); + var option2 = new Option("-y"); + option2.Aliases.Add("--dupe"); + + var command = new RootCommand + { + option1, + option2 + }; + + var validate = () => command.ThrowIfInvalid(); + + validate.Should() + .Throw() + .Which + .Message + .Should() + .Be($"Duplicate alias '--dupe' found on command '{command.Name}'."); + } + + [Fact] + public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_on_a_subcommand() + { + var option1 = new Option("--dupe"); + var option2 = new Option("--ok"); + option2.Aliases.Add("--dupe"); + + var command = new RootCommand + { + new Command("subcommand") + { + option1, + option2 + } + }; + + var validate = () => command.ThrowIfInvalid(); + + validate.Should() + .Throw() + .Which + .Message + .Should() + .Be("Duplicate alias '--dupe' found on command 'subcommand'."); + } + + [Fact] + public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_aliases_on_the_root_command() + { + var command1 = new Command("dupe"); + var command2 = new Command("not-a-dupe"); + command2.Aliases.Add("dupe"); + + var rootCommand = new RootCommand + { + command1, + command2 + }; + + var validate = () => rootCommand.ThrowIfInvalid(); + + validate.Should() + .Throw() + .Which + .Message + .Should() + .Be($"Duplicate alias 'dupe' found on command '{rootCommand.Name}'."); + } + + [Fact] + public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_aliases_on_a_subcommand() + { + var command = new RootCommand + { + new Command("subcommand") + { + new Command("dupe"), + new Command("not-a-dupe") { Aliases = { "dupe" } } + } + }; + + var validate = () => command.ThrowIfInvalid(); + + validate.Should() + .Throw() + .Which + .Message + .Should() + .Be("Duplicate alias 'dupe' found on command 'subcommand'."); + } + + [Fact] + public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_on_the_root_command() + { + var option = new Option("dupe"); + var command = new Command("not-a-dupe"); + command.Aliases.Add("dupe"); + + var rootCommand = new RootCommand + { + option, + command + }; + + var validate = () => rootCommand.ThrowIfInvalid(); + + validate.Should() + .Throw() + .Which + .Message + .Should() + .Be($"Duplicate alias 'dupe' found on command '{rootCommand.Name}'."); + } + + [Fact] + public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_on_a_subcommand() + { + var option = new Option("dupe"); + var command = new Command("not-a-dupe"); + command.Aliases.Add("dupe"); + + var rootCommand = new RootCommand + { + new Command("subcommand") + { + option, + command + } + }; + + var validate = () => rootCommand.ThrowIfInvalid(); + + validate.Should() + .Throw() + .Which + .Message + .Should() + .Be("Duplicate alias 'dupe' found on command 'subcommand'."); + } + + [Fact] + public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_global_option_aliases_on_the_root_command() + { + var option1 = new Option("--dupe") { Recursive = true }; + var option2 = new Option("-y") { Recursive = true }; + option2.Aliases.Add("--dupe"); + + var command = new RootCommand(); + command.Options.Add(option1); + command.Options.Add(option2); + + var validate = () => command.ThrowIfInvalid(); + + validate.Should() + .Throw() + .Which + .Message + .Should() + .Be($"Duplicate alias '--dupe' found on command '{command.Name}'."); + } + + [Fact] + public void ThrowIfInvalid_does_not_throw_if_global_option_alias_is_the_same_as_local_option_alias() + { + var rootCommand = new RootCommand + { + new Command("subcommand") + { + new Option("--dupe") + } + }; + rootCommand.Options.Add(new Option("--dupe") { Recursive = true }); + + var validate = () => rootCommand.ThrowIfInvalid(); + + validate.Should().NotThrow(); + } + + [Fact] + public void ThrowIfInvalid_does_not_throw_if_global_option_alias_is_the_same_as_subcommand_alias() + { + var rootCommand = new RootCommand + { + new Command("subcommand") + { + new Command("--dupe") + } + }; + rootCommand.Options.Add(new Option("--dupe") { Recursive = true }); + + var validate = () => rootCommand.ThrowIfInvalid(); + + validate.Should().NotThrow(); + } + + [Fact] + public void ThrowIfInvalid_throws_if_a_command_is_its_own_parent() + { + var command = new RootCommand(); + command.Add(command); + + var validate = () => command.ThrowIfInvalid(); + + validate.Should() + .Throw() + .Which + .Message + .Should() + .Be($"Cycle detected in command tree. Command '{command.Name}' is its own ancestor."); + } + + [Fact] + public void ThrowIfInvalid_throws_if_a_parentage_cycle_is_detected() + { + var command = new Command("command"); + var rootCommand = new RootCommand { command }; + command.Add(rootCommand); + + var validate = () => command.ThrowIfInvalid(); + + validate.Should() + .Throw() + .Which + .Message + .Should() + .Be($"Cycle detected in command tree. Command '{command.Name}' is its own ancestor."); + } +} \ No newline at end of file diff --git a/src/dotnet-interactive.Tests/CommandLine/CommandLineParserTests.cs b/src/dotnet-interactive.Tests/CommandLine/CommandLineParserTests.cs index f547175557..6bd5056fec 100644 --- a/src/dotnet-interactive.Tests/CommandLine/CommandLineParserTests.cs +++ b/src/dotnet-interactive.Tests/CommandLine/CommandLineParserTests.cs @@ -2,10 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.CommandLine.Invocation; -using System.CommandLine.IO; -using System.CommandLine.NamingConventionBinder; -using System.CommandLine.Parsing; +using System.CommandLine; using System.Diagnostics; using System.IO; using System.Linq; @@ -13,10 +10,8 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; - using FluentAssertions; using FluentAssertions.Execution; - using Microsoft.DotNet.Interactive.App.CommandLine; using Microsoft.DotNet.Interactive.App.Connection; using Microsoft.DotNet.Interactive.App.Tests.Extensions; @@ -26,18 +21,17 @@ using Microsoft.DotNet.Interactive.Tests.Utility; using Microsoft.DotNet.Interactive.Utility; using Microsoft.Extensions.DependencyInjection; - using Xunit; using Xunit.Abstractions; +using CommandLineParser = Microsoft.DotNet.Interactive.App.CommandLine.CommandLineParser; namespace Microsoft.DotNet.Interactive.App.Tests.CommandLine; public class CommandLineParserTests : IDisposable { private readonly ITestOutputHelper _output; - private readonly TestConsole _console = new(); - private StartupOptions _startOptions; - private readonly Parser _parser; + private StartupOptions _startupOptions; + private readonly RootCommand _rootCommand; private readonly FileInfo _connectionFile; private readonly DirectoryInfo _kernelSpecInstallPath; private readonly ServiceCollection _serviceCollection; @@ -51,25 +45,25 @@ public CommandLineParserTests(ITestOutputHelper output) SentinelExists = false }; - _parser = CommandLineParser.Create( + _rootCommand = CommandLineParser.Create( _serviceCollection, - startServer: (options, invocationContext) => + startWebServer: startupOptions => { - _startOptions = options; + _startupOptions = startupOptions; }, - jupyter: (startupOptions, console, startServer, context) => + startJupyter: (startupOptions, _) => { - _startOptions = startupOptions; + _startupOptions = startupOptions; return Task.FromResult(1); }, - startKernelHost: (startupOptions, host, console) => + startStdio: (startupOptions, _) => { - _startOptions = startupOptions; + _startupOptions = startupOptions; return Task.FromResult(1); }, - startHttp: (startupOptions, console, startServer, context) => + startHttp: (startupOptions, _) => { - _startOptions = startupOptions; + _startupOptions = startupOptions; return Task.FromResult(1); }, telemetrySender: new FakeTelemetrySender(firstTimeUseNoticeSentinel)); @@ -81,8 +75,8 @@ public CommandLineParserTests(ITestOutputHelper output) private Kernel GetKernel() { return _serviceCollection - .FirstOrDefault(s => s.ServiceType == typeof(Kernel)) - .ImplementationInstance.As(); + .FirstOrDefault(s => s.ServiceType == typeof(Kernel)) + .ImplementationInstance.As(); } public void Dispose() @@ -95,9 +89,9 @@ public async Task It_parses_log_output_directory() { var logPath = new DirectoryInfo(Path.GetTempPath()); - await _parser.InvokeAsync($"jupyter --log-path {logPath} {_connectionFile}", _console); + await _rootCommand.Parse($"jupyter --log-path {logPath} {_connectionFile}").InvokeAsync(); - _startOptions + _startupOptions .LogPath .FullName .Should() @@ -128,8 +122,8 @@ public async Task stdio_mode_honors_log_path() // wait for log file to be created var logFile = await logPath.Directory.WaitForFile( - timeout: waitTime, - predicate: _ => true); // any matching file is the one we want + timeout: waitTime, + predicate: _ => true); // any matching file is the one we want logFile.Should().NotBeNull($"a log file should have been created at {logFile.FullName}"); // check log file for expected contents @@ -156,9 +150,9 @@ public async Task stdio_mode_honors_log_path() [Fact] public async Task It_parses_verbose_option() { - await _parser.InvokeAsync($"jupyter --verbose {_connectionFile}", _console); + await _rootCommand.Parse($"jupyter --verbose {_connectionFile}").InvokeAsync(); - _startOptions + _startupOptions .Verbose .Should() .BeTrue(); @@ -167,41 +161,34 @@ public async Task It_parses_verbose_option() [Fact] public void jupyter_command_parses_port_range_option() { - var result = _parser.Parse($"jupyter --http-port-range 3000-4000 {_connectionFile}"); - - var binder = new ModelBinder(); - - var options = (StartupOptions)binder.CreateInstance(new InvocationContext(result).BindingContext); + _rootCommand.Parse($"jupyter --http-port-range 3000-4000 {_connectionFile}").Invoke(); - options - .HttpPortRange - .Should() - .BeEquivalentToPreferringRuntimeMemberTypes(new HttpPortRange(3000, 4000)); + _startupOptions.HttpPortRange + .Should() + .BeEquivalentToPreferringRuntimeMemberTypes(new HttpPortRange(3000, 4000)); } [Fact] public void jupyter_command_help_shows_default_port_range() { - _parser.Invoke("jupyter -h", _console); + var output = new StringWriter(); - _console.Out.ToString().Should().Contain("default: 2048-3000"); + _rootCommand.Parse("jupyter -h").Invoke(new() { Output = output }); + + output.ToString().Should().Match("*default:*2048-3000*"); } [Fact] public void jupyter_command_parses_http_local_only_option() { - var result = _parser.Parse($"jupyter --http-local-only {_connectionFile}"); - - var binder = new ModelBinder(); + _rootCommand.Parse($"jupyter --http-local-only {_connectionFile}").InvokeAsync(); - var options = (StartupOptions)binder.CreateInstance(new InvocationContext(result).BindingContext); - - options + _startupOptions .GetAllNetworkInterfaces .Should() .Match(x => x == StartupOptions.GetNetworkInterfacesHttpLocalOnly); - options + _startupOptions .GetAllNetworkInterfaces .Should() .Match(x => x != NetworkInterface.GetAllNetworkInterfaces); @@ -210,18 +197,14 @@ public void jupyter_command_parses_http_local_only_option() [Fact] public void jupyter_command_default_network_interface_if_no_http_local_only_option() { - var result = _parser.Parse($"jupyter {_connectionFile}"); - - var binder = new ModelBinder(); - - var options = (StartupOptions)binder.CreateInstance(new InvocationContext(result).BindingContext); + _rootCommand.Parse($"jupyter {_connectionFile}").InvokeAsync(); - options + _startupOptions .GetAllNetworkInterfaces .Should() .Match(x => x != StartupOptions.GetNetworkInterfacesHttpLocalOnly); - options + _startupOptions .GetAllNetworkInterfaces .Should() .Match(x => x == NetworkInterface.GetAllNetworkInterfaces); @@ -232,7 +215,7 @@ public void jupyter_install_command_parses_path_option() { Directory.CreateDirectory(_kernelSpecInstallPath.FullName); - _parser.InvokeAsync($"jupyter install --path {_kernelSpecInstallPath}"); + _rootCommand.Parse($"jupyter install --path {_kernelSpecInstallPath}").Invoke(); var installedKernels = _kernelSpecInstallPath.GetDirectories(); @@ -245,69 +228,61 @@ public void jupyter_install_command_parses_path_option() [Fact] public void jupyter_install_command_does_not_parse_http_port_option() { - var result = _parser.Parse("jupyter install --http-port 8000"); + var result = _rootCommand.Parse("jupyter install --http-port 8000"); result.Errors - .Select(e => e.Message) - .Should() - .Contain(errorMessage => errorMessage == "Unrecognized command or argument '--http-port'."); + .Select(e => e.Message) + .Should() + .Contain(errorMessage => errorMessage == "Unrecognized command or argument '--http-port'."); } [Fact] public void jupyter_install_command_parses_port_range_option() { - var result = _parser.Parse("jupyter install --http-port-range 3000-4000"); - - var binder = new ModelBinder(); + var result = _rootCommand.Parse("jupyter install --http-port-range 3000-4000"); - var options = (StartupOptions)binder.CreateInstance(new InvocationContext(result).BindingContext); + var startupOptions = StartupOptions.Parse(result); - options + startupOptions .HttpPortRange .Should() .BeEquivalentToPreferringRuntimeMemberTypes(new HttpPortRange(3000, 4000)); } [Fact] - public async Task jupyter_command_returns_error_if_connection_file_path_is_not_passed() + public void jupyter_command_returns_error_if_connection_file_path_is_not_passed() { - var testConsole = new TestConsole(); + var result = _rootCommand.Parse("jupyter"); - await _parser.InvokeAsync("jupyter", testConsole); - - testConsole.Error.ToString().Should().Contain("Required argument missing for command: 'jupyter'."); + result.Errors.Should().Contain(e => e.Message == "Required argument missing for command: 'jupyter'."); } [Fact] public void jupyter_command_does_not_parse_http_port_option() { - var result = _parser.Parse($"jupyter {_connectionFile} --http-port 8000"); + var result = _rootCommand.Parse($"jupyter {_connectionFile} --http-port 8000"); result.Errors - .Select(e => e.Message) - .Should() - .Contain(errorMessage => errorMessage == "Unrecognized command or argument '--http-port'."); + .Select(e => e.Message) + .Should() + .Contain(errorMessage => errorMessage == "Unrecognized command or argument '--http-port'."); } [Fact] public async Task jupyter_command_enables_http_api_when_http_port_range_is_specified() { - await _parser.InvokeAsync($"jupyter --http-port-range 3000-5000 {_connectionFile}"); + await _rootCommand.Parse($"jupyter --http-port-range 3000-5000 {_connectionFile}").InvokeAsync(); - _startOptions.EnableHttpApi.Should().BeTrue(); + _startupOptions.EnableHttpApi.Should().BeTrue(); } [Fact] public void jupyter_command_parses_connection_file_path() { - var result = _parser.Parse($"jupyter {_connectionFile}"); - - var binder = new ModelBinder(); - - var options = (JupyterOptions)binder.CreateInstance(new InvocationContext(result).BindingContext); + _rootCommand.Parse($"jupyter {_connectionFile}").Invoke(); - options - .ConnectionFile + _startupOptions + .JupyterConnectionFile .FullName .Should() .Be(_connectionFile.FullName); @@ -316,40 +291,36 @@ public void jupyter_command_parses_connection_file_path() [Fact] public async Task jupyter_command_enables_http_api_by_default() { - await _parser.InvokeAsync($"jupyter {_connectionFile}"); + await _rootCommand.Parse($"jupyter {_connectionFile}").InvokeAsync(); - _startOptions.EnableHttpApi.Should().BeTrue(); + _startupOptions.EnableHttpApi.Should().BeTrue(); } [Fact] public async Task jupyter_command_by_default_uses_port_rage() { - await _parser.InvokeAsync($"jupyter {_connectionFile}"); + await _rootCommand.Parse($"jupyter {_connectionFile}").InvokeAsync(); using var scope = new AssertionScope(); - _startOptions.HttpPortRange.Should().NotBeNull(); - _startOptions.HttpPortRange.Start.Should().Be(HttpPortRange.Default.Start); - _startOptions.HttpPortRange.End.Should().Be(HttpPortRange.Default.End); + _startupOptions.HttpPortRange.Should().NotBeNull(); + _startupOptions.HttpPortRange.Start.Should().Be(HttpPortRange.Default.Start); + _startupOptions.HttpPortRange.End.Should().Be(HttpPortRange.Default.End); } [Fact] public void jupyter_command_default_kernel_option_value() { - var result = _parser.Parse($"jupyter {Path.GetTempFileName()}"); - var binder = new ModelBinder(); - var options = (JupyterOptions)binder.CreateInstance(new InvocationContext(result).BindingContext); + _rootCommand.Parse($"jupyter {Path.GetTempFileName()}").Invoke(); - options.DefaultKernel.Should().Be("csharp"); + _startupOptions.DefaultKernel.Should().Be("csharp"); } [Fact] public void jupyter_command_honors_default_kernel_option() { - var result = _parser.Parse($"jupyter --default-kernel bsharp {Path.GetTempFileName()}"); - var binder = new ModelBinder(); - var options = (JupyterOptions)binder.CreateInstance(new InvocationContext(result).BindingContext); + _rootCommand.Parse($"jupyter --default-kernel bsharp {Path.GetTempFileName()}").Invoke(); - options.DefaultKernel.Should().Be("bsharp"); + _startupOptions.DefaultKernel.Should().Be("bsharp"); } [Fact] @@ -357,52 +328,43 @@ public async Task jupyter_command_returns_error_if_connection_file_path_does_not { var expected = "not_exist.json"; - var testConsole = new TestConsole(); - await _parser.InvokeAsync($"jupyter {expected}", testConsole); + var error = new StringWriter(); + await _rootCommand.Parse($"jupyter {expected}").InvokeAsync(new() { Output = new StringWriter(), Error = error }); - testConsole.Error.ToString().Should().ContainAll("File does not exist", "not_exist.json"); + error.ToString().Should().ContainAll("File does not exist", "not_exist.json"); } [Fact] public void stdio_command_kernel_host_defaults_to_process_id() { - var result = _parser.Parse("stdio"); + _rootCommand.Parse("stdio").Invoke(); - var binder = new ModelBinder(); + // FIX: (stdio_command_kernel_host_defaults_to_process_id) maybe broken because Uri parsing was removed? - var options = (StartupOptions)binder.CreateInstance(new InvocationContext(result).BindingContext); - - options.KernelHost - .Should() - .Be(new Uri($"kernel://pid-{Process.GetCurrentProcess().Id}")); + _startupOptions.KernelHostUri + .Should() + .Be(new Uri($"kernel://pid-{Process.GetCurrentProcess().Id}")); } [Fact] - public void stdio_command_kernel_name_can_be_specified() + public void stdio_command_kernel_host_uri_can_be_specified() { - var result = _parser.Parse("stdio --kernel-host some-kernel-name"); - - var binder = new ModelBinder(); + _rootCommand.Parse("stdio --kernel-host some-kernel-name").Invoke(); + ; - var options = (StartupOptions)binder.CreateInstance(new InvocationContext(result).BindingContext); - - options.KernelHost - .Should() - .Be(new Uri("kernel://some-kernel-name")); + _startupOptions.KernelHostUri + .Should() + .Be(new Uri("kernel://some-kernel-name")); } [Fact] public void stdio_command_working_dir_defaults_to_process_current() { - var result = _parser.Parse("stdio"); + _rootCommand.Parse("stdio").Invoke(); - var binder = new ModelBinder(); - - var options = (StartupOptions)binder.CreateInstance(new InvocationContext(result).BindingContext); - - options.WorkingDir.FullName - .Should() - .Be(Environment.CurrentDirectory); + _startupOptions.WorkingDir.FullName + .Should() + .Be(Environment.CurrentDirectory); } [Fact] @@ -416,97 +378,89 @@ public void stdio_command_working_dir_can_be_specified() _ => "/some/dir" }; - var result = _parser.Parse($"stdio --working-dir {workingDir}"); - - var binder = new ModelBinder(); + var result = _rootCommand.Parse($"stdio --working-dir {workingDir}"); - var options = (StartupOptions)binder.CreateInstance(new InvocationContext(result).BindingContext); + var startupOptions = StartupOptions.Parse(result); - options.WorkingDir.FullName - .Should() - .Be(workingDir); + startupOptions.WorkingDir.FullName + .Should() + .Be(workingDir); } - [Fact] public void stdio_command_does_not_support_http_port_and_http_port_range_options_at_same_time() { - var result = _parser.Parse("stdio --http-port 8000 --http-port-range 3000-4000"); + var result = _rootCommand.Parse("stdio --http-port 8000 --http-port-range 3000-4000"); result.Errors - .Select(e => e.Message) - .Should() - .Contain(errorMessage => errorMessage == "Cannot specify both --http-port-range and --http-port together"); + .Select(e => e.Message) + .Should() + .Contain(errorMessage => errorMessage == "Cannot specify both --http-port-range and --http-port together"); } [Fact] public void stdio_command_parses_http_port_options() { - var result = _parser.Parse("stdio --http-port 8000"); - - var binder = new ModelBinder(); - - var options = (StartupOptions)binder.CreateInstance(new InvocationContext(result).BindingContext); + _rootCommand.Parse("stdio --http-port 8000").Invoke(); - options.HttpPort.PortNumber.Should().Be(8000); + _startupOptions.HttpPort.PortNumber.Should().Be(8000); } [Fact] public async Task stdio_command_parses_http_port_range_options() { - await _parser.InvokeAsync("stdio --http-port-range 3000-4000"); + await _rootCommand.Parse("stdio --http-port-range 3000-4000").InvokeAsync(); using var scope = new AssertionScope(); - _startOptions.HttpPortRange.Should().NotBeNull(); - _startOptions.HttpPortRange.Start.Should().Be(3000); - _startOptions.HttpPortRange.End.Should().Be(4000); + _startupOptions.HttpPortRange.Should().NotBeNull(); + _startupOptions.HttpPortRange.Start.Should().Be(3000); + _startupOptions.HttpPortRange.End.Should().Be(4000); } [Fact] public async Task stdio_command_requires_api_bootstrapping_when_http_is_enabled() { - await _parser.InvokeAsync("stdio --http-port-range 3000-4000"); + await _rootCommand.Parse("stdio --http-port-range 3000-4000").InvokeAsync(); var kernel = GetKernel(); kernel.FrontendEnvironment.As() - .RequiresAutomaticBootstrapping - .Should() - .BeTrue(); + .RequiresAutomaticBootstrapping + .Should() + .BeTrue(); } [Fact] public void stdio_command_defaults_to_csharp_kernel() { - var result = _parser.Parse("stdio"); - var binder = new ModelBinder(); - var options = (StdIOOptions)binder.CreateInstance(new InvocationContext(result).BindingContext); + _rootCommand.Parse("stdio").Invoke(); - options.DefaultKernel.Should().Be("csharp"); + _startupOptions.DefaultKernel.Should().Be("csharp"); } [Fact] public async Task stdio_command_does_not_enable_http_api_by_default() { - await _parser.InvokeAsync("stdio"); + // FIX: (stdio_command_does_not_enable_http_api_by_default) inline + var parseResult = _rootCommand.Parse("stdio"); + parseResult.Errors.Should().BeEmpty(); - _startOptions.EnableHttpApi.Should().BeFalse(); - } + await parseResult.InvokeAsync(); + _startupOptions.EnableHttpApi.Should().BeFalse(); + } [Fact] public void stdio_command_honors_default_kernel_option() { - var result = _parser.Parse("stdio --default-kernel bsharp"); - var binder = new ModelBinder(); - var options = (StdIOOptions)binder.CreateInstance(new InvocationContext(result).BindingContext); + _rootCommand.Parse("stdio --default-kernel bsharp").Invoke(); - options.DefaultKernel.Should().Be("bsharp"); + _startupOptions.DefaultKernel.Should().Be("bsharp"); } [Fact] public void Parser_configuration_is_valid() { - _parser.Configuration.ThrowIfInvalid(); + _rootCommand.ThrowIfInvalid(); } } \ No newline at end of file diff --git a/src/dotnet-interactive.Tests/CommandLine/FirstTimeUseSentinelTests.cs b/src/dotnet-interactive.Tests/CommandLine/FirstTimeUseSentinelTests.cs index bb02695b09..7b41f1bd50 100644 --- a/src/dotnet-interactive.Tests/CommandLine/FirstTimeUseSentinelTests.cs +++ b/src/dotnet-interactive.Tests/CommandLine/FirstTimeUseSentinelTests.cs @@ -2,15 +2,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.CommandLine.IO; -using System.CommandLine.Parsing; +using System.CommandLine; using System.IO; using System.Threading.Tasks; -using Microsoft.DotNet.Interactive.App.CommandLine; using Microsoft.DotNet.Interactive.Telemetry; using Microsoft.Extensions.DependencyInjection; using Pocket; using Xunit; +using CommandLineParser = Microsoft.DotNet.Interactive.App.CommandLine.CommandLineParser; namespace Microsoft.DotNet.Interactive.App.Tests.CommandLine; @@ -26,7 +25,7 @@ public FirstTimeUseSentinelTests() _disposables.Add(() => _connectionFile.Delete()); } - private static Parser CreateParser(bool sentinelExists) + private static RootCommand CreateParser(bool sentinelExists) { var firstTimeUseNoticeSentinel = new FirstTimeUseNoticeSentinel( @@ -39,9 +38,9 @@ private static Parser CreateParser(bool sentinelExists) return CommandLineParser.Create( new ServiceCollection(), - startServer: (options, invocationContext) => { }, - jupyter: (startupOptions, console, startServer, context) => Task.FromResult(1), - startKernelHost: (startupOptions, kernelHost, console) => Task.FromResult(1), + startWebServer: _ => { }, + startJupyter: (_, _) => Task.FromResult(1), + startStdio: (_, _) => Task.FromResult(1), telemetrySender: new FakeTelemetrySender(firstTimeUseNoticeSentinel)); } @@ -53,18 +52,18 @@ public void Dispose() [Fact] public async Task First_time_use_sentinel_does_not_exist_then_print_telemetry_first_time_use_welcome_message() { - var console = new TestConsole(); + var console = new StringWriter(); var parser = CreateParser(false); - await parser.InvokeAsync($"jupyter {_connectionFile}", console); - Assert.Contains("Telemetry", console.Out.ToString()); + await parser.Parse($"jupyter {_connectionFile}").InvokeAsync(new() { Output = console }); + Assert.Contains("Telemetry", console.ToString()); } [Fact] public async Task First_time_use_sentinel_exists_then_do_not_print_telemetry_first_time_use_welcome_message() { - var console = new TestConsole(); + var console = new StringWriter(); var parser = CreateParser(true); - await parser.InvokeAsync($"jupyter {_connectionFile}", console); - Assert.DoesNotContain("Telemetry", console.Out.ToString()); + await parser.Parse($"jupyter {_connectionFile}").InvokeAsync(new() { Output = console }); + Assert.DoesNotContain("Telemetry", console.ToString()); } } \ No newline at end of file diff --git a/src/dotnet-interactive.Tests/CommandLine/StartupTelemetryTests.cs b/src/dotnet-interactive.Tests/CommandLine/StartupTelemetryTests.cs index a60bf80140..bcfb251db8 100644 --- a/src/dotnet-interactive.Tests/CommandLine/StartupTelemetryTests.cs +++ b/src/dotnet-interactive.Tests/CommandLine/StartupTelemetryTests.cs @@ -2,28 +2,22 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.CommandLine.IO; -using System.CommandLine.Parsing; +using System.CommandLine; using System.IO; using System.Threading.Tasks; - using FluentAssertions; - -using Microsoft.DotNet.Interactive.App.CommandLine; using Microsoft.DotNet.Interactive.Telemetry; using Microsoft.Extensions.DependencyInjection; - using Pocket; - using Xunit; +using CommandLineParser = Microsoft.DotNet.Interactive.App.CommandLine.CommandLineParser; namespace Microsoft.DotNet.Interactive.App.Tests.CommandLine; public class StartupTelemetryTests : IDisposable { private readonly FakeTelemetrySender _fakeTelemetrySender; - private readonly TestConsole _console = new(); - private readonly Parser _parser; + private readonly RootCommand _rootCommand; private readonly FileInfo _connectionFile; private readonly CompositeDisposable _disposables = new(); private readonly FakeFirstTimeUseNoticeSentinel _firstTimeUseNoticeSentinel; @@ -47,11 +41,11 @@ public StartupTelemetryTests() _fakeTelemetrySender = new FakeTelemetrySender(_firstTimeUseNoticeSentinel); - _parser = CommandLineParser.Create( + _rootCommand = CommandLineParser.Create( new ServiceCollection(), - startServer: (options, invocationContext) => { }, - jupyter: (startupOptions, console, startServer, context) => Task.FromResult(1), - startKernelHost: (startupOptions, host, console) => Task.FromResult(1), + startWebServer: _ => { }, + startJupyter: (_, _) => Task.FromResult(1), + startStdio: (_, _) => Task.FromResult(1), telemetrySender: _fakeTelemetrySender); } @@ -60,7 +54,8 @@ public StartupTelemetryTests() [Fact] public async Task Jupyter_standalone_command_sends_telemetry() { - await _parser.InvokeAsync($"jupyter {_connectionFile}", _console); + var parseResult = _rootCommand.Parse($"jupyter {_connectionFile}"); + await parseResult.InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && x.Properties["verb"] == "JUPYTER".ToSha256Hash() && @@ -70,14 +65,14 @@ public async Task Jupyter_standalone_command_sends_telemetry() [Fact] public async Task Jupyter_standalone_command_has_one_entry() { - await _parser.InvokeAsync($"jupyter {_connectionFile}", _console); + await _rootCommand.Parse($"jupyter {_connectionFile}").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().HaveCount(1); } [Fact] public async Task Jupyter_default_kernel_csharp_sends_telemetry() { - await _parser.InvokeAsync($"jupyter --default-kernel csharp {_connectionFile}", _console); + await _rootCommand.Parse($"jupyter --default-kernel csharp {_connectionFile}").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && x.Properties["verb"] == "JUPYTER".ToSha256Hash() && @@ -87,14 +82,14 @@ public async Task Jupyter_default_kernel_csharp_sends_telemetry() [Fact] public async Task Jupyter_default_kernel_csharp_has_one_entry() { - await _parser.InvokeAsync($"jupyter --default-kernel csharp {_connectionFile}", _console); + await _rootCommand.Parse($"jupyter --default-kernel csharp {_connectionFile}").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().HaveCount(1); } [Fact] public async Task Jupyter_default_kernel_fsharp_sends_telemetry() { - await _parser.InvokeAsync($"jupyter --default-kernel fsharp {_connectionFile}", _console); + await _rootCommand.Parse($"jupyter --default-kernel fsharp {_connectionFile}").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && x.Properties["verb"] == "JUPYTER".ToSha256Hash() && @@ -104,14 +99,14 @@ public async Task Jupyter_default_kernel_fsharp_sends_telemetry() [Fact] public async Task Jupyter_default_kernel_fsharp_has_one_entry() { - await _parser.InvokeAsync($"jupyter --default-kernel fsharp {_connectionFile}", _console); + await _rootCommand.Parse($"jupyter --default-kernel fsharp {_connectionFile}").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().HaveCount(1); } [Fact] public async Task Jupyter_install_sends_telemetry() { - await _parser.InvokeAsync("jupyter install", _console); + await _rootCommand.Parse("jupyter install").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && x.Properties["verb"] == "JUPYTER".ToSha256Hash() && @@ -121,7 +116,7 @@ public async Task Jupyter_install_sends_telemetry() [Fact] public async Task Jupyter_install_has_one_entry() { - await _parser.InvokeAsync($"jupyter install", _console); + await _rootCommand.Parse($"jupyter install").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().HaveCount(1); } @@ -129,7 +124,7 @@ public async Task Jupyter_install_has_one_entry() public async Task Jupyter_default_kernel_csharp_ignore_connection_file_sends_telemetry() { var tmp = Path.GetTempFileName(); - await _parser.InvokeAsync($"jupyter --default-kernel csharp {_connectionFile}", _console); + await _rootCommand.Parse($"jupyter --default-kernel csharp {_connectionFile}").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && x.Properties["verb"] == "JUPYTER".ToSha256Hash() && @@ -140,7 +135,7 @@ public async Task Jupyter_default_kernel_csharp_ignore_connection_file_sends_tel [Fact] public async Task Jupyter_default_kernel_csharp_ignore_connection_file_has_one_entry() { - await _parser.InvokeAsync($"jupyter --default-kernel csharp {_connectionFile}", _console); + await _rootCommand.Parse($"jupyter --default-kernel csharp {_connectionFile}").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().HaveCount(1); } @@ -148,7 +143,7 @@ public async Task Jupyter_default_kernel_csharp_ignore_connection_file_has_one_e public async Task Jupyter_ignore_connection_file_sends_telemetry() { // Do not capture connection file - await _parser.InvokeAsync($"jupyter {_connectionFile}", _console); + await _rootCommand.Parse($"jupyter {_connectionFile}").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && x.Properties["verb"] == "JUPYTER".ToSha256Hash() && @@ -158,14 +153,14 @@ public async Task Jupyter_ignore_connection_file_sends_telemetry() [Fact] public async Task Jupyter_ignore_connection_file_has_one_entry() { - await _parser.InvokeAsync($"jupyter {_connectionFile}", _console); + await _rootCommand.Parse($"jupyter {_connectionFile}").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().HaveCount(1); } [Fact] public async Task Jupyter_with_verbose_option_sends_telemetry_just_for_jupyter_command() { - await _parser.InvokeAsync($"--verbose jupyter {_connectionFile}", _console); + await _rootCommand.Parse($"--verbose jupyter {_connectionFile}").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && x.Properties["verb"] == "JUPYTER".ToSha256Hash() && @@ -175,14 +170,14 @@ public async Task Jupyter_with_verbose_option_sends_telemetry_just_for_jupyter_c [Fact] public async Task Jupyter_with_verbose_option_has_one_entry() { - await _parser.InvokeAsync($"--verbose jupyter {_connectionFile}", _console); + await _rootCommand.Parse($"--verbose jupyter {_connectionFile}").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().HaveCount(1); } [Fact] public async Task Jupyter_with_invalid_argument_does_not_send_any_telemetry() { - await _parser.InvokeAsync("jupyter invalidargument", _console); + await _rootCommand.Parse("jupyter invalidargument").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().BeEmpty(); } @@ -190,14 +185,14 @@ public async Task Jupyter_with_invalid_argument_does_not_send_any_telemetry() public async Task Jupyter_default_kernel_with_invalid_kernel_does_not_send_any_telemetry() { // Do not capture anything, especially "oops". - await _parser.InvokeAsync($"jupyter --default-kernel oops {_connectionFile}", _console); + await _rootCommand.Parse($"jupyter --default-kernel oops {_connectionFile}").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().BeEmpty(); } [Fact] public async Task Jupyter_command_sends_frontend_telemetry() { - await _parser.InvokeAsync($"jupyter {_connectionFile}", _console); + await _rootCommand.Parse($"jupyter {_connectionFile}").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && x.Properties.Count == 3 && @@ -210,7 +205,7 @@ public async Task Jupyter_command_sends_frontend_telemetry() public async Task Jupyter_install_command_sends_default_frontend_telemetry() { var defaultFrontend = GetDefaultFrontendName(); - await _parser.InvokeAsync("jupyter install", _console); + await _rootCommand.Parse("jupyter install").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && x.Properties.Count == 3 && @@ -223,14 +218,15 @@ public async Task Jupyter_install_command_sends_default_frontend_telemetry() [Fact] public async Task Invalid_command_is_does_not_send_any_telemetry() { - await _parser.InvokeAsync("invalidcommand", _console); + await _rootCommand.Parse("invalidcommand").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().BeEmpty(); } [Fact] public async Task stdio_command_sends_frontend_telemetry() { - await _parser.InvokeAsync("[synapse] stdio", _console); + _rootCommand.Parse("[synapse] stdio").Errors.Should().HaveCount(0); + await _rootCommand.Parse("[synapse] stdio").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && x.Properties.Count == 3 && @@ -245,7 +241,7 @@ public async Task githubCodeSpaces_is_a_valid_frontend_for_stdio() Environment.SetEnvironmentVariable("CODESPACES", "true"); try { - await _parser.InvokeAsync("[vscode] stdio", _console); + await _rootCommand.Parse("[vscode] stdio").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( @@ -267,7 +263,7 @@ public async Task frontend_can_be_set_via_environment_variable() Environment.SetEnvironmentVariable("DOTNET_INTERACTIVE_FRONTEND_NAME", "test_runner"); try { - await _parser.InvokeAsync("stdio", _console); + await _rootCommand.Parse("stdio").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( @@ -286,7 +282,7 @@ public async Task frontend_can_be_set_via_environment_variable() [Fact] public async Task vscode_is_a_valid_frontend_for_stdio() { - await _parser.InvokeAsync("[vscode] stdio", _console); + await _rootCommand.Parse("[vscode] stdio").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && @@ -300,7 +296,7 @@ public async Task vscode_is_a_valid_frontend_for_stdio() public async Task stdio_command_sends_default_frontend_telemetry() { var defaultFrontend = GetDefaultFrontendName(); - await _parser.InvokeAsync("stdio", _console); + await _rootCommand.Parse("stdio").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && x.Properties.Count == 3 && @@ -319,7 +315,7 @@ private static string GetDefaultFrontendName() [Fact] public async Task stdio_standalone_command_sends_telemetry() { - await _parser.InvokeAsync("stdio", _console); + await _rootCommand.Parse("stdio").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && x.Properties.Count >= 2 && @@ -330,14 +326,14 @@ public async Task stdio_standalone_command_sends_telemetry() [Fact] public async Task stdio_command_has_one_entry() { - await _parser.InvokeAsync("stdio", _console); + await _rootCommand.Parse("stdio").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().HaveCount(1); } [Fact] public async Task stdio_default_kernel_csharp_sends_telemetry() { - await _parser.InvokeAsync("stdio --default-kernel csharp", _console); + await _rootCommand.Parse("stdio --default-kernel csharp").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && x.Properties.Count >= 2 && @@ -348,14 +344,14 @@ public async Task stdio_default_kernel_csharp_sends_telemetry() [Fact] public async Task stdio_default_kernel_csharp_has_one_entry() { - await _parser.InvokeAsync("stdio --default-kernel csharp", _console); + await _rootCommand.Parse("stdio --default-kernel csharp").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().HaveCount(1); } [Fact] public async Task stdio_default_kernel_fsharp_sends_telemetry() { - await _parser.InvokeAsync("stdio --default-kernel fsharp", _console); + await _rootCommand.Parse("stdio --default-kernel fsharp").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().Contain( x => x.EventName == "command" && x.Properties.Count >= 2 && @@ -366,7 +362,7 @@ public async Task stdio_default_kernel_fsharp_sends_telemetry() [Fact] public async Task stdio_default_kernel_fsharp_has_one_entry() { - await _parser.InvokeAsync("stdio --default-kernel fsharp", _console); + await _rootCommand.Parse("stdio --default-kernel fsharp").InvokeAsync(new() { Output = new StringWriter() }); _fakeTelemetrySender.TelemetryEvents.Should().HaveCount(1); } @@ -379,8 +375,9 @@ public async Task Show_first_time_message_if_environment_variable_is_not_set_and Environment.SetEnvironmentVariable(environmentVariableName, null); try { - await _parser.InvokeAsync($"jupyter {_connectionFile}", _console); - _console.Out.ToString().Should().Contain(TelemetrySender.WelcomeMessage); + var output = new StringWriter(); + await _rootCommand.Parse($"jupyter {_connectionFile}").InvokeAsync(new() { Output = output }); + output.ToString().Should().Contain(TelemetrySender.WelcomeMessage); } finally { @@ -397,8 +394,9 @@ public async Task Do_not_show_first_time_message_if_environment_variable_is_set( Environment.SetEnvironmentVariable(environmentVariableName, "1"); try { - await _parser.InvokeAsync($"jupyter {_connectionFile}", _console); - _console.Out.ToString().Should().NotContain(TelemetrySender.WelcomeMessage); + var output = new StringWriter(); + await _rootCommand.Parse($"jupyter {_connectionFile}").InvokeAsync(new() { Output = output }); + output.ToString().Should().NotContain(TelemetrySender.WelcomeMessage); } finally { @@ -436,16 +434,15 @@ public async Task stdio_command_sends_frontend_telemetry_when_frontend_is_VS( var parser = CommandLineParser.Create( new ServiceCollection(), - startServer: (options, invocationContext) => { }, - jupyter: (startupOptions, console, startServer, context) => Task.FromResult(1), - startKernelHost: (startupOptions, host, console) => Task.FromResult(1), + startWebServer: _ => { }, + startJupyter: (_, _) => Task.FromResult(1), + startStdio: (_, _) => Task.FromResult(1), telemetrySender: telemetrySender); - await parser.InvokeAsync( + await parser.Parse( $""" - [vs] stdio --working-dir {Directory.GetCurrentDirectory()} --kernel-host 9628-5c7e913f-8966-4afe-8d37-cc863292a352 - """, - _console); + [vs] stdio --working-dir {Directory.GetCurrentDirectory()} --kernel-host 9628-5c7e913f-8966-4afe-8d37-cc863292a352 + """).InvokeAsync(new() { Output = new StringWriter() }); if (isSkipFirstTimeExperienceEnvironmentVariableSet || firstTimeExperienceSentinelExists) { diff --git a/src/Microsoft.DotNet.Interactive.Tests/Utility/EnumerableExtensions.cs b/src/dotnet-interactive.Tests/Extensions/EnumerableExtensions.cs similarity index 92% rename from src/Microsoft.DotNet.Interactive.Tests/Utility/EnumerableExtensions.cs rename to src/dotnet-interactive.Tests/Extensions/EnumerableExtensions.cs index 871cdf1d34..ba498acb48 100644 --- a/src/Microsoft.DotNet.Interactive.Tests/Utility/EnumerableExtensions.cs +++ b/src/dotnet-interactive.Tests/Extensions/EnumerableExtensions.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -namespace Microsoft.DotNet.Interactive.Tests.Utility; +namespace Microsoft.DotNet.Interactive.App.Tests.Extensions; public static class EnumerableExtensions { diff --git a/src/dotnet-interactive.Tests/InProcessTestServer.cs b/src/dotnet-interactive.Tests/InProcessTestServer.cs index 7b5f919850..6d1752cb2d 100644 --- a/src/dotnet-interactive.Tests/InProcessTestServer.cs +++ b/src/dotnet-interactive.Tests/InProcessTestServer.cs @@ -2,15 +2,15 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.CommandLine.IO; -using System.CommandLine.Parsing; +using System.CommandLine; +using System.IO; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.TestHost; -using Microsoft.DotNet.Interactive.App.CommandLine; using Microsoft.DotNet.Interactive.Connection; using Microsoft.Extensions.DependencyInjection; +using CommandLineParser = Microsoft.DotNet.Interactive.App.CommandLine.CommandLineParser; namespace Microsoft.DotNet.Interactive.App.Tests; @@ -24,9 +24,9 @@ public static async Task StartServer(string args, Action(TaskCreationOptions.RunContinuationsAsynchronously); - var parser = CommandLineParser.Create( + var rootCommand = CommandLineParser.Create( server._serviceCollection, - (startupOptions, invocationContext) => + startupOptions => { servicesSetup?.Invoke(server._serviceCollection); var builder = Program.ConstructWebHostBuilder( @@ -37,7 +37,13 @@ public static async Task StartServer(string args, Action _kernelInstallations = new(); - private TestConsole Console { get; } = new(); - + private StringWriter Output { get; } = new(); + private StringWriter Error { get; } = new(); [Fact] public async Task Returns_success_output_when_kernel_installation_succeeded() @@ -30,11 +29,11 @@ public async Task Returns_success_output_when_kernel_installation_succeeded() UnpackKernelsSpecTo(kernelDir); var jupyterKernelSpecModuleSimulator = new JupyterKernelSpecModuleSimulator(true); - var kernelSpecInstaller = new JupyterKernelSpecInstaller(Console, jupyterKernelSpecModuleSimulator); + var kernelSpecInstaller = new JupyterKernelSpecInstaller(Output, Error, jupyterKernelSpecModuleSimulator); var result = await kernelSpecInstaller.TryInstallKernelAsync(kernelDir); - var output = Console.Out.ToString(); + var output = Output.ToString(); using var scope = new AssertionScope(); result.Should().BeTrue(); @@ -59,9 +58,9 @@ public async Task Uses_default_kernel_paths_when_kernelspec_module_is_not_on_pat Source = typeof(System.Diagnostics.Process).FullName }); - var kernelSpecInstaller = new JupyterKernelSpecInstaller(Console, jupyterKernelSpecModuleSimulator); + var kernelSpecInstaller = new JupyterKernelSpecInstaller(Output, Error, jupyterKernelSpecModuleSimulator); var result = await kernelSpecInstaller.TryInstallKernelAsync(kernelDir); - var output = Console.Out.ToString(); + var output = Output.ToString(); using var scope = new AssertionScope(); result.Should().BeTrue(); @@ -84,9 +83,9 @@ public async Task Fails_to_install_kernels_when_jupyter_is_not_installed() }); var defaultPath = jupyterKernelSpecModuleSimulator.GetDefaultKernelSpecDirectory(); - var kernelSpecInstaller = new JupyterKernelSpecInstaller(Console, jupyterKernelSpecModuleSimulator); + var kernelSpecInstaller = new JupyterKernelSpecInstaller(Output, Error, jupyterKernelSpecModuleSimulator); var result = await kernelSpecInstaller.TryInstallKernelAsync(kernelDir); - var error = Console.Error.ToString(); + var error = Error.ToString(); using var scope = new AssertionScope(); result.Should().BeFalse(); diff --git a/src/dotnet-interactive.Tests/dotnet-interactive.Tests.v3.ncrunchproject b/src/dotnet-interactive.Tests/dotnet-interactive.Tests.v3.ncrunchproject index b20f0fa0e8..04d014b306 100644 --- a/src/dotnet-interactive.Tests/dotnet-interactive.Tests.v3.ncrunchproject +++ b/src/dotnet-interactive.Tests/dotnet-interactive.Tests.v3.ncrunchproject @@ -83,12 +83,6 @@ Microsoft.DotNet.Interactive.App.Tests.TypeScriptInterfacesContractTests - - Microsoft.DotNet.Interactive.App.Tests.CommandLine.FirstTimeUseSentinelTests - - - Microsoft.DotNet.Interactive.App.Tests.CommandLine.StartupTelemetryTests - StdIoBehaviorTests @@ -96,13 +90,13 @@ Microsoft.DotNet.Interactive.App.Tests.StdioConnectionTests - Microsoft.DotNet.Interactive.App.Tests.HttpApiTests.stdio_mode_returns_javascript_api_via_http + Microsoft.DotNet.Interactive.App.Tests.StdIoBehaviorTests.Quit_command_causes_stdio_process_to_end Microsoft.DotNet.Interactive.App.Tests.CommandLine.CommandLineParserTests.stdio_mode_honors_log_path - Microsoft.DotNet.Interactive.App.Tests.StdIoBehaviorTests.Quit_command_causes_stdio_process_to_end + Microsoft.DotNet.Interactive.App.Tests.HttpApiTests.stdio_mode_returns_javascript_api_via_http diff --git a/src/dotnet-interactive/CommandLine/CommandLineParser.cs b/src/dotnet-interactive/CommandLine/CommandLineParser.cs index fbbadb917d..f095d89850 100644 --- a/src/dotnet-interactive/CommandLine/CommandLineParser.cs +++ b/src/dotnet-interactive/CommandLine/CommandLineParser.cs @@ -3,12 +3,9 @@ using System; using System.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.Invocation; -using System.CommandLine.IO; -using System.CommandLine.NamingConventionBinder; using System.CommandLine.Parsing; using System.IO; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -28,20 +25,16 @@ namespace Microsoft.DotNet.Interactive.App.CommandLine; public static class CommandLineParser { - public delegate void StartServer( - StartupOptions options, - InvocationContext context); + public delegate void StartWebServer( + StartupOptions options); - public delegate Task Jupyter( + public delegate Task StartJupyter( StartupOptions options, - IConsole console, - StartServer startServer = null, - InvocationContext context = null); + StartWebServer startWebServer = null); public delegate Task StartKernelHost( StartupOptions startupOptions, - KernelHost kernelHost, - IConsole console); + KernelHost kernelHost); public delegate Task StartNotebookParser( NotebookParserServer notebookParserServer, @@ -49,15 +42,13 @@ public delegate Task StartNotebookParser( public delegate Task StartHttp( StartupOptions options, - IConsole console, - StartServer startServer = null, - InvocationContext context = null); + StartWebServer startWebServer = null); - public static Parser Create( + public static RootCommand Create( IServiceCollection services, - StartServer startServer = null, - Jupyter jupyter = null, - StartKernelHost startKernelHost = null, + StartWebServer startWebServer = null, + StartJupyter startJupyter = null, + StartKernelHost startStdio = null, StartNotebookParser startNotebookParser = null, StartHttp startHttp = null, Action onServerStarted = null, @@ -71,21 +62,21 @@ public static Parser Create( } var disposeOnQuit = new CompositeDisposable(); - startServer ??= (startupOptions, invocationContext) => + startWebServer ??= startupOptions => { operation.Info("constructing webhost"); var webHost = Program.ConstructWebHost(startupOptions); disposeOnQuit.Add(webHost); - operation.Info("starting kestrel server"); + operation.Info("starting kestrel server"); webHost.Start(); onServerStarted?.Invoke(); webHost.WaitForShutdown(); operation.Dispose(); }; - jupyter ??= JupyterCommand.Do; + startJupyter ??= JupyterCommand.Do; - startKernelHost ??= StdIoMode.Do; + startStdio ??= StdIoMode.Do; startNotebookParser ??= ParseNotebookCommand.RunParserServer; @@ -99,113 +90,124 @@ public static Parser Create( buildInfo.AssemblyInformationalVersion, new FirstTimeUseNoticeSentinel(buildInfo.AssemblyInformationalVersion)); - var verboseOption = new Option( - "--verbose", - LocalizationResources.Cli_dotnet_interactive_verbose_Description()); - - var logPathOption = new Option( - "--log-path", - LocalizationResources.Cli_dotnet_interactive_log_path_Description()); + var verboseOption = new Option("--verbose") + { + Description = LocalizationResources.Cli_dotnet_interactive_verbose_Description(), + Recursive = true + }; - var pathOption = new Option( - "--path", - LocalizationResources.Cli_dotnet_interactive_jupyter_install_path_Description()) - .ExistingOnly(); + var logPathOption = new Option("--log-path") + { + Description = LocalizationResources.Cli_dotnet_interactive_log_path_Description(), + Recursive = true + }; - var defaultKernelOption = new Option( - "--default-kernel", - description: LocalizationResources.Cli_dotnet_interactive_jupyter_default_kernel_Description(), - getDefaultValue: () => "csharp").AddCompletions("fsharp", "csharp", "pwsh"); + var httpLocalOnlyOption = new Option("--http-local-only") + { + Description = LocalizationResources.Cli_dotnet_interactive_jupyter_http_local_only_Description() + }; - var rootCommand = DotnetInteractive(); + Uri ParseKernelHost(ArgumentResult x) => + x.Tokens.Count is 0 + ? KernelHost.CreateHostUriForCurrentProcessId() + : KernelHost.CreateHostUri(x.Tokens[0].Value); - rootCommand.AddCommand(Jupyter()); - rootCommand.AddCommand(StdIO()); - rootCommand.AddCommand(NotebookParser()); + var kernelHostOption = new Option("--kernel-host") + { + CustomParser = ParseKernelHost, + DefaultValueFactory = ParseKernelHost, + Description = LocalizationResources.Cli_dotnet_interactive_stdio_kernel_host_Description() + }; - var eventBuilder = new StartupTelemetryEventBuilder(Sha256Hasher.ToSha256HashWithNormalizedCasing); + var jupyterConnectionFileArg = new Argument("connection-file") + { + Description = LocalizationResources.Cli_dotnet_interactive_jupyter_connection_file_Description() + }.AcceptExistingOnly(); - return new CommandLineBuilder(rootCommand) - .UseDefaults() - .CancelOnProcessTermination() - .AddMiddleware(async (context, next) => + var jupyterInstallPathOption = new Option("--path") { - if (context.ParseResult.Errors.Count == 0) - { - telemetrySender.TrackStartupEvent(context.ParseResult, eventBuilder); - } + Description = LocalizationResources.Cli_dotnet_interactive_jupyter_install_path_Description() + } + .AcceptExistingOnly(); - // If sentinel does not exist, print the welcome message showing the telemetry notification. - if (!TelemetrySender.SkipFirstTimeExperience && - !telemetrySender.FirstTimeUseNoticeSentinelExists()) - { - context.Console.Out.WriteLine(); - context.Console.Out.WriteLine(TelemetrySender.WelcomeMessage); + var defaultKernelOption = new Option("--default-kernel") + { + Description = LocalizationResources.Cli_dotnet_interactive_jupyter_default_kernel_Description(), + DefaultValueFactory = _ => "csharp" + }; + defaultKernelOption.CompletionSources.Add("fsharp", "csharp", "pwsh", "http"); - telemetrySender.CreateFirstTimeUseNoticeSentinelIfNotExists(); - } + var workingDirOption = new Option("--working-dir") + { + DefaultValueFactory = _ => new DirectoryInfo(Environment.CurrentDirectory), + Description = LocalizationResources.Cli_dotnet_interactive_stdio_working_directory_Description() + }; - await next(context); - }) - .Build(); + var rootCommand = DotnetInteractive(); + + rootCommand.Add(Jupyter()); + rootCommand.Add(StdIO()); + rootCommand.Add(NotebookParser()); + + // directives used for marking telemetry for different frontend clients + rootCommand.Directives.Add(new("jupyter")); + rootCommand.Directives.Add(new("synapse")); + rootCommand.Directives.Add(new("vs")); + rootCommand.Directives.Add(new("vscode")); + + return rootCommand; RootCommand DotnetInteractive() { - var command = new RootCommand + var command = new RootCommand("dotnet-interactive") { - Name = "dotnet-interactive", Description = LocalizationResources.Cli_dotnet_interactive_Description() }; - command.AddGlobalOption(logPathOption); - command.AddGlobalOption(verboseOption); + command.Add(logPathOption); + command.Add(verboseOption); return command; } Command Jupyter() { - var httpPortRangeOption = new Option( - "--http-port-range", - parseArgument: result => result.Tokens.Count == 0 ? HttpPortRange.Default : ParsePortRangeOption(result), - description: LocalizationResources.Cli_dotnet_interactive_jupyter_install_http_port_range_Description(), - isDefault: true); - - var httpLocalOnlyOption = new Option( - "--http-local-only", - description: LocalizationResources.Cli_dotnet_interactive_jupyter_http_local_only_Description() - ); + var httpPortRangeOption = new Option("--http-port-range") + { + CustomParser = ParsePortRangeOption, + DefaultValueFactory = ParsePortRangeOption, + Description = LocalizationResources.Cli_dotnet_interactive_jupyter_install_http_port_range_Description() + }; + var jupyterCommand = new Command("jupyter", LocalizationResources.Cli_dotnet_interactive_jupyter_Description()) { defaultKernelOption, httpLocalOnlyOption, httpPortRangeOption, - new Argument - { - Name = "connection-file", - Description = LocalizationResources.Cli_dotnet_interactive_jupyter_connection_file_Description() - }.ExistingOnly() + jupyterConnectionFileArg }; - jupyterCommand.Handler = CommandHandler.Create(JupyterHandler); + jupyterCommand.SetAction(JupyterHandler); - var installCommand = new Command("install", LocalizationResources.Cli_dotnet_interactive_jupyter_install_Description()) + var installCommand = new Command("install") { - httpPortRangeOption, - pathOption + Description = LocalizationResources.Cli_dotnet_interactive_jupyter_install_Description(), }; + installCommand.Add(httpPortRangeOption); + installCommand.Add(jupyterInstallPathOption); - installCommand.Handler = CommandHandler.Create((context, httpPortRange, path) => JupyterInstallHandler(httpPortRange, path, context)); + installCommand.SetAction(JupyterInstallHandler); - jupyterCommand.AddCommand(installCommand); + jupyterCommand.Add(installCommand); return jupyterCommand; - async Task JupyterHandler(StartupOptions startupOptions, JupyterOptions options, IConsole console, InvocationContext context, CancellationToken cancellationToken) + async Task JupyterHandler(ParseResult parseResult, CancellationToken cancellationToken) { - var frontendEnvironment = new HtmlNotebookFrontendEnvironment(); - var kernel = KernelBuilder.CreateKernel(options.DefaultKernel, frontendEnvironment, startupOptions, telemetrySender); + var startupOptions = StartupOptions.Parse(parseResult); + var jupyterOptions = new JupyterOptions(parseResult.GetValue(jupyterConnectionFileArg), parseResult.GetValue(defaultKernelOption)); + var kernel = KernelBuilder.CreateKernel(jupyterOptions.DefaultKernel, new HtmlNotebookFrontendEnvironment(), startupOptions, telemetrySender); cancellationToken.Register(kernel.Dispose); await JupyterClientKernelExtension.LoadAsync(kernel); @@ -214,7 +216,7 @@ async Task JupyterHandler(StartupOptions startupOptions, JupyterOptions opt var clientSideKernelClient = new SignalRBackchannelKernelClient(); - services.AddSingleton(_ => ConnectionInformation.Load(options.ConnectionFile)) + services.AddSingleton(_ => ConnectionInformation.Load(jupyterOptions.ConnectionFile)) .AddSingleton(clientSideKernelClient) .AddSingleton(c => { @@ -225,71 +227,77 @@ async Task JupyterHandler(StartupOptions startupOptions, JupyterOptions opt .AddSingleton() .AddSingleton(); - var result = await jupyter(startupOptions, console, startServer, context); + SendStartupTelemetry(parseResult, telemetrySender); + + var result = await startJupyter(startupOptions, startWebServer); return result; } - Task JupyterInstallHandler(HttpPortRange httpPortRange, DirectoryInfo path, InvocationContext context) + Task JupyterInstallHandler(ParseResult parseResult, CancellationToken cancellationToken) { - var jupyterInstallCommand = new JupyterInstallCommand(new JupyterKernelSpecInstaller(context.Console), httpPortRange, path); + SendStartupTelemetry(parseResult, telemetrySender); + + var jupyterInstallCommand = new JupyterInstallCommand( + new JupyterKernelSpecInstaller( + parseResult.InvocationConfiguration.Output, + parseResult.InvocationConfiguration.Error), + parseResult.GetValue(httpPortRangeOption), + parseResult.GetValue(jupyterInstallPathOption)); return jupyterInstallCommand.InvokeAsync(); } } Command StdIO() { - var httpPortRangeOption = new Option( - "--http-port-range", - parseArgument: result => result.Tokens.Count == 0 ? HttpPortRange.Default : ParsePortRangeOption(result), - description: LocalizationResources.Cli_dotnet_interactive_stdio_http_port_range_Description()); - - var httpPortOption = new Option( - "--http-port", - description: LocalizationResources.Cli_dotnet_interactive_stdio_http_port_Description(), - parseArgument: result => + // FIX: (Create) can this be removed? + var previewOption = new Option("--preview") + { + Description = LocalizationResources.Cli_dotnet_interactive_stdio_preview_Description() + }; + + var httpPortRangeOption = new Option("--http-port-range") + { + CustomParser = ParsePortRangeOption, + Description = LocalizationResources.Cli_dotnet_interactive_jupyter_install_http_port_range_Description() + }; + + var httpPortOption = new Option("--http-port") + { + Description = LocalizationResources.Cli_dotnet_interactive_stdio_http_port_Description(), + CustomParser = result => { - if (result.FindResultFor(httpPortRangeOption) is { } conflictingOption) + if (result.GetResult(httpPortRangeOption) is { Implicit: false } conflictingOption) { - var parsed = result.Parent as OptionResult; - result.ErrorMessage = - LocalizationResources.Cli_dotnet_interactive_stdio_http_port_ErrorMessageCannotSpecifyBoth(conflictingOption.Token.Value, parsed.Token.Value); + var parsed = (OptionResult)result.Parent; + result.AddError( + LocalizationResources.Cli_dotnet_interactive_stdio_http_port_ErrorMessageCannotSpecifyBoth( + conflictingOption.IdentifierToken?.Value, + parsed!.IdentifierToken?.Value)); return null; } - if (result.Tokens.Count == 0) + if (result.Tokens.Count is 0) { return HttpPort.Auto; } var source = result.Tokens[0].Value; - if (source == "*") + if (source is "*") { return HttpPort.Auto; } if (!int.TryParse(source, out var portNumber)) { - result.ErrorMessage = LocalizationResources.Cli_dotnet_interactive_stdio_http_port_ErrorMessageMustSpecifyPortNumber(); + result.AddError(LocalizationResources.Cli_dotnet_interactive_stdio_http_port_ErrorMessageMustSpecifyPortNumber()); return null; } return new HttpPort(portNumber); - }); - - var kernelHostOption = new Option( - "--kernel-host", - parseArgument: x => x.Tokens.Count == 0 ? KernelHost.CreateHostUriForCurrentProcessId() : KernelHost.CreateHostUri(x.Tokens[0].Value), - isDefault: true, - description: LocalizationResources.Cli_dotnet_interactive_stdio_kernel_host_Description()); - - var previewOption = new Option("--preview", description: LocalizationResources.Cli_dotnet_interactive_stdio_preview_Description()); - - var workingDirOption = new Option( - "--working-dir", - () => new DirectoryInfo(Environment.CurrentDirectory), - LocalizationResources.Cli_dotnet_interactive_stdio_working_directory_Description()); + }, + }; var stdIOCommand = new Command( "stdio", @@ -302,140 +310,151 @@ Command StdIO() previewOption, workingDirOption }; + + stdIOCommand.SetAction(async (parseResult, cancellationToken) => + { + using var _ = + parseResult.InvocationConfiguration.Output is StringWriter + ? Disposable.Empty + : Program.StartToolLogging(parseResult.GetValue(logPathOption)); - stdIOCommand.Handler = CommandHandler.Create( - async (startupOptions, options, console, context, cancellationToken) => - { - using var _ = - console is TestConsole - ? Disposable.Empty - : Program.StartToolLogging(startupOptions.LogPath); + using var operation = Log.OnEnterAndExit(); + operation.Trace("Command line: {0}", Environment.CommandLine); + operation.Trace("Process ID: {0}", Environment.ProcessId); - using var operation = Log.OnEnterAndExit(); - operation.Trace("Command line: {0}", Environment.CommandLine); - operation.Trace("Process ID: {0}", Environment.ProcessId); + var startupOptions = StartupOptions.Parse(parseResult); - Console.InputEncoding = Encoding.UTF8; - Console.OutputEncoding = Encoding.UTF8; - Environment.CurrentDirectory = startupOptions.WorkingDir.FullName; + Console.InputEncoding = Encoding.UTF8; + Console.OutputEncoding = Encoding.UTF8; + Environment.CurrentDirectory = startupOptions.WorkingDir.FullName; - FrontendEnvironment frontendEnvironment = startupOptions.EnableHttpApi - ? new HtmlNotebookFrontendEnvironment() - : new BrowserFrontendEnvironment(); + FrontendEnvironment frontendEnvironment = startupOptions.EnableHttpApi + ? new HtmlNotebookFrontendEnvironment() + : new BrowserFrontendEnvironment(); - var kernel = KernelBuilder.CreateKernel( - options.DefaultKernel, - frontendEnvironment, - startupOptions, - telemetrySender); + var kernel = KernelBuilder.CreateKernel( + parseResult.GetValue(defaultKernelOption), + frontendEnvironment, + startupOptions, + telemetrySender); - services.AddKernel(kernel); + services.AddKernel(kernel); - cancellationToken.Register(() => kernel.Dispose()); + cancellationToken.Register(() => kernel.Dispose()); - var sender = KernelCommandAndEventSender.FromTextWriter( - Console.Out, - KernelHost.CreateHostUri("stdio")); + var sender = KernelCommandAndEventSender.FromTextWriter( + Console.Out, + KernelHost.CreateHostUri("stdio")); - var receiver = KernelCommandAndEventReceiver.FromTextReader(Console.In); + var receiver = KernelCommandAndEventReceiver.FromTextReader(Console.In); - var host = kernel.UseHost( - sender, - receiver, - startupOptions.KernelHost); + var host = kernel.UseHost( + sender, + receiver, + startupOptions.KernelHostUri); - kernel.UseQuitCommand(() => - { - host.Dispose(); - Environment.Exit(0); - return Task.CompletedTask; - }); + kernel.UseQuitCommand(() => + { + host.Dispose(); + Environment.Exit(0); + return Task.CompletedTask; + }); - var isVSCode = context.ParseResult.Directives.Contains("vscode") || - !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CODESPACES")); + var isVSCode = parseResult.Tokens.Any(t => t is { Value: "[vscode]", Type: TokenType.Directive }) || + !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CODESPACES")); - if (isVSCode) - { - await VSCodeClientKernelExtension.LoadAsync(kernel); - } + if (isVSCode) + { + await VSCodeClientKernelExtension.LoadAsync(kernel); + } + + SendStartupTelemetry(parseResult, telemetrySender); + + if (startupOptions.EnableHttpApi) + { + var clientSideKernelClient = new SignalRBackchannelKernelClient(); + + services.AddSingleton(clientSideKernelClient); - if (startupOptions.EnableHttpApi) + if (isVSCode) { - var clientSideKernelClient = new SignalRBackchannelKernelClient(); - - services.AddSingleton(clientSideKernelClient); - - if (isVSCode) - { - ((HtmlNotebookFrontendEnvironment)frontendEnvironment).RequiresAutomaticBootstrapping = false; - } - else - { - kernel.Add( - new JavaScriptKernel(clientSideKernelClient).UseValueSharing(), - new[] { "js" }); - } - - onServerStarted ??= () => - { - var _ = host.ConnectAsync(); - }; - await startHttp(startupOptions, console, startServer, context); + ((HtmlNotebookFrontendEnvironment)frontendEnvironment).RequiresAutomaticBootstrapping = false; } else { - await startKernelHost(startupOptions, host, console); + kernel.Add( + new JavaScriptKernel(clientSideKernelClient).UseValueSharing(), + ["js"]); } - return 0; - }); + onServerStarted ??= () => + { + var _ = host.ConnectAsync(); + }; + + await startHttp(startupOptions, startWebServer); + } + else + { + await startStdio(startupOptions, host); + } + + return 0; + }); return stdIOCommand; } Command NotebookParser() { - var notebookParserCommand = new Command( - "notebook-parser", - LocalizationResources.Cli_dotnet_interactive_notebook_parserDescription()); - notebookParserCommand.Handler = CommandHandler.Create(async (InvocationContext context) => + var notebookParserCommand = new Command("notebook-parser") + { + Description = LocalizationResources.Cli_dotnet_interactive_notebook_parserDescription() + }; + + notebookParserCommand.SetAction(async (parseResult, cancellationToken) => { Console.InputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8; var notebookParserServer = new NotebookParserServer(Console.In, Console.Out); - context.GetCancellationToken().Register(() => notebookParserServer.Dispose()); - await startNotebookParser(notebookParserServer, context.ParseResult.GetValueForOption(logPathOption)); + cancellationToken.Register(() => notebookParserServer.Dispose()); + await startNotebookParser(notebookParserServer, parseResult.GetValue(logPathOption)); }); return notebookParserCommand; } static HttpPortRange ParsePortRangeOption(ArgumentResult result) { + if (result.Tokens.Count == 0) + { + return HttpPortRange.Default; + } + var source = result.Tokens[0].Value; if (string.IsNullOrWhiteSpace(source)) { - result.ErrorMessage = LocalizationResources.Cli_ErrorMessageMustSpecifyPortRange(); + result.AddError(LocalizationResources.Cli_ErrorMessageMustSpecifyPortRange()); return null; } - var parts = source.Split(new[] { "-" }, StringSplitOptions.RemoveEmptyEntries); + var parts = source.Split(["-"], StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) { - result.ErrorMessage = LocalizationResources.Cli_ErrorMessageMustSpecifyPortRange(); + result.AddError(LocalizationResources.Cli_ErrorMessageMustSpecifyPortRange()); return null; } if (!int.TryParse(parts[0], out var start) || !int.TryParse(parts[1], out var end)) { - result.ErrorMessage = LocalizationResources.CliErrorMessageMustSpecifyPortRangeAsStartPortEndPort(); + result.AddError(LocalizationResources.CliErrorMessageMustSpecifyPortRangeAsStartPortEndPort()); return null; } if (start > end) { - result.ErrorMessage = LocalizationResources.CliErrorMessageStartPortMustBeLower(); + result.AddError(LocalizationResources.CliErrorMessageStartPortMustBeLower()); return null; } @@ -443,4 +462,24 @@ static HttpPortRange ParsePortRangeOption(ArgumentResult result) return pr; } } + + private static void SendStartupTelemetry(ParseResult parseResult, TelemetrySender telemetrySender) + { + var eventBuilder = new StartupTelemetryEventBuilder(Sha256Hasher.ToSha256HashWithNormalizedCasing); + + if (parseResult.Errors.Count == 0) + { + telemetrySender.TrackStartupEvent(parseResult, eventBuilder); + } + + // If sentinel does not exist, print the welcome message showing the telemetry notification. + if (!TelemetrySender.SkipFirstTimeExperience && + !telemetrySender.FirstTimeUseNoticeSentinelExists()) + { + Console.Out.WriteLine(); + Console.Out.WriteLine(TelemetrySender.WelcomeMessage); + + telemetrySender.CreateFirstTimeUseNoticeSentinelIfNotExists(); + } + } } \ No newline at end of file diff --git a/src/dotnet-interactive/CommandLine/HttpCommand.cs b/src/dotnet-interactive/CommandLine/HttpCommand.cs index 6de93c6e42..8c95759c0a 100644 --- a/src/dotnet-interactive/CommandLine/HttpCommand.cs +++ b/src/dotnet-interactive/CommandLine/HttpCommand.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.CommandLine; -using System.CommandLine.Invocation; using System.Threading.Tasks; namespace Microsoft.DotNet.Interactive.App.CommandLine; @@ -11,11 +9,9 @@ public static class HttpCommand { public static Task Do( StartupOptions startupOptions, - IConsole console, - CommandLineParser.StartServer startServer = null, - InvocationContext context = null) + CommandLineParser.StartWebServer startWebServer = null) { - startServer?.Invoke(startupOptions, context); + startWebServer?.Invoke(startupOptions); return Task.FromResult(0); } diff --git a/src/dotnet-interactive/CommandLine/JupyterCommand.cs b/src/dotnet-interactive/CommandLine/JupyterCommand.cs index 4e2347817e..f0be7aa791 100644 --- a/src/dotnet-interactive/CommandLine/JupyterCommand.cs +++ b/src/dotnet-interactive/CommandLine/JupyterCommand.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.CommandLine; -using System.CommandLine.Invocation; using System.Threading.Tasks; namespace Microsoft.DotNet.Interactive.App.CommandLine; @@ -11,11 +9,9 @@ public static class JupyterCommand { public static Task Do( StartupOptions startupOptions, - IConsole console, - CommandLineParser.StartServer startServer = null, - InvocationContext context = null) + CommandLineParser.StartWebServer startWebServer = null) { - startServer?.Invoke(startupOptions, context); + startWebServer?.Invoke(startupOptions); return Task.FromResult(0); } diff --git a/src/dotnet-interactive/CommandLine/StartupOptions.cs b/src/dotnet-interactive/CommandLine/StartupOptions.cs index 90bbe81bb6..6ccbd705f8 100644 --- a/src/dotnet-interactive/CommandLine/StartupOptions.cs +++ b/src/dotnet-interactive/CommandLine/StartupOptions.cs @@ -1,10 +1,11 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.DotNet.Interactive.Http; using System; +using System.CommandLine; using System.IO; using System.Net.NetworkInformation; -using Microsoft.DotNet.Interactive.Http; namespace Microsoft.DotNet.Interactive.App.CommandLine; @@ -15,42 +16,64 @@ public StartupOptions( bool verbose = false, HttpPortRange httpPortRange = null, HttpPort httpPort = null, - Uri kernelHost = null, + Uri kernelHostUri = null, DirectoryInfo workingDir = null, - bool httpLocalOnly = false - ) + bool httpLocalOnly = false, + FileInfo jupyterConnectionFile = null, + string defaultKernel = null) { LogPath = logPath; Verbose = verbose; HttpPortRange = httpPortRange; HttpPort = httpPort; - KernelHost = kernelHost; + KernelHostUri = kernelHostUri; WorkingDir = workingDir; + JupyterConnectionFile = jupyterConnectionFile; + DefaultKernel = defaultKernel; if (httpLocalOnly) + { GetAllNetworkInterfaces = GetNetworkInterfacesHttpLocalOnly; + } else + { GetAllNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces; + } } public DirectoryInfo LogPath { get; } public bool Verbose { get; } - public HttpPort HttpPort { get; internal set; } + public HttpPort HttpPort { get; set; } + + public HttpPortRange HttpPortRange { get; } + + public Uri KernelHostUri { get; } - public HttpPortRange HttpPortRange { get; internal set; } - - public Uri KernelHost { get; } + public DirectoryInfo WorkingDir { get; } - public DirectoryInfo WorkingDir { get; internal set; } + public FileInfo JupyterConnectionFile { get; } + + public string DefaultKernel { get; } public Func GetAllNetworkInterfaces { get; } public bool EnableHttpApi => HttpPort is not null || HttpPortRange is not null; public static NetworkInterface[] GetNetworkInterfacesHttpLocalOnly() - { + { return []; } -} + + public static StartupOptions Parse(ParseResult parseResult) => + new(parseResult.GetValue("--log-path"), + parseResult.GetValue("--verbose"), + parseResult.GetValue("--http-port-range"), + parseResult.GetValue("--http-port"), + parseResult.GetValue("--kernel-host"), + parseResult.GetValue("--working-dir"), + parseResult.GetValue("--http-local-only"), + parseResult.GetValue("connection-file"), + parseResult.GetValue("--default-kernel")); +} \ No newline at end of file diff --git a/src/dotnet-interactive/CommandLine/StdIOOptions.cs b/src/dotnet-interactive/CommandLine/StdIOOptions.cs deleted file mode 100644 index 113def8902..0000000000 --- a/src/dotnet-interactive/CommandLine/StdIOOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.DotNet.Interactive.App.CommandLine; - -public class StdIOOptions -{ - public StdIOOptions(string defaultKernel) - { - DefaultKernel = defaultKernel; - } - - public string DefaultKernel { get; } -} \ No newline at end of file diff --git a/src/dotnet-interactive/CommandLine/StdIoMode.cs b/src/dotnet-interactive/CommandLine/StdIoMode.cs index 278d516e52..0783321967 100644 --- a/src/dotnet-interactive/CommandLine/StdIoMode.cs +++ b/src/dotnet-interactive/CommandLine/StdIoMode.cs @@ -1,14 +1,13 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.CommandLine; using System.Threading.Tasks; namespace Microsoft.DotNet.Interactive.App.CommandLine; internal static class StdIoMode { - public static async Task Do(StartupOptions startupOptions, KernelHost kernelHost, IConsole console) + public static async Task Do(StartupOptions startupOptions, KernelHost kernelHost) { await kernelHost.ConnectAndWaitAsync(); return 0; diff --git a/src/dotnet-interactive/Http/HttpPortRange.cs b/src/dotnet-interactive/Http/HttpPortRange.cs index 3fdb6fcf2f..80d676171e 100644 --- a/src/dotnet-interactive/Http/HttpPortRange.cs +++ b/src/dotnet-interactive/Http/HttpPortRange.cs @@ -15,7 +15,7 @@ public HttpPortRange(int start, int end) public int End { get; } - public static HttpPortRange Default { get; } = new(2048,3000); + public static HttpPortRange Default { get; } = new(2048, 3000); public override string ToString() => $"{Start}-{End}"; } \ No newline at end of file diff --git a/src/dotnet-interactive/JupyterKernelSpecInstaller.cs b/src/dotnet-interactive/JupyterKernelSpecInstaller.cs index 191963861e..d728cb2fa5 100644 --- a/src/dotnet-interactive/JupyterKernelSpecInstaller.cs +++ b/src/dotnet-interactive/JupyterKernelSpecInstaller.cs @@ -3,8 +3,6 @@ using Microsoft.DotNet.Interactive.Jupyter; using System; -using System.CommandLine; -using System.CommandLine.IO; using System.ComponentModel; using System.IO; using System.Linq; @@ -15,16 +13,18 @@ namespace Microsoft.DotNet.Interactive.App; public class JupyterKernelSpecInstaller : IJupyterKernelSpecInstaller { - private readonly IConsole _console; + private readonly TextWriter _stdOut; + private readonly TextWriter _stdErr; private readonly IJupyterKernelSpecModule _kernelSpecModule; - public JupyterKernelSpecInstaller(IConsole console) : this(console, new JupyterKernelSpecModule()) + public JupyterKernelSpecInstaller(TextWriter stdOut, TextWriter stdErr) : this(stdOut, stdErr, new JupyterKernelSpecModule()) { } - public JupyterKernelSpecInstaller(IConsole console, IJupyterKernelSpecModule jupyterKernelSpecModule) + public JupyterKernelSpecInstaller(TextWriter stdOut, TextWriter stdErr, IJupyterKernelSpecModule jupyterKernelSpecModule) { - _console = console ?? throw new ArgumentNullException(nameof(console)); + _stdOut = stdOut ?? throw new ArgumentNullException(nameof(stdOut)); + _stdErr = stdErr ?? throw new ArgumentNullException(nameof(stdErr)); _kernelSpecModule = jupyterKernelSpecModule; } @@ -42,8 +42,8 @@ public async Task TryInstallKernelAsync(DirectoryInfo sourceDirectory, Dir var result = await _kernelSpecModule.InstallKernelAsync(sourceDirectory); if (result.ExitCode == 0) { - _console.Out.WriteLine("Installing using jupyter kernelspec module."); - _console.Out.WriteLine($"Installed \"{kernelDisplayName}\" kernel."); + _stdOut.WriteLine("Installing using jupyter kernelspec module."); + _stdOut.WriteLine($"Installed \"{kernelDisplayName}\" kernel."); return true; } } @@ -52,7 +52,7 @@ public async Task TryInstallKernelAsync(DirectoryInfo sourceDirectory, Dir // file not found when executing process if (!w32e.Source.Contains(typeof(System.Diagnostics.Process).FullName)) { - _console.Error.WriteLine($"Failed to install \"{kernelDisplayName}\" kernel."); + _stdErr.WriteLine($"Failed to install \"{kernelDisplayName}\" kernel."); throw; } } @@ -67,22 +67,22 @@ private bool InstallKernelSpecToDirectory(DirectoryInfo sourceDirectory, Directo { if (!destination.Exists) { - _console.Error.WriteLine($"The kernelspec path {destination.FullName} does not exist."); - _console.Error.WriteLine($"Failed to install \"{kernelDisplayName}\" kernel."); + _stdErr.WriteLine($"The kernelspec path {destination.FullName} does not exist."); + _stdErr.WriteLine($"Failed to install \"{kernelDisplayName}\" kernel."); return false; } - _console.Out.WriteLine($"Installing using path {destination.FullName}."); + _stdOut.WriteLine($"Installing using path {destination.FullName}."); var succeeded = CopyKernelSpecFiles(sourceDirectory, destination); if (succeeded) { - _console.Out.WriteLine($"Installed \"{kernelDisplayName}\" kernel."); + _stdOut.WriteLine($"Installed \"{kernelDisplayName}\" kernel."); } else { - _console.Error.WriteLine( + _stdErr.WriteLine( $"Failed to install \"{kernelDisplayName}\" kernel."); } @@ -112,7 +112,7 @@ private bool CopyKernelSpecFiles(DirectoryInfo source, DirectoryInfo destination if (!destination.Exists) { - _console.Error.WriteLine($"Directory {destination.FullName} does not exist."); + _stdErr.WriteLine($"Directory {destination.FullName} does not exist."); return false; } @@ -140,7 +140,7 @@ private bool CopyKernelSpecFiles(DirectoryInfo source, DirectoryInfo destination } catch (IOException ioe) { - _console.Error.WriteLine(ioe.Message); + _stdErr.WriteLine(ioe.Message); return false; } diff --git a/src/dotnet-interactive/KernelBuilder.cs b/src/dotnet-interactive/KernelBuilder.cs index 8046a48262..5fd95e685d 100644 --- a/src/dotnet-interactive/KernelBuilder.cs +++ b/src/dotnet-interactive/KernelBuilder.cs @@ -27,7 +27,7 @@ internal static CompositeKernel CreateKernel( string defaultKernelName, FrontendEnvironment frontendEnvironment, StartupOptions startupOptions, - TelemetrySender telemetrySender) + TelemetrySender telemetrySender = null) { using var _ = Log.OnEnterAndExit("Creating kernels"); @@ -84,7 +84,7 @@ internal static CompositeKernel CreateKernel( .UseCodeExpansions(GetCodeExpansionConfiguration(secretManager)); kernel.AddConnectDirective(new ConnectSignalRDirective()); - kernel.AddConnectDirective(new ConnectStdIoDirective(startupOptions.KernelHost)); + kernel.AddConnectDirective(new ConnectStdIoDirective(startupOptions.KernelHostUri)); kernel.AddConnectDirective( new ConnectJupyterKernelDirective() @@ -95,7 +95,10 @@ internal static CompositeKernel CreateKernel( kernel.DefaultKernelName = defaultKernelName; - kernel.UseTelemetrySender(telemetrySender); + if (telemetrySender is not null) + { + kernel.UseTelemetrySender(telemetrySender); + } return kernel; } diff --git a/src/dotnet-interactive/KernelExtensions.cs b/src/dotnet-interactive/KernelExtensions.cs index 4b97dfa9c3..b38106bb4a 100644 --- a/src/dotnet-interactive/KernelExtensions.cs +++ b/src/dotnet-interactive/KernelExtensions.cs @@ -448,6 +448,11 @@ public static CompositeKernel UseTelemetrySender( this CompositeKernel kernel, TelemetrySender telemetrySender) { + if (telemetrySender is null) + { + throw new ArgumentNullException(nameof(telemetrySender)); + } + var executionOrder = 0; var sessionId = Guid.NewGuid().ToString(); var subscription = kernel.KernelEvents.Subscribe(SendTelemetryFor); diff --git a/src/dotnet-interactive/Program.cs b/src/dotnet-interactive/Program.cs index 4904e4d9e1..0d7276dc07 100644 --- a/src/dotnet-interactive/Program.cs +++ b/src/dotnet-interactive/Program.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.CommandLine.Parsing; using System.Globalization; using System.IO; using System.Net; @@ -21,6 +20,7 @@ using Serilog.Sinks.RollingFileAlternate; using static Pocket.Logger; using SerilogLoggerConfiguration = Serilog.LoggerConfiguration; +using CommandLineParser = Microsoft.DotNet.Interactive.App.CommandLine.CommandLineParser; namespace Microsoft.DotNet.Interactive.App; @@ -33,7 +33,9 @@ public static async Task Main(string[] args) Console.OutputEncoding = Encoding.UTF8; SetCultureFromEnvironmentVariables(); - return await CommandLineParser.Create(_serviceCollection).InvokeAsync(args); + var rootCommand = CommandLineParser.Create(_serviceCollection); + + return await rootCommand.Parse(args).InvokeAsync(); } public static void SetCultureFromEnvironmentVariables() diff --git a/src/dotnet-interactive/dotnet-interactive.csproj b/src/dotnet-interactive/dotnet-interactive.csproj index 4161d523e7..3d64ed32e7 100644 --- a/src/dotnet-interactive/dotnet-interactive.csproj +++ b/src/dotnet-interactive/dotnet-interactive.csproj @@ -12,7 +12,7 @@ Microsoft.dotnet-interactive dotnet-interactive true - Command line tool for interactive programming with C#, F#, and PowerShell, including support for Jupyter Notebooks. + Command line tool for interactive programming with C#, F#, PowerShell, and more. Powers Polyglot Notebooks and provides .NET support for Jupyter. polyglot notebook dotnet interactive Jupyter csharp fsharp PowerShell true .NET Interactive @@ -66,7 +66,6 @@ - diff --git a/src/interface-generator/Program.cs b/src/interface-generator/Program.cs index 8b48bb977a..1138c38ad7 100644 --- a/src/interface-generator/Program.cs +++ b/src/interface-generator/Program.cs @@ -10,23 +10,24 @@ class Program { static int Main(string[] args) { - var existingOnlyOption = new Option("--out-file") + var outFileOption = new Option("--out-file") { Description = "Location to write the generated interface file", - IsRequired = true - }.ExistingOnly(); + Required = true + }.AcceptExistingOnly(); var command = new RootCommand { - existingOnlyOption + outFileOption }; - command.SetHandler(async (FileInfo f) => + command.SetAction(async (parseResult, cancellationToken) => { var generated = InterfaceGenerator.Generate(); - await File.WriteAllTextAsync(f.FullName, generated); - }, existingOnlyOption); + await File.WriteAllTextAsync(parseResult.GetValue(outFileOption).FullName, generated, cancellationToken); + return 0; + }); - return command.Invoke(args); + return command.Parse(args).Invoke(); } } \ No newline at end of file