diff --git a/Directory.Packages.props b/Directory.Packages.props index 788923bf48d6..55e9f6432cc2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,6 +36,7 @@ + diff --git a/sdk.slnx b/sdk.slnx index 0246a040f447..a919e135d791 100644 --- a/sdk.slnx +++ b/sdk.slnx @@ -56,6 +56,8 @@ + + diff --git a/src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj b/src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj index d5c6f3848416..b670cb74b33d 100644 --- a/src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj +++ b/src/BuiltInTools/DotNetDeltaApplier/Microsoft.Extensions.DotNetDeltaApplier.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs b/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs new file mode 100644 index 000000000000..b895b7f4c098 --- /dev/null +++ b/src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DotNet.Watch; + +internal sealed class DotNetWatchOptions +{ + public bool IsVerbose { get; init; } + public bool IsQuiet { get; init; } + public bool NoLaunchProfile { get; init; } + public ImmutableArray ApplicationArguments { get; init; } +} + +internal static class DotNetWatchLauncher +{ + public static async Task RunAsync( + string projectPath, + string workingDirectory, + DotNetWatchOptions options) + { + var globalOptions = new GlobalOptions() + { + Quiet = options.IsQuiet, + Verbose = options.IsVerbose, + NoHotReload = false, + NonInteractive = true, + }; + + var commandArguments = new List(); + if (options.NoLaunchProfile) + { + commandArguments.Add("--no-launch-profile"); + } + + commandArguments.AddRange(options.ApplicationArguments); + + var rootProjectOptions = new ProjectOptions() + { + IsRootProject = true, + ProjectPath = projectPath, + WorkingDirectory = workingDirectory, + TargetFramework = null, + BuildArguments = [], + NoLaunchProfile = options.NoLaunchProfile, + LaunchProfileName = null, + Command = "run", + CommandArguments = [.. commandArguments], + LaunchEnvironmentVariables = [], + }; + + var console = new PhysicalConsole(TestFlags.None); + var reporter = new ConsoleReporter(console, globalOptions.Verbose, globalOptions.Quiet, suppressEmojis: false); + var environmentOptions = EnvironmentOptions.FromEnvironment(); + var processRunner = new ProcessRunner(environmentOptions.GetProcessCleanupTimeout(isHotReloadEnabled: true)); + var loggerFactory = new LoggerFactory(reporter); + var logger = loggerFactory.CreateLogger(DotNetWatchContext.DefaultLogComponentName); + + using var context = new DotNetWatchContext() + { + ProcessOutputReporter = reporter, + LoggerFactory = loggerFactory, + Logger = logger, + BuildLogger = loggerFactory.CreateLogger(DotNetWatchContext.BuildLogComponentName), + ProcessRunner = processRunner, + Options = globalOptions, + EnvironmentOptions = environmentOptions, + RootProjectOptions = rootProjectOptions, + BrowserRefreshServerFactory = new BrowserRefreshServerFactory(), + BrowserLauncher = new BrowserLauncher(logger, environmentOptions), + }; + + using var shutdownHandler = new ShutdownHandler(console, logger); + + try + { + var watcher = new HotReloadDotNetWatcher(context, console, runtimeProcessLauncherFactory: null); + await watcher.WatchAsync(shutdownHandler.CancellationToken); + } + catch (OperationCanceledException) when (shutdownHandler.CancellationToken.IsCancellationRequested) + { + // Ctrl+C forced an exit + } + catch (Exception e) + { + logger.LogError("An unexpected error occurred: {Exception}", e.ToString()); + return false; + } + + return true; + } +} diff --git a/src/BuiltInTools/Watch.Aspire/Microsoft.DotNet.HotReload.Watch.Aspire.csproj b/src/BuiltInTools/Watch.Aspire/Microsoft.DotNet.HotReload.Watch.Aspire.csproj new file mode 100644 index 000000000000..727c6a6058a9 --- /dev/null +++ b/src/BuiltInTools/Watch.Aspire/Microsoft.DotNet.HotReload.Watch.Aspire.csproj @@ -0,0 +1,30 @@ + + + + + $(SdkTargetFramework) + MicrosoftAspNetCore + Microsoft.DotNet.Watch + + + true + Microsoft.DotNet.HotReload.Watch.Aspire + + A supporting package for Aspire CLI: + https://github.com/dotnet/aspire + + + + + + + + + + + + + + + + diff --git a/src/BuiltInTools/Watch.Aspire/Properties/AssemblyInfo.cs b/src/BuiltInTools/Watch.Aspire/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..c71f5836e8e5 --- /dev/null +++ b/src/BuiltInTools/Watch.Aspire/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +// ⚠ ONLY ASSEMBLIES BUILT FROM dotnet/aspire MAY BE ADDED HERE ⚠ +[assembly: InternalsVisibleTo("Aspire.Cli, PublicKey = 00240000048000009400000006020000002400005253413100040000010001004b86c4cb78549b34bab61a3b1800e23bfeb5b3ec390074041536a7e3cbd97f5f04cf0f857155a8928eaa29ebfd11cfbbad3ba70efea7bda3226c6a8d370a4cd303f714486b6ebc225985a638471e6ef571cc92a4613c00b8fa65d61ccee0cbe5f36330c9a01f4183559f1bef24cc2917c6d913e3a541333a1d05d9bed22b38cb")] diff --git a/src/BuiltInTools/dotnet-watch/HotReload/AppModels/BlazorWebAssemblyAppModel.cs b/src/BuiltInTools/Watch/AppModels/BlazorWebAssemblyAppModel.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/HotReload/AppModels/BlazorWebAssemblyAppModel.cs rename to src/BuiltInTools/Watch/AppModels/BlazorWebAssemblyAppModel.cs diff --git a/src/BuiltInTools/dotnet-watch/HotReload/AppModels/BlazorWebAssemblyHostedAppModel.cs b/src/BuiltInTools/Watch/AppModels/BlazorWebAssemblyHostedAppModel.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/HotReload/AppModels/BlazorWebAssemblyHostedAppModel.cs rename to src/BuiltInTools/Watch/AppModels/BlazorWebAssemblyHostedAppModel.cs diff --git a/src/BuiltInTools/dotnet-watch/HotReload/AppModels/DefaultAppModel.cs b/src/BuiltInTools/Watch/AppModels/DefaultAppModel.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/HotReload/AppModels/DefaultAppModel.cs rename to src/BuiltInTools/Watch/AppModels/DefaultAppModel.cs diff --git a/src/BuiltInTools/dotnet-watch/HotReload/AppModels/HotReloadAppModel.cs b/src/BuiltInTools/Watch/AppModels/HotReloadAppModel.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/HotReload/AppModels/HotReloadAppModel.cs rename to src/BuiltInTools/Watch/AppModels/HotReloadAppModel.cs diff --git a/src/BuiltInTools/dotnet-watch/HotReload/AppModels/WebApplicationAppModel.cs b/src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs similarity index 97% rename from src/BuiltInTools/dotnet-watch/HotReload/AppModels/WebApplicationAppModel.cs rename to src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs index 4ca515a862a6..cefcd43cf320 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/AppModels/WebApplicationAppModel.cs +++ b/src/BuiltInTools/Watch/AppModels/WebApplicationAppModel.cs @@ -40,7 +40,7 @@ internal abstract class WebApplicationAppModel(DotNetWatchContext context) : Hot protected WebAssemblyHotReloadClient CreateWebAssemblyClient(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer browserRefreshServer, ProjectGraphNode clientProject) { var capabilities = clientProject.GetWebAssemblyCapabilities().ToImmutableArray(); - var targetFramework = clientProject.GetTargetFrameworkVersion() ?? throw new InvalidOperationException("Project doesn't define TargetFrameworkVersion"); + var targetFramework = clientProject.GetTargetFrameworkVersion() ?? throw new InvalidOperationException($"Project doesn't define {PropertyNames.TargetFrameworkMoniker}"); return new WebAssemblyHotReloadClient(clientLogger, agentLogger, browserRefreshServer, capabilities, targetFramework, context.EnvironmentOptions.TestFlags.HasFlag(TestFlags.MockBrowser)); } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/AppModels/WebServerAppModel.cs b/src/BuiltInTools/Watch/AppModels/WebServerAppModel.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/HotReload/AppModels/WebServerAppModel.cs rename to src/BuiltInTools/Watch/AppModels/WebServerAppModel.cs diff --git a/src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs b/src/BuiltInTools/Watch/Aspire/AspireServiceFactory.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Aspire/AspireServiceFactory.cs rename to src/BuiltInTools/Watch/Aspire/AspireServiceFactory.cs diff --git a/src/BuiltInTools/dotnet-watch/Browser/BrowserLauncher.cs b/src/BuiltInTools/Watch/Browser/BrowserLauncher.cs similarity index 98% rename from src/BuiltInTools/dotnet-watch/Browser/BrowserLauncher.cs rename to src/BuiltInTools/Watch/Browser/BrowserLauncher.cs index 422cb1b8a774..c7a9f1789dcc 100644 --- a/src/BuiltInTools/dotnet-watch/Browser/BrowserLauncher.cs +++ b/src/BuiltInTools/Watch/Browser/BrowserLauncher.cs @@ -118,7 +118,7 @@ private bool CanLaunchBrowser(ProjectOptions projectOptions, [NotNullWhen(true)] return false; } - if (!CommandLineOptions.IsCodeExecutionCommand(projectOptions.Command)) + if (!projectOptions.IsCodeExecutionCommand) { logger.LogDebug("Command '{Command}' does not support launching browsers.", projectOptions.Command); return false; diff --git a/src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServerFactory.cs b/src/BuiltInTools/Watch/Browser/BrowserRefreshServerFactory.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Browser/BrowserRefreshServerFactory.cs rename to src/BuiltInTools/Watch/Browser/BrowserRefreshServerFactory.cs diff --git a/src/BuiltInTools/dotnet-watch/Build/BuildNames.cs b/src/BuiltInTools/Watch/Build/BuildNames.cs similarity index 97% rename from src/BuiltInTools/dotnet-watch/Build/BuildNames.cs rename to src/BuiltInTools/Watch/Build/BuildNames.cs index d92d5af86c14..f5f8c3463a3f 100644 --- a/src/BuiltInTools/dotnet-watch/Build/BuildNames.cs +++ b/src/BuiltInTools/Watch/Build/BuildNames.cs @@ -7,6 +7,7 @@ internal static class PropertyNames { public const string TargetFramework = nameof(TargetFramework); public const string TargetFrameworkIdentifier = nameof(TargetFrameworkIdentifier); + public const string TargetFrameworkMoniker = nameof(TargetFrameworkMoniker); public const string TargetPath = nameof(TargetPath); public const string EnableDefaultItems = nameof(EnableDefaultItems); public const string TargetFrameworks = nameof(TargetFrameworks); diff --git a/src/BuiltInTools/dotnet-watch/Build/BuildReporter.cs b/src/BuiltInTools/Watch/Build/BuildReporter.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Build/BuildReporter.cs rename to src/BuiltInTools/Watch/Build/BuildReporter.cs diff --git a/src/BuiltInTools/Watch/Build/BuildUtilities.cs b/src/BuiltInTools/Watch/Build/BuildUtilities.cs new file mode 100644 index 000000000000..1a336530697e --- /dev/null +++ b/src/BuiltInTools/Watch/Build/BuildUtilities.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Watch; + +internal static class BuildUtilities +{ + // Parses name=value pairs passed to --property. Skips invalid input. + public static IEnumerable<(string key, string value)> ParseBuildProperties(IEnumerable arguments) + => from argument in arguments + let colon = argument.IndexOf(':') + where colon >= 0 && argument[0..colon] is "--property" or "-property" or "/property" or "/p" or "-p" or "--p" + let eq = argument.IndexOf('=', colon) + where eq >= 0 + let name = argument[(colon + 1)..eq].Trim() + let value = argument[(eq + 1)..] + where name is not [] + select (name, value); +} diff --git a/src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs b/src/BuiltInTools/Watch/Build/EvaluationResult.cs similarity index 98% rename from src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs rename to src/BuiltInTools/Watch/Build/EvaluationResult.cs index 966ea12c87c4..e0c2bb6ffa0e 100644 --- a/src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs +++ b/src/BuiltInTools/Watch/Build/EvaluationResult.cs @@ -48,7 +48,7 @@ public void WatchFiles(FileWatcher fileWatcher) // See https://github.com/dotnet/project-system/blob/main/docs/well-known-project-properties.md - var globalOptions = CommandLineOptions.ParseBuildProperties(buildArguments) + var globalOptions = BuildUtilities.ParseBuildProperties(buildArguments) .ToImmutableDictionary(keySelector: arg => arg.key, elementSelector: arg => arg.value) .SetItem(PropertyNames.DotNetWatchBuild, "true") .SetItem(PropertyNames.DesignTimeBuild, "true") diff --git a/src/BuiltInTools/dotnet-watch/Build/FileItem.cs b/src/BuiltInTools/Watch/Build/FileItem.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Build/FileItem.cs rename to src/BuiltInTools/Watch/Build/FileItem.cs diff --git a/src/BuiltInTools/dotnet-watch/Build/FilePathExclusions.cs b/src/BuiltInTools/Watch/Build/FilePathExclusions.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Build/FilePathExclusions.cs rename to src/BuiltInTools/Watch/Build/FilePathExclusions.cs diff --git a/src/BuiltInTools/dotnet-watch/Build/ProjectGraphUtilities.cs b/src/BuiltInTools/Watch/Build/ProjectGraphUtilities.cs similarity index 96% rename from src/BuiltInTools/dotnet-watch/Build/ProjectGraphUtilities.cs rename to src/BuiltInTools/Watch/Build/ProjectGraphUtilities.cs index ca8f616ebcce..6ce2ff9f099b 100644 --- a/src/BuiltInTools/dotnet-watch/Build/ProjectGraphUtilities.cs +++ b/src/BuiltInTools/Watch/Build/ProjectGraphUtilities.cs @@ -2,10 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; +using System.Runtime.Versioning; using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; using Microsoft.Build.Graph; -using Microsoft.DotNet.Cli; using Microsoft.Extensions.Logging; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -83,7 +83,16 @@ public static IEnumerable GetTargetFrameworks(this ProjectGraphNode proj => projectNode.GetStringListPropertyValue(PropertyNames.TargetFrameworks); public static Version? GetTargetFrameworkVersion(this ProjectGraphNode projectNode) - => EnvironmentVariableNames.TryParseTargetFrameworkVersion(projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetFrameworkVersion)); + { + try + { + return new FrameworkName(projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetFrameworkMoniker)).Version; + } + catch + { + return null; + } + } public static IEnumerable GetWebAssemblyCapabilities(this ProjectGraphNode projectNode) => projectNode.GetStringListPropertyValue(PropertyNames.WebAssemblyHotReloadCapabilities); diff --git a/src/BuiltInTools/dotnet-watch/Build/ProjectInstanceId.cs b/src/BuiltInTools/Watch/Build/ProjectInstanceId.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Build/ProjectInstanceId.cs rename to src/BuiltInTools/Watch/Build/ProjectInstanceId.cs diff --git a/src/BuiltInTools/dotnet-watch/Build/ProjectNodeMap.cs b/src/BuiltInTools/Watch/Build/ProjectNodeMap.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Build/ProjectNodeMap.cs rename to src/BuiltInTools/Watch/Build/ProjectNodeMap.cs diff --git a/src/BuiltInTools/dotnet-watch/CommandLine/DotNetWatchContext.cs b/src/BuiltInTools/Watch/Context/DotNetWatchContext.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/CommandLine/DotNetWatchContext.cs rename to src/BuiltInTools/Watch/Context/DotNetWatchContext.cs diff --git a/src/BuiltInTools/dotnet-watch/CommandLine/EnvironmentOptions.cs b/src/BuiltInTools/Watch/Context/EnvironmentOptions.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/CommandLine/EnvironmentOptions.cs rename to src/BuiltInTools/Watch/Context/EnvironmentOptions.cs diff --git a/src/BuiltInTools/dotnet-watch/CommandLine/EnvironmentVariables.cs b/src/BuiltInTools/Watch/Context/EnvironmentVariables.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/CommandLine/EnvironmentVariables.cs rename to src/BuiltInTools/Watch/Context/EnvironmentVariables.cs diff --git a/src/BuiltInTools/dotnet-watch/CommandLine/GlobalOptions.cs b/src/BuiltInTools/Watch/Context/GlobalOptions.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/CommandLine/GlobalOptions.cs rename to src/BuiltInTools/Watch/Context/GlobalOptions.cs diff --git a/src/BuiltInTools/dotnet-watch/CommandLine/ProjectOptions.cs b/src/BuiltInTools/Watch/Context/ProjectOptions.cs similarity index 85% rename from src/BuiltInTools/dotnet-watch/CommandLine/ProjectOptions.cs rename to src/BuiltInTools/Watch/Context/ProjectOptions.cs index 12b8b889f1f7..20eb2eed69eb 100644 --- a/src/BuiltInTools/dotnet-watch/CommandLine/ProjectOptions.cs +++ b/src/BuiltInTools/Watch/Context/ProjectOptions.cs @@ -27,4 +27,10 @@ internal sealed record ProjectOptions /// Additional environment variables to set to the running process. /// public required IReadOnlyList<(string name, string value)> LaunchEnvironmentVariables { get; init; } + + /// + /// Returns true if the command executes the code of the target project. + /// + public bool IsCodeExecutionCommand + => Command is "run" or "test"; } diff --git a/src/BuiltInTools/dotnet-watch/FileWatcher/ChangeKind.cs b/src/BuiltInTools/Watch/FileWatcher/ChangeKind.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/FileWatcher/ChangeKind.cs rename to src/BuiltInTools/Watch/FileWatcher/ChangeKind.cs diff --git a/src/BuiltInTools/dotnet-watch/FileWatcher/DirectoryWatcher.cs b/src/BuiltInTools/Watch/FileWatcher/DirectoryWatcher.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/FileWatcher/DirectoryWatcher.cs rename to src/BuiltInTools/Watch/FileWatcher/DirectoryWatcher.cs diff --git a/src/BuiltInTools/dotnet-watch/FileWatcher/EventBasedDirectoryWatcher.cs b/src/BuiltInTools/Watch/FileWatcher/EventBasedDirectoryWatcher.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/FileWatcher/EventBasedDirectoryWatcher.cs rename to src/BuiltInTools/Watch/FileWatcher/EventBasedDirectoryWatcher.cs diff --git a/src/BuiltInTools/dotnet-watch/FileWatcher/FileWatcher.cs b/src/BuiltInTools/Watch/FileWatcher/FileWatcher.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/FileWatcher/FileWatcher.cs rename to src/BuiltInTools/Watch/FileWatcher/FileWatcher.cs diff --git a/src/BuiltInTools/dotnet-watch/FileWatcher/PollingDirectoryWatcher.cs b/src/BuiltInTools/Watch/FileWatcher/PollingDirectoryWatcher.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/FileWatcher/PollingDirectoryWatcher.cs rename to src/BuiltInTools/Watch/FileWatcher/PollingDirectoryWatcher.cs diff --git a/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs b/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs similarity index 95% rename from src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs rename to src/BuiltInTools/Watch/HotReload/CompilationHandler.cs index cedccb2351f1..f525ea87eee7 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs +++ b/src/BuiltInTools/Watch/HotReload/CompilationHandler.cs @@ -6,7 +6,7 @@ using Microsoft.Build.Graph; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api; +using Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api; using Microsoft.DotNet.HotReload; using Microsoft.Extensions.Logging; @@ -17,7 +17,7 @@ internal sealed class CompilationHandler : IDisposable public readonly IncrementalMSBuildWorkspace Workspace; private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; - private readonly WatchHotReloadService _hotReloadService; + private readonly HotReloadService _hotReloadService; private readonly ProcessRunner _processRunner; /// @@ -36,7 +36,7 @@ internal sealed class CompilationHandler : IDisposable /// /// All updates that were attempted. Includes updates whose application failed. /// - private ImmutableList _previousUpdates = []; + private ImmutableList _previousUpdates = []; private bool _isDisposed; @@ -46,7 +46,7 @@ public CompilationHandler(ILoggerFactory loggerFactory, ILogger logger, ProcessR _logger = logger; _processRunner = processRunner; Workspace = new IncrementalMSBuildWorkspace(logger); - _hotReloadService = new WatchHotReloadService(Workspace.CurrentSolution.Services, () => ValueTask.FromResult(GetAggregateCapabilities())); + _hotReloadService = new HotReloadService(Workspace.CurrentSolution.Services, () => ValueTask.FromResult(GetAggregateCapabilities())); } public void Dispose() @@ -226,7 +226,7 @@ private static void PrepareCompilations(Solution solution, string projectPath, C } public async ValueTask<( - ImmutableArray projectUpdates, + ImmutableArray projectUpdates, ImmutableArray projectsToRebuild, ImmutableArray projectsToRedeploy, ImmutableArray terminatedProjects)> HandleManagedCodeChangesAsync( @@ -242,14 +242,14 @@ private static void PrepareCompilations(Solution solution, string projectPath, C let runningProject = GetCorrespondingRunningProject(project, runningProjects) where runningProject != null let autoRestartProject = autoRestart || runningProject.ProjectNode.IsAutoRestartEnabled() - select (project.Id, info: new WatchHotReloadService.RunningProjectInfo() { RestartWhenChangesHaveNoEffect = autoRestartProject })) + select (project.Id, info: new HotReloadService.RunningProjectInfo() { RestartWhenChangesHaveNoEffect = autoRestartProject })) .ToImmutableDictionary(e => e.Id, e => e.info); var updates = await _hotReloadService.GetUpdatesAsync(currentSolution, runningProjectInfos, cancellationToken); await DisplayResultsAsync(updates, runningProjectInfos, cancellationToken); - if (updates.Status is WatchHotReloadService.Status.NoChangesToApply or WatchHotReloadService.Status.Blocked) + if (updates.Status is HotReloadService.Status.NoChangesToApply or HotReloadService.Status.Blocked) { // If Hot Reload is blocked (due to compilation error) we ignore the current // changes and await the next file change. @@ -291,7 +291,7 @@ private static void PrepareCompilations(Solution solution, string projectPath, C return (updates.ProjectUpdates, projectsToRebuild, projectsToRedeploy, terminatedProjects); } - public async ValueTask ApplyUpdatesAsync(ImmutableArray updates, CancellationToken cancellationToken) + public async ValueTask ApplyUpdatesAsync(ImmutableArray updates, CancellationToken cancellationToken) { Debug.Assert(!updates.IsEmpty); @@ -330,7 +330,7 @@ await ForEachProjectAsync(projectsToUpdate, async (runningProject, cancellationT } // msbuild workspace doesn't set TFM if the project is not multi-targeted - var tfm = WatchHotReloadService.GetTargetFramework(project); + var tfm = HotReloadService.GetTargetFramework(project); if (tfm == null) { return projectsWithPath[0]; @@ -339,18 +339,18 @@ await ForEachProjectAsync(projectsToUpdate, async (runningProject, cancellationT return projectsWithPath.SingleOrDefault(p => string.Equals(p.ProjectNode.GetTargetFramework(), tfm, StringComparison.OrdinalIgnoreCase)); } - private async ValueTask DisplayResultsAsync(WatchHotReloadService.Updates2 updates, ImmutableDictionary runningProjectInfos, CancellationToken cancellationToken) + private async ValueTask DisplayResultsAsync(HotReloadService.Updates updates, ImmutableDictionary runningProjectInfos, CancellationToken cancellationToken) { switch (updates.Status) { - case WatchHotReloadService.Status.ReadyToApply: + case HotReloadService.Status.ReadyToApply: break; - case WatchHotReloadService.Status.NoChangesToApply: + case HotReloadService.Status.NoChangesToApply: _logger.Log(MessageDescriptor.NoCSharpChangesToApply); break; - case WatchHotReloadService.Status.Blocked: + case HotReloadService.Status.Blocked: _logger.Log(MessageDescriptor.UnableToApplyChanges); break; @@ -378,7 +378,7 @@ await ForEachProjectAsync( void ReportCompilationDiagnostics(DiagnosticSeverity severity) { - foreach (var diagnostic in updates.CompilationDiagnostics) + foreach (var diagnostic in updates.PersistentDiagnostics) { if (diagnostic.Id == "CS8002") { @@ -419,7 +419,7 @@ void ReportRudeEdits() .Where(p => !updates.ProjectsToRestart.ContainsKey(p)) .ToHashSet(); - foreach (var (projectId, diagnostics) in updates.RudeEdits) + foreach (var (projectId, diagnostics) in updates.TransientDiagnostics) { foreach (var diagnostic in diagnostics) { @@ -629,7 +629,7 @@ private async ValueTask> TerminateRunningProjects(IEnumerable private static Task ForEachProjectAsync(ImmutableDictionary> projects, Func action, CancellationToken cancellationToken) => Task.WhenAll(projects.SelectMany(entry => entry.Value).Select(project => action(project, cancellationToken))).WaitAsync(cancellationToken); - private static ImmutableArray ToManagedCodeUpdates(ImmutableArray updates) + private static ImmutableArray ToManagedCodeUpdates(ImmutableArray updates) => [.. updates.Select(update => new HotReloadManagedCodeUpdate(update.ModuleId, update.MetadataDelta, update.ILDelta, update.PdbDelta, update.UpdatedTypes, update.RequiredCapabilities))]; } } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadClients.cs b/src/BuiltInTools/Watch/HotReload/HotReloadClients.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/HotReload/HotReloadClients.cs rename to src/BuiltInTools/Watch/HotReload/HotReloadClients.cs diff --git a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs b/src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs rename to src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs diff --git a/src/BuiltInTools/dotnet-watch/HotReload/HotReloadEventSource.cs b/src/BuiltInTools/Watch/HotReload/HotReloadEventSource.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/HotReload/HotReloadEventSource.cs rename to src/BuiltInTools/Watch/HotReload/HotReloadEventSource.cs diff --git a/src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs b/src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs rename to src/BuiltInTools/Watch/HotReload/IncrementalMSBuildWorkspace.cs diff --git a/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs b/src/BuiltInTools/Watch/HotReload/ScopedCssFileHandler.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs rename to src/BuiltInTools/Watch/HotReload/ScopedCssFileHandler.cs diff --git a/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs b/src/BuiltInTools/Watch/HotReload/StaticFileHandler.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs rename to src/BuiltInTools/Watch/HotReload/StaticFileHandler.cs diff --git a/src/BuiltInTools/Watch/Microsoft.DotNet.HotReload.Watch.csproj b/src/BuiltInTools/Watch/Microsoft.DotNet.HotReload.Watch.csproj new file mode 100644 index 000000000000..f4d7130dd6be --- /dev/null +++ b/src/BuiltInTools/Watch/Microsoft.DotNet.HotReload.Watch.csproj @@ -0,0 +1,78 @@ + + + + + + + + + $(SdkTargetFramework) + Library + Hot Reload watch implementation + Microsoft.DotNet.Watch + MicrosoftAspNetCore + + + $(NoWarn);CS9057 + + + true + Microsoft.DotNet.HotReload.Watch + Package containing implementation of Hot Reload watch tool. + + $(NoWarn);NU5128 + + + + + + + + + + + + + + + + + + all + Content + true + false + TargetFramework;TargetFrameworks + hotreload\net6.0\Microsoft.AspNetCore.Watch.BrowserRefresh.dll + PreserveNewest + + + + all + Content + true + false + TargetFramework=net10.0 + hotreload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll + PreserveNewest + + + + all + Content + true + false + TargetFramework=net6.0 + hotreload\net6.0\Microsoft.Extensions.DotNetDeltaApplier.dll + PreserveNewest + + + + diff --git a/src/BuiltInTools/dotnet-watch/Process/IRuntimeProcessLauncher.cs b/src/BuiltInTools/Watch/Process/IRuntimeProcessLauncher.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Process/IRuntimeProcessLauncher.cs rename to src/BuiltInTools/Watch/Process/IRuntimeProcessLauncher.cs diff --git a/src/BuiltInTools/dotnet-watch/Process/IRuntimeProcessLauncherFactory.cs b/src/BuiltInTools/Watch/Process/IRuntimeProcessLauncherFactory.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Process/IRuntimeProcessLauncherFactory.cs rename to src/BuiltInTools/Watch/Process/IRuntimeProcessLauncherFactory.cs diff --git a/src/BuiltInTools/dotnet-watch/Process/LaunchSettingsProfile.cs b/src/BuiltInTools/Watch/Process/LaunchSettingsProfile.cs similarity index 85% rename from src/BuiltInTools/dotnet-watch/Process/LaunchSettingsProfile.cs rename to src/BuiltInTools/Watch/Process/LaunchSettingsProfile.cs index a1c6397bbf6f..1ec08ab04ea2 100644 --- a/src/BuiltInTools/dotnet-watch/Process/LaunchSettingsProfile.cs +++ b/src/BuiltInTools/Watch/Process/LaunchSettingsProfile.cs @@ -5,8 +5,6 @@ using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; -using Microsoft.DotNet.Cli.Commands; -using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch @@ -31,18 +29,18 @@ internal sealed class LaunchSettingsProfile var projectDirectory = Path.GetDirectoryName(projectPath); Debug.Assert(projectDirectory != null); - var launchSettingsPath = CommonRunHelpers.GetPropertiesLaunchSettingsPath(projectDirectory, "Properties"); + var launchSettingsPath = GetPropertiesLaunchSettingsPath(projectDirectory, "Properties"); bool hasLaunchSettings = File.Exists(launchSettingsPath); var projectNameWithoutExtension = Path.GetFileNameWithoutExtension(projectPath); - var runJsonPath = CommonRunHelpers.GetFlatLaunchSettingsPath(projectDirectory, projectNameWithoutExtension); + var runJsonPath = GetFlatLaunchSettingsPath(projectDirectory, projectNameWithoutExtension); bool hasRunJson = File.Exists(runJsonPath); if (hasLaunchSettings) { if (hasRunJson) { - logger.LogWarning(CliCommandStrings.RunCommandWarningRunJsonNotUsed, runJsonPath, launchSettingsPath); + logger.LogWarning("Warning: Settings from '{JsonPath}' are not used because '{LaunchSettingsPath}' has precedence.", runJsonPath, launchSettingsPath); } } else if (hasRunJson) @@ -98,6 +96,12 @@ internal sealed class LaunchSettingsProfile return namedProfile; } + private static string GetPropertiesLaunchSettingsPath(string directoryPath, string propertiesDirectoryName) + => Path.Combine(directoryPath, propertiesDirectoryName, "launchSettings.json"); + + private static string GetFlatLaunchSettingsPath(string directoryPath, string projectNameWithoutExtension) + => Path.Join(directoryPath, $"{projectNameWithoutExtension}.run.json"); + private static LaunchSettingsProfile? ReadDefaultLaunchProfile(LaunchSettingsJson? launchSettings, ILogger logger) { if (launchSettings is null || launchSettings.Profiles is null) diff --git a/src/BuiltInTools/dotnet-watch/Process/ProcessLaunchResult.cs b/src/BuiltInTools/Watch/Process/ProcessLaunchResult.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Process/ProcessLaunchResult.cs rename to src/BuiltInTools/Watch/Process/ProcessLaunchResult.cs diff --git a/src/BuiltInTools/dotnet-watch/Process/ProcessRunner.cs b/src/BuiltInTools/Watch/Process/ProcessRunner.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Process/ProcessRunner.cs rename to src/BuiltInTools/Watch/Process/ProcessRunner.cs diff --git a/src/BuiltInTools/dotnet-watch/Process/ProcessSpec.cs b/src/BuiltInTools/Watch/Process/ProcessSpec.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Process/ProcessSpec.cs rename to src/BuiltInTools/Watch/Process/ProcessSpec.cs diff --git a/src/BuiltInTools/dotnet-watch/Process/ProjectLauncher.cs b/src/BuiltInTools/Watch/Process/ProjectLauncher.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Process/ProjectLauncher.cs rename to src/BuiltInTools/Watch/Process/ProjectLauncher.cs diff --git a/src/BuiltInTools/dotnet-watch/Process/RunningProject.cs b/src/BuiltInTools/Watch/Process/RunningProject.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Process/RunningProject.cs rename to src/BuiltInTools/Watch/Process/RunningProject.cs diff --git a/src/BuiltInTools/dotnet-watch/Process/WebServerProcessStateObserver.cs b/src/BuiltInTools/Watch/Process/WebServerProcessStateObserver.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Process/WebServerProcessStateObserver.cs rename to src/BuiltInTools/Watch/Process/WebServerProcessStateObserver.cs diff --git a/src/BuiltInTools/Watch/Properties/AssemblyInfo.cs b/src/BuiltInTools/Watch/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..a4346ad81507 --- /dev/null +++ b/src/BuiltInTools/Watch/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("dotnet-watch, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("dotnet-watch.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.DotNet.HotReload.Watch.Aspire, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/BuiltInTools/dotnet-watch/UI/BuildOutput.cs b/src/BuiltInTools/Watch/UI/BuildOutput.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/UI/BuildOutput.cs rename to src/BuiltInTools/Watch/UI/BuildOutput.cs diff --git a/src/BuiltInTools/dotnet-watch/UI/ConsoleInputReader.cs b/src/BuiltInTools/Watch/UI/ConsoleInputReader.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/UI/ConsoleInputReader.cs rename to src/BuiltInTools/Watch/UI/ConsoleInputReader.cs diff --git a/src/BuiltInTools/dotnet-watch/UI/ConsoleReporter.cs b/src/BuiltInTools/Watch/UI/ConsoleReporter.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/UI/ConsoleReporter.cs rename to src/BuiltInTools/Watch/UI/ConsoleReporter.cs diff --git a/src/BuiltInTools/Watch/UI/IConsole.cs b/src/BuiltInTools/Watch/UI/IConsole.cs new file mode 100644 index 000000000000..54b1d8cfec76 --- /dev/null +++ b/src/BuiltInTools/Watch/UI/IConsole.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +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 interface IConsole +{ + event Action KeyPressed; + TextWriter Out { get; } + TextWriter Error { get; } + ConsoleColor ForegroundColor { get; set; } + void ResetColor(); + void Clear(); +} diff --git a/src/BuiltInTools/dotnet-watch/UI/IReporter.cs b/src/BuiltInTools/Watch/UI/IReporter.cs similarity index 99% rename from src/BuiltInTools/dotnet-watch/UI/IReporter.cs rename to src/BuiltInTools/Watch/UI/IReporter.cs index c6f64394da88..c09f6403be33 100644 --- a/src/BuiltInTools/dotnet-watch/UI/IReporter.cs +++ b/src/BuiltInTools/Watch/UI/IReporter.cs @@ -169,7 +169,6 @@ public MessageDescriptor WithSeverityWhen(MessageSeverity severity, bool conditi : this; public static readonly ImmutableDictionary ComponentEmojis = ImmutableDictionary.Empty - .Add(Program.LogComponentName, Emoji.Watch) .Add(DotNetWatchContext.DefaultLogComponentName, Emoji.Watch) .Add(DotNetWatchContext.BuildLogComponentName, Emoji.Build) .Add(HotReloadDotNetWatcher.ClientLogComponentName, Emoji.HotReload) diff --git a/src/BuiltInTools/dotnet-watch/UI/OutputLine.cs b/src/BuiltInTools/Watch/UI/OutputLine.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/UI/OutputLine.cs rename to src/BuiltInTools/Watch/UI/OutputLine.cs diff --git a/src/BuiltInTools/dotnet-watch/UI/PhysicalConsole.cs b/src/BuiltInTools/Watch/UI/PhysicalConsole.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/UI/PhysicalConsole.cs rename to src/BuiltInTools/Watch/UI/PhysicalConsole.cs diff --git a/src/BuiltInTools/dotnet-watch/UI/RestartPrompt.cs b/src/BuiltInTools/Watch/UI/RestartPrompt.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/UI/RestartPrompt.cs rename to src/BuiltInTools/Watch/UI/RestartPrompt.cs diff --git a/src/BuiltInTools/Watch/UI/ShutdownHandler.cs b/src/BuiltInTools/Watch/UI/ShutdownHandler.cs new file mode 100644 index 000000000000..70cc2355fe43 --- /dev/null +++ b/src/BuiltInTools/Watch/UI/ShutdownHandler.cs @@ -0,0 +1,44 @@ +// 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 ShutdownHandler : IDisposable +{ + private readonly CancellationTokenSource _cancellationSource = new(); + public CancellationToken CancellationToken { get; } + + private volatile bool _disposed; + + public ShutdownHandler(IConsole console, ILogger logger) + { + CancellationToken = _cancellationSource.Token; + + console.KeyPressed += key => + { + if (!_disposed && key.Modifiers.HasFlag(ConsoleModifiers.Control) && key.Key == ConsoleKey.C) + { + // if we already canceled, we force immediate shutdown: + var forceShutdown = _cancellationSource.IsCancellationRequested; + + if (!forceShutdown) + { + logger.Log(MessageDescriptor.ShutdownRequested); + _cancellationSource.Cancel(); + } + else + { + Environment.Exit(0); + } + } + }; + } + + public void Dispose() + { + _disposed = true; + _cancellationSource.Dispose(); + } +} diff --git a/src/BuiltInTools/dotnet-watch/Utilities/CommandLineUtilities.cs b/src/BuiltInTools/Watch/Utilities/CommandLineUtilities.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Utilities/CommandLineUtilities.cs rename to src/BuiltInTools/Watch/Utilities/CommandLineUtilities.cs diff --git a/src/BuiltInTools/dotnet-watch/Utilities/Disposables.cs b/src/BuiltInTools/Watch/Utilities/Disposables.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Utilities/Disposables.cs rename to src/BuiltInTools/Watch/Utilities/Disposables.cs diff --git a/src/BuiltInTools/dotnet-watch/Utilities/PathUtilities.cs b/src/BuiltInTools/Watch/Utilities/PathUtilities.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Utilities/PathUtilities.cs rename to src/BuiltInTools/Watch/Utilities/PathUtilities.cs diff --git a/src/BuiltInTools/dotnet-watch/Utilities/ProcessUtilities.cs b/src/BuiltInTools/Watch/Utilities/ProcessUtilities.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Utilities/ProcessUtilities.cs rename to src/BuiltInTools/Watch/Utilities/ProcessUtilities.cs diff --git a/src/BuiltInTools/dotnet-watch/Utilities/Versions.cs b/src/BuiltInTools/Watch/Utilities/Versions.cs similarity index 100% rename from src/BuiltInTools/dotnet-watch/Utilities/Versions.cs rename to src/BuiltInTools/Watch/Utilities/Versions.cs diff --git a/src/BuiltInTools/dotnet-watch.slnf b/src/BuiltInTools/dotnet-watch.slnf index 09d529c61e9c..03d5853c5e55 100644 --- a/src/BuiltInTools/dotnet-watch.slnf +++ b/src/BuiltInTools/dotnet-watch.slnf @@ -16,12 +16,14 @@ "src\\BuiltInTools\\HotReloadAgent\\Microsoft.DotNet.HotReload.Agent.shproj", "src\\BuiltInTools\\HotReloadClient\\Microsoft.DotNet.HotReload.Client.Package.csproj", "src\\BuiltInTools\\HotReloadClient\\Microsoft.DotNet.HotReload.Client.shproj", + "src\\BuiltInTools\\Watch.Aspire\\Microsoft.DotNet.HotReload.Watch.Aspire.csproj", + "src\\BuiltInTools\\Watch\\Microsoft.DotNet.HotReload.Watch.csproj", "src\\BuiltInTools\\dotnet-watch\\dotnet-watch.csproj", "test\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj", + "test\\Microsoft.DotNet.HotReload.Client.Tests\\Microsoft.DotNet.HotReload.Client.Tests.csproj", "test\\Microsoft.Extensions.DotNetDeltaApplier.Tests\\Microsoft.Extensions.DotNetDeltaApplier.Tests.csproj", "test\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj", "test\\Microsoft.WebTools.AspireService.Tests\\Microsoft.WebTools.AspireService.Tests.csproj", - "test\\Microsoft.DotNet.HotReload.Client.Tests\\Microsoft.DotNet.HotReload.Client.Tests.csproj", "test\\dotnet-watch.Tests\\dotnet-watch.Tests.csproj" ] } diff --git a/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs b/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs index 95e98a584af6..b5a64838044e 100644 --- a/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs +++ b/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs @@ -377,22 +377,4 @@ public ProjectOptions GetProjectOptions(string projectPath, string workingDirect BuildArguments = BuildArguments, TargetFramework = TargetFramework, }; - - // Parses name=value pairs passed to --property. Skips invalid input. - public static IEnumerable<(string key, string value)> ParseBuildProperties(IEnumerable arguments) - => from argument in arguments - let colon = argument.IndexOf(':') - where colon >= 0 && argument[0..colon] is "--property" or "-property" or "/property" or "/p" or "-p" or "--p" - let eq = argument.IndexOf('=', colon) - where eq >= 0 - let name = argument[(colon + 1)..eq].Trim() - let value = argument[(eq + 1)..] - where name is not [] - select (name, value); - - /// - /// Returns true if the command executes the code of the target project. - /// - public static bool IsCodeExecutionCommand(string commandName) - => commandName is "run" or "test"; } diff --git a/src/BuiltInTools/dotnet-watch/Program.cs b/src/BuiltInTools/dotnet-watch/Program.cs index b2b9bd03bbd9..0cbf1966891f 100644 --- a/src/BuiltInTools/dotnet-watch/Program.cs +++ b/src/BuiltInTools/dotnet-watch/Program.cs @@ -67,7 +67,7 @@ public static async Task Main(string[] args) private static Program? TryCreate(IReadOnlyList args, IConsole console, EnvironmentOptions environmentOptions, bool verbose, out int errorCode) { var parsingLoggerFactory = new LoggerFactory(new ConsoleReporter(console, verbose, quiet: false, environmentOptions.SuppressEmojis)); - var options = CommandLineOptions.Parse(args, parsingLoggerFactory.CreateLogger(LogComponentName), console.Out, out errorCode); + var options = CommandLineOptions.Parse(args, parsingLoggerFactory.CreateLogger(DotNetWatchContext.DefaultLogComponentName), console.Out, out errorCode); if (options == null) { // an error reported or help printed: @@ -82,7 +82,7 @@ public static async Task Main(string[] args) // internal for testing internal static Program? TryCreate(CommandLineOptions options, IConsole console, EnvironmentOptions environmentOptions, LoggerFactory loggerFactory, IProcessOutputReporter processOutputReporter, out int errorCode) { - var logger = loggerFactory.CreateLogger(LogComponentName); + var logger = loggerFactory.CreateLogger(DotNetWatchContext.DefaultLogComponentName); var workingDirectory = environmentOptions.WorkingDirectory; logger.LogDebug("Working directory: '{Directory}'", workingDirectory); @@ -151,41 +151,21 @@ private static bool TryFindProject(string searchBase, CommandLineOptions options // internal for testing internal async Task RunAsync() { - var shutdownCancellationSourceDisposed = false; - var shutdownCancellationSource = new CancellationTokenSource(); - var shutdownCancellationToken = shutdownCancellationSource.Token; var isHotReloadEnabled = IsHotReloadEnabled(); var processRunner = new ProcessRunner(environmentOptions.GetProcessCleanupTimeout(isHotReloadEnabled)); - console.KeyPressed += key => - { - if (!shutdownCancellationSourceDisposed && key.Modifiers.HasFlag(ConsoleModifiers.Control) && key.Key == ConsoleKey.C) - { - // if we already canceled, we force immediate shutdown: - var forceShutdown = shutdownCancellationSource.IsCancellationRequested; - - if (!forceShutdown) - { - logger.Log(MessageDescriptor.ShutdownRequested); - shutdownCancellationSource.Cancel(); - } - else - { - Environment.Exit(0); - } - } - }; + using var shutdownHandler = new ShutdownHandler(console, logger); try { - if (shutdownCancellationToken.IsCancellationRequested) + if (shutdownHandler.CancellationToken.IsCancellationRequested) { return 1; } if (options.List) { - return await ListFilesAsync(processRunner, shutdownCancellationToken); + return await ListFilesAsync(processRunner, shutdownHandler.CancellationToken); } if (environmentOptions.IsPollingEnabled) @@ -198,16 +178,16 @@ internal async Task RunAsync() if (isHotReloadEnabled) { var watcher = new HotReloadDotNetWatcher(context, console, runtimeProcessLauncherFactory: null); - await watcher.WatchAsync(shutdownCancellationToken); + await watcher.WatchAsync(shutdownHandler.CancellationToken); } else { - await DotNetWatcher.WatchAsync(context, shutdownCancellationToken); + await DotNetWatcher.WatchAsync(context, shutdownHandler.CancellationToken); } return 0; } - catch (OperationCanceledException) when (shutdownCancellationToken.IsCancellationRequested) + catch (OperationCanceledException) when (shutdownHandler.CancellationToken.IsCancellationRequested) { // Ctrl+C forced an exit return 0; @@ -217,11 +197,6 @@ internal async Task RunAsync() logger.LogError("An unexpected error occurred: {Exception}", e.ToString()); return 1; } - finally - { - shutdownCancellationSourceDisposed = true; - shutdownCancellationSource.Dispose(); - } } // internal for testing diff --git a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json index b6981fd0ae9d..52e962bf0ac4 100644 --- a/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json +++ b/src/BuiltInTools/dotnet-watch/Properties/launchSettings.json @@ -3,7 +3,7 @@ "dotnet-watch": { "commandName": "Project", "commandLineArgs": "--verbose -bl", - "workingDirectory": "$(RepoRoot)src\\Assets\\TestProjects\\BlazorWasmWithLibrary\\blazorwasm", + "workingDirectory": "C:\\Temp\\Blazor-WasmHosted\\blazorwasmhosted", "environmentVariables": { "DOTNET_WATCH_DEBUG_SDK_DIRECTORY": "$(RepoRoot)artifacts\\bin\\redist\\$(Configuration)\\dotnet\\sdk\\$(Version)", "DCP_IDE_REQUEST_TIMEOUT_SECONDS": "100000", diff --git a/src/BuiltInTools/dotnet-watch/UI/IConsole.cs b/src/BuiltInTools/dotnet-watch/UI/IConsole.cs deleted file mode 100644 index 0860a30eeae6..000000000000 --- a/src/BuiltInTools/dotnet-watch/UI/IConsole.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -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 interface IConsole - { - event Action KeyPressed; - TextWriter Out { get; } - TextWriter Error { get; } - ConsoleColor ForegroundColor { get; set; } - void ResetColor(); - void Clear(); - } -} diff --git a/src/BuiltInTools/dotnet-watch/Watch/BuildEvaluator.cs b/src/BuiltInTools/dotnet-watch/Watch/BuildEvaluator.cs index ec3b10c8b235..756abbda6bc4 100644 --- a/src/BuiltInTools/dotnet-watch/Watch/BuildEvaluator.cs +++ b/src/BuiltInTools/dotnet-watch/Watch/BuildEvaluator.cs @@ -46,7 +46,7 @@ public IReadOnlyList GetProcessArguments(int iteration) { if (!_context.EnvironmentOptions.SuppressMSBuildIncrementalism && iteration > 0 && - CommandLineOptions.IsCodeExecutionCommand(_context.RootProjectOptions.Command)) + _context.RootProjectOptions.IsCodeExecutionCommand) { if (RequiresRevaluation) { diff --git a/src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs b/src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs index d1c580ecb386..ae17dabc6122 100644 --- a/src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs +++ b/src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs @@ -124,7 +124,7 @@ void AddFile(string filePath, string? staticWebAssetPath) ProjectGraph? projectGraph = null; if (requireProjectGraph != null) { - var globalOptions = CommandLineOptions.ParseBuildProperties(buildArguments) + var globalOptions = BuildUtilities.ParseBuildProperties(buildArguments) .ToImmutableDictionary(keySelector: arg => arg.key, elementSelector: arg => arg.value); projectGraph = ProjectGraphUtilities.TryLoadProjectGraph(rootProjectFile, globalOptions, Logger, requireProjectGraph.Value, cancellationToken); diff --git a/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj b/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj index 9d089001e2b2..ca12648ba8da 100644 --- a/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj +++ b/src/BuiltInTools/dotnet-watch/dotnet-watch.csproj @@ -1,10 +1,4 @@  - - - - - - $(SdkTargetFramework) exe @@ -34,18 +28,9 @@ - - - - - - - - - @@ -60,35 +45,7 @@ PreserveNewest - - all - Content - true - false - TargetFramework;TargetFrameworks - hotreload\net6.0\Microsoft.AspNetCore.Watch.BrowserRefresh.dll - PreserveNewest - - - - all - Content - true - false - TargetFramework=net10.0 - hotreload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll - PreserveNewest - - - - all - Content - true - false - TargetFramework=net6.0 - hotreload\net6.0\Microsoft.Extensions.DotNetDeltaApplier.dll - PreserveNewest - + @@ -105,12 +62,7 @@ Keep excluded files in sync with the list in GenerateLayout.targets. --> - <_DotnetWatchInputFile Include="$(TargetDir)**" - Condition="('%(Filename)' != 'Microsoft.CodeAnalysis' and - '%(Filename)' != 'Microsoft.CodeAnalysis.resources' and - '%(Filename)' != 'Microsoft.CodeAnalysis.CSharp' and - '%(Filename)' != 'Microsoft.CodeAnalysis.CSharp.resources') or - $([MSBuild]::ValueOrDefault('%(FullPath)', '').Contains('BuildHost'))" /> + <_DotnetWatchInputFile Include="$(TargetDir)**" Condition="('%(Filename)' != 'Microsoft.CodeAnalysis' and '%(Filename)' != 'Microsoft.CodeAnalysis.resources' and '%(Filename)' != 'Microsoft.CodeAnalysis.CSharp' and '%(Filename)' != 'Microsoft.CodeAnalysis.CSharp.resources') or $([MSBuild]::ValueOrDefault('%(FullPath)', '').Contains('BuildHost'))" /> diff --git a/test/dotnet-watch.Tests/Build/BuildUtilitiesTests.cs b/test/dotnet-watch.Tests/Build/BuildUtilitiesTests.cs new file mode 100644 index 000000000000..03161ae09339 --- /dev/null +++ b/test/dotnet-watch.Tests/Build/BuildUtilitiesTests.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Watch.UnitTests.Build; + +public class BuildUtilitiesTests +{ + [Theory] + [InlineData("-p:P=V", "P", "V")] + [InlineData("-p:P==", "P", "=")] + [InlineData("-p:P=A=B", "P", "A=B")] + [InlineData("-p: P\t = V ", "P", " V ")] + [InlineData("-p:P=", "P", "")] + public void BuildProperties_Valid(string argValue, string name, string value) + { + var properties = BuildUtilities.ParseBuildProperties([argValue]); + AssertEx.SequenceEqual([(name, value)], properties); + } + + [Theory] + [InlineData("P")] + [InlineData("=P3")] + [InlineData("=")] + [InlineData("==")] + public void BuildProperties_Invalid(string argValue) + { + var properties = BuildUtilities.ParseBuildProperties([argValue]); + AssertEx.SequenceEqual([], properties); + } +} diff --git a/test/dotnet-watch.Tests/CommandLine/CommandLineOptionsTests.cs b/test/dotnet-watch.Tests/CommandLine/CommandLineOptionsTests.cs index 768eb47b262b..f13b2dcb3524 100644 --- a/test/dotnet-watch.Tests/CommandLine/CommandLineOptionsTests.cs +++ b/test/dotnet-watch.Tests/CommandLine/CommandLineOptionsTests.cs @@ -53,29 +53,6 @@ public void HelpArgs(string[] args) Assert.Contains("Usage:", output.ToString()); } - [Theory] - [InlineData("-p:P=V", "P", "V")] - [InlineData("-p:P==", "P", "=")] - [InlineData("-p:P=A=B", "P", "A=B")] - [InlineData("-p: P\t = V ", "P", " V ")] - [InlineData("-p:P=", "P", "")] - public void BuildProperties_Valid(string argValue, string name, string value) - { - var properties = CommandLineOptions.ParseBuildProperties([argValue]); - AssertEx.SequenceEqual([(name, value)], properties); - } - - [Theory] - [InlineData("P")] - [InlineData("=P3")] - [InlineData("=")] - [InlineData("==")] - public void BuildProperties_Invalid(string argValue) - { - var properties = CommandLineOptions.ParseBuildProperties([argValue]); - AssertEx.SequenceEqual([], properties); - } - [Fact] public void ImplicitCommand() {