diff --git a/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs b/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs index 90327f46c06f..3fb1e784c7b7 100644 --- a/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs +++ b/src/BuiltInTools/DotNetDeltaApplier/HotReloadAgent.cs @@ -14,7 +14,7 @@ internal class HotReloadAgent : IDisposable { private readonly Action _log; private readonly AssemblyLoadEventHandler _assemblyLoad; - private readonly ConcurrentDictionary> _deltas = new(); + private readonly ConcurrentDictionary> _deltas = new(); private readonly ConcurrentDictionary _appliedAssemblies = new(); private volatile UpdateHandlerActions? _handlerActions; @@ -196,6 +196,10 @@ public void ApplyDeltas(IReadOnlyList deltas) { System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate(assembly, item.MetadataDelta, item.ILDelta, ReadOnlySpan.Empty); } + + // Additionally stash the deltas away so it may be applied to assemblies loaded later. + var cachedDeltas = _deltas.GetOrAdd(item.ModuleId, static _ => new()); + cachedDeltas.Add(item); } handlerActions.ClearCache.ForEach(a => a(updatedTypes)); diff --git a/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs b/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs index 6e78cf953f78..f66a3be5fe16 100644 --- a/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs +++ b/src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs @@ -53,11 +53,6 @@ public static async Task ReceiveDeltas(HotReloadAgent hotReloadAgent) Log("Attempting to apply deltas."); hotReloadAgent.ApplyDeltas(update.Deltas); - - // We want to base this off of mvids, but we'll figure that out eventually. - var applyResult = update.ChangedFile is string changedFile && changedFile.EndsWith(".razor", StringComparison.Ordinal) ? - ApplyResult.Success : - ApplyResult.Success_RefreshBrowser; pipeClient.WriteByte((byte)ApplyResult.Success); } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs index 07da436848cf..1d8ccca49706 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs @@ -115,6 +115,8 @@ public async ValueTask TryHandleFileChange(DotNetWatchContext context, Fil { _reporter.Verbose("No deltas modified. Applying changes to clear diagnostics."); await _deltaApplier.Apply(context, file.FilePath, updates, cancellationToken); + // Even if there were diagnostics, continue treating this as a success + _reporter.Output("No hot reload changes to apply."); } else { @@ -123,7 +125,8 @@ public async ValueTask TryHandleFileChange(DotNetWatchContext context, Fil } HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler); - // Even if there were diagnostics, continue treating this as a success + // Return true so that the watcher continues to keep the current hot reload session alive. If there were errors, this allows the user to fix errors and continue + // working on the running app. return true; } @@ -145,6 +148,11 @@ public async ValueTask TryHandleFileChange(DotNetWatchContext context, Fil var applyState = await _deltaApplier.Apply(context, file.FilePath, updates, cancellationToken); _reporter.Verbose($"Received {(applyState ? "successful" : "failed")} apply from delta applier."); HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler); + if (applyState) + { + _reporter.Output($"Hot reload of changes succeeded."); + } + return applyState; } @@ -171,7 +179,7 @@ private ImmutableArray GetDiagnostics(Solution solution, CancellationTok if (item.Severity == DiagnosticSeverity.Error) { var diagnostic = CSharpDiagnosticFormatter.Instance.Format(item); - _reporter.Output(diagnostic); + _reporter.Output("\x1B[40m\x1B[31m" + diagnostic); projectDiagnostics = projectDiagnostics.Add(diagnostic); } } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs b/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs index 54381ee6e45c..af1bdf383329 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/DefaultDeltaApplier.cs @@ -17,10 +17,11 @@ namespace Microsoft.DotNet.Watcher.Tools { internal class DefaultDeltaApplier : IDeltaApplier { + private static readonly string _namedPipeName = Guid.NewGuid().ToString(); private readonly IReporter _reporter; - private readonly string _namedPipeName = Guid.NewGuid().ToString(); private Task _task; private NamedPipeServerStream _pipe; + private bool _refreshBrowserAfterFileChange; public DefaultDeltaApplier(IReporter reporter) { @@ -29,14 +30,8 @@ public DefaultDeltaApplier(IReporter reporter) public bool SuppressBrowserRefreshAfterApply { get; init; } - public async ValueTask InitializeAsync(DotNetWatchContext context, CancellationToken cancellationToken) + public ValueTask InitializeAsync(DotNetWatchContext context, CancellationToken cancellationToken) { - if (_pipe is not null) - { - _pipe.Close(); - await _pipe.DisposeAsync(); - } - _pipe = new NamedPipeServerStream(_namedPipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); _task = _pipe.WaitForConnectionAsync(cancellationToken); @@ -48,7 +43,15 @@ public async ValueTask InitializeAsync(DotNetWatchContext context, CancellationT // Configure the app for EnC context.ProcessSpec.EnvironmentVariables["DOTNET_MODIFIABLE_ASSEMBLIES"] = "debug"; context.ProcessSpec.EnvironmentVariables["DOTNET_HOTRELOAD_NAMEDPIPE_NAME"] = _namedPipeName; + + // If there's any .razor file, we'll assume this is a blazor app and not cause a browser refresh. + if (!SuppressBrowserRefreshAfterApply) + { + _refreshBrowserAfterFileChange = !context.FileSet.Any(f => f.FilePath.EndsWith(".razor", StringComparison.Ordinal)); + } } + + return default; } public async ValueTask Apply(DotNetWatchContext context, string changedFile, ImmutableArray solutionUpdate, CancellationToken cancellationToken) @@ -110,11 +113,14 @@ public async ValueTask Apply(DotNetWatchContext context, string changedFil if (!SuppressBrowserRefreshAfterApply && context.BrowserRefreshServer is not null) { - if (result == ApplyResult.Success_RefreshBrowser) + // For a Web app, we have the option of either letting the app update the UI or + // refresh the browser. In general, for Blazor apps, we will choose not to refresh the UI + // and for other apps we'll always refresh + if (_refreshBrowserAfterFileChange) { await context.BrowserRefreshServer.ReloadAsync(cancellationToken); } - else if (result == ApplyResult.Success) + else { await context.BrowserRefreshServer.SendJsonSerlialized(new HotReloadApplied()); } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs b/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs index b372160b3ada..09ecf1a1df79 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/NamedPipeContract.cs @@ -98,6 +98,5 @@ internal enum ApplyResult { Failed = -1, Success = 0, - Success_RefreshBrowser = 1, } } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs index 5f0c22532ba6..a6b31a3715c2 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs @@ -39,6 +39,7 @@ public async ValueTask TryHandleFileChange(DotNetWatchContext context, Fil return false; } await HandleBrowserRefresh(context.BrowserRefreshServer, file, cancellationToken); + _reporter.Output("Hot reload of scoped css succeeded."); HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.ScopedCssHandler); return true; } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs index 7c0d7aab5a86..920227125c58 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs @@ -33,6 +33,7 @@ public async ValueTask TryHandleFileChange(DotNetWatchContext context, Fil _reporter.Verbose($"Handling file change event for static content {file.FilePath}."); await HandleBrowserRefresh(context.BrowserRefreshServer, file, cancellationToken); HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.StaticHandler); + _reporter.Output("Hot reload of static file succeeded."); return true; } diff --git a/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs b/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs index 23532919a041..3beb77f612bc 100644 --- a/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs +++ b/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs @@ -128,8 +128,7 @@ public async Task WatchAsync(DotNetWatchContext context, CancellationToken cance if (await hotReload.TryHandleFileChange(context, fileItem, combinedCancellationSource.Token)) { var totalTime = TimeSpan.FromTicks(Stopwatch.GetTimestamp() - start); - _reporter.Output($"Hot reload of changes succeeded."); - _reporter.Verbose($"Hot reload applied in {totalTime.TotalMilliseconds}ms."); + _reporter.Verbose($"Hot reload change handled in {totalTime.TotalMilliseconds}ms."); } else { diff --git a/src/BuiltInTools/dotnet-watch/Program.cs b/src/BuiltInTools/dotnet-watch/Program.cs index 94bb15822610..6052ef05ab41 100644 --- a/src/BuiltInTools/dotnet-watch/Program.cs +++ b/src/BuiltInTools/dotnet-watch/Program.cs @@ -67,7 +67,13 @@ public Program(IConsole console, string workingDirectory) // AppContext.BaseDirectory = $sdkRoot\$sdkVersion\DotnetTools\dotnet-watch\$version\tools\net6.0\any\ // MSBuild.dll is located at $sdkRoot\$sdkVersion\MSBuild.dll var sdkRootDirectory = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", ".."); +#if DEBUG + // In the usual case, use the SDK that contains the dotnet-watch. However during local testing, it's + // much more common to run dotnet-watch from a different SDK. Use the ambient SDK in that case. + MSBuildLocator.RegisterDefaults(); +#else MSBuildLocator.RegisterMSBuildPath(sdkRootDirectory); +#endif Ensure.NotNull(console, nameof(console)); Ensure.NotNullOrEmpty(workingDirectory, nameof(workingDirectory));