public async Task Prints_to_console_when_kernel_installation_succeeded() { var console = new TestConsole(); var jupyterCommandLine = new JupyterInstallCommand(console, new InMemoryJupyterKernelSpec(true, null)); await jupyterCommandLine.InvokeAsync(); var consoleOut = console.Out.ToString(); consoleOut.Should().MatchEquivalentOf("*[InstallKernelSpec] Installed kernelspec .net-csharp in *.net-csharp*"); consoleOut.Should().MatchEquivalentOf("*[InstallKernelSpec] Installed kernelspec .net-fsharp in *.net-fsharp*"); consoleOut.Should().Contain(".NET kernel installation succeeded"); }
public async Task Returns_error_when_jupyter_paths_could_not_be_obtained() { var console = new TestConsole(); var installCommand = new JupyterInstallCommand( console, new InMemoryJupyterKernelSpec(false, error: new[] { "Could not find jupyter kernelspec module" })); await installCommand.InvokeAsync(); console.Error.ToString().Should() .Contain(".NET kernel installation failed") .And .Contain("Could not find jupyter kernelspec module"); }
public async Task Appends_http_port_range_arguments() { var console = new TestConsole(); var kernelSpec = new InMemoryJupyterKernelSpec(true, null); var jupyterCommandLine = new JupyterInstallCommand(console, kernelSpec, new PortRange(100, 400)); await jupyterCommandLine.InvokeAsync(); kernelSpec.InstalledKernelSpecs .Should() .HaveCount(3) .And .Match(s => s.All(k => k.Contains("--http-port-range"))); }
public async Task Appends_http_port_range_arguments() { var console = new TestConsole(); var kernelSpecModule = new JupyterKernelSpecModuleSimulator(true); var kernelSpecInstaller = new JupyterKernelSpecInstaller(console, kernelSpecModule); var jupyterCommandLine = new JupyterInstallCommand(console, kernelSpecInstaller, new HttpPortRange(100, 400)); await jupyterCommandLine.InvokeAsync(); kernelSpecModule.InstalledKernelSpecs .Should() .HaveCount(3) .And .Match(s => s.All(k => k.Contains("--http-port-range"))); }
public async Task Prints_to_console_when_kernel_installation_succeeded() { var console = new TestConsole(); var kernelSpecModule = new JupyterKernelSpecModuleSimulator(true); var kernelSpecInstaller = new JupyterKernelSpecInstaller(console, kernelSpecModule); var jupyterCommandLine = new JupyterInstallCommand(console, kernelSpecInstaller); await jupyterCommandLine.InvokeAsync(); var consoleOut = console.Out.ToString(); using var scope = new AssertionScope(); consoleOut.Should().Contain("Installed \".NET (F#)\" kernel."); consoleOut.Should().Contain("Installed \".NET (C#)\" kernel."); }
public async Task kernel_spec_is_not_broken(string displayName) { var _configuration = new Configuration() .UsingExtension($"kernelspec_{displayName.Replace(".", "_").Replace(" ", "_")}.txt") .SetInteractive(Debugger.IsAttached); var console = new TestConsole(); var kernelSpecModule = new JupyterKernelSpecModuleSimulator(true); var kernelSpecInstaller = new JupyterKernelSpecInstaller(console, kernelSpecModule); var jupyterCommandLine = new JupyterInstallCommand(console, kernelSpecInstaller, new HttpPortRange(100, 400)); await jupyterCommandLine.InvokeAsync(); var kernelSpec = kernelSpecModule.InstalledKernelSpecs.Single(k => k.Contains(displayName)); this.Assent(kernelSpec, _configuration); }
public async Task Returns_error_when_jupyter_paths_could_not_be_obtained() { var console = new TestConsole(); var kernelSpecModule = new JupyterKernelSpecModuleSimulator(false); var kernelSpecInstaller = new JupyterKernelSpecInstaller(console, kernelSpecModule); var installCommand = new JupyterInstallCommand( console, kernelSpecInstaller); await installCommand.InvokeAsync(); var consoleError = console.Error.ToString(); using var scope = new AssertionScope(); consoleError.Should().Contain("Failed to install \".NET (F#)\" kernel."); consoleError.Should().Contain("Failed to install \".NET (C#)\" kernel."); consoleError.Should().Contain("Failed to install \".NET (PowerShell)\" kernel."); }
public static Parser Create( IServiceCollection services, StartServer startServer = null, Jupyter jupyter = null, StartStdIO startStdIO = null, StartHttp startHttp = null, Action onServerStarted = null, ITelemetry telemetry = null, IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = null) { var operation = Log.OnEnterAndExit(); if (services == null) { throw new ArgumentNullException(nameof(services)); } var disposeOnQuit = new CompositeDisposable(); startServer ??= (startupOptions, invocationContext) => { operation.Info("constructing webhost"); var webHost = Program.ConstructWebHost(startupOptions); disposeOnQuit.Add(webHost); operation.Info("starting kestrel server"); webHost.Start(); onServerStarted?.Invoke(); webHost.WaitForShutdown(); operation.Dispose(); }; jupyter ??= JupyterCommand.Do; startStdIO ??= StdIOCommand.Do; startHttp ??= HttpCommand.Do; // Setup first time use notice sentinel. firstTimeUseNoticeSentinel ??= new FirstTimeUseNoticeSentinel(VersionSensor.Version().AssemblyInformationalVersion); var clearTextProperties = new[] { "frontend" }; // Setup telemetry. telemetry ??= new Telemetry.Telemetry( VersionSensor.Version().AssemblyInformationalVersion, firstTimeUseNoticeSentinel, "dotnet/interactive/cli"); var filter = new TelemetryFilter( Sha256Hasher.HashWithNormalizedCasing, clearTextProperties, (commandResult, directives, entryItems) => { // add frontend var frontendTelemetryAdded = false; foreach (var directive in directives) { switch (directive.Key) { case "vscode": case "jupyter": case "synapse": frontendTelemetryAdded = true; entryItems.Add(new KeyValuePair <string, string>("frontend", directive.Key)); break; } } if (!frontendTelemetryAdded) { if (commandResult.Command.Name == "jupyter") { entryItems.Add(new KeyValuePair <string, string>("frontend", "jupyter")); } else { entryItems.Add(new KeyValuePair <string, string>("frontend", "unknown")); } } }); var verboseOption = new Option <bool>( "--verbose", "Enable verbose logging to the console"); var logPathOption = new Option <DirectoryInfo>( "--log-path", "Enable file logging to the specified directory"); var pathOption = new Option <DirectoryInfo>( "--path", "Installs the kernelspecs to the specified directory") .ExistingOnly(); var defaultKernelOption = new Option <string>( "--default-kernel", description: "The default language for the kernel", getDefaultValue: () => "csharp"); var rootCommand = DotnetInteractive(); rootCommand.AddCommand(Jupyter()); rootCommand.AddCommand(StdIO()); rootCommand.AddCommand(HttpServer()); return(new CommandLineBuilder(rootCommand) .UseDefaults() .UseMiddleware(async(context, next) => { if (context.ParseResult.Errors.Count == 0) { telemetry.SendFiltered(filter, context.ParseResult); } // If sentinel does not exist, print the welcome message showing the telemetry notification. if (!Telemetry.Telemetry.SkipFirstTimeExperience && !firstTimeUseNoticeSentinel.Exists()) { context.Console.Out.WriteLine(); context.Console.Out.WriteLine(Telemetry.Telemetry.WelcomeMessage); firstTimeUseNoticeSentinel.CreateIfNotExists(); } await next(context); }) .Build()); RootCommand DotnetInteractive() { var command = new RootCommand { Name = "dotnet-interactive", Description = "Interactive programming for .NET." }; command.AddGlobalOption(logPathOption); command.AddGlobalOption(verboseOption); return(command); } Command Jupyter() { var httpPortRangeOption = new Option <HttpPortRange>( "--http-port-range", parseArgument: result => result.Tokens.Count == 0 ? HttpPortRange.Default : ParsePortRangeOption(result), description: "Specifies the range of ports to use to enable HTTP services", isDefault: true); var jupyterCommand = new Command("jupyter", "Starts dotnet-interactive as a Jupyter kernel") { defaultKernelOption, httpPortRangeOption, new Argument <FileInfo> { Name = "connection-file" }.ExistingOnly() }; jupyterCommand.Handler = CommandHandler.Create <StartupOptions, JupyterOptions, IConsole, InvocationContext, CancellationToken>(JupyterHandler); var installCommand = new Command("install", "Install the .NET kernel for Jupyter") { httpPortRangeOption, pathOption }; installCommand.Handler = CommandHandler.Create <IConsole, InvocationContext, HttpPortRange, DirectoryInfo>(InstallHandler); jupyterCommand.AddCommand(installCommand); return(jupyterCommand); Task <int> JupyterHandler(StartupOptions startupOptions, JupyterOptions options, IConsole console, InvocationContext context, CancellationToken cancellationToken) { var frontendEnvironment = new HtmlNotebookFrontedEnvironment(); var kernel = CreateKernel(options.DefaultKernel, frontendEnvironment, startupOptions); services.AddKernel(kernel); services.AddSingleton(c => ConnectionInformation.Load(options.ConnectionFile)) .AddSingleton(c => { return(new JupyterRequestContextScheduler(delivery => c.GetRequiredService <JupyterRequestContextHandler>() .Handle(delivery))); }) .AddSingleton(c => new JupyterRequestContextHandler(kernel)) .AddSingleton <IHostedService, Shell>() .AddSingleton <IHostedService, Heartbeat>(); return(jupyter(startupOptions, console, startServer, context)); } Task <int> InstallHandler(IConsole console, InvocationContext context, HttpPortRange httpPortRange, DirectoryInfo path) { var jupyterInstallCommand = new JupyterInstallCommand(console, new JupyterKernelSpecInstaller(console), httpPortRange, path); return(jupyterInstallCommand.InvokeAsync()); } } Command HttpServer() { var httpPortOption = new Option <HttpPort>( "--http-port", description: "Specifies the port on which to enable HTTP services", parseArgument: result => { if (result.Tokens.Count == 0) { return(HttpPort.Auto); } var source = result.Tokens[0].Value; if (source == "*") { return(HttpPort.Auto); } if (!int.TryParse(source, out var portNumber)) { result.ErrorMessage = "Must specify a port number or *."; return(null); } return(new HttpPort(portNumber)); }, isDefault: true); var httpCommand = new Command("http", "Starts dotnet-interactive with kernel functionality exposed over http") { defaultKernelOption, httpPortOption }; httpCommand.Handler = CommandHandler.Create <StartupOptions, KernelHttpOptions, IConsole, InvocationContext>( (startupOptions, options, console, context) => { var frontendEnvironment = new BrowserFrontendEnvironment(); var kernel = CreateKernel(options.DefaultKernel, frontendEnvironment, startupOptions); services.AddKernel(kernel); onServerStarted ??= () => { console.Out.WriteLine("Application started. Press Ctrl+C to shut down."); }; return(startHttp(startupOptions, console, startServer, context)); }); return(httpCommand); } Command StdIO() { var httpPortRangeOption = new Option <HttpPortRange>( "--http-port-range", parseArgument: result => result.Tokens.Count == 0 ? HttpPortRange.Default : ParsePortRangeOption(result), description: "Specifies the range of ports to use to enable HTTP services"); var httpPortOption = new Option <HttpPort>( "--http-port", description: "Specifies the port on which to enable HTTP services", parseArgument: result => { if (result.FindResultFor(httpPortRangeOption) is { } conflictingOption) { var parsed = result.Parent as OptionResult; result.ErrorMessage = $"Cannot specify both {conflictingOption.Token.Value} and {parsed.Token.Value} together"; return(null); } if (result.Tokens.Count == 0) { return(HttpPort.Auto); } var source = result.Tokens[0].Value; if (source == "*") { return(HttpPort.Auto); } if (!int.TryParse(source, out var portNumber)) { result.ErrorMessage = "Must specify a port number or *."; return(null); } return(new HttpPort(portNumber)); }); var workingDirOption = new Option <DirectoryInfo>( "--working-dir", () => new DirectoryInfo(Environment.CurrentDirectory), "Working directory to which to change after launching the kernel."); var stdIOCommand = new Command( "stdio", "Starts dotnet-interactive with kernel functionality exposed over standard I/O") { defaultKernelOption, httpPortRangeOption, httpPortOption, workingDirOption }; stdIOCommand.Handler = CommandHandler.Create <StartupOptions, StdIOOptions, IConsole, InvocationContext, CancellationToken>( (startupOptions, options, console, context, cancellationToken) => { FrontendEnvironment frontendEnvironment = startupOptions.EnableHttpApi ? new HtmlNotebookFrontedEnvironment() : new BrowserFrontendEnvironment(); var kernel = CreateKernel(options.DefaultKernel, frontendEnvironment, startupOptions); services.AddKernel(kernel); kernel.UseQuitCommand(disposeOnQuit, cancellationToken); var kernelServer = kernel.CreateKernelServer(startupOptions.WorkingDir); if (startupOptions.EnableHttpApi) { if (context.ParseResult.Directives.Contains("vscode")) { ((HtmlNotebookFrontedEnvironment)frontendEnvironment).RequiresAutomaticBootstrapping = false; } kernelServer.Start(); onServerStarted ??= () => { kernelServer.NotifyIsReady(); }; return(startHttp(startupOptions, console, startServer, context)); } disposeOnQuit.Add(kernel); return(startStdIO( startupOptions, kernelServer, console)); }); return(stdIOCommand); }
public static Parser Create( IServiceCollection services, StartServer startServer = null, Jupyter jupyter = null, StartStdIO startStdIO = null, ITelemetry telemetry = null, IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = null) { if (services == null) { throw new ArgumentNullException(nameof(services)); } startServer ??= (startupOptions, invocationContext) => Program.ConstructWebHost(startupOptions).Run(); jupyter ??= JupyterCommand.Do; startStdIO ??= StdIOCommand.Do; // Setup first time use notice sentinel. firstTimeUseNoticeSentinel ??= new FirstTimeUseNoticeSentinel(VersionSensor.Version().AssemblyInformationalVersion); // Setup telemetry. telemetry ??= new Telemetry.Telemetry( VersionSensor.Version().AssemblyInformationalVersion, firstTimeUseNoticeSentinel); var filter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing); var verboseOption = new Option <bool>( "--verbose", "Enable verbose logging to the console"); var httpPortOption = new Option <HttpPort>( "--http-port", description: "Specifies the port on which to enable HTTP services", parseArgument: result => { var source = result.Tokens[0].Value; if (source == "*") { return(HttpPort.Auto); } if (!int.TryParse(source, out var portNumber)) { result.ErrorMessage = "Must specify a port number or *."; return(null); } return(new HttpPort(portNumber)); }); var httpPortRangeOption = new Option <PortRange>( "--http-port-range", parseArgument: result => { if (result.Parent.Parent.Children.FirstOrDefault(c => c.Symbol == httpPortOption) is OptionResult conflictingOption) { var parsed = result.Parent as OptionResult; result.ErrorMessage = $"Cannot specify both {conflictingOption.Token.Value} and {parsed.Token.Value} together"; return(null); } var source = result.Tokens[0].Value; if (string.IsNullOrWhiteSpace(source)) { result.ErrorMessage = "Must specify a port range"; return(null); } var parts = source.Split(new[] { "-" }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) { result.ErrorMessage = "Must specify a port range"; return(null); } if (!int.TryParse(parts[0], out var start) || !int.TryParse(parts[1], out var end)) { result.ErrorMessage = "Must specify a port range as StartPort-EndPort"; return(null); } if (start > end) { result.ErrorMessage = "Start port must be lower then end port"; return(null); } var pr = new PortRange(start, end); return(pr); }, description: "Specifies the range of port to use to enable HTTP services"); var logPathOption = new Option <DirectoryInfo>( "--log-path", "Enable file logging to the specified directory"); var pathOption = new Option <DirectoryInfo>( "--path", "Installs the kernelspecs to the specified directory") .ExistingOnly(); var defaultKernelOption = new Option <string>( "--default-kernel", description: "The default language for the kernel", getDefaultValue: () => "csharp"); var rootCommand = DotnetInteractive(); rootCommand.AddCommand(Jupyter()); rootCommand.AddCommand(StdIO()); rootCommand.AddCommand(HttpServer()); return(new CommandLineBuilder(rootCommand) .UseDefaults() .UseMiddleware(async(context, next) => { if (context.ParseResult.Errors.Count == 0) { telemetry.SendFiltered(filter, context.ParseResult); } // If sentinel does not exist, print the welcome message showing the telemetry notification. if (!firstTimeUseNoticeSentinel.Exists() && !Telemetry.Telemetry.SkipFirstTimeExperience) { context.Console.Out.WriteLine(); context.Console.Out.WriteLine(Telemetry.Telemetry.WelcomeMessage); firstTimeUseNoticeSentinel.CreateIfNotExists(); } await next(context); }) .Build()); RootCommand DotnetInteractive() { var command = new RootCommand { Name = "dotnet-interactive", Description = "Interactive programming for .NET." }; command.AddOption(logPathOption); command.AddOption(verboseOption); return(command); } Command Jupyter() { var command = new Command("jupyter", "Starts dotnet-interactive as a Jupyter kernel") { defaultKernelOption, logPathOption, verboseOption, httpPortRangeOption, new Argument <FileInfo> { Name = "connection-file" }.ExistingOnly() }; command.Handler = CommandHandler.Create <StartupOptions, JupyterOptions, IConsole, InvocationContext>(JupyterHandler); var installCommand = new Command("install", "Install the .NET kernel for Jupyter") { logPathOption, verboseOption, httpPortRangeOption, pathOption }; installCommand.Handler = CommandHandler.Create <IConsole, InvocationContext, PortRange, DirectoryInfo>(InstallHandler); command.AddCommand(installCommand); return(command); Task <int> JupyterHandler(StartupOptions startupOptions, JupyterOptions options, IConsole console, InvocationContext context) { services = RegisterKernelInServiceCollection(services, startupOptions, options.DefaultKernel); services.AddSingleton(c => ConnectionInformation.Load(options.ConnectionFile)) .AddSingleton <FrontendEnvironment>(c => c.GetService <BrowserFrontendEnvironment>()) .AddSingleton(c => { return(CommandScheduler.Create <JupyterRequestContext>(delivery => c.GetRequiredService <ICommandHandler <JupyterRequestContext> >() .Trace() .Handle(delivery))); }) .AddSingleton(c => new JupyterRequestContextHandler( c.GetRequiredService <IKernel>()) .Trace()) .AddSingleton <IHostedService, Shell>() .AddSingleton <IHostedService, Heartbeat>(); return(jupyter(startupOptions, console, startServer, context)); } Task <int> InstallHandler(IConsole console, InvocationContext context, PortRange httpPortRange, DirectoryInfo path) { var jupyterInstallCommand = new JupyterInstallCommand(console, new JupyterKernelSpecInstaller(console), httpPortRange, path); return(jupyterInstallCommand.InvokeAsync()); } } Command HttpServer() { var command = new Command("http", "Starts dotnet-interactive with kernel functionality exposed over http") { defaultKernelOption, httpPortOption, logPathOption, httpPortRangeOption }; command.Handler = CommandHandler.Create <StartupOptions, KernelHttpOptions, IConsole, InvocationContext>( (startupOptions, options, console, context) => { if (startupOptions.HttpPort == null && startupOptions.HttpPortRange == null) { startupOptions.HttpPort = HttpPort.Auto; } RegisterKernelInServiceCollection(services, startupOptions, options.DefaultKernel); return(jupyter(startupOptions, console, startServer, context)); }); return(command); } Command StdIO() { var command = new Command( "stdio", "Starts dotnet-interactive with kernel functionality exposed over standard I/O") { defaultKernelOption, logPathOption, httpPortOption, httpPortRangeOption }; command.Handler = CommandHandler.Create <StartupOptions, StdIOOptions, IConsole, InvocationContext>( (startupOptions, options, console, context) => { if (startupOptions.EnableHttpApi) { RegisterKernelInServiceCollection(services, startupOptions, options.DefaultKernel, kernel => StdIOCommand.CreateServer(kernel, console)); startServer?.Invoke(startupOptions, context); return(Task.FromResult(0)); } return(startStdIO( startupOptions, CreateKernel(options.DefaultKernel, new BrowserFrontendEnvironment(), startupOptions, null), console)); }); return(command); } }
public static Parser Create( IServiceCollection services, StartServer startServer = null, Jupyter jupyter = null, StartKernelHost startKernelHost = null, StartNotebookParser startNotebookParser = null, StartHttp startHttp = null, Action onServerStarted = null, ITelemetry telemetry = null, IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = null) { var operation = Log.OnEnterAndExit(); if (services is null) { throw new ArgumentNullException(nameof(services)); } var disposeOnQuit = new CompositeDisposable(); startServer ??= (startupOptions, invocationContext) => { operation.Info("constructing webhost"); var webHost = Program.ConstructWebHost(startupOptions); disposeOnQuit.Add(webHost); operation.Info("starting kestrel server"); webHost.Start(); onServerStarted?.Invoke(); webHost.WaitForShutdown(); operation.Dispose(); }; jupyter ??= JupyterCommand.Do; startKernelHost ??= KernelHostLauncher.Do; startNotebookParser ??= ParseNotebookCommand.Do; startHttp ??= HttpCommand.Do; var isVSCode = false; // Setup first time use notice sentinel. firstTimeUseNoticeSentinel ??= new FirstTimeUseNoticeSentinel(VersionSensor.Version().AssemblyInformationalVersion); var clearTextProperties = new[] { "frontend" }; // Setup telemetry. telemetry ??= new Telemetry.Telemetry( VersionSensor.Version().AssemblyInformationalVersion, firstTimeUseNoticeSentinel, "dotnet/interactive/cli"); var filter = new TelemetryFilter( Sha256Hasher.HashWithNormalizedCasing, clearTextProperties, (commandResult, directives, entryItems) => { // add frontend var frontendTelemetryAdded = false; // check if is codespaces if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CODESPACES"))) { frontendTelemetryAdded = true; isVSCode = true; entryItems.Add(new KeyValuePair <string, string>("frontend", "gitHubCodeSpaces")); } if (!frontendTelemetryAdded) { foreach (var directive in directives) { switch (directive.Key) { case "jupyter": case "synapse": case "vscode": frontendTelemetryAdded = true; isVSCode = directive.Key.ToLowerInvariant() == "vscode"; entryItems.Add(new KeyValuePair <string, string>("frontend", directive.Key)); break; } } } if (!frontendTelemetryAdded) { switch (commandResult.Command.Name) { case "jupyter": entryItems.Add(new KeyValuePair <string, string>("frontend", commandResult.Command.Name)); frontendTelemetryAdded = true; break; } } if (!frontendTelemetryAdded) { var frontendName = Environment.GetEnvironmentVariable("DOTNET_INTERACTIVE_FRONTEND_NAME"); if (string.IsNullOrWhiteSpace(frontendName)) { frontendName = "unknown"; } entryItems.Add(new KeyValuePair <string, string>("frontend", frontendName)); } }); var verboseOption = new Option <bool>( "--verbose", "Enable verbose logging to the console"); var logPathOption = new Option <DirectoryInfo>( "--log-path", "Enable file logging to the specified directory"); var pathOption = new Option <DirectoryInfo>( "--path", "Installs the kernelspecs to the specified directory") .ExistingOnly(); var defaultKernelOption = new Option <string>( "--default-kernel", description: "The default language for the kernel", getDefaultValue: () => "csharp").AddCompletions("fsharp", "csharp", "pwsh"); var rootCommand = DotnetInteractive(); rootCommand.AddCommand(Jupyter()); rootCommand.AddCommand(StdIO()); rootCommand.AddCommand(NotebookParser()); rootCommand.AddCommand(HttpServer()); return(new CommandLineBuilder(rootCommand) .UseDefaults() .AddMiddleware(async(context, next) => { if (context.ParseResult.Errors.Count == 0) { telemetry.SendFiltered(filter, context.ParseResult); } // If sentinel does not exist, print the welcome message showing the telemetry notification. if (!Telemetry.Telemetry.SkipFirstTimeExperience && !firstTimeUseNoticeSentinel.Exists()) { context.Console.Out.WriteLine(); context.Console.Out.WriteLine(Telemetry.Telemetry.WelcomeMessage); firstTimeUseNoticeSentinel.CreateIfNotExists(); } await next(context); }) .Build()); RootCommand DotnetInteractive() { var command = new RootCommand { Name = "dotnet-interactive", Description = "Interactive programming for .NET." }; command.AddGlobalOption(logPathOption); command.AddGlobalOption(verboseOption); return(command); } Command Jupyter() { var httpPortRangeOption = new Option <HttpPortRange>( "--http-port-range", parseArgument: result => result.Tokens.Count == 0 ? HttpPortRange.Default : ParsePortRangeOption(result), description: "Specifies the range of ports to use to enable HTTP services", isDefault: true); var jupyterCommand = new Command("jupyter", "Starts dotnet-interactive as a Jupyter kernel") { defaultKernelOption, httpPortRangeOption, new Argument <FileInfo> { Name = "connection-file", Description = "The path to a connection file provided by Jupyter" }.ExistingOnly() }; jupyterCommand.Handler = CommandHandler.Create <StartupOptions, JupyterOptions, IConsole, InvocationContext, CancellationToken>(JupyterHandler); var installCommand = new Command("install", "Install the .NET kernel for Jupyter") { httpPortRangeOption, pathOption }; installCommand.Handler = CommandHandler.Create <IConsole, InvocationContext, HttpPortRange, DirectoryInfo>(InstallHandler); jupyterCommand.AddCommand(installCommand); return(jupyterCommand); async Task <int> JupyterHandler(StartupOptions startupOptions, JupyterOptions options, IConsole console, InvocationContext context, CancellationToken cancellationToken) { var frontendEnvironment = new HtmlNotebookFrontendEnvironment(); var kernel = CreateKernel(options.DefaultKernel, frontendEnvironment, startupOptions); kernel.Add( new JavaScriptKernel(), new[] { "js" }); services.AddKernel(kernel); kernel.VisitSubkernels(k => { switch (k) { case CSharpKernel csharpKernel: csharpKernel.UseJupyterHelpers(); break; case FSharpKernel fsharpKernel: fsharpKernel.UseJupyterHelpers(); break; case PowerShellKernel powerShellKernel: powerShellKernel.UseJupyterHelpers(); break; } }); var clientSideKernelClient = new SignalRBackchannelKernelClient(); services.AddSingleton(c => ConnectionInformation.Load(options.ConnectionFile)) .AddSingleton(clientSideKernelClient) .AddSingleton(c => { return(new JupyterRequestContextScheduler(delivery => c.GetRequiredService <JupyterRequestContextHandler>() .Handle(delivery))); }) .AddSingleton(c => new JupyterRequestContextHandler(kernel)) .AddSingleton <IHostedService, Shell>() .AddSingleton <IHostedService, Heartbeat>(); var result = await jupyter(startupOptions, console, startServer, context); return(result); } Task <int> InstallHandler(IConsole console, InvocationContext context, HttpPortRange httpPortRange, DirectoryInfo path) { var jupyterInstallCommand = new JupyterInstallCommand(console, new JupyterKernelSpecInstaller(console), httpPortRange, path); return(jupyterInstallCommand.InvokeAsync()); } } Command HttpServer() { var httpPortOption = new Option <HttpPort>( "--http-port", description: "Specifies the port on which to enable HTTP services", parseArgument: result => { if (result.Tokens.Count == 0) { return(HttpPort.Auto); } var source = result.Tokens[0].Value; if (source == "*") { return(HttpPort.Auto); } if (!int.TryParse(source, out var portNumber)) { result.ErrorMessage = "Must specify a port number or *."; return(null); } return(new HttpPort(portNumber)); }, isDefault: true); var httpCommand = new Command("http", "Starts dotnet-interactive with kernel functionality exposed over http") { defaultKernelOption, httpPortOption }; httpCommand.Handler = CommandHandler.Create <StartupOptions, KernelHttpOptions, IConsole, InvocationContext>( (startupOptions, options, console, context) => { var frontendEnvironment = new BrowserFrontendEnvironment(); var kernel = CreateKernel(options.DefaultKernel, frontendEnvironment, startupOptions); kernel.Add( new JavaScriptKernel(), new[] { "js" }); services.AddKernel(kernel) .AddSingleton(new SignalRBackchannelKernelClient()); onServerStarted ??= () => { console.Out.WriteLine("Application started. Press Ctrl+C to shut down."); }; return(startHttp(startupOptions, console, startServer, context)); }); return(httpCommand); } Command StdIO() { var httpPortRangeOption = new Option <HttpPortRange>( "--http-port-range", parseArgument: result => result.Tokens.Count == 0 ? HttpPortRange.Default : ParsePortRangeOption(result), description: "Specifies the range of ports to use to enable HTTP services"); var httpPortOption = new Option <HttpPort>( "--http-port", description: "Specifies the port on which to enable HTTP services", parseArgument: result => { if (result.FindResultFor(httpPortRangeOption) is { } conflictingOption) { var parsed = result.Parent as OptionResult; result.ErrorMessage = $"Cannot specify both {conflictingOption.Token.Value} and {parsed.Token.Value} together"; return(null); } if (result.Tokens.Count == 0) { return(HttpPort.Auto); } var source = result.Tokens[0].Value; if (source == "*") { return(HttpPort.Auto); } if (!int.TryParse(source, out var portNumber)) { result.ErrorMessage = "Must specify a port number or *."; return(null); } return(new HttpPort(portNumber)); }); var workingDirOption = new Option <DirectoryInfo>( "--working-dir", () => new DirectoryInfo(Environment.CurrentDirectory), "Working directory to which to change after launching the kernel."); var stdIOCommand = new Command( "stdio", "Starts dotnet-interactive with kernel functionality exposed over standard I/O") { defaultKernelOption, httpPortRangeOption, httpPortOption, workingDirOption }; stdIOCommand.Handler = CommandHandler.Create <StartupOptions, StdIOOptions, IConsole, InvocationContext>( async(startupOptions, options, console, context) => { Console.InputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8; Environment.CurrentDirectory = startupOptions.WorkingDir.FullName; FrontendEnvironment frontendEnvironment = startupOptions.EnableHttpApi ? new HtmlNotebookFrontendEnvironment() : new BrowserFrontendEnvironment(); var kernel = CreateKernel(options.DefaultKernel, frontendEnvironment, startupOptions); services.AddKernel(kernel); kernel = kernel.UseQuitCommand(); var host = new KernelHost(kernel, new KernelCommandAndEventTextStreamSender(Console.Out), new MultiplexingKernelCommandAndEventReceiver( new KernelCommandAndEventTextReceiver(Console.In))); if (isVSCode) { var vscodeSetup = new VSCodeClientKernelsExtension(); await vscodeSetup.OnLoadAsync(kernel); } if (startupOptions.EnableHttpApi) { var clientSideKernelClient = new SignalRBackchannelKernelClient(); services.AddSingleton(clientSideKernelClient); if (isVSCode) { ((HtmlNotebookFrontendEnvironment)frontendEnvironment).RequiresAutomaticBootstrapping = false; } else { kernel.Add( new JavaScriptKernel(clientSideKernelClient), new[] { "js" }); } onServerStarted ??= () => { var _ = host.ConnectAsync(); }; await startHttp(startupOptions, console, startServer, context); } else { if (!isVSCode) { await host.CreateProxyKernelOnDefaultConnectorAsync(new KernelInfo("javascript", "javascript") { Aliases = new[] { "js" }, DestinationUri = new Uri("kernel://webview/javascript") }); } await startKernelHost(startupOptions, host, console); } return(0); }); return(stdIOCommand); }
public static Parser Create( IServiceCollection services, StartServer startServer = null, Jupyter jupyter = null, StartStdIO startStdIO = null, StartHttp startHttp = null, ITelemetry telemetry = null, IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = null) { if (services == null) { throw new ArgumentNullException(nameof(services)); } var disposeOnQuit = new CompositeDisposable(); startServer ??= (startupOptions, invocationContext) => { var webHost = Program.ConstructWebHost(startupOptions); disposeOnQuit.Add(webHost); webHost.Run(); }; jupyter ??= JupyterCommand.Do; startStdIO ??= StdIOCommand.Do; startHttp ??= HttpCommand.Do; // Setup first time use notice sentinel. firstTimeUseNoticeSentinel ??= new FirstTimeUseNoticeSentinel(VersionSensor.Version().AssemblyInformationalVersion); // Setup telemetry. telemetry ??= new Telemetry.Telemetry( VersionSensor.Version().AssemblyInformationalVersion, firstTimeUseNoticeSentinel); var filter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing); var verboseOption = new Option <bool>( "--verbose", "Enable verbose logging to the console"); var logPathOption = new Option <DirectoryInfo>( "--log-path", "Enable file logging to the specified directory"); var pathOption = new Option <DirectoryInfo>( "--path", "Installs the kernelspecs to the specified directory") .ExistingOnly(); var defaultKernelOption = new Option <string>( "--default-kernel", description: "The default language for the kernel", getDefaultValue: () => "csharp"); var rootCommand = DotnetInteractive(); rootCommand.AddCommand(Jupyter()); rootCommand.AddCommand(StdIO()); rootCommand.AddCommand(HttpServer()); return(new CommandLineBuilder(rootCommand) .UseDefaults() .UseMiddleware(async(context, next) => { if (context.ParseResult.Errors.Count == 0) { telemetry.SendFiltered(filter, context.ParseResult); } // If sentinel does not exist, print the welcome message showing the telemetry notification. if (!firstTimeUseNoticeSentinel.Exists() && !Telemetry.Telemetry.SkipFirstTimeExperience) { context.Console.Out.WriteLine(); context.Console.Out.WriteLine(Telemetry.Telemetry.WelcomeMessage); firstTimeUseNoticeSentinel.CreateIfNotExists(); } await next(context); }) .Build()); RootCommand DotnetInteractive() { var command = new RootCommand { Name = "dotnet-interactive", Description = "Interactive programming for .NET." }; command.AddOption(logPathOption); command.AddOption(verboseOption); return(command); } Command Jupyter() { var httpPortRangeOption = new Option <HttpPortRange>( "--http-port-range", parseArgument: result => result.Tokens.Count == 0 ? HttpPortRange.Default : ParsePortRangeOption(result), description: "Specifies the range of ports to use to enable HTTP services", isDefault: true); var command = new Command("jupyter", "Starts dotnet-interactive as a Jupyter kernel") { defaultKernelOption, logPathOption, verboseOption, httpPortRangeOption, new Argument <FileInfo> { Name = "connection-file" }.ExistingOnly() }; command.Handler = CommandHandler.Create <StartupOptions, JupyterOptions, IConsole, InvocationContext, CancellationToken>(JupyterHandler); var installCommand = new Command("install", "Install the .NET kernel for Jupyter") { httpPortRangeOption, logPathOption, verboseOption, pathOption }; installCommand.Handler = CommandHandler.Create <IConsole, InvocationContext, HttpPortRange, DirectoryInfo>(InstallHandler); command.AddCommand(installCommand); return(command); Task <int> JupyterHandler(StartupOptions startupOptions, JupyterOptions options, IConsole console, InvocationContext context, CancellationToken cancellationToken) { services = RegisterKernelInServiceCollection( services, startupOptions, options.DefaultKernel, serviceCollection => { serviceCollection.AddSingleton(_ => new HtmlNotebookFrontedEnvironment()); serviceCollection.AddSingleton <FrontendEnvironment>(c => c.GetService <HtmlNotebookFrontedEnvironment>()); }); services.AddSingleton(c => ConnectionInformation.Load(options.ConnectionFile)) .AddSingleton(c => { return(CommandScheduler.Create <JupyterRequestContext>(delivery => c.GetRequiredService <ICommandHandler <JupyterRequestContext> >() .Trace() .Handle(delivery))); }) .AddSingleton(c => new JupyterRequestContextHandler( c.GetRequiredService <IKernel>()) .Trace()) .AddSingleton <IHostedService, Shell>() .AddSingleton <IHostedService, Heartbeat>(); return(jupyter(startupOptions, console, startServer, context)); } Task <int> InstallHandler(IConsole console, InvocationContext context, HttpPortRange httpPortRange, DirectoryInfo path) { var jupyterInstallCommand = new JupyterInstallCommand(console, new JupyterKernelSpecInstaller(console), httpPortRange, path); return(jupyterInstallCommand.InvokeAsync()); } } Command HttpServer() { var httpPortOption = new Option <HttpPort>( "--http-port", description: "Specifies the port on which to enable HTTP services", parseArgument: result => { if (result.Tokens.Count == 0) { return(HttpPort.Auto); } var source = result.Tokens[0].Value; if (source == "*") { return(HttpPort.Auto); } if (!int.TryParse(source, out var portNumber)) { result.ErrorMessage = "Must specify a port number or *."; return(null); } return(new HttpPort(portNumber)); }, isDefault: true); var command = new Command("http", "Starts dotnet-interactive with kernel functionality exposed over http") { defaultKernelOption, httpPortOption, logPathOption }; command.Handler = CommandHandler.Create <StartupOptions, KernelHttpOptions, IConsole, InvocationContext>( (startupOptions, options, console, context) => { RegisterKernelInServiceCollection( services, startupOptions, options.DefaultKernel, serviceCollection => { serviceCollection.AddSingleton(_ => { var frontendEnvironment = new BrowserFrontendEnvironment(); return(frontendEnvironment); }); serviceCollection.AddSingleton <FrontendEnvironment>(c => c.GetRequiredService <BrowserFrontendEnvironment>()); }); return(startHttp(startupOptions, console, startServer, context)); }); return(command); } Command StdIO() { var httpPortRangeOption = new Option <HttpPortRange>( "--http-port-range", parseArgument: result => result.Tokens.Count == 0 ? HttpPortRange.Default : ParsePortRangeOption(result), description: "Specifies the range of ports to use to enable HTTP services"); var command = new Command( "stdio", "Starts dotnet-interactive with kernel functionality exposed over standard I/O") { defaultKernelOption, httpPortRangeOption }; command.Handler = CommandHandler.Create <StartupOptions, StdIOOptions, IConsole, InvocationContext, CancellationToken>( (startupOptions, options, console, context, cancellationToken) => { if (startupOptions.EnableHttpApi) { RegisterKernelInServiceCollection( services, startupOptions, options.DefaultKernel, serviceCollection => { serviceCollection.AddSingleton(_ => new HtmlNotebookFrontedEnvironment()); serviceCollection.AddSingleton <FrontendEnvironment>(c => c.GetService <HtmlNotebookFrontedEnvironment>()); }, kernel => { StdIOCommand.CreateServer(kernel, console); kernel.UseQuiCommand(disposeOnQuit, cancellationToken); }); return(startHttp(startupOptions, console, startServer, context)); } { var kernel = CreateKernel(options.DefaultKernel, new BrowserFrontendEnvironment(), startupOptions); disposeOnQuit.Add(kernel); kernel.UseQuiCommand(disposeOnQuit, cancellationToken); return(startStdIO( startupOptions, kernel, console)); } }); return(command); }