diff --git a/dev-proxy/CommandHandlers/ProxyCommandHandler.cs b/dev-proxy/CommandHandlers/ProxyCommandHandler.cs index 65d845e6..2b5b8ceb 100755 --- a/dev-proxy/CommandHandlers/ProxyCommandHandler.cs +++ b/dev-proxy/CommandHandlers/ProxyCommandHandler.cs @@ -1,165 +1,168 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.DevProxy.Abstractions; -using Microsoft.VisualStudio.Threading; -using System.CommandLine; -using System.CommandLine.Invocation; - -namespace Microsoft.DevProxy.CommandHandlers; - -public class ProxyCommandHandler(IPluginEvents pluginEvents, - Option[] options, - ISet urlsToWatch, - ILogger logger) : ICommandHandler -{ - private readonly IPluginEvents _pluginEvents = pluginEvents ?? throw new ArgumentNullException(nameof(pluginEvents)); - private readonly Option[] _options = options ?? throw new ArgumentNullException(nameof(options)); - private readonly ISet _urlsToWatch = urlsToWatch ?? throw new ArgumentNullException(nameof(urlsToWatch)); - private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - public static ProxyConfiguration Configuration { get => ConfigurationFactory.Value; } - - public int Invoke(InvocationContext context) - { - var joinableTaskContext = new JoinableTaskContext(); - var joinableTaskFactory = new JoinableTaskFactory(joinableTaskContext); - - return joinableTaskFactory.Run(async () => await InvokeAsync(context)); - } - - public async Task InvokeAsync(InvocationContext context) - { - ParseOptions(context); - _pluginEvents.RaiseOptionsLoaded(new OptionsLoadedArgs(context, _options)); - await CheckForNewVersionAsync(); - - try - { - var builder = WebApplication.CreateBuilder(); - builder.Logging.AddFilter("Microsoft.Hosting.*", LogLevel.Error); - builder.Logging.AddFilter("Microsoft.AspNetCore.*", LogLevel.Error); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => ConfigurationFactory.Value); - builder.Services.AddSingleton(_pluginEvents); - builder.Services.AddSingleton(_logger); - builder.Services.AddSingleton(_urlsToWatch); - builder.Services.AddHostedService(); - - builder.Services.AddControllers(); - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); - - builder.Services.Configure(options => - { - options.LowercaseUrls = true; - }); - - builder.WebHost.ConfigureKestrel(options => - { - options.ListenLocalhost(ConfigurationFactory.Value.ApiPort); - _logger.LogInformation("Dev Proxy API listening on http://localhost:{Port}...", ConfigurationFactory.Value.ApiPort); - }); - - var app = builder.Build(); - app.UseSwagger(); - app.UseSwaggerUI(); - app.MapControllers(); - await app.RunAsync(); - - return 0; - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred while running Dev Proxy"); - var inner = ex.InnerException; - - while (inner is not null) - { - _logger.LogError(inner, "============ Inner exception ============"); - inner = inner.InnerException; - } -#if DEBUG - throw; // so debug tools go straight to the source of the exception when attached -#else - return 1; -#endif - } - - } - - private void ParseOptions(InvocationContext context) - { - var port = context.ParseResult.GetValueForOption(ProxyHost.PortOptionName, _options); - if (port is not null) - { - Configuration.Port = port.Value; - } - var ipAddress = context.ParseResult.GetValueForOption(ProxyHost.IpAddressOptionName, _options); - if (ipAddress is not null) - { - Configuration.IPAddress = ipAddress; - } - var record = context.ParseResult.GetValueForOption(ProxyHost.RecordOptionName, _options); - if (record is not null) - { - Configuration.Record = record.Value; - } - var watchPids = context.ParseResult.GetValueForOption?>(ProxyHost.WatchPidsOptionName, _options); - if (watchPids is not null) - { - Configuration.WatchPids = watchPids; - } - var watchProcessNames = context.ParseResult.GetValueForOption?>(ProxyHost.WatchProcessNamesOptionName, _options); - if (watchProcessNames is not null) - { - Configuration.WatchProcessNames = watchProcessNames; - } - var rate = context.ParseResult.GetValueForOption(ProxyHost.RateOptionName, _options); - if (rate is not null) - { - Configuration.Rate = rate.Value; - } - var noFirstRun = context.ParseResult.GetValueForOption(ProxyHost.NoFirstRunOptionName, _options); - if (noFirstRun is not null) - { - Configuration.NoFirstRun = noFirstRun.Value; - } - var asSystemProxy = context.ParseResult.GetValueForOption(ProxyHost.AsSystemProxyOptionName, _options); - if (asSystemProxy is not null) - { - Configuration.AsSystemProxy = asSystemProxy.Value; - } - var installCert = context.ParseResult.GetValueForOption(ProxyHost.InstallCertOptionName, _options); - if (installCert is not null) - { - Configuration.InstallCert = installCert.Value; - } - } - - private async Task CheckForNewVersionAsync() - { - var newReleaseInfo = await UpdateNotification.CheckForNewVersionAsync(Configuration.NewVersionNotification); - if (newReleaseInfo != null) - { - _logger.LogInformation( - "New Dev Proxy version {version} is available.{newLine}See https://aka.ms/devproxy/upgrade for more information.", - newReleaseInfo.Version, - Environment.NewLine - ); - } - } - - private static readonly Lazy ConfigurationFactory = new(() => - { - var builder = new ConfigurationBuilder(); - var configuration = builder - .AddJsonFile(ProxyHost.ConfigFile, optional: true, reloadOnChange: true) - .Build(); - var configObject = new ProxyConfiguration(); - configuration.Bind(configObject); - - return configObject; - }); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DevProxy.Abstractions; +using Microsoft.VisualStudio.Threading; +using System.CommandLine; +using System.CommandLine.Invocation; + +namespace Microsoft.DevProxy.CommandHandlers; + +public class ProxyCommandHandler(IPluginEvents pluginEvents, + Option[] options, + ISet urlsToWatch, + ILogger logger) : ICommandHandler +{ + private readonly IPluginEvents _pluginEvents = pluginEvents ?? throw new ArgumentNullException(nameof(pluginEvents)); + private readonly Option[] _options = options ?? throw new ArgumentNullException(nameof(options)); + private readonly ISet _urlsToWatch = urlsToWatch ?? throw new ArgumentNullException(nameof(urlsToWatch)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + public static ProxyConfiguration Configuration { get => ConfigurationFactory.Value; } + + public int Invoke(InvocationContext context) + { + var joinableTaskContext = new JoinableTaskContext(); + var joinableTaskFactory = new JoinableTaskFactory(joinableTaskContext); + + return joinableTaskFactory.Run(async () => await InvokeAsync(context)); + } + + public async Task InvokeAsync(InvocationContext context) + { + ParseOptions(context); + _pluginEvents.RaiseOptionsLoaded(new OptionsLoadedArgs(context, _options)); + await CheckForNewVersionAsync(); + + try + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.AddFilter("Microsoft.Hosting.*", LogLevel.Error); + builder.Logging.AddFilter("Microsoft.AspNetCore.*", LogLevel.Error); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => ConfigurationFactory.Value); + builder.Services.AddSingleton(_pluginEvents); + builder.Services.AddSingleton(_logger); + builder.Services.AddSingleton(_urlsToWatch); + builder.Services.AddHostedService(); + + builder.Services.AddControllers(); + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + builder.Services.Configure(options => + { + options.LowercaseUrls = true; + }); + + builder.WebHost.ConfigureKestrel(options => + { + options.ListenLocalhost(ConfigurationFactory.Value.ApiPort); + _logger.LogInformation("Dev Proxy API listening on http://localhost:{Port}...", ConfigurationFactory.Value.ApiPort); + }); + + var app = builder.Build(); + app.UseSwagger(); + app.UseSwaggerUI(); + app.MapControllers(); + await app.RunAsync(); + + return 0; + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while running Dev Proxy"); + var inner = ex.InnerException; + + while (inner is not null) + { + _logger.LogError(inner, "============ Inner exception ============"); + inner = inner.InnerException; + } +#if DEBUG + throw; // so debug tools go straight to the source of the exception when attached +#else + return 1; +#endif + } + } + + private void ParseOptions(InvocationContext context) + { + var port = context.ParseResult.GetValueForOption(ProxyHost.PortOptionName, _options); + if (port is not null) + { + Configuration.Port = port.Value; + } + var ipAddress = context.ParseResult.GetValueForOption(ProxyHost.IpAddressOptionName, _options); + if (ipAddress is not null) + { + Configuration.IPAddress = ipAddress; + } + var record = context.ParseResult.GetValueForOption(ProxyHost.RecordOptionName, _options); + if (record is not null) + { + Configuration.Record = record.Value; + } + var watchPids = context.ParseResult.GetValueForOption?>(ProxyHost.WatchPidsOptionName, _options); + if (watchPids is not null) + { + Configuration.WatchPids = watchPids; + } + var watchProcessNames = context.ParseResult.GetValueForOption?>(ProxyHost.WatchProcessNamesOptionName, _options); + if (watchProcessNames is not null) + { + Configuration.WatchProcessNames = watchProcessNames; + } + var rate = context.ParseResult.GetValueForOption(ProxyHost.RateOptionName, _options); + if (rate is not null) + { + Configuration.Rate = rate.Value; + } + var noFirstRun = context.ParseResult.GetValueForOption(ProxyHost.NoFirstRunOptionName, _options); + if (noFirstRun is not null) + { + Configuration.NoFirstRun = noFirstRun.Value; + } + var asSystemProxy = context.ParseResult.GetValueForOption(ProxyHost.AsSystemProxyOptionName, _options); + if (asSystemProxy is not null) + { + Configuration.AsSystemProxy = asSystemProxy.Value; + } + var installCert = context.ParseResult.GetValueForOption(ProxyHost.InstallCertOptionName, _options); + if (installCert is not null) + { + Configuration.InstallCert = installCert.Value; + } + } + + private async Task CheckForNewVersionAsync() + { + var newReleaseInfo = await UpdateNotification.CheckForNewVersionAsync(Configuration.NewVersionNotification); + if (newReleaseInfo != null) + { + _logger.LogInformation( + "New Dev Proxy version {version} is available.{newLine}See https://aka.ms/devproxy/upgrade for more information.", + newReleaseInfo.Version, + Environment.NewLine + ); + } + } + + private static readonly Lazy ConfigurationFactory = new(() => + { + var builder = new ConfigurationBuilder(); + var configuration = builder + .AddJsonFile(ProxyHost.ConfigFile, optional: true, reloadOnChange: true) + .Build(); + var configObject = new ProxyConfiguration(); + configuration.Bind(configObject); + + return configObject; + }); +} diff --git a/dev-proxy/Program.cs b/dev-proxy/Program.cs index c9107943..07e3b964 100644 --- a/dev-proxy/Program.cs +++ b/dev-proxy/Program.cs @@ -1,119 +1,119 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.DevProxy; -using Microsoft.DevProxy.Abstractions; -using Microsoft.DevProxy.Abstractions.LanguageModel; -using Microsoft.DevProxy.CommandHandlers; -using Microsoft.DevProxy.Logging; -using Microsoft.Extensions.Logging.Console; -using System.CommandLine; - -_ = Announcement.ShowAsync(); - -PluginEvents pluginEvents = new(); - -(ILogger, ILoggerFactory) BuildLogger() -{ - var loggerFactory = LoggerFactory.Create(builder => - { - builder - .AddConsole(options => - { - options.FormatterName = ProxyConsoleFormatter.DefaultCategoryName; - options.LogToStandardErrorThreshold = LogLevel.Warning; - }) - .AddConsoleFormatter(options => { - options.IncludeScopes = true; - options.ShowSkipMessages = ProxyCommandHandler.Configuration.ShowSkipMessages; - }) - .AddRequestLogger(pluginEvents) - .SetMinimumLevel(ProxyHost.LogLevel ?? ProxyCommandHandler.Configuration.LogLevel); - }); - return (loggerFactory.CreateLogger(ProxyConsoleFormatter.DefaultCategoryName), loggerFactory); -} - -var (logger, loggerFactory) = BuildLogger(); - -var lmClient = new OllamaLanguageModelClient(ProxyCommandHandler.Configuration.LanguageModel, logger); -IProxyContext context = new ProxyContext(ProxyCommandHandler.Configuration, ProxyEngine.Certificate, lmClient); -ProxyHost proxyHost = new(); - -// this is where the root command is created which contains all commands and subcommands -RootCommand rootCommand = proxyHost.GetRootCommand(logger); - -// store the global options that are created automatically for us -// rootCommand doesn't return the global options, so we have to store them manually -string[] globalOptions = ["--version", "--help", "-h", "/h", "-?", "/?"]; - -// check if any of the global options are present -var hasGlobalOption = args.Any(arg => globalOptions.Contains(arg)); - -// get the list of available subcommands -var subCommands = rootCommand.Children.OfType().Select(c => c.Name).ToArray(); - -// check if any of the subcommands are present -var hasSubCommand = args.Any(arg => subCommands.Contains(arg)); - -if (hasGlobalOption || hasSubCommand) -{ - // we don't need to load plugins if the user is using a global option or using a subcommand, so we can exit early - await rootCommand.InvokeAsync(args); - return; -} - -var pluginLoader = new PluginLoader(logger, loggerFactory); -PluginLoaderResult loaderResults = await pluginLoader.LoadPluginsAsync(pluginEvents, context); -// have all the plugins init -pluginEvents.RaiseInit(new InitArgs()); - -var options = loaderResults.ProxyPlugins - .SelectMany(p => p.GetOptions()) - // remove duplicates by comparing the option names - .GroupBy(o => o.Name) - .Select(g => g.First()) - .ToList(); -options.ForEach(rootCommand.AddOption); -// register all plugin commands -loaderResults.ProxyPlugins - .SelectMany(p => p.GetCommands()) - .ToList() - .ForEach(rootCommand.AddCommand); - -rootCommand.Handler = proxyHost.GetCommandHandler(pluginEvents, [.. options], loaderResults.UrlsToWatch, logger); - -// filter args to retrieve options -var incomingOptions = args.Where(arg => arg.StartsWith('-')).ToArray(); - -// remove the global options from the incoming options -incomingOptions = incomingOptions.Except(globalOptions).ToArray(); - -// compare the incoming options against the root command options -foreach (var option in rootCommand.Options) -{ - // get the option aliases - var aliases = option.Aliases.ToArray(); - - // iterate over aliases - foreach (string alias in aliases) - { - // if the alias is present - if (incomingOptions.Contains(alias)) - { - // remove the option from the incoming options - incomingOptions = incomingOptions.Where(val => val != alias).ToArray(); - } - } -} - -// list the remaining incoming options as unknown in the output -if (incomingOptions.Length > 0) -{ - logger.LogError("Unknown option(s): {unknownOptions}", string.Join(" ", incomingOptions)); - logger.LogInformation("TIP: Use --help view available options"); - logger.LogInformation("TIP: Are you missing a plugin? See: https://aka.ms/devproxy/plugins"); -} -else -{ - await rootCommand.InvokeAsync(args); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DevProxy; +using Microsoft.DevProxy.Abstractions; +using Microsoft.DevProxy.Abstractions.LanguageModel; +using Microsoft.DevProxy.CommandHandlers; +using Microsoft.DevProxy.Logging; +using System.CommandLine; + +_ = Announcement.ShowAsync(); + +PluginEvents pluginEvents = new(); + +(ILogger, ILoggerFactory) BuildLogger() +{ + var loggerFactory = LoggerFactory.Create(builder => + { + builder + .AddConsole(options => + { + options.FormatterName = ProxyConsoleFormatter.DefaultCategoryName; + options.LogToStandardErrorThreshold = LogLevel.Warning; + }) + .AddConsoleFormatter(options => + { + options.IncludeScopes = true; + options.ShowSkipMessages = ProxyCommandHandler.Configuration.ShowSkipMessages; + }) + .AddRequestLogger(pluginEvents) + .SetMinimumLevel(ProxyHost.LogLevel ?? ProxyCommandHandler.Configuration.LogLevel); + }); + return (loggerFactory.CreateLogger(ProxyConsoleFormatter.DefaultCategoryName), loggerFactory); +} + +var (logger, loggerFactory) = BuildLogger(); + +var lmClient = new OllamaLanguageModelClient(ProxyCommandHandler.Configuration.LanguageModel, logger); +IProxyContext context = new ProxyContext(ProxyCommandHandler.Configuration, ProxyEngine.Certificate, lmClient); +ProxyHost proxyHost = new(); + +// this is where the root command is created which contains all commands and subcommands +RootCommand rootCommand = proxyHost.GetRootCommand(logger); + +// store the global options that are created automatically for us +// rootCommand doesn't return the global options, so we have to store them manually +string[] globalOptions = ["--version", "--help", "-h", "/h", "-?", "/?"]; + +// check if any of the global options are present +var hasGlobalOption = args.Any(arg => globalOptions.Contains(arg)); + +// get the list of available subcommands +var subCommands = rootCommand.Children.OfType().Select(c => c.Name).ToArray(); + +// check if any of the subcommands are present +var hasSubCommand = args.Any(arg => subCommands.Contains(arg)); + +if (hasGlobalOption || hasSubCommand) +{ + // we don't need to load plugins if the user is using a global option or using a subcommand, so we can exit early + await rootCommand.InvokeAsync(args); + return; +} + +var pluginLoader = new PluginLoader(logger, loggerFactory); +PluginLoaderResult loaderResults = await pluginLoader.LoadPluginsAsync(pluginEvents, context); +// have all the plugins init +pluginEvents.RaiseInit(new InitArgs()); + +var options = loaderResults.ProxyPlugins + .SelectMany(p => p.GetOptions()) + // remove duplicates by comparing the option names + .GroupBy(o => o.Name) + .Select(g => g.First()) + .ToList(); +options.ForEach(rootCommand.AddOption); +// register all plugin commands +loaderResults.ProxyPlugins + .SelectMany(p => p.GetCommands()) + .ToList() + .ForEach(rootCommand.AddCommand); + +rootCommand.Handler = proxyHost.GetCommandHandler(pluginEvents, [.. options], loaderResults.UrlsToWatch, logger); + +// filter args to retrieve options +var incomingOptions = args.Where(arg => arg.StartsWith('-')).ToArray(); + +// remove the global options from the incoming options +incomingOptions = incomingOptions.Except(globalOptions).ToArray(); + +// compare the incoming options against the root command options +foreach (var option in rootCommand.Options) +{ + // get the option aliases + var aliases = option.Aliases.ToArray(); + + // iterate over aliases + foreach (string alias in aliases) + { + // if the alias is present + if (incomingOptions.Contains(alias)) + { + // remove the option from the incoming options + incomingOptions = incomingOptions.Where(val => val != alias).ToArray(); + } + } +} + +// list the remaining incoming options as unknown in the output +if (incomingOptions.Length > 0) +{ + logger.LogError("Unknown option(s): {unknownOptions}", string.Join(" ", incomingOptions)); + logger.LogInformation("TIP: Use --help view available options"); + logger.LogInformation("TIP: Are you missing a plugin? See: https://aka.ms/devproxy/plugins"); +} +else +{ + await rootCommand.InvokeAsync(args); +} diff --git a/dev-proxy/ProxyEngine.cs b/dev-proxy/ProxyEngine.cs index 86ac8152..4dc91826 100755 --- a/dev-proxy/ProxyEngine.cs +++ b/dev-proxy/ProxyEngine.cs @@ -160,6 +160,10 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } _pluginEvents.AfterRequestLog += AfterRequestLogAsync; + if (!isInteractive) + { + return; + } try { @@ -169,16 +173,13 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await Task.Delay(10, stoppingToken); } - // we need this check or proxy will fail with an exception - // when run for example in VSCode's integrated terminal - if (isInteractive) - { - await ReadKeysAsync(); - } + + await ReadKeysAsync(); } } catch (TaskCanceledException) { + throw; } } @@ -335,7 +336,10 @@ private void StopProxy() _proxyServer.ServerCertificateValidationCallback -= OnCertificateValidationAsync; _proxyServer.ClientCertificateSelectionCallback -= OnCertificateSelectionAsync; - _proxyServer.Stop(); + if (_proxyServer.ProxyRunning) + { + _proxyServer.Stop(); + } } if (RunTime.IsMac && _config.AsSystemProxy)