diff --git a/.vsts-ci.yml b/.vsts-ci.yml index af45421f086b..14937d384bfb 100644 --- a/.vsts-ci.yml +++ b/.vsts-ci.yml @@ -104,7 +104,7 @@ extends: publishTaskPrefix: 1ES. populateInternalRuntimeVariables: true runtimeSourceProperties: /p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) - locBranch: release/10.0.1xx + locBranch: release/10.0.2xx # WORKAROUND: BinSkim requires the folder exist prior to scanning. preSteps: - powershell: New-Item -ItemType Directory -Path $(Build.SourcesDirectory)/artifacts/bin -Force diff --git a/Directory.Build.props b/Directory.Build.props index 1101288f76a8..f079f24c8e12 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,7 +12,6 @@ - true @@ -75,9 +74,7 @@ - false false - diff --git a/documentation/general/analyzer-redirecting.md b/documentation/general/analyzer-redirecting.md index 993445af4ce6..45c59deab17d 100644 --- a/documentation/general/analyzer-redirecting.md +++ b/documentation/general/analyzer-redirecting.md @@ -48,11 +48,31 @@ And metadata at `metadata.json`: ```json { - "AspNetCoreAnalyzers": "9.0.0-preview.5.24306.11", - "NetCoreAnalyzers": "9.0.0-preview.5.24306.7", - "WindowsDesktopAnalyzers": "9.0.0-preview.5.24306.8", - "SDKAnalyzers": "9.0.100-dev", - "WebSDKAnalyzers": "9.0.100-dev", + "AspNetCoreAnalyzers": + { + "Version": "9.0.0-preview.5.24306.11", + "Files": ["analyzers\\dotnet\\cs\\Microsoft.AspNetCore.App.Analyzers.dll"] + }, + "NetCoreAnalyzers": + { + "Version": "9.0.0-preview.5.24306.7", + "Files": ["analyzers\\dotnet\\cs\\System.Text.RegularExpressions.Generator.dll"] + }, + "WindowsDesktopAnalyzers": + { + "Version": "9.0.0-preview.5.24306.8", + "Files": ["analyzers\\dotnet\\System.Windows.Forms.Analyzers.dll"] + }, + "SDKAnalyzers": + { + "Version": "9.0.100-dev", + "Files": ["Sdks\\Microsoft.NET.Sdk\\analyzers\\Microsoft.CodeAnalysis.NetAnalyzers.dll"] + }, + "WebSDKAnalyzers": + { + "Version": "9.0.100-dev", + "Files": ["Sdks\\Microsoft.NET.Sdk.Web\\analyzers\\cs\\Microsoft.AspNetCore.Analyzers.dll"] + }, } ``` @@ -73,7 +93,7 @@ will be redirected to {InstallDir}\SDKAnalyzers\Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll ``` -where `metadata.json` has `"SDKAnalyzers": "9.0.100-dev"`, because +where `metadata.json` has `"SDKAnalyzers": { "Version": "9.0.100-dev" }`, because 1. the suffix `Sdks\Microsoft.NET.Sdk\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll` matches, and 2. the version `9.0.100-preview.5.24307.3` has the same major and minor component (`9.0`) as the version `9.0.100-dev` (both versions are read from the paths, not DLL metadata). diff --git a/eng/Analyzers.props b/eng/Analyzers.props deleted file mode 100644 index 170e51092bb9..000000000000 --- a/eng/Analyzers.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/eng/Versions.props b/eng/Versions.props index 3cc2266df2a6..a640137af702 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -58,7 +58,6 @@ 2.0.3 13.0.3 4.8.6 - 1.2.0-beta.435 4.0.5 2.0.0-beta5.25279.2 1.1.2 diff --git a/src/BuiltInTools/HotReloadAgent/HotReloadAgent.cs b/src/BuiltInTools/HotReloadAgent/HotReloadAgent.cs index f64180144078..61af429cfd25 100644 --- a/src/BuiltInTools/HotReloadAgent/HotReloadAgent.cs +++ b/src/BuiltInTools/HotReloadAgent/HotReloadAgent.cs @@ -297,8 +297,12 @@ public void ApplyStaticAssetUpdate(RuntimeStaticAssetUpdate update) /// public static void ClearHotReloadEnvironmentVariables(Type startupHookType) { - Environment.SetEnvironmentVariable(AgentEnvironmentVariables.DotNetStartupHooks, - RemoveCurrentAssembly(startupHookType, Environment.GetEnvironmentVariable(AgentEnvironmentVariables.DotNetStartupHooks)!)); + var startupHooks = Environment.GetEnvironmentVariable(AgentEnvironmentVariables.DotNetStartupHooks); + if (!string.IsNullOrEmpty(startupHooks)) + { + Environment.SetEnvironmentVariable(AgentEnvironmentVariables.DotNetStartupHooks, + RemoveCurrentAssembly(startupHookType, startupHooks)); + } Environment.SetEnvironmentVariable(AgentEnvironmentVariables.DotNetWatchHotReloadNamedPipeName, null); Environment.SetEnvironmentVariable(AgentEnvironmentVariables.HotReloadDeltaClientLogMessages, null); @@ -307,10 +311,7 @@ public static void ClearHotReloadEnvironmentVariables(Type startupHookType) // internal for testing internal static string RemoveCurrentAssembly(Type startupHookType, string environment) { - if (environment is "") - { - return environment; - } + Debug.Assert(!string.IsNullOrEmpty(environment), $"{nameof(environment)} must be set"); var comparison = Path.DirectorySeparatorChar == '\\' ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; diff --git a/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs b/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs index 260b35c23042..6a796e464d79 100644 --- a/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs +++ b/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs @@ -11,8 +11,7 @@ public static async Task RunAsync(string workingDirectory, DotNetWatchOpti { var globalOptions = new GlobalOptions() { - Quiet = options.IsQuiet, - Verbose = options.IsVerbose, + LogLevel = options.LogLevel, NoHotReload = false, NonInteractive = true, }; @@ -42,10 +41,10 @@ public static async Task RunAsync(string workingDirectory, DotNetWatchOpti var muxerPath = Path.GetFullPath(Path.Combine(options.SdkDirectory, "..", "..", "dotnet" + PathUtilities.ExecutableExtension)); var console = new PhysicalConsole(TestFlags.None); - var reporter = new ConsoleReporter(console, globalOptions.Verbose, globalOptions.Quiet, suppressEmojis: false); + var reporter = new ConsoleReporter(console, suppressEmojis: false); var environmentOptions = EnvironmentOptions.FromEnvironment(muxerPath); var processRunner = new ProcessRunner(environmentOptions.GetProcessCleanupTimeout(isHotReloadEnabled: true)); - var loggerFactory = new LoggerFactory(reporter); + var loggerFactory = new LoggerFactory(reporter, globalOptions.LogLevel); var logger = loggerFactory.CreateLogger(DotNetWatchContext.DefaultLogComponentName); using var context = new DotNetWatchContext() diff --git a/src/BuiltInTools/Watch.Aspire/DotNetWatchOptions.cs b/src/BuiltInTools/Watch.Aspire/DotNetWatchOptions.cs index 2ccf84b885b5..1d8f526c1fff 100644 --- a/src/BuiltInTools/Watch.Aspire/DotNetWatchOptions.cs +++ b/src/BuiltInTools/Watch.Aspire/DotNetWatchOptions.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.CommandLine; using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch; @@ -17,8 +18,7 @@ internal sealed class DotNetWatchOptions public required string ProjectPath { get; init; } public required ImmutableArray ApplicationArguments { get; init; } - public bool IsVerbose { get; init; } - public bool IsQuiet { get; init; } + public LogLevel LogLevel { get; init; } public bool NoLaunchProfile { get; init; } public static bool TryParse(string[] args, [NotNullWhen(true)] out DotNetWatchOptions? options) @@ -71,8 +71,7 @@ public static bool TryParse(string[] args, [NotNullWhen(true)] out DotNetWatchOp { SdkDirectory = parseResult.GetRequiredValue(sdkOption), ProjectPath = parseResult.GetRequiredValue(projectOption), - IsQuiet = parseResult.GetValue(quietOption), - IsVerbose = parseResult.GetValue(verboseOption), + LogLevel = parseResult.GetValue(quietOption) ? LogLevel.Warning : parseResult.GetValue(verboseOption) ? LogLevel.Debug : LogLevel.Information, ApplicationArguments = [.. parseResult.GetValue(applicationArguments) ?? []], NoLaunchProfile = parseResult.GetValue(noLaunchProfileOption), }; diff --git a/src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs b/src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs index cefcd43cf320..0f1fbb74d5d8 100644 --- a/src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs +++ b/src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs @@ -71,13 +71,13 @@ public bool IsServerSupported(ProjectGraphNode projectNode, ILogger logger) { if (context.EnvironmentOptions.SuppressBrowserRefresh) { - logger.Log(MessageDescriptor.SkippingConfiguringBrowserRefresh_SuppressedViaEnvironmentVariable.WithSeverityWhen(MessageSeverity.Error, RequiresBrowserRefresh), EnvironmentVariables.Names.SuppressBrowserRefresh); + logger.Log(MessageDescriptor.SkippingConfiguringBrowserRefresh_SuppressedViaEnvironmentVariable.WithLevelWhen(LogLevel.Error, RequiresBrowserRefresh), EnvironmentVariables.Names.SuppressBrowserRefresh); return false; } if (!projectNode.IsNetCoreApp(minVersion: s_minimumSupportedVersion)) { - logger.Log(MessageDescriptor.SkippingConfiguringBrowserRefresh_TargetFrameworkNotSupported.WithSeverityWhen(MessageSeverity.Error, RequiresBrowserRefresh)); + logger.Log(MessageDescriptor.SkippingConfiguringBrowserRefresh_TargetFrameworkNotSupported.WithLevelWhen(LogLevel.Error, RequiresBrowserRefresh)); return false; } diff --git a/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs b/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs index 70c121ccf7c1..8eb705b17926 100644 --- a/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs +++ b/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch { @@ -35,6 +36,7 @@ internal sealed record EnvironmentOptions( bool SuppressBrowserRefresh = false, bool SuppressEmojis = false, bool RestartOnRudeEdit = false, + LogLevel? CliLogLevel = null, string? AutoReloadWebSocketHostName = null, int? AutoReloadWebSocketPort = null, string? BrowserPath = null, @@ -53,6 +55,7 @@ internal sealed record EnvironmentOptions( SuppressBrowserRefresh: EnvironmentVariables.SuppressBrowserRefresh, SuppressEmojis: EnvironmentVariables.SuppressEmojis, RestartOnRudeEdit: EnvironmentVariables.RestartOnRudeEdit, + CliLogLevel: EnvironmentVariables.CliLogLevel, AutoReloadWebSocketHostName: EnvironmentVariables.AutoReloadWSHostName, AutoReloadWebSocketPort: EnvironmentVariables.AutoReloadWSPort, BrowserPath: EnvironmentVariables.BrowserPath, diff --git a/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs b/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs index 28763d7f3223..41ac7d88edea 100644 --- a/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs +++ b/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Logging; + namespace Microsoft.DotNet.Watch; internal static class EnvironmentVariables @@ -20,7 +22,19 @@ public static class Names public const string SuppressBrowserRefresh = "DOTNET_WATCH_SUPPRESS_BROWSER_REFRESH"; } - public static bool VerboseCliOutput => ReadBool("DOTNET_CLI_CONTEXT_VERBOSE"); + public static LogLevel? CliLogLevel + { + get + { + var value = Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE"); + return string.Equals(value, "trace", StringComparison.OrdinalIgnoreCase) + ? LogLevel.Trace + : ParseBool(value) + ? LogLevel.Debug + : null; + } + } + public static bool IsPollingEnabled => ReadBool("DOTNET_USE_POLLING_FILE_WATCHER"); public static bool SuppressEmojis => ReadBool("DOTNET_WATCH_SUPPRESS_EMOJIS"); public static bool RestartOnRudeEdit => ReadBool("DOTNET_WATCH_RESTART_ON_RUDE_EDIT"); @@ -46,11 +60,14 @@ public static class Names public static string? BrowserPath => Environment.GetEnvironmentVariable("DOTNET_WATCH_BROWSER_PATH"); private static bool ReadBool(string variableName) - => Environment.GetEnvironmentVariable(variableName) is var value && (value == "1" || bool.TryParse(value, out var boolValue) && boolValue); + => ParseBool(Environment.GetEnvironmentVariable(variableName)); private static TimeSpan? ReadTimeSpan(string variableName) => Environment.GetEnvironmentVariable(variableName) is var value && long.TryParse(value, out var intValue) && intValue >= 0 ? TimeSpan.FromMilliseconds(intValue) : null; private static int? ReadInt(string variableName) => Environment.GetEnvironmentVariable(variableName) is var value && int.TryParse(value, out var intValue) ? intValue : null; + + private static bool ParseBool(string? value) + => value == "1" || bool.TryParse(value, out var boolValue) && boolValue; } diff --git a/src/BuiltInTools/Watch/Context/GlobalOptions.cs b/src/BuiltInTools/Watch/Context/GlobalOptions.cs index 0f24692c8522..2dc002f6070e 100644 --- a/src/BuiltInTools/Watch/Context/GlobalOptions.cs +++ b/src/BuiltInTools/Watch/Context/GlobalOptions.cs @@ -1,12 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Logging; + namespace Microsoft.DotNet.Watch; internal sealed class GlobalOptions { - public bool Quiet { get; init; } - public bool Verbose { get; init; } + public LogLevel LogLevel { get; init; } public bool NoHotReload { get; init; } public bool NonInteractive { get; init; } diff --git a/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs b/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs index 2929d4f1e71e..e95ccc1be34f 100644 --- a/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs +++ b/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs @@ -521,7 +521,7 @@ void ReportDiagnostic(Diagnostic diagnostic, MessageDescriptor descriptor, strin { errorsToDisplayInApp.Add(MessageDescriptor.RestartingApplicationToApplyChanges.GetMessage()); } - else if (descriptor.Severity != MessageSeverity.None) + else if (descriptor.Level != LogLevel.None) { errorsToDisplayInApp.Add(descriptor.GetMessage(args)); } diff --git a/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs index 5466d27b32c2..52df0fa62829 100644 --- a/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs +++ b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs @@ -31,7 +31,7 @@ public HotReloadDotNetWatcher(DotNetWatchContext context, IConsole console, IRun _runtimeProcessLauncherFactory = runtimeProcessLauncherFactory; if (!context.Options.NonInteractive) { - var consoleInput = new ConsoleInputReader(_console, context.Options.Quiet, context.EnvironmentOptions.SuppressEmojis); + var consoleInput = new ConsoleInputReader(_console, context.Options.LogLevel, context.EnvironmentOptions.SuppressEmojis); var noPrompt = context.EnvironmentOptions.RestartOnRudeEdit; if (noPrompt) diff --git a/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs b/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs index 06b79772ddf6..2fc7e54a9b34 100644 --- a/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs +++ b/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs @@ -230,16 +230,13 @@ private Task UpdateSolutionAsync(Solution newSolution, string operationDisplayNa private async Task ReportSolutionFilesAsync(Solution solution, int updateId, string operationDisplayName, CancellationToken cancellationToken) { -#if DEBUG - _logger.LogDebug("Solution: {Path}", solution.FilePath); + _logger.LogDebug("Solution after {Operation}: v{Version}", operationDisplayName, updateId); - if (!_logger.IsEnabled(LogLevel.Debug)) + if (!_logger.IsEnabled(LogLevel.Trace)) { return; } - _logger.LogDebug("Solution after {Operation}: v{Version}", operationDisplayName, updateId); - foreach (var project in solution.Projects) { _logger.LogDebug(" Project: {Path}", project.FilePath); @@ -265,8 +262,5 @@ async ValueTask InspectDocumentAsync(TextDocument document, string kind) var text = await document.GetTextAsync(cancellationToken); _logger.LogDebug(" {Kind}: {FilePath} [{Checksum}]", kind, document.FilePath, Convert.ToBase64String(text.GetChecksum().ToArray())); } -#else - await Task.CompletedTask; -#endif } } diff --git a/src/BuiltInTools/Watch/UI/ConsoleInputReader.cs b/src/BuiltInTools/Watch/UI/ConsoleInputReader.cs index 4c9e930b245a..233d320765ba 100644 --- a/src/BuiltInTools/Watch/UI/ConsoleInputReader.cs +++ b/src/BuiltInTools/Watch/UI/ConsoleInputReader.cs @@ -1,15 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Logging; + namespace Microsoft.DotNet.Watch { - internal sealed class ConsoleInputReader(IConsole console, bool quiet, bool suppressEmojis) + internal sealed class ConsoleInputReader(IConsole console, LogLevel logLevel, bool suppressEmojis) { private readonly object _writeLock = new(); public async Task GetKeyAsync(string prompt, Func validateInput, CancellationToken cancellationToken) { - if (quiet) + if (logLevel > LogLevel.Information) { return ConsoleKey.Escape; } diff --git a/src/BuiltInTools/Watch/UI/ConsoleReporter.cs b/src/BuiltInTools/Watch/UI/ConsoleReporter.cs index 198e518590bb..60b156142b08 100644 --- a/src/BuiltInTools/Watch/UI/ConsoleReporter.cs +++ b/src/BuiltInTools/Watch/UI/ConsoleReporter.cs @@ -9,10 +9,8 @@ namespace Microsoft.DotNet.Watch /// This API supports infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// - internal sealed class ConsoleReporter(IConsole console, bool verbose, bool quiet, bool suppressEmojis) : IReporter, IProcessOutputReporter + internal sealed class ConsoleReporter(IConsole console, bool suppressEmojis) : IReporter, IProcessOutputReporter { - public bool IsVerbose { get; } = verbose; - public bool IsQuiet { get; } = quiet; public bool SuppressEmojis { get; } = suppressEmojis; private readonly Lock _writeLock = new(); @@ -50,33 +48,18 @@ private void WriteLine(TextWriter writer, string message, ConsoleColor? color, E } } - public void Report(EventId id, Emoji emoji, MessageSeverity severity, string message) + public void Report(EventId id, Emoji emoji, LogLevel level, string message) { - switch (severity) + var color = level switch { - case MessageSeverity.Error: - // Use stdout for error messages to preserve ordering with respect to other output. - WriteLine(console.Error, message, ConsoleColor.Red, emoji); - break; + LogLevel.Critical or LogLevel.Error => ConsoleColor.Red, + LogLevel.Warning => ConsoleColor.Yellow, + LogLevel.Information => (ConsoleColor?)null, + _ => ConsoleColor.DarkGray, + }; - case MessageSeverity.Warning: - WriteLine(console.Error, message, ConsoleColor.Yellow, emoji); - break; - - case MessageSeverity.Output: - if (!IsQuiet) - { - WriteLine(console.Error, message, color: null, emoji); - } - break; - - case MessageSeverity.Verbose: - if (IsVerbose) - { - WriteLine(console.Error, message, ConsoleColor.DarkGray, emoji); - } - break; - } + // Use stdout for error messages to preserve ordering with respect to other output. + WriteLine(console.Error, message, color, emoji); } } } diff --git a/src/BuiltInTools/Watch/UI/IReporter.cs b/src/BuiltInTools/Watch/UI/IReporter.cs index 2a118db336ee..82b60996e1ab 100644 --- a/src/BuiltInTools/Watch/UI/IReporter.cs +++ b/src/BuiltInTools/Watch/UI/IReporter.cs @@ -2,21 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; -using System.Diagnostics; using Microsoft.DotNet.HotReload; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch { - internal enum MessageSeverity - { - None, - Verbose, - Output, - Warning, - Error, - } - internal enum Emoji { Default = 0, @@ -66,61 +56,43 @@ public static string GetLogMessagePrefix(this Emoji emoji) public static void Log(this ILogger logger, MessageDescriptor descriptor, params object?[] args) { logger.Log( - descriptor.Severity.ToLogLevel(), + descriptor.Level, descriptor.Id, state: (descriptor, args), exception: null, formatter: static (state, _) => state.descriptor.GetMessage(state.args)); } - - public static LogLevel ToLogLevel(this MessageSeverity severity) - => severity switch - { - MessageSeverity.None => LogLevel.None, - MessageSeverity.Verbose => LogLevel.Debug, - MessageSeverity.Output => LogLevel.Information, - MessageSeverity.Warning => LogLevel.Warning, - MessageSeverity.Error => LogLevel.Error, - _ => throw new InvalidOperationException() - }; - - public static MessageSeverity ToSeverity(this LogLevel level) - => level switch - { - LogLevel.Debug => MessageSeverity.Verbose, - LogLevel.Information => MessageSeverity.Output, - LogLevel.Warning => MessageSeverity.Warning, - LogLevel.Error => MessageSeverity.Error, - LogLevel.None => MessageSeverity.None, - _ => throw new InvalidOperationException() - }; } - internal sealed class LoggerFactory(IReporter reporter) : ILoggerFactory + internal sealed class LoggerFactory(IReporter reporter, LogLevel level) : ILoggerFactory { - private sealed class Logger(IReporter reporter, string categoryName) : ILogger + private sealed class Logger(IReporter reporter, LogLevel level, string categoryName) : ILogger { public bool IsEnabled(LogLevel logLevel) - => reporter.IsVerbose || logLevel > LogLevel.Debug; + => logLevel >= level; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { + if (!IsEnabled(logLevel)) + { + return; + } + var (name, display) = LoggingUtilities.ParseCategoryName(categoryName); var prefix = display != null ? $"[{display}] " : ""; - var severity = logLevel.ToSeverity(); var descriptor = eventId.Id != 0 ? MessageDescriptor.GetDescriptor(eventId) : default; - var emoji = severity switch + var emoji = logLevel switch { _ when descriptor.Emoji != Emoji.Default => descriptor.Emoji, - MessageSeverity.Error => Emoji.Error, - MessageSeverity.Warning => Emoji.Warning, + LogLevel.Error => Emoji.Error, + LogLevel.Warning => Emoji.Warning, _ when MessageDescriptor.ComponentEmojis.TryGetValue(name, out var componentEmoji) => componentEmoji, _ => Emoji.Watch }; - reporter.Report(eventId, emoji, severity, prefix + formatter(state, exception)); + reporter.Report(eventId, emoji, logLevel, prefix + formatter(state, exception)); } public IDisposable? BeginScope(TState state) where TState : notnull @@ -132,27 +104,27 @@ public void Dispose() } public ILogger CreateLogger(string categoryName) - => new Logger(reporter, categoryName); + => new Logger(reporter, level, categoryName); public void AddProvider(ILoggerProvider provider) => throw new NotImplementedException(); } - internal readonly record struct MessageDescriptor(string Format, Emoji Emoji, MessageSeverity Severity, EventId Id) + internal readonly record struct MessageDescriptor(string Format, Emoji Emoji, LogLevel Level, EventId Id) { private static int s_id; private static ImmutableDictionary s_descriptors = []; - private static MessageDescriptor Create(string format, Emoji emoji, MessageSeverity severity) + private static MessageDescriptor Create(string format, Emoji emoji, LogLevel level) // reserve event id 0 for ad-hoc messages - => Create(new EventId(++s_id), format, emoji, severity); + => Create(new EventId(++s_id), format, emoji, level); private static MessageDescriptor Create(LogEvent logEvent, Emoji emoji) - => Create(logEvent.Id, logEvent.Message, emoji, logEvent.Level.ToSeverity()); + => Create(logEvent.Id, logEvent.Message, emoji, logEvent.Level); - private static MessageDescriptor Create(EventId id, string format, Emoji emoji, MessageSeverity severity) + private static MessageDescriptor Create(EventId id, string format, Emoji emoji, LogLevel level) { - var descriptor = new MessageDescriptor(format, emoji, severity, id.Id); + var descriptor = new MessageDescriptor(format, emoji, level, id.Id); s_descriptors = s_descriptors.Add(id, descriptor); return descriptor; } @@ -163,9 +135,18 @@ public static MessageDescriptor GetDescriptor(EventId id) public string GetMessage(params object?[] args) => Id.Id == 0 ? Format : string.Format(Format, args); - public MessageDescriptor WithSeverityWhen(MessageSeverity severity, bool condition) - => condition && Severity != severity - ? this with { Severity = severity, Emoji = severity switch { MessageSeverity.Error => Emoji.Error, MessageSeverity.Warning => Emoji.Warning, _ => Emoji } } + public MessageDescriptor WithLevelWhen(LogLevel level, bool condition) + => condition && Level != level + ? this with + { + Level = level, + Emoji = level switch + { + LogLevel.Error or LogLevel.Critical => Emoji.Error, + LogLevel.Warning => Emoji.Warning, + _ => Emoji + } + } : this; public static readonly ImmutableDictionary ComponentEmojis = ImmutableDictionary.Empty @@ -179,83 +160,82 @@ public MessageDescriptor WithSeverityWhen(MessageSeverity severity, bool conditi .Add(AspireServiceFactory.AspireLogComponentName, Emoji.Aspire); // predefined messages used for testing: - public static readonly MessageDescriptor HotReloadSessionStarting = Create("Hot reload session starting.", Emoji.HotReload, MessageSeverity.None); - public static readonly MessageDescriptor HotReloadSessionStarted = Create("Hot reload session started.", Emoji.HotReload, MessageSeverity.Verbose); - public static readonly MessageDescriptor ProjectsRebuilt = Create("Projects rebuilt ({0})", Emoji.HotReload, MessageSeverity.Verbose); - public static readonly MessageDescriptor ProjectsRestarted = Create("Projects restarted ({0})", Emoji.HotReload, MessageSeverity.Verbose); - public static readonly MessageDescriptor ProjectDependenciesDeployed = Create("Project dependencies deployed ({0})", Emoji.HotReload, MessageSeverity.Verbose); - public static readonly MessageDescriptor FixBuildError = Create("Fix the error to continue or press Ctrl+C to exit.", Emoji.Watch, MessageSeverity.Warning); - public static readonly MessageDescriptor WaitingForChanges = Create("Waiting for changes", Emoji.Watch, MessageSeverity.Output); - public static readonly MessageDescriptor LaunchedProcess = Create("Launched '{0}' with arguments '{1}': process id {2}", Emoji.Launch, MessageSeverity.Verbose); - public static readonly MessageDescriptor HotReloadChangeHandled = Create("Hot reload change handled in {0}ms.", Emoji.HotReload, MessageSeverity.Verbose); + public static readonly MessageDescriptor HotReloadSessionStarting = Create("Hot reload session starting.", Emoji.HotReload, LogLevel.None); + public static readonly MessageDescriptor HotReloadSessionStarted = Create("Hot reload session started.", Emoji.HotReload, LogLevel.Debug); + public static readonly MessageDescriptor ProjectsRebuilt = Create("Projects rebuilt ({0})", Emoji.HotReload, LogLevel.Debug); + public static readonly MessageDescriptor ProjectsRestarted = Create("Projects restarted ({0})", Emoji.HotReload, LogLevel.Debug); + public static readonly MessageDescriptor ProjectDependenciesDeployed = Create("Project dependencies deployed ({0})", Emoji.HotReload, LogLevel.Debug); + public static readonly MessageDescriptor FixBuildError = Create("Fix the error to continue or press Ctrl+C to exit.", Emoji.Watch, LogLevel.Warning); + public static readonly MessageDescriptor WaitingForChanges = Create("Waiting for changes", Emoji.Watch, LogLevel.Information); + public static readonly MessageDescriptor LaunchedProcess = Create("Launched '{0}' with arguments '{1}': process id {2}", Emoji.Launch, LogLevel.Debug); + public static readonly MessageDescriptor HotReloadChangeHandled = Create("Hot reload change handled in {0}ms.", Emoji.HotReload, LogLevel.Debug); public static readonly MessageDescriptor HotReloadSucceeded = Create(LogEvents.HotReloadSucceeded, Emoji.HotReload); public static readonly MessageDescriptor UpdatesApplied = Create(LogEvents.UpdatesApplied, Emoji.HotReload); public static readonly MessageDescriptor Capabilities = Create(LogEvents.Capabilities, Emoji.HotReload); - public static readonly MessageDescriptor WaitingForFileChangeBeforeRestarting = Create("Waiting for a file to change before restarting ...", Emoji.Wait, MessageSeverity.Warning); - public static readonly MessageDescriptor WatchingWithHotReload = Create("Watching with Hot Reload.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor RestartInProgress = Create("Restart in progress.", Emoji.Restart, MessageSeverity.Output); - public static readonly MessageDescriptor RestartRequested = Create("Restart requested.", Emoji.Restart, MessageSeverity.Output); - public static readonly MessageDescriptor ShutdownRequested = Create("Shutdown requested. Press Ctrl+C again to force exit.", Emoji.Stop, MessageSeverity.Output); - public static readonly MessageDescriptor ApplyUpdate_Error = Create("{0}{1}", Emoji.Error, MessageSeverity.Error); - public static readonly MessageDescriptor ApplyUpdate_Warning = Create("{0}{1}", Emoji.Warning, MessageSeverity.Warning); - public static readonly MessageDescriptor ApplyUpdate_Verbose = Create("{0}{1}", Emoji.Default, MessageSeverity.Verbose); - public static readonly MessageDescriptor ApplyUpdate_ChangingEntryPoint = Create("{0} Press \"Ctrl + R\" to restart.", Emoji.Warning, MessageSeverity.Warning); - public static readonly MessageDescriptor ApplyUpdate_FileContentDoesNotMatchBuiltSource = Create("{0} Expected if a source file is updated that is linked to project whose build is not up-to-date.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor ConfiguredToLaunchBrowser = Create("dotnet-watch is configured to launch a browser on ASP.NET Core application startup.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor ConfiguredToUseBrowserRefresh = Create("Using browser-refresh middleware", Emoji.Default, MessageSeverity.Verbose); - public static readonly MessageDescriptor SkippingConfiguringBrowserRefresh_SuppressedViaEnvironmentVariable = Create("Skipping configuring browser-refresh middleware since its refresh server suppressed via environment variable {0}.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor SkippingConfiguringBrowserRefresh_TargetFrameworkNotSupported = Create("Skipping configuring browser-refresh middleware since the target framework version is not supported. For more information see 'https://aka.ms/dotnet/watch/unsupported-tfm'.", Emoji.Watch, MessageSeverity.Warning); + public static readonly MessageDescriptor WaitingForFileChangeBeforeRestarting = Create("Waiting for a file to change before restarting ...", Emoji.Wait, LogLevel.Warning); + public static readonly MessageDescriptor WatchingWithHotReload = Create("Watching with Hot Reload.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor RestartInProgress = Create("Restart in progress.", Emoji.Restart, LogLevel.Information); + public static readonly MessageDescriptor RestartRequested = Create("Restart requested.", Emoji.Restart, LogLevel.Information); + public static readonly MessageDescriptor ShutdownRequested = Create("Shutdown requested. Press Ctrl+C again to force exit.", Emoji.Stop, LogLevel.Information); + public static readonly MessageDescriptor ApplyUpdate_Error = Create("{0}{1}", Emoji.Error, LogLevel.Error); + public static readonly MessageDescriptor ApplyUpdate_Warning = Create("{0}{1}", Emoji.Warning, LogLevel.Warning); + public static readonly MessageDescriptor ApplyUpdate_Verbose = Create("{0}{1}", Emoji.Default, LogLevel.Debug); + public static readonly MessageDescriptor ApplyUpdate_ChangingEntryPoint = Create("{0} Press \"Ctrl + R\" to restart.", Emoji.Warning, LogLevel.Warning); + public static readonly MessageDescriptor ApplyUpdate_FileContentDoesNotMatchBuiltSource = Create("{0} Expected if a source file is updated that is linked to project whose build is not up-to-date.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor ConfiguredToLaunchBrowser = Create("dotnet-watch is configured to launch a browser on ASP.NET Core application startup.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor ConfiguredToUseBrowserRefresh = Create("Using browser-refresh middleware", Emoji.Default, LogLevel.Debug); + public static readonly MessageDescriptor SkippingConfiguringBrowserRefresh_SuppressedViaEnvironmentVariable = Create("Skipping configuring browser-refresh middleware since its refresh server suppressed via environment variable {0}.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor SkippingConfiguringBrowserRefresh_TargetFrameworkNotSupported = Create("Skipping configuring browser-refresh middleware since the target framework version is not supported. For more information see 'https://aka.ms/dotnet/watch/unsupported-tfm'.", Emoji.Watch, LogLevel.Warning); public static readonly MessageDescriptor UpdatingDiagnostics = Create(LogEvents.UpdatingDiagnostics, Emoji.Default); public static readonly MessageDescriptor FailedToReceiveResponseFromConnectedBrowser = Create(LogEvents.FailedToReceiveResponseFromConnectedBrowser, Emoji.Default); public static readonly MessageDescriptor NoBrowserConnected = Create(LogEvents.NoBrowserConnected, Emoji.Default); - public static readonly MessageDescriptor LaunchingBrowser = Create("Launching browser: {0} {1}", Emoji.Default, MessageSeverity.Verbose); + public static readonly MessageDescriptor LaunchingBrowser = Create("Launching browser: {0} {1}", Emoji.Default, LogLevel.Debug); public static readonly MessageDescriptor RefreshingBrowser = Create(LogEvents.RefreshingBrowser, Emoji.Default); public static readonly MessageDescriptor ReloadingBrowser = Create(LogEvents.ReloadingBrowser, Emoji.Default); public static readonly MessageDescriptor RefreshServerRunningAt = Create(LogEvents.RefreshServerRunningAt, Emoji.Default); public static readonly MessageDescriptor ConnectedToRefreshServer = Create(LogEvents.ConnectedToRefreshServer, Emoji.Default); - public static readonly MessageDescriptor RestartingApplicationToApplyChanges = Create("Restarting application to apply changes ...", Emoji.Default, MessageSeverity.Output); - public static readonly MessageDescriptor RestartingApplication = Create("Restarting application ...", Emoji.Default, MessageSeverity.Output); - public static readonly MessageDescriptor IgnoringChangeInHiddenDirectory = Create("Ignoring change in hidden directory '{0}': {1} '{2}'", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor IgnoringChangeInOutputDirectory = Create("Ignoring change in output directory: {0} '{1}'", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor IgnoringChangeInExcludedFile = Create("Ignoring change in excluded file '{0}': {1}. Path matches {2} glob '{3}' set in '{4}'.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor FileAdditionTriggeredReEvaluation = Create("File addition triggered re-evaluation.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor ReEvaluationCompleted = Create("Re-evaluation completed.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor ProjectChangeTriggeredReEvaluation = Create("Project change triggered re-evaluation.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor NoCSharpChangesToApply = Create("No C# changes to apply.", Emoji.Watch, MessageSeverity.Output); - public static readonly MessageDescriptor Exited = Create("Exited", Emoji.Watch, MessageSeverity.Output); - public static readonly MessageDescriptor ExitedWithUnknownErrorCode = Create("Exited with unknown error code", Emoji.Error, MessageSeverity.Error); - public static readonly MessageDescriptor ExitedWithErrorCode = Create("Exited with error code {0}", Emoji.Error, MessageSeverity.Error); - public static readonly MessageDescriptor FailedToLaunchProcess = Create("Failed to launch '{0}' with arguments '{1}': {2}", Emoji.Error, MessageSeverity.Error); - public static readonly MessageDescriptor ApplicationFailed = Create("Application failed: {0}", Emoji.Error, MessageSeverity.Error); - public static readonly MessageDescriptor ProcessRunAndExited = Create("Process id {0} ran for {1}ms and exited with exit code {2}.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor WaitingForProcessToExitWithin = Create("Waiting for process {0} to exit within {1}s.", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor WaitingForProcessToExit = Create("Waiting for process {0} to exit ({1}).", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor FailedToKillProcess = Create("Failed to kill process {0}: {1}.", Emoji.Error, MessageSeverity.Error); - public static readonly MessageDescriptor TerminatingProcess = Create("Terminating process {0} ({1}).", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor FailedToSendSignalToProcess = Create("Failed to send {0} signal to process {1}: {2}", Emoji.Warning, MessageSeverity.Warning); - public static readonly MessageDescriptor ErrorReadingProcessOutput = Create("Error reading {0} of process {1}: {2}", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor HotReloadOfScopedCssSucceeded = Create("Hot reload of scoped css succeeded.", Emoji.HotReload, MessageSeverity.Output); - public static readonly MessageDescriptor HotReloadOfScopedCssPartiallySucceeded = Create("Hot reload of scoped css partially succeeded: {0} project(s) out of {1} were updated.", Emoji.HotReload, MessageSeverity.Output); - public static readonly MessageDescriptor HotReloadOfScopedCssFailed = Create("Hot reload of scoped css failed.", Emoji.Error, MessageSeverity.Error); - public static readonly MessageDescriptor HotReloadOfStaticAssetsSucceeded = Create("Hot reload of static assets succeeded.", Emoji.HotReload, MessageSeverity.Output); + public static readonly MessageDescriptor RestartingApplicationToApplyChanges = Create("Restarting application to apply changes ...", Emoji.Default, LogLevel.Information); + public static readonly MessageDescriptor RestartingApplication = Create("Restarting application ...", Emoji.Default, LogLevel.Information); + public static readonly MessageDescriptor IgnoringChangeInHiddenDirectory = Create("Ignoring change in hidden directory '{0}': {1} '{2}'", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor IgnoringChangeInOutputDirectory = Create("Ignoring change in output directory: {0} '{1}'", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor IgnoringChangeInExcludedFile = Create("Ignoring change in excluded file '{0}': {1}. Path matches {2} glob '{3}' set in '{4}'.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor FileAdditionTriggeredReEvaluation = Create("File addition triggered re-evaluation.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor ReEvaluationCompleted = Create("Re-evaluation completed.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor ProjectChangeTriggeredReEvaluation = Create("Project change triggered re-evaluation.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor NoCSharpChangesToApply = Create("No C# changes to apply.", Emoji.Watch, LogLevel.Information); + public static readonly MessageDescriptor Exited = Create("Exited", Emoji.Watch, LogLevel.Information); + public static readonly MessageDescriptor ExitedWithUnknownErrorCode = Create("Exited with unknown error code", Emoji.Error, LogLevel.Error); + public static readonly MessageDescriptor ExitedWithErrorCode = Create("Exited with error code {0}", Emoji.Error, LogLevel.Error); + public static readonly MessageDescriptor FailedToLaunchProcess = Create("Failed to launch '{0}' with arguments '{1}': {2}", Emoji.Error, LogLevel.Error); + public static readonly MessageDescriptor ApplicationFailed = Create("Application failed: {0}", Emoji.Error, LogLevel.Error); + public static readonly MessageDescriptor ProcessRunAndExited = Create("Process id {0} ran for {1}ms and exited with exit code {2}.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor WaitingForProcessToExitWithin = Create("Waiting for process {0} to exit within {1}s.", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor WaitingForProcessToExit = Create("Waiting for process {0} to exit ({1}).", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor FailedToKillProcess = Create("Failed to kill process {0}: {1}.", Emoji.Error, LogLevel.Error); + public static readonly MessageDescriptor TerminatingProcess = Create("Terminating process {0} ({1}).", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor FailedToSendSignalToProcess = Create("Failed to send {0} signal to process {1}: {2}", Emoji.Warning, LogLevel.Warning); + public static readonly MessageDescriptor ErrorReadingProcessOutput = Create("Error reading {0} of process {1}: {2}", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor HotReloadOfScopedCssSucceeded = Create("Hot reload of scoped css succeeded.", Emoji.HotReload, LogLevel.Information); + public static readonly MessageDescriptor HotReloadOfScopedCssPartiallySucceeded = Create("Hot reload of scoped css partially succeeded: {0} project(s) out of {1} were updated.", Emoji.HotReload, LogLevel.Information); + public static readonly MessageDescriptor HotReloadOfScopedCssFailed = Create("Hot reload of scoped css failed.", Emoji.Error, LogLevel.Error); + public static readonly MessageDescriptor HotReloadOfStaticAssetsSucceeded = Create("Hot reload of static assets succeeded.", Emoji.HotReload, LogLevel.Information); public static readonly MessageDescriptor SendingStaticAssetUpdateRequest = Create(LogEvents.SendingStaticAssetUpdateRequest, Emoji.Default); - public static readonly MessageDescriptor HotReloadCapabilities = Create("Hot reload capabilities: {0}.", Emoji.HotReload, MessageSeverity.Verbose); - public static readonly MessageDescriptor HotReloadSuspended = Create("Hot reload suspended. To continue hot reload, press \"Ctrl + R\".", Emoji.HotReload, MessageSeverity.Output); - public static readonly MessageDescriptor UnableToApplyChanges = Create("Unable to apply changes due to compilation errors.", Emoji.HotReload, MessageSeverity.Output); - public static readonly MessageDescriptor RestartNeededToApplyChanges = Create("Restart is needed to apply the changes.", Emoji.HotReload, MessageSeverity.Output); - public static readonly MessageDescriptor HotReloadEnabled = Create("Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload.", Emoji.HotReload, MessageSeverity.Output); - public static readonly MessageDescriptor PressCtrlRToRestart = Create("Press Ctrl+R to restart.", Emoji.LightBulb, MessageSeverity.Output); - public static readonly MessageDescriptor HotReloadCanceledProcessExited = Create("Hot reload canceled because the process exited.", Emoji.HotReload, MessageSeverity.Verbose); - public static readonly MessageDescriptor ApplicationKind_BlazorHosted = Create("Application kind: BlazorHosted. '{0}' references BlazorWebAssembly project '{1}'.", Emoji.Default, MessageSeverity.Verbose); - public static readonly MessageDescriptor ApplicationKind_BlazorWebAssembly = Create("Application kind: BlazorWebAssembly.", Emoji.Default, MessageSeverity.Verbose); - public static readonly MessageDescriptor ApplicationKind_WebApplication = Create("Application kind: WebApplication.", Emoji.Default, MessageSeverity.Verbose); - public static readonly MessageDescriptor ApplicationKind_Default = Create("Application kind: Default.", Emoji.Default, MessageSeverity.Verbose); - public static readonly MessageDescriptor WatchingFilesForChanges = Create("Watching {0} file(s) for changes", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor WatchingFilesForChanges_FilePath = Create("> {0}", Emoji.Watch, MessageSeverity.Verbose); - public static readonly MessageDescriptor Building = Create("Building {0} ...", Emoji.Default, MessageSeverity.Output); - public static readonly MessageDescriptor BuildSucceeded = Create("Build succeeded: {0}", Emoji.Default, MessageSeverity.Output); - public static readonly MessageDescriptor BuildFailed = Create("Build failed: {0}", Emoji.Default, MessageSeverity.Output); - + public static readonly MessageDescriptor HotReloadCapabilities = Create("Hot reload capabilities: {0}.", Emoji.HotReload, LogLevel.Debug); + public static readonly MessageDescriptor HotReloadSuspended = Create("Hot reload suspended. To continue hot reload, press \"Ctrl + R\".", Emoji.HotReload, LogLevel.Information); + public static readonly MessageDescriptor UnableToApplyChanges = Create("Unable to apply changes due to compilation errors.", Emoji.HotReload, LogLevel.Information); + public static readonly MessageDescriptor RestartNeededToApplyChanges = Create("Restart is needed to apply the changes.", Emoji.HotReload, LogLevel.Information); + public static readonly MessageDescriptor HotReloadEnabled = Create("Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload.", Emoji.HotReload, LogLevel.Information); + public static readonly MessageDescriptor PressCtrlRToRestart = Create("Press Ctrl+R to restart.", Emoji.LightBulb, LogLevel.Information); + public static readonly MessageDescriptor HotReloadCanceledProcessExited = Create("Hot reload canceled because the process exited.", Emoji.HotReload, LogLevel.Debug); + public static readonly MessageDescriptor ApplicationKind_BlazorHosted = Create("Application kind: BlazorHosted. '{0}' references BlazorWebAssembly project '{1}'.", Emoji.Default, LogLevel.Debug); + public static readonly MessageDescriptor ApplicationKind_BlazorWebAssembly = Create("Application kind: BlazorWebAssembly.", Emoji.Default, LogLevel.Debug); + public static readonly MessageDescriptor ApplicationKind_WebApplication = Create("Application kind: WebApplication.", Emoji.Default, LogLevel.Debug); + public static readonly MessageDescriptor ApplicationKind_Default = Create("Application kind: Default.", Emoji.Default, LogLevel.Debug); + public static readonly MessageDescriptor WatchingFilesForChanges = Create("Watching {0} file(s) for changes", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor WatchingFilesForChanges_FilePath = Create("> {0}", Emoji.Watch, LogLevel.Debug); + public static readonly MessageDescriptor Building = Create("Building {0} ...", Emoji.Default, LogLevel.Information); + public static readonly MessageDescriptor BuildSucceeded = Create("Build succeeded: {0}", Emoji.Default, LogLevel.Information); + public static readonly MessageDescriptor BuildFailed = Create("Build failed: {0}", Emoji.Default, LogLevel.Information); } internal interface IProcessOutputReporter @@ -277,9 +257,6 @@ internal interface IProcessOutputReporter internal interface IReporter { - void Report(EventId id, Emoji emoji, MessageSeverity severity, string message); - - public bool IsVerbose - => false; + void Report(EventId id, Emoji emoji, LogLevel level, string message); } } diff --git a/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs b/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs index ba8e3317c62b..5a5306fe2ddf 100644 --- a/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs +++ b/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs @@ -5,12 +5,11 @@ using System.CommandLine; using System.CommandLine.Parsing; using System.Data; -using System.Diagnostics; -using Microsoft.Build.Logging; using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.DotNet.Cli.Commands.Build; using Microsoft.DotNet.Cli.Commands.Run; -using Microsoft.DotNet.Cli.Extensions; +using Microsoft.DotNet.Cli.Commands.Test; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch; @@ -43,65 +42,15 @@ internal sealed class CommandLineOptions public string Command => ExplicitCommand ?? DefaultCommand; - // this option is referenced from inner logic and so needs to be reference-able - public static Option NonInteractiveOption = new Option("--non-interactive") { Description = Resources.Help_NonInteractive, Arity = ArgumentArity.Zero }; - public static CommandLineOptions? Parse(IReadOnlyList args, ILogger logger, TextWriter output, out int errorCode) { - // dotnet watch specific options: - var quietOption = new Option("--quiet", "-q") { Description = Resources.Help_Quiet, Arity = ArgumentArity.Zero }; - var verboseOption = new Option("--verbose") { Description = Resources.Help_Verbose, Arity = ArgumentArity.Zero }; - var listOption = new Option("--list") { Description = Resources.Help_List, Arity = ArgumentArity.Zero }; - var noHotReloadOption = new Option("--no-hot-reload") { Description = Resources.Help_NoHotReload, Arity = ArgumentArity.Zero }; - - verboseOption.Validators.Add(v => - { - if (v.GetValue(quietOption) && v.GetValue(verboseOption)) - { - v.AddError(Resources.Error_QuietAndVerboseSpecified); - } - }); - - Option[] watchOptions = - [ - quietOption, - verboseOption, - listOption, - noHotReloadOption, - NonInteractiveOption - ]; - - // Options we need to know about that are passed through to the subcommand: - var shortProjectOption = new Option("-p") { Hidden = true, Arity = ArgumentArity.ZeroOrOne, AllowMultipleArgumentsPerToken = false }; - var longProjectOption = new Option("--project") { Hidden = true, Arity = ArgumentArity.ZeroOrOne, AllowMultipleArgumentsPerToken = false }; - var launchProfileOption = new Option("--launch-profile", "-lp") { Hidden = true, Arity = ArgumentArity.ZeroOrOne, AllowMultipleArgumentsPerToken = false }; - var noLaunchProfileOption = new Option("--no-launch-profile") { Hidden = true, Arity = ArgumentArity.Zero }; - - var rootCommand = new RootCommand(Resources.Help) - { - Directives = { new EnvironmentVariablesDirective() }, - }; - - foreach (var watchOption in watchOptions) - { - rootCommand.Options.Add(watchOption); - } - - rootCommand.Options.Add(longProjectOption); - rootCommand.Options.Add(shortProjectOption); - rootCommand.Options.Add(launchProfileOption); - rootCommand.Options.Add(noLaunchProfileOption); - - // We process all tokens that do not match any of the above options - // to find the subcommand (the first unmatched token preceding "--") - // and all its options and arguments. - rootCommand.TreatUnmatchedTokensAsErrors = false; + var definition = new DotnetWatchCommandDefinition(); // We parse the command line outside of the action since the action // might not be invoked in presence of unmatched tokens. // We just need to know if the root command was invoked to handle --help. var rootCommandInvoked = false; - rootCommand.SetAction(parseResult => rootCommandInvoked = true); + definition.SetAction(parseResult => rootCommandInvoked = true); ParserConfiguration parseConfig = new() { @@ -110,7 +59,7 @@ internal sealed class CommandLineOptions }; // parse without forwarded options first: - var parseResult = rootCommand.Parse(args, parseConfig); + var parseResult = definition.Parse(args, parseConfig); if (ReportErrors(parseResult, logger)) { errorCode = 1; @@ -124,11 +73,11 @@ internal sealed class CommandLineOptions foreach (var buildOption in buildOptions) { - rootCommand.Options.Add(buildOption); + definition.Options.Add(buildOption); } // reparse with forwarded options: - parseResult = rootCommand.Parse(args, parseConfig); + parseResult = definition.Parse(args, parseConfig); if (ReportErrors(parseResult, logger)) { errorCode = 1; @@ -148,10 +97,10 @@ internal sealed class CommandLineOptions return null; } - var projectValue = parseResult.GetValue(longProjectOption); + var projectValue = parseResult.GetValue(definition.LongProjectOption); if (string.IsNullOrEmpty(projectValue)) { - var projectShortValue = parseResult.GetValue(shortProjectOption); + var projectShortValue = parseResult.GetValue(definition.ShortProjectOption); if (!string.IsNullOrEmpty(projectShortValue)) { logger.LogWarning(Resources.Warning_ProjectAbbreviationDeprecated); @@ -159,7 +108,12 @@ internal sealed class CommandLineOptions } } - var commandArguments = GetCommandArguments(parseResult, watchOptions, explicitCommand, out var binLogToken, out var binLogPath); + var commandArguments = GetCommandArguments( + parseResult, + command, + explicitCommand, + out var binLogToken, + out var binLogPath); // We assume that forwarded options, if any, are intended for dotnet build. var buildArguments = buildOptions.Select(option => option.ForwardingFunction!(parseResult)).SelectMany(args => args).ToList(); @@ -171,15 +125,20 @@ internal sealed class CommandLineOptions var targetFrameworkOption = (Option?)buildOptions.SingleOrDefault(option => option.Name == "--framework"); + var logLevel = parseResult.GetValue(definition.VerboseOption) + ? LogLevel.Debug + : parseResult.GetValue(definition.QuietOption) + ? LogLevel.Warning + : LogLevel.Information; + return new() { - List = parseResult.GetValue(listOption), + List = parseResult.GetValue(definition.ListOption), GlobalOptions = new() { - Quiet = parseResult.GetValue(quietOption), - NoHotReload = parseResult.GetValue(noHotReloadOption), - NonInteractive = parseResult.GetValue(NonInteractiveOption), - Verbose = parseResult.GetValue(verboseOption), + LogLevel = logLevel, + NoHotReload = parseResult.GetValue(definition.NoHotReloadOption), + NonInteractive = parseResult.GetValue(definition.NonInteractiveOption), BinaryLogPath = ParseBinaryLogFilePath(binLogPath), }, @@ -187,8 +146,8 @@ internal sealed class CommandLineOptions ExplicitCommand = explicitCommand?.Name, ProjectPath = projectValue, - LaunchProfileName = parseResult.GetValue(launchProfileOption), - NoLaunchProfile = parseResult.GetValue(noLaunchProfileOption), + LaunchProfileName = parseResult.GetValue(definition.LaunchProfileOption), + NoLaunchProfile = parseResult.GetValue(definition.NoLaunchProfileOption), BuildArguments = buildArguments, TargetFramework = targetFrameworkOption != null ? parseResult.GetValue(targetFrameworkOption) : null, }; @@ -216,11 +175,13 @@ internal sealed class CommandLineOptions private static IReadOnlyList GetCommandArguments( ParseResult parseResult, - IReadOnlyList