From 3d92130b2998833a24d70d7644f843118bb5ccf5 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 3 Jun 2024 14:17:58 -0400 Subject: [PATCH] feat: Add disagnostic indocator for hot-reload process --- .../RemoteControlServer.cs | 287 +++++++--- .../IServerProcessor.cs | 13 + .../HotReload/FileUpdateProcessor.cs | 19 +- .../CompilationWorkspaceProvider.cs | 31 +- ...ServerHotReloadProcessor.MetadataUpdate.cs | 88 +-- .../HotReload/ServerHotReloadProcessor.cs | 514 ++++++++++++++++-- .../ClientHotReloadProcessor.Agent.cs | 18 +- ...ClientHotReloadProcessor.MetadataUpdate.cs | 59 +- .../HotReload/ClientHotReloadProcessor.cs | 36 +- .../HotReload/HotReloadMode.cs | 8 +- .../HotReload/HotReloadStatusView.xaml | 87 +++ .../HotReload/HotReloadStatusView.xaml.cs | 80 +++ .../HotReload/Messages/AssemblyDeltaReload.cs | 2 +- .../HotReload/Messages/ConfigureServer.cs | 2 +- .../HotReload/Messages/FileReload.cs | 8 +- .../HotReload/Messages/HotReloadConstants.cs | 12 - .../HotReload/Messages/HotReloadResult.cs | 40 ++ .../HotReload/Messages/HotReloadState.cs | 29 + .../Messages/HotReloadStatusMessage.cs | 24 + .../Messages/HotReloadWorkspaceLoadResult.cs | 2 +- .../HotReload/Messages/UpdateFile.cs | 26 +- .../HotReload/Messages/UpdateFileResponse.cs | 28 + .../HotReload/Messages/XamlLoadError.cs | 2 +- .../MetadataUpdater/HotReloadAgent.cs | 30 +- .../HotReload/WindowExtensions.cs | 5 +- .../Uno.UI.RemoteControl.Skia.csproj | 7 + 26 files changed, 1213 insertions(+), 244 deletions(-) create mode 100644 src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml create mode 100644 src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml.cs delete mode 100644 src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadConstants.cs create mode 100644 src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadResult.cs create mode 100644 src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadState.cs create mode 100644 src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs create mode 100644 src/Uno.UI.RemoteControl/HotReload/Messages/UpdateFileResponse.cs diff --git a/src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs b/src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs index ef7bb341d2a9..cfd8c5ed6311 100644 --- a/src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs +++ b/src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.Contracts; using System.IO; using System.IO.Pipes; +using System.Linq; using System.Net.WebSockets; using System.Reflection; using System.Runtime.Loader; @@ -18,7 +20,6 @@ using Uno.UI.RemoteControl.HotReload.Messages; using Uno.UI.RemoteControl.Messages; using Uno.UI.RemoteControl.Messaging.IdeChannel; -using static Uno.UI.RemoteControl.Host.RemoteControlServer; namespace Uno.UI.RemoteControl.Host; @@ -27,6 +28,7 @@ internal class RemoteControlServer : IRemoteControlServer, IDisposable private readonly object _loadContextGate = new(); private static readonly Dictionary _loadContexts = new(); private readonly Dictionary _processors = new(); + private readonly CancellationTokenSource _ct = new(); private string? _resolveAssemblyLocation; private WebSocket? _socket; @@ -145,18 +147,13 @@ public async Task RunAsync(WebSocket socket, CancellationToken ct) { if (frame.Name == ProcessorsDiscovery.Name) { - ProcessDiscoveryFrame(frame); + await ProcessDiscoveryFrame(frame); continue; } if (frame.Name == KeepAliveMessage.Name) { - if (this.Log().IsEnabled(LogLevel.Trace)) - { - this.Log().LogTrace($"Client Keepalive frame"); - } - - await SendFrame(new KeepAliveMessage()); + await ProcessPingFrame(frame); continue; } } @@ -168,7 +165,18 @@ public async Task RunAsync(WebSocket socket, CancellationToken ct) this.Log().LogDebug("Received Frame [{Scope} / {Name}] to be processed by {processor}", frame.Scope, frame.Name, processor); } - await processor.ProcessFrame(frame); + try + { + DevServerDiagnostics.Current = DiagnosticsSink.Instance; + await processor.ProcessFrame(frame); + } + catch (Exception e) + { + if (this.Log().IsEnabled(LogLevel.Error)) + { + this.Log().LogError(e, "Failed to process frame [{Scope} / {Name}]", frame.Scope, frame.Name); + } + } } else { @@ -182,132 +190,235 @@ public async Task RunAsync(WebSocket socket, CancellationToken ct) private async Task TryStartIDEChannelAsync() { + if (_ideChannelServer is { } oldChannel) + { + oldChannel.MessageFromIDE -= ProcessIdeMessage; + } + _ideChannelServer = await _ideChannelProvider.GetIdeChannelServerAsync(); + + if (_ideChannelServer is { } newChannel) + { + newChannel.MessageFromIDE += ProcessIdeMessage; + } } - private void ProcessDiscoveryFrame(Frame frame) + private void ProcessIdeMessage(object? sender, IdeMessage message) { - var msg = JsonConvert.DeserializeObject(frame.Content)!; - var serverAssemblyName = typeof(IServerProcessor).Assembly.GetName().Name; - - var assemblies = new List(); + if (_processors.TryGetValue(message.Scope, out var processor)) + { + if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().LogDebug("Received message [{Scope} / {Name}] to be processed by {processor}", message.Scope, message.GetType().Name, processor); + } - _resolveAssemblyLocation = string.Empty; + var process = processor.ProcessIdeMessage(message, _ct.Token); - if (!_appInstanceIds.Contains(msg.AppInstanceId)) + if (this.Log().IsEnabled(LogLevel.Error)) + { + process = process.ContinueWith( + t => this.Log().LogError($"Failed to process message {message}: {t.Exception?.Flatten()}"), + _ct.Token, + TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent, + TaskScheduler.Default); + } + } + else { - _appInstanceIds.Add(msg.AppInstanceId); + if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().LogDebug("Unknown Frame [{Scope} / {Name}]", message.Scope, message.GetType().Name); + } } + } - var assemblyLoadContext = GetAssemblyLoadContext(msg.AppInstanceId); - - // If BasePath is a specific file, try and load that - if (File.Exists(msg.BasePath)) + private async Task ProcessPingFrame(Frame frame) + { + KeepAliveMessage pong; + if (frame.TryGetContent(out KeepAliveMessage? ping)) { - try - { - using var fs = File.Open(msg.BasePath, FileMode.Open, FileAccess.Read, FileShare.Read); - assemblies.Add(assemblyLoadContext.LoadFromStream(fs)); + pong = new() { SequenceId = ping.SequenceId }; - _resolveAssemblyLocation = msg.BasePath; + if (ping.AssemblyVersion != pong.AssemblyVersion && this.Log().IsEnabled(LogLevel.Warning)) + { + this.Log().LogTrace( + $"Client ping frame (a.k.a. KeepAlive), but version differs from server (server: {pong.AssemblyVersion} | client: {ping.AssemblyVersion})." + + $"This usually indicates that an old instance of the dev-server is being re-used or a partial deployment of the application." + + "Some feature like hot-reload are most likely to fail. To fix this, you might have to restart Visual Studio."); } - catch (Exception exc) + else if (this.Log().IsEnabled(LogLevel.Trace)) { - if (this.Log().IsEnabled(LogLevel.Error)) - { - this.Log().LogError("Failed to load assembly {BasePath} : {Exc}", msg.BasePath, exc); - } + this.Log().LogTrace($"Client ping frame (a.k.a. KeepAlive) with valid version ({ping.AssemblyVersion})."); } } else { - // As BasePath is a directory, try and load processors from assemblies within that dir - var basePath = msg.BasePath.Replace('/', Path.DirectorySeparatorChar); - -#if NET9_0_OR_GREATER - basePath = Path.Combine(basePath, "net9.0"); -#elif NET8_0_OR_GREATER - basePath = Path.Combine(basePath, "net8.0"); -#endif + pong = new(); - // Additional processors may not need the directory added immediately above. - if (!Directory.Exists(basePath)) + if (this.Log().IsEnabled(LogLevel.Warning)) { - basePath = msg.BasePath; + this.Log().LogTrace( + "Client ping frame (a.k.a. KeepAlive), but failed to deserialize it's content. " + + $"This usually indicates a version mismatch between client and server (server: {pong.AssemblyVersion})." + + "Some feature like hot-reload are most likely to fail. To fix this, you might have to restart Visual Studio."); } + } + + await SendFrame(pong); + } + + private async Task ProcessDiscoveryFrame(Frame frame) + { + var assemblies = new List<(string path, System.Reflection.Assembly assembly)>(); + var discoveredProcessors = new List(); + try + { + var msg = JsonConvert.DeserializeObject(frame.Content)!; + var serverAssemblyName = typeof(IServerProcessor).Assembly.GetName().Name; + + _resolveAssemblyLocation = string.Empty; - foreach (var file in Directory.GetFiles(basePath, "Uno.*.dll")) + if (!_appInstanceIds.Contains(msg.AppInstanceId)) { - if (Path.GetFileNameWithoutExtension(file).Equals(serverAssemblyName, StringComparison.OrdinalIgnoreCase)) - { - continue; - } + _appInstanceIds.Add(msg.AppInstanceId); + } - if (this.Log().IsEnabled(LogLevel.Debug)) - { - this.Log().LogDebug("Discovery: Loading {File}", file); - } + var assemblyLoadContext = GetAssemblyLoadContext(msg.AppInstanceId); + // If BasePath is a specific file, try and load that + if (File.Exists(msg.BasePath)) + { try { - assemblies.Add(assemblyLoadContext.LoadFromAssemblyPath(file)); + using var fs = File.Open(msg.BasePath, FileMode.Open, FileAccess.Read, FileShare.Read); + assemblies.Add((msg.BasePath, assemblyLoadContext.LoadFromStream(fs))); + + _resolveAssemblyLocation = msg.BasePath; } catch (Exception exc) { - // With additional processors there may be duplicates of assemblies already loaded - if (this.Log().IsEnabled(LogLevel.Debug)) + if (this.Log().IsEnabled(LogLevel.Error)) { - this.Log().LogDebug("Failed to load assembly {File} : {Exc}", file, exc); + this.Log().LogError("Failed to load assembly {BasePath} : {Exc}", msg.BasePath, exc); } } } - } - - foreach (var asm in assemblies) - { - try + else { - if (assemblies.Count > 1 || string.IsNullOrEmpty(_resolveAssemblyLocation)) + // As BasePath is a directory, try and load processors from assemblies within that dir + var basePath = msg.BasePath.Replace('/', Path.DirectorySeparatorChar); + +#if NET9_0_OR_GREATER + basePath = Path.Combine(basePath, "net9.0"); +#elif NET8_0_OR_GREATER + basePath = Path.Combine(basePath, "net8.0"); +#endif + + // Additional processors may not need the directory added immediately above. + if (!Directory.Exists(basePath)) { - _resolveAssemblyLocation = asm.Location; + basePath = msg.BasePath; } - var attributes = asm.GetCustomAttributes(typeof(ServerProcessorAttribute), false); - - foreach (var processorAttribute in attributes) + foreach (var file in Directory.GetFiles(basePath, "Uno.*.dll")) { - if (processorAttribute is ServerProcessorAttribute processor) + if (Path.GetFileNameWithoutExtension(file).Equals(serverAssemblyName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().LogDebug("Discovery: Loading {File}", file); + } + + try { + assemblies.Add((file, assemblyLoadContext.LoadFromAssemblyPath(file))); + } + catch (Exception exc) + { + // With additional processors there may be duplicates of assemblies already loaded if (this.Log().IsEnabled(LogLevel.Debug)) { - this.Log().LogDebug("Discovery: Registering {ProcessorType}", processor.ProcessorType); + this.Log().LogDebug("Failed to load assembly {File} : {Exc}", file, exc); } + } + } + } - if (asm.CreateInstance(processor.ProcessorType.FullName!, ignoreCase: false, bindingAttr: BindingFlags.Instance | BindingFlags.Public, binder: null, args: new[] { this }, culture: null, activationAttributes: null) is IServerProcessor serverProcessor) - { - RegisterProcessor(serverProcessor); - } - else + foreach (var asm in assemblies) + { + try + { + if (assemblies.Count > 1 || string.IsNullOrEmpty(_resolveAssemblyLocation)) + { + _resolveAssemblyLocation = asm.path; + } + + var attributes = asm.assembly.GetCustomAttributes(typeof(ServerProcessorAttribute), false); + + foreach (var processorAttribute in attributes) + { + if (processorAttribute is ServerProcessorAttribute processor) { if (this.Log().IsEnabled(LogLevel.Debug)) { - this.Log().LogDebug("Failed to create server processor {ProcessorType}", processor.ProcessorType); + this.Log().LogDebug("Discovery: Registering {ProcessorType}", processor.ProcessorType); + } + + try + { + if (asm.assembly.CreateInstance(processor.ProcessorType.FullName!, ignoreCase: false, bindingAttr: BindingFlags.Instance | BindingFlags.Public, binder: null, args: new[] { this }, culture: null, activationAttributes: null) is IServerProcessor serverProcessor) + { + discoveredProcessors.Add(new(asm.path, processor.ProcessorType.FullName!, VersionHelper.GetVersion(processor.ProcessorType), IsLoaded: true)); + RegisterProcessor(serverProcessor); + } + else + { + discoveredProcessors.Add(new(asm.path, processor.ProcessorType.FullName!, VersionHelper.GetVersion(processor.ProcessorType), IsLoaded: false)); + if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().LogDebug("Failed to create server processor {ProcessorType}", processor.ProcessorType); + } + } + } + catch (Exception error) + { + discoveredProcessors.Add(new(asm.path, processor.ProcessorType.FullName!, VersionHelper.GetVersion(processor.ProcessorType), IsLoaded: false, LoadError: error.ToString())); + if (this.Log().IsEnabled(LogLevel.Error)) + { + this.Log().LogError(error, "Failed to create server processor {ProcessorType}", processor.ProcessorType); + } } } } } - } - catch (Exception exc) - { - if (this.Log().IsEnabled(LogLevel.Error)) + catch (Exception exc) { - this.Log().LogError("Failed to create instance of server processor in {Asm} : {Exc}", asm, exc); + if (this.Log().IsEnabled(LogLevel.Error)) + { + this.Log().LogError("Failed to create instance of server processor in {Asm} : {Exc}", asm, exc); + } } } - } - // Being thorough about trying to ensure everything is unloaded - assemblies.Clear(); + // Being thorough about trying to ensure everything is unloaded + assemblies.Clear(); + } + catch (Exception exc) + { + if (this.Log().IsEnabled(LogLevel.Error)) + { + this.Log().LogError("Failed to process discovery frame: {Exc}", exc); + } + } + finally + { + await SendFrame(new ProcessorsDiscoveryResponse( + assemblies.Select(asm => asm.path).ToImmutableList(), + discoveredProcessors.ToImmutableList())); + } } public async Task SendFrame(IMessage message) @@ -343,6 +454,8 @@ public async Task SendMessageToIDEAsync(IdeMessage message) public void Dispose() { + _ct.Cancel(false); + foreach (var processor in _processors) { processor.Value.Dispose(); @@ -379,4 +492,16 @@ public void Dispose() } } } + + private class DiagnosticsSink : DevServerDiagnostics.ISink + { + public static DiagnosticsSink Instance { get; } = new(); + + private DiagnosticsSink() { } + + /// + public void ReportInvalidFrame(Frame frame) + => typeof(RemoteControlServer).Log().LogError($"Got an invalid frame for type {typeof(TContent).Name} [{frame.Scope} / {frame.Name}]"); + } + } diff --git a/src/Uno.UI.RemoteControl.Messaging/IServerProcessor.cs b/src/Uno.UI.RemoteControl.Messaging/IServerProcessor.cs index 70e4a29e3f40..9173d13fd8a2 100644 --- a/src/Uno.UI.RemoteControl.Messaging/IServerProcessor.cs +++ b/src/Uno.UI.RemoteControl.Messaging/IServerProcessor.cs @@ -1,6 +1,8 @@ using System; +using System.Threading; using System.Threading.Tasks; using Uno.UI.RemoteControl.HotReload.Messages; +using Uno.UI.RemoteControl.Messaging.IdeChannel; namespace Uno.UI.RemoteControl.Host { @@ -8,7 +10,18 @@ public interface IServerProcessor : IDisposable { string Scope { get; } + /// + /// Processes a frame from the Client + /// + /// The frame received from the client. Task ProcessFrame(Frame frame); + + /// + /// Processes a message from the IDE + /// + /// The message received from the IDE. + /// The cancellation token. + Task ProcessIdeMessage(IdeMessage message, CancellationToken ct); } [System.AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)] diff --git a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/FileUpdateProcessor.cs b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/FileUpdateProcessor.cs index 3d39b4822e4b..91efc3640555 100644 --- a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/FileUpdateProcessor.cs +++ b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/FileUpdateProcessor.cs @@ -1,10 +1,12 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Uno.Extensions; using Uno.UI.RemoteControl.HotReload.Messages; +using Uno.UI.RemoteControl.Messaging.IdeChannel; [assembly: Uno.UI.RemoteControl.Host.ServerProcessorAttribute(typeof(Uno.UI.RemoteControl.Host.HotReload.FileUpdateProcessor))] @@ -12,6 +14,17 @@ namespace Uno.UI.RemoteControl.Host.HotReload; partial class FileUpdateProcessor : IServerProcessor, IDisposable { + // ******************************************* + // ******************************************* + // ***************** WARNING ***************** + // ******************************************* + // ******************************************* + // + // This processor is present only for legacy purposes. + // The Scope of the UpdateFile message has been changed from WellKnownScopes.Testing to WellKnownScopes.HotReload. + // This processor will only handle requests made on the old scope, like old version of the runtime-test engine. + // The new processor that is handling those messages is now the ServerHotReloadProcessor. + private readonly IRemoteControlServer _remoteControlServer; public FileUpdateProcessor(IRemoteControlServer remoteControlServer) @@ -19,7 +32,7 @@ public FileUpdateProcessor(IRemoteControlServer remoteControlServer) _remoteControlServer = remoteControlServer; } - public string Scope => HotReloadConstants.TestingScopeName; + public string Scope => WellKnownScopes.Testing; public void Dispose() { @@ -37,6 +50,10 @@ public Task ProcessFrame(Frame frame) return Task.CompletedTask; } + /// + public Task ProcessIdeMessage(IdeMessage message, CancellationToken ct) + => Task.CompletedTask; + private void ProcessUpdateFile(UpdateFile? message) { if (message?.IsValid() is not true) diff --git a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/MetadataUpdates/CompilationWorkspaceProvider.cs b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/MetadataUpdates/CompilationWorkspaceProvider.cs index e41152ffa7be..96703f88b4bd 100644 --- a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/MetadataUpdates/CompilationWorkspaceProvider.cs +++ b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/MetadataUpdates/CompilationWorkspaceProvider.cs @@ -17,26 +17,12 @@ internal static class CompilationWorkspaceProvider { private static string MSBuildBasePath = ""; - public static Task<(Solution, WatchHotReloadService)> CreateWorkspaceAsync( + public static async Task<(Solution, WatchHotReloadService)> CreateWorkspaceAsync( string projectPath, IReporter reporter, string[] metadataUpdateCapabilities, Dictionary properties, - CancellationToken cancellationToken) - { - var taskCompletionSource = new TaskCompletionSource<(Solution, WatchHotReloadService)>(TaskCreationOptions.RunContinuationsAsynchronously); - CreateProject(taskCompletionSource, projectPath, reporter, metadataUpdateCapabilities, properties, cancellationToken); - - return taskCompletionSource.Task; - } - - static async void CreateProject( - TaskCompletionSource<(Solution, WatchHotReloadService)> taskCompletionSource, - string projectPath, - IReporter reporter, - string[] metadataUpdateCapabilities, - Dictionary properties, - CancellationToken cancellationToken) + CancellationToken ct) { if (properties.TryGetValue("UnoEnCLogPath", out var EnCLogPath)) { @@ -78,31 +64,30 @@ static async void CreateProject( reporter.Verbose($"MSBuildWorkspace {diag.Diagnostic}"); }; - await workspace.OpenProjectAsync(projectPath, cancellationToken: cancellationToken); + await workspace.OpenProjectAsync(projectPath, cancellationToken: ct); break; } catch (InvalidOperationException) when (i > 1) { // When we load the work space right after the app was started, it happens that it "app build" is not yet completed, preventing us to open the project. // We retry a few times to let the build complete. - await Task.Delay(5_000, cancellationToken); + await Task.Delay(5_000, ct); } } var currentSolution = workspace.CurrentSolution; var hotReloadService = new WatchHotReloadService(workspace.Services, metadataUpdateCapabilities); - await hotReloadService.StartSessionAsync(currentSolution, cancellationToken); + await hotReloadService.StartSessionAsync(currentSolution, ct); // Read the documents to memory - await Task.WhenAll( - currentSolution.Projects.SelectMany(p => p.Documents.Concat(p.AdditionalDocuments)).Select(d => d.GetTextAsync(cancellationToken))); + await Task.WhenAll(currentSolution.Projects.SelectMany(p => p.Documents.Concat(p.AdditionalDocuments)).Select(d => d.GetTextAsync(ct))); // Warm up the compilation. This would help make the deltas for first edit appear much more quickly foreach (var project in currentSolution.Projects) { - await project.GetCompilationAsync(cancellationToken); + await project.GetCompilationAsync(ct); } - taskCompletionSource.TrySetResult((currentSolution, hotReloadService)); + return (currentSolution, hotReloadService); } public static void InitializeRoslyn(string? workDir) diff --git a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.MetadataUpdate.cs b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.MetadataUpdate.cs index 3a30b799dc6f..213757f40f49 100644 --- a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.MetadataUpdate.cs +++ b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.MetadataUpdate.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Reactive.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -20,12 +21,16 @@ using Uno.Disposables; using Uno.Extensions; using Uno.UI.RemoteControl.Host.HotReload.MetadataUpdates; +using Uno.UI.RemoteControl.HotReload; using Uno.UI.RemoteControl.HotReload.Messages; +using Uno.UI.RemoteControl.Messaging.HotReload; namespace Uno.UI.RemoteControl.Host.HotReload { partial class ServerHotReloadProcessor : IServerProcessor, IDisposable { + private static readonly StringComparer _pathsComparer = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; + private FileSystemWatcher[]? _solutionWatchers; private CompositeDisposable? _solutionWatcherEventsDisposable; @@ -36,7 +41,7 @@ partial class ServerHotReloadProcessor : IServerProcessor, IDisposable private bool _useRoslynHotReload; - private void InitializeMetadataUpdater(ConfigureServer configureServer) + private bool InitializeMetadataUpdater(ConfigureServer configureServer) { _ = bool.TryParse(_remoteControlServer.GetServerConfiguration("metadata-updates"), out _useRoslynHotReload); @@ -47,6 +52,12 @@ private void InitializeMetadataUpdater(ConfigureServer configureServer) CompilationWorkspaceProvider.InitializeRoslyn(Path.GetDirectoryName(configureServer.ProjectPath)); InitializeInner(configureServer); + + return true; + } + else + { + return false; } } @@ -55,6 +66,8 @@ private void InitializeInner(ConfigureServer configureServer) => _initializeTask { try { + await Notify(HotReloadEvent.Initializing); + var result = await CompilationWorkspaceProvider.CreateWorkspaceAsync( configureServer.ProjectPath, _reporter, @@ -64,7 +77,8 @@ private void InitializeInner(ConfigureServer configureServer) => _initializeTask ObserveSolutionPaths(result.Item1); - await _remoteControlServer.SendFrame(new HotReloadWorkspaceLoadResult() { WorkspaceInitialized = true }); + await _remoteControlServer.SendFrame(new HotReloadWorkspaceLoadResult { WorkspaceInitialized = true }); + await Notify(HotReloadEvent.Ready); return result; } @@ -72,7 +86,8 @@ private void InitializeInner(ConfigureServer configureServer) => _initializeTask { Console.WriteLine($"Failed to initialize compilation workspace: {e}"); - await _remoteControlServer.SendFrame(new HotReloadWorkspaceLoadResult() { WorkspaceInitialized = false }); + await _remoteControlServer.SendFrame(new HotReloadWorkspaceLoadResult { WorkspaceInitialized = false }); + await Notify(HotReloadEvent.Disabled); throw; } @@ -115,53 +130,41 @@ private void ObserveSolutionPaths(Solution solution) foreach (var watcher in _solutionWatchers) { - // Create an observable instead of using the FromEventPattern which - // does not register to events properly. - // Renames are required for the WriteTemporary->DeleteOriginal->RenameToOriginal that - // Visual Studio uses to save files. - - var changes = Observable.Create(o => - { - - void changed(object s, FileSystemEventArgs args) => o.OnNext(args.FullPath); - void renamed(object s, RenamedEventArgs args) => o.OnNext(args.FullPath); - - watcher.Changed += changed; - watcher.Created += changed; - watcher.Renamed += renamed; - - return Disposable.Create(() => - { - watcher.Changed -= changed; - watcher.Created -= changed; - watcher.Renamed -= renamed; - }); - }); - - var disposable = changes - .Buffer(TimeSpan.FromMilliseconds(250)) - .Subscribe(filePaths => - { - ProcessMetadataChanges(filePaths.Distinct()); - }, e => Console.WriteLine($"Error {e}")); + var disposable = ToObservable(watcher).Subscribe( + filePaths => _ = ProcessMetadataChanges(filePaths.Distinct()), + e => Console.WriteLine($"Error {e}")); _solutionWatcherEventsDisposable.Add(disposable); - } } - private void ProcessMetadataChanges(IEnumerable filePaths) + private async Task ProcessMetadataChanges(IEnumerable filePaths) { - if (_useRoslynHotReload) + if (_useRoslynHotReload) // Note: Always true here?! { - foreach (var file in filePaths) + var files = filePaths.ToImmutableHashSet(_pathsComparer); + var hotReload = await StartOrContinueHotReload(files); + + try { - ProcessSolutionChanged(CancellationToken.None, file).Wait(); + // Note: We should process all files at once here! + foreach (var file in files) + { + ProcessSolutionChanged(hotReload, file, CancellationToken.None).Wait(); + } + } + catch (Exception e) + { + _reporter.Warn($"Internal error while processing hot-reload ({e.Message})."); + } + finally + { + await hotReload.CompleteUsingIntermediates(); } } } - private async Task ProcessSolutionChanged(CancellationToken cancellationToken, string file) + private async Task ProcessSolutionChanged(HotReloadOperation hotReload, string file, CancellationToken cancellationToken) { if (!await EnsureSolutionInitializedAsync() || _currentSolution is null || _hotReloadService is null) { @@ -216,10 +219,12 @@ private async Task ProcessSolutionChanged(CancellationToken cancellationTo if (diagnostics.IsDefaultOrEmpty) { await UpdateMetadata(file, updates); + hotReload.NotifyIntermediate(file, HotReloadResult.NoChanges); } else { _reporter.Output($"Got {diagnostics.Length} errors"); + hotReload.NotifyIntermediate(file, HotReloadResult.Failed); } // HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler); @@ -236,6 +241,8 @@ private async Task ProcessSolutionChanged(CancellationToken cancellationTo _reporter.Verbose(CSharpDiagnosticFormatter.Instance.Format(diagnostic, CultureInfo.InvariantCulture)); } + hotReload.NotifyIntermediate(file, HotReloadResult.RudeEdit); + // HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler); return false; } @@ -245,6 +252,7 @@ private async Task ProcessSolutionChanged(CancellationToken cancellationTo sw.Stop(); await UpdateMetadata(file, updates); + hotReload.NotifyIntermediate(file, HotReloadResult.Success); // HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler); return true; @@ -347,10 +355,10 @@ private ImmutableArray GetErrorDiagnostics(Solution solution, Cancellati } - [MemberNotNullWhen(true, nameof(_currentSolution))] + [MemberNotNullWhen(true, nameof(_currentSolution), nameof(_hotReloadService))] private async ValueTask EnsureSolutionInitializedAsync() { - if (_currentSolution != null) + if (_currentSolution is not null && _hotReloadService is not null) { return true; } diff --git a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs index 3f717cc25837..32c9ba00655e 100644 --- a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs +++ b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs @@ -1,16 +1,22 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reactive.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Elfie.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Uno.Disposables; using Uno.Extensions; using Uno.UI.RemoteControl.HotReload.Messages; +using Uno.UI.RemoteControl.Messaging.HotReload; +using Uno.UI.RemoteControl.Messaging.IdeChannel; [assembly: Uno.UI.RemoteControl.Host.ServerProcessorAttribute(typeof(Uno.UI.RemoteControl.Host.HotReload.ServerHotReloadProcessor))] @@ -27,23 +33,334 @@ public ServerHotReloadProcessor(IRemoteControlServer remoteControlServer) _remoteControlServer = remoteControlServer; } - public string Scope => HotReloadConstants.HotReload; + public string Scope => WellKnownScopes.HotReload; - public Task ProcessFrame(Frame frame) + public async Task ProcessFrame(Frame frame) { switch (frame.Name) { case ConfigureServer.Name: - ProcessConfigureServer(JsonConvert.DeserializeObject(frame.Content)!); + ProcessConfigureServer(frame.GetContent()); break; case XamlLoadError.Name: - ProcessXamlLoadError(JsonConvert.DeserializeObject(frame.Content)!); + ProcessXamlLoadError(frame.GetContent()); break; + case UpdateFile.Name: + await ProcessUpdateFile(frame.GetContent()); + break; + } + } + + /// + public async Task ProcessIdeMessage(IdeMessage message, CancellationToken ct) + { + switch (message) + { + case HotReloadRequestedIdeMessage hrRequested: + // Note: For now the IDE will notify the ProcessingFiles only in case of force hot reload request sent by client! + await Notify(HotReloadEvent.ProcessingFiles, HotReloadEventSource.IDE); + if (_pendingHotReloadRequestToIde.TryGetValue(hrRequested.RequestId, out var request)) + { + request.TrySetResult(hrRequested.Result); + } + break; + + case HotReloadEventIdeMessage evt: + await Notify(evt.Event, HotReloadEventSource.IDE); + break; + } + } + + #region Hot-relaod state + private HotReloadState _globalState; // This actually contains only the initializing stat (i.e. Disabled, Initializing, Idle). Processing state is _current != null. + private HotReloadOperation? _current; // I.e. head of the operation chain list + + public enum HotReloadEventSource + { + IDE, + DevServer + } + + private async ValueTask EnsureHotReloadStarted() + { + if (_current is null) + { + await StartHotReload(null); + } + } + + private async ValueTask StartHotReload(ImmutableHashSet? filesPaths) + { + var previous = _current; + HotReloadOperation? current, @new; + while (true) + { + @new = new HotReloadOperation(this, previous, filesPaths); + current = Interlocked.CompareExchange(ref _current, @new, previous); + if (current == previous) + { + break; + } + else + { + previous = current; + } + } + + // Notify the start of new hot-reload operation + await SendUpdate(); + + return @new; + } + + private async ValueTask StartOrContinueHotReload(ImmutableHashSet? filesPaths = null) + => _current is { } current && (filesPaths is null || current.TryMerge(filesPaths)) + ? current + : await StartHotReload(filesPaths); + + private ValueTask AbortHotReload() + => _current?.Complete(HotReloadResult.Aborted) ?? SendUpdate(); + + private async ValueTask Notify(HotReloadEvent evt, HotReloadEventSource source = HotReloadEventSource.DevServer) + { + switch (evt) + { + // Global state events + case HotReloadEvent.Disabled: + _globalState = HotReloadState.Disabled; + await AbortHotReload(); + break; + + case HotReloadEvent.Initializing: + _globalState = HotReloadState.Initializing; + await SendUpdate(); + break; + + case HotReloadEvent.Ready: + _globalState = HotReloadState.Idle; + await SendUpdate(); + break; + + // Pending hot-reload events + case HotReloadEvent.ProcessingFiles: + await EnsureHotReloadStarted(); + break; + + case HotReloadEvent.Completed: + await (await StartOrContinueHotReload()).DeferComplete(HotReloadResult.Success); + break; + + case HotReloadEvent.NoChanges: + await (await StartOrContinueHotReload()).Complete(HotReloadResult.NoChanges); + break; + case HotReloadEvent.Failed: + await (await StartOrContinueHotReload()).Complete(HotReloadResult.Failed); + break; + + case HotReloadEvent.RudeEdit: + case HotReloadEvent.RudeEditDialogButton: + await (await StartOrContinueHotReload()).Complete(HotReloadResult.RudeEdit); + break; + } + } + + private async ValueTask SendUpdate(HotReloadOperation? completing = null) + { + var state = _globalState; + var operations = ImmutableList.Empty; + + if (state is not HotReloadState.Disabled && (_current ?? completing) is { } current) + { + var infos = ImmutableList.CreateBuilder(); + var foundCompleting = completing is null; + LoadInfos(current); + if (!foundCompleting) + { + LoadInfos(completing); + } + + operations = infos.ToImmutable(); + + void LoadInfos(HotReloadOperation? operation) + { + while (operation is not null) + { + if (operation.Result is null) + { + state = HotReloadState.Processing; + } + + foundCompleting |= operation == completing; + infos.Add(new(operation.Id, operation.FilePaths, operation.Result)); + operation = operation.Previous!; + } + } } - return Task.CompletedTask; + await _remoteControlServer.SendFrame(new HotReloadStatusMessage(state, operations)); } + /// + /// A hot-reload operation that is in progress. + /// + private class HotReloadOperation + { + // Delay to wait without any update to consider operation was aborted. + private static readonly TimeSpan _timeoutDelay = TimeSpan.FromSeconds(30); + + private static readonly ImmutableHashSet _empty = ImmutableHashSet.Empty.WithComparer(_pathsComparer); + private static long _count; + + private readonly ServerHotReloadProcessor _owner; + private readonly HotReloadOperation? _previous; + private readonly Timer _timeout; + + private ImmutableHashSet _filePaths; + private int /* HotReloadResult */ _result = -1; + private CancellationTokenSource? _deferredCompletion; + + public long Id { get; } = Interlocked.Increment(ref _count); + + public HotReloadOperation? Previous => _previous; + + public ImmutableHashSet FilePaths => _filePaths; + + public HotReloadResult? Result => _result is -1 ? null : (HotReloadResult)_result; + + /// The previous hot-reload operation which has to be considered as aborted when this new one completes. + public HotReloadOperation(ServerHotReloadProcessor owner, HotReloadOperation? previous, ImmutableHashSet? filePaths = null) + { + _owner = owner; + _previous = previous; + _filePaths = filePaths ?? _empty; + + _timeout = new Timer( + static that => _ = ((HotReloadOperation)that!).Complete(HotReloadResult.Aborted), + this, + _timeoutDelay, + Timeout.InfiniteTimeSpan); + } + + /// + /// Attempts to update the if we determine that the provided paths are corresponding to this operation. + /// + /// + /// True if this operation should be considered as valid for the given file paths (and has been merged with original paths), + /// false if the given paths does not belong to this operation. + /// + public bool TryMerge(ImmutableHashSet filePaths) + { + if (_result is not -1) + { + return false; + } + + var original = _filePaths; + while (true) + { + ImmutableHashSet updated; + if (original.IsEmpty) + { + updated = filePaths; + } + else if (original.Any(filePaths.Contains)) + { + updated = original.Union(filePaths); + } + else + { + return false; + } + + var current = Interlocked.CompareExchange(ref _filePaths, updated, original); + if (current == original) + { + _timeout.Change(_timeoutDelay, Timeout.InfiniteTimeSpan); + return true; + } + else + { + original = current; + } + } + } + + // Note: This is a patch until the dev-server based hot-reload treat files per batch instead of file per file. + private HotReloadResult _aggregatedResult = HotReloadResult.NoChanges; + private int _aggregatedFilesCount; + public void NotifyIntermediate(string file, HotReloadResult result) + { + if (Interlocked.Increment(ref _aggregatedFilesCount) is 1) + { + _aggregatedResult = result; + return; + } + + _aggregatedResult = (HotReloadResult)Math.Max((int)_aggregatedResult, (int)result); + _timeout.Change(_timeoutDelay, Timeout.InfiniteTimeSpan); + } + + public async ValueTask CompleteUsingIntermediates() + { + Debug.Assert(_aggregatedFilesCount == _filePaths.Count); + await Complete(_aggregatedResult); + } + + /// + /// As errors might get a bit after the complete from the IDE, we can defer the completion of the operation. + /// + public async ValueTask DeferComplete(HotReloadResult result, Exception? exception = null) + { + Debug.Assert(result != HotReloadResult.InternalError || exception is not null); // For internal error we should always provide an exception! + + if (Interlocked.CompareExchange(ref _deferredCompletion, new CancellationTokenSource(), null) is null) + { + _timeout.Change(_timeoutDelay, Timeout.InfiniteTimeSpan); + await Task.Delay(TimeSpan.FromSeconds(1), _deferredCompletion.Token); + if (!_deferredCompletion.IsCancellationRequested) + { + await Complete(result, exception); + } + } + } + + public ValueTask Complete(HotReloadResult result, Exception? exception = null) + => Complete(result, exception, isFromNext: false); + + private async ValueTask Complete(HotReloadResult result, Exception? exception, bool isFromNext) + { + Debug.Assert(result != HotReloadResult.InternalError || exception is not null); // For internal error we should always provide an exception! + + // Remove this from current + Interlocked.CompareExchange(ref _owner._current, null, this); + _deferredCompletion?.Cancel(false); // No matter if already completed + + // Check if not already disposed + if (Interlocked.CompareExchange(ref _result, (int)result, -1) is not -1) + { + return; // Already completed + } + + await _timeout.DisposeAsync(); + + // Consider previous hot-reload operation(s) as aborted (this is actually a chain list) + if (_previous is not null) + { + await _previous.Complete( + HotReloadResult.Aborted, + new TimeoutException("An more recent hot-reload operation has completed."), + isFromNext: true); + } + + if (!isFromNext) // Only the head of the list should request update + { + await _owner.SendUpdate(this); + } + } + } + #endregion + + #region XamlLoadError private void ProcessXamlLoadError(XamlLoadError xamlLoadError) { if (this.Log().IsEnabled(LogLevel.Error)) @@ -54,7 +371,9 @@ private void ProcessXamlLoadError(XamlLoadError xamlLoadError) $"{xamlLoadError.StackTrace}"); } } + #endregion + #region ConfigureServer private void ProcessConfigureServer(ConfigureServer configureServer) { if (this.Log().IsEnabled(LogLevel.Debug)) @@ -63,7 +382,11 @@ private void ProcessConfigureServer(ConfigureServer configureServer) this.Log().LogDebug($"Xaml Search Paths: {string.Join(", ", configureServer.XamlPaths)}"); } - InitializeMetadataUpdater(configureServer); + if (!InitializeMetadataUpdater(configureServer)) + { + // We are relying on IDE (or XAML only), we won't have any other hot-reload initialization steps. + _ = Notify(HotReloadEvent.Ready); + } _watchers = configureServer.XamlPaths .Select(p => new FileSystemWatcher @@ -84,32 +407,8 @@ private void ProcessConfigureServer(ConfigureServer configureServer) foreach (var watcher in _watchers) { - // Create an observable instead of using the FromEventPattern which - // does not register to events properly. - // Renames are required for the WriteTemporary->DeleteOriginal->RenameToOriginal that - // Visual Studio uses to save files. - - var changes = Observable.Create(o => - { - - void changed(object s, FileSystemEventArgs args) => o.OnNext(args.FullPath); - void renamed(object s, RenamedEventArgs args) => o.OnNext(args.FullPath); - - watcher.Changed += changed; - watcher.Created += changed; - watcher.Renamed += renamed; - - return Disposable.Create(() => - { - watcher.Changed -= changed; - watcher.Created -= changed; - watcher.Renamed -= renamed; - }); - }); - - var disposable = changes - .Buffer(TimeSpan.FromMilliseconds(250)) - .Subscribe(filePaths => + var disposable = ToObservable(watcher).Subscribe( + filePaths => { var files = filePaths .Distinct() @@ -117,31 +416,135 @@ private void ProcessConfigureServer(ConfigureServer configureServer) Path.GetExtension(f).Equals(".xaml", StringComparison.OrdinalIgnoreCase) || Path.GetExtension(f).Equals(".cs", StringComparison.OrdinalIgnoreCase)); - foreach (var file in filePaths) + foreach (var file in files) { OnSourceFileChanged(file); } - }, e => Console.WriteLine($"Error {e}")); + }, + e => Console.WriteLine($"Error {e}")); _watcherEventsDisposable.Add(disposable); } + + void OnSourceFileChanged(string fullPath) + => Task.Run(async () => + { + if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().LogDebug($"File {fullPath} changed"); + } + + await _remoteControlServer.SendFrame( + new FileReload + { + Content = File.ReadAllText(fullPath), + FilePath = fullPath + }); + }); } + #endregion - private void OnSourceFileChanged(string fullPath) - => Task.Run(async () => + #region UpdateFile + private readonly ConcurrentDictionary> _pendingHotReloadRequestToIde = new(); + + private async Task ProcessUpdateFile(UpdateFile message) + { + var hotReload = await StartHotReload(ImmutableHashSet.Empty.Add(Path.GetFullPath(message.FilePath))); + + try { + var (result, error) = DoUpdateFile(); + if ((int)result < 300 && !message.IsForceHotReloadDisabled) + { + await RequestHotReloadToIde(hotReload.Id); + } + + await _remoteControlServer.SendFrame(new UpdateFileResponse(message.RequestId, message.FilePath, result, error, hotReload.Id)); + } + catch (Exception ex) + { + await hotReload.Complete(HotReloadResult.InternalError, ex); + await _remoteControlServer.SendFrame(new UpdateFileResponse(message.RequestId, message.FilePath, FileUpdateResult.Failed, ex.Message)); + } + + (FileUpdateResult, string?) DoUpdateFile() + { + if (message?.IsValid() is not true) + { + if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().LogDebug($"Got an invalid update file frame ({message})"); + } + + return (FileUpdateResult.BadRequest, "Invalid request"); + } + + if (!File.Exists(message.FilePath)) + { + if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().LogDebug($"Requested file '{message.FilePath}' does not exists."); + } + + return (FileUpdateResult.FileNotFound, $"Requested file '{message.FilePath}' does not exists."); + } + if (this.Log().IsEnabled(LogLevel.Debug)) { - this.Log().LogDebug($"File {fullPath} changed"); + this.Log().LogDebug($"Apply Changes to {message.FilePath}"); + } + + var originalContent = File.ReadAllText(message.FilePath); + if (this.Log().IsEnabled(LogLevel.Trace)) + { + this.Log().LogTrace($"Original content: {message.FilePath}"); + } + + var updatedContent = originalContent.Replace(message.OldText, message.NewText); + if (this.Log().IsEnabled(LogLevel.Trace)) + { + this.Log().LogTrace($"Updated content: {message.FilePath}"); } - await _remoteControlServer.SendFrame( - new FileReload() + if (updatedContent == originalContent) + { + if (this.Log().IsEnabled(LogLevel.Debug)) { - Content = File.ReadAllText(fullPath), - FilePath = fullPath - }); - }); + this.Log().LogDebug($"No changes detected in {message.FilePath}"); + } + + return (FileUpdateResult.NoChanges, null); + } + + File.WriteAllText(message.FilePath, updatedContent); + return (FileUpdateResult.Success, null); + } + } + + private async Task RequestHotReloadToIde(long sequenceId) + { + var hrRequest = new ForceHotReloadIdeMessage(sequenceId); + var hrRequested = new TaskCompletionSource(); + + try + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + await using var ctReg = cts.Token.Register(() => hrRequested.TrySetCanceled()); + + await _remoteControlServer.SendMessageToIDEAsync(hrRequest); + + return await hrRequested.Task is { IsSuccess: true }; + } + catch (Exception) + { + return false; + } + finally + { + _pendingHotReloadRequestToIde.TryRemove(hrRequest.CorrelationId, out _); + } + } + #endregion public void Dispose() { @@ -166,5 +569,30 @@ public void Dispose() _hotReloadService?.EndSession(); } + + #region Helpers + private static IObservable> ToObservable(FileSystemWatcher watcher) + => Observable.Create(o => + { + // Create an observable instead of using the FromEventPattern which + // does not register to events properly. + // Renames are required for the WriteTemporary->DeleteOriginal->RenameToOriginal that + // Visual Studio uses to save files. + + void changed(object s, FileSystemEventArgs args) => o.OnNext(args.FullPath); + void renamed(object s, RenamedEventArgs args) => o.OnNext(args.FullPath); + + watcher.Changed += changed; + watcher.Created += changed; + watcher.Renamed += renamed; + + return Disposable.Create(() => + { + watcher.Changed -= changed; + watcher.Created -= changed; + watcher.Renamed -= renamed; + }); + }).Buffer(TimeSpan.FromMilliseconds(250)); + #endregion } } diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs index 9542dc19a7bf..71d069a67f1c 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs @@ -1,4 +1,5 @@ -#nullable enable + +#nullable enable using System; using System.Collections.Generic; @@ -141,6 +142,16 @@ private string[] GetMetadataUpdateCapabilities() return Array.Empty(); } + private enum HotReloadSource + { + Runtime, + DevServer, + Manual + } +#pragma warning disable CS0414 // Field is assigned but its value is never used + private static HotReloadSource _source; +#pragma warning restore CS0414 // Field is assigned but its value is never used + private void AssemblyReload(AssemblyDeltaReload assemblyDeltaReload) { try @@ -174,6 +185,7 @@ private void AssemblyReload(AssemblyDeltaReload assemblyDeltaReload) UpdatedTypes = ReadIntArray(changedTypesReader) }; + _source = HotReloadSource.DevServer; _agent?.ApplyDeltas(new[] { delta }); if (this.Log().IsEnabled(LogLevel.Trace)) @@ -196,6 +208,10 @@ private void AssemblyReload(AssemblyDeltaReload assemblyDeltaReload) this.Log().Error($"An exception occurred when applying IL Delta for {assemblyDeltaReload.FilePath} ({assemblyDeltaReload.ModuleId})", e); } } + finally + { + _source = default; // runtime + } } static int[] ReadIntArray(BinaryReader binaryReader) diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs index 3c3b5b1aced2..a4e86740a634 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -17,6 +18,9 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; +using Uno.Diagnostics.UI; +using static Microsoft.UI.Xaml.Markup.Reader.XamlConstants; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace Uno.UI.RemoteControl.HotReload; @@ -27,6 +31,7 @@ partial class ClientHotReloadProcessor private static ElementUpdateAgent? _elementAgent; private static Logger _log = typeof(ClientHotReloadProcessor).Log(); + private static Window? _currentWindow; private static ElementUpdateAgent ElementAgent { @@ -65,7 +70,33 @@ private static async Task ShouldReload() } } - internal static Window? CurrentWindow { get; set; } + internal static Window? CurrentWindow + { + get => _currentWindow; + set + { + if (_currentWindow is not null) + { + _currentWindow.Activated -= ShowDiagnosticsOnFirstActivation; + } + + _currentWindow = value; + + if (_currentWindow is not null) + { + _currentWindow.Activated += ShowDiagnosticsOnFirstActivation; + } + } + } + + private static void ShowDiagnosticsOnFirstActivation(object snd, WindowActivatedEventArgs windowActivatedEventArgs) + { + if (snd is Window { RootElement.XamlRoot: { } xamlRoot } window) + { + window.Activated -= ShowDiagnosticsOnFirstActivation; + DiagnosticsOverlay.Get(xamlRoot).IsVisible = true; + } + } private static async Task ReloadWithUpdatedTypes(Type[] updatedTypes) { @@ -382,8 +413,30 @@ private static void ReplaceViewInstance(UIElement instance, Type replacementType } } + /// + /// Forces a hot reload update + /// + public static void ForceHotReloadUpdate() + { + try + { + _source = HotReloadSource.Manual; + UpdateApplication(Array.Empty()); + } + finally + { + _source = default; + } + } + + /// + /// Entry point for .net MetadataUpdateHandler, do not use directly. + /// + [EditorBrowsable(EditorBrowsableState.Never)] public static void UpdateApplication(Type[] types) { + // TODO: Diag.Report --> Real handler or force reload + foreach (var type in types) { try @@ -401,9 +454,9 @@ public static void UpdateApplication(Type[] types) } } catch (Exception error) - { - if (_log.IsEnabled(LogLevel.Error)) { + if (_log.IsEnabled(LogLevel.Error)) + { _log.Error($"Error while processing MetadataUpdateOriginalTypeAttribute for {type}", error); } } diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs index c5b00452cb1b..3c0fe0a86163 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs @@ -1,17 +1,13 @@ -using System; +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Reflection; -using System.Text; +using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; -using Uno.Extensions; using Uno.Foundation.Logging; using Uno.UI.RemoteControl.HotReload.Messages; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Markup; +using Uno.Diagnostics.UI; namespace Uno.UI.RemoteControl.HotReload; @@ -20,6 +16,7 @@ public partial class ClientHotReloadProcessor : IClientProcessor private string? _projectPath; private string[]? _xamlPaths; private readonly IRemoteControlClient _rcClient; + private readonly DiagnosticView _diagView = DiagnosticView.Register("Hot reload", (view, status) => view.Update(status)); private HotReloadMode? _forcedHotReloadMode; private Dictionary? _msbuildProperties; @@ -31,7 +28,7 @@ public ClientHotReloadProcessor(IRemoteControlClient rcClient) partial void InitializeMetadataUpdater(); - string IRemoteControlProcessor.Scope => HotReloadConstants.HotReload; + string IClientProcessor.Scope => WellKnownScopes.HotReload; public async Task Initialize() => await ConfigureServer(); @@ -41,15 +38,19 @@ public async Task ProcessFrame(Messages.Frame frame) switch (frame.Name) { case AssemblyDeltaReload.Name: - AssemblyReload(JsonConvert.DeserializeObject(frame.Content)!); + AssemblyReload(frame.GetContent()); break; case FileReload.Name: - await ProcessFileReload(JsonConvert.DeserializeObject(frame.Content)!); + await ProcessFileReload(frame.GetContent()); break; case HotReloadWorkspaceLoadResult.Name: - WorkspaceLoadResult(JsonConvert.DeserializeObject(frame.Content)!); + WorkspaceLoadResult(frame.GetContent()); + break; + + case HotReloadStatusMessage.Name: + await ProcessStatus(frame.GetContent()); break; default: @@ -59,8 +60,6 @@ public async Task ProcessFrame(Messages.Frame frame) } break; } - - return; } private async Task ProcessFileReload(HotReload.Messages.FileReload fileReload) @@ -80,6 +79,7 @@ _forcedHotReloadMode is null } } + #region Configure hot-reload private async Task ConfigureServer() { var assembly = _rcClient.AppType.Assembly; @@ -155,4 +155,10 @@ private string GetMSBuildProperty(string property, string defaultValue = "") return output; } + #endregion + + private async Task ProcessStatus(HotReloadStatusMessage status) + { + _diagView.Update(status); + } } diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadMode.cs b/src/Uno.UI.RemoteControl/HotReload/HotReloadMode.cs index d74417f72450..dee71d903f9d 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadMode.cs +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadMode.cs @@ -10,11 +10,17 @@ internal enum HotReloadMode /// /// Hot reload using Metadata updates /// + /// This can be metadata-updates pushed by either VS or the dev-server. MetadataUpdates, /// - /// Hot Reload using partial updated types discovery + /// Hot Reload using partial updated types discovery. /// + /// + /// In some cases application's MetadataUpdateHandlers are not invoked by the IDE. + /// When this mode is active, application listen for FileReload (a.k.a. FileUpdated) messages, enumerates (after a small delay) all types loaded in the application to detect changes + /// and invokes the MetadataUpdateHandlers **for types flags with the CreateNewOnMetadataUpdateAttribute**. + /// Partial, /// diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml new file mode 100644 index 000000000000..7218a2a4bedd --- /dev/null +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml.cs b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml.cs new file mode 100644 index 000000000000..40856caf59d5 --- /dev/null +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Uno.UI.RemoteControl.HotReload.Messages; + +namespace Uno.UI.RemoteControl.HotReload; + +internal sealed partial class HotReloadStatusView : UserControl +{ + private (long id, string state) _currentResult = (-1, "None"); + + public HotReloadStatusView() + { + InitializeComponent(); + } + + public void Update(HotReloadStatusMessage? status) + { + ToolTipService.SetToolTip(Root, GetStatusSummary(status)); + + if (status is null) + { + return; + } + + VisualStateManager.GoToState(this, GetStatusVisualState(status), true); + if (GetResultVisualState(status) is { } resultState) + { + VisualStateManager.GoToState(this, resultState, true); + } + } + + public static string GetStatusSummary(HotReloadStatusMessage? status) + => status?.State switch + { + HotReloadState.Disabled => "Hot-reload is disable.", + HotReloadState.Initializing => "Hot-reload is initializing.", + HotReloadState.Idle => "Hot-reload server is ready and listing for file changes.", + HotReloadState.Processing => "Hot-reload server is processing file changes", + _ => "Unable to determine the state of the hot-reload server." + }; + + private static string GetStatusVisualState(HotReloadStatusMessage status) + => status.State switch + { + HotReloadState.Disabled => "Disabled", + HotReloadState.Initializing => "Initializing", + HotReloadState.Idle => "Idle", + HotReloadState.Processing => "Processing", + _ => "Unknown" + }; + + private string? GetResultVisualState(HotReloadStatusMessage status) + { + var op = status.Operations.MaxBy(op => op.Id); + if (op is null) + { + return null; // No state change + } + + var updated = (op.Id, GetStateName(op)); + if (_currentResult == updated) + { + return null; // No state change + } + + _currentResult = updated; + return _currentResult.state; + + static string GetStateName(HotReloadOperationInfo op) + => op.Result switch + { + null => "None", + HotReloadResult.NoChanges => "Success", + HotReloadResult.Success => "Success", + _ => "Failed" + }; + } +} diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/AssemblyDeltaReload.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/AssemblyDeltaReload.cs index 8cf7a6ab64a4..acf13b6daa24 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/AssemblyDeltaReload.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/AssemblyDeltaReload.cs @@ -26,7 +26,7 @@ internal class AssemblyDeltaReload : IMessage public string? UpdatedTypes { get; set; } [JsonIgnore] - public string Scope => HotReloadConstants.HotReload; + public string Scope => WellKnownScopes.HotReload; [JsonIgnore] string IMessage.Name => Name; diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/ConfigureServer.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/ConfigureServer.cs index 727973e888c5..98427b522f6c 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/ConfigureServer.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/ConfigureServer.cs @@ -28,7 +28,7 @@ public ConfigureServer(string projectPath, string[] xamlPaths, string[] metadata public bool EnableMetadataUpdates { get; set; } - public string Scope => HotReloadConstants.HotReload; + public string Scope => WellKnownScopes.HotReload; string IMessage.Name => Name; diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/FileReload.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/FileReload.cs index 2acbef991e5b..e981104fbd56 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/FileReload.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/FileReload.cs @@ -4,7 +4,11 @@ namespace Uno.UI.RemoteControl.HotReload.Messages { - internal class FileReload : IMessage + /// + /// Message sent by the dev-server when it detects a file change in the solution. + /// + /// This is being sent only for xaml and cs files. + internal class FileReload : IMessage // a.k.a. FileUpdated { public const string Name = nameof(FileReload); @@ -15,7 +19,7 @@ internal class FileReload : IMessage public string? Content { get; set; } [JsonIgnore] - public string Scope => HotReloadConstants.HotReload; + public string Scope => WellKnownScopes.HotReload; [JsonIgnore] string IMessage.Name => Name; diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadConstants.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadConstants.cs deleted file mode 100644 index a73be4a088e2..000000000000 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadConstants.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Uno.UI.RemoteControl.HotReload.Messages -{ - internal class HotReloadConstants - { - public const string TestingScopeName = "UnoRuntimeTests"; - public const string HotReload = "hotreload"; - } -} diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadResult.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadResult.cs new file mode 100644 index 000000000000..1ac7d3736454 --- /dev/null +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadResult.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq; + +namespace Uno.UI.RemoteControl.HotReload.Messages; + +/// +/// The result of an hot-reload operation. +/// +public enum HotReloadResult +{ + /// + /// Hot-reload completed with no changes. + /// + NoChanges = 0, + + /// + /// Successful hot-reload. + /// + Success = 1, + + /// + /// Cannot hot-reload due to rude edit. + /// + RudeEdit = 2, + + /// + /// Cannot hot-reload due to compilation errors. + /// + Failed = 3, + + /// + /// We didn't get any response for that hot-reload operation, result might or might not have been sent to app. + /// + Aborted = 256, + + /// + /// The dev-server failed to process the hot-reload sequence. + /// + InternalError = 512 +} diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadState.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadState.cs new file mode 100644 index 000000000000..fad733d8e646 --- /dev/null +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadState.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; + +namespace Uno.UI.RemoteControl.HotReload.Messages; + +public enum HotReloadState +{ + /// + /// Hot reload is disabled. + /// Usually this indicates that the server failed to load the workspace. + /// + Disabled = -1, + + /// + /// The server is initializing. + /// Usually this indicates that the server is loading the workspace. + /// + Initializing = 0, + + /// + /// Indicates that the IDE/server is ready to process changes. + /// + Idle = 1, + + /// + /// The IDE/server is computing changeset. + /// + Processing = 2 +} diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs new file mode 100644 index 000000000000..ed3810fd5c31 --- /dev/null +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using Newtonsoft.Json; + +namespace Uno.UI.RemoteControl.HotReload.Messages; + +public record HotReloadStatusMessage( + [property: JsonProperty] HotReloadState State, + [property: JsonProperty] IImmutableList Operations) + : IMessage +{ + public const string Name = nameof(HotReloadStatusMessage); + + /// + [JsonProperty] + public string Scope => WellKnownScopes.HotReload; + + /// + [JsonProperty] + string IMessage.Name => Name; +} + +public record HotReloadOperationInfo(long Id, ImmutableHashSet FilePaths, HotReloadResult? Result); diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadWorkspaceLoadResult.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadWorkspaceLoadResult.cs index c5758f82880d..bc907b6f07b2 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadWorkspaceLoadResult.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadWorkspaceLoadResult.cs @@ -12,7 +12,7 @@ internal class HotReloadWorkspaceLoadResult : IMessage public bool WorkspaceInitialized { get; set; } [JsonIgnore] - public string Scope => HotReloadConstants.HotReload; + public string Scope => WellKnownScopes.HotReload; [JsonIgnore] string IMessage.Name => Name; diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/UpdateFile.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/UpdateFile.cs index 3d7f193c6e86..61ed1aae3901 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/UpdateFile.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/UpdateFile.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using Uno.Extensions; +using Uno.UI.RemoteControl.Messaging.IdeChannel; namespace Uno.UI.RemoteControl.HotReload.Messages; @@ -9,6 +10,12 @@ public class UpdateFile : IMessage { public const string Name = nameof(UpdateFile); + /// + /// ID of this file update request. + /// + [JsonProperty] + public string RequestId { get; } = Guid.NewGuid().ToString(); + [JsonProperty] public string FilePath { get; set; } = string.Empty; @@ -18,8 +25,14 @@ public class UpdateFile : IMessage [JsonProperty] public string NewText { get; set; } = string.Empty; + /// + /// Disable the forced hot-reload requested on VS after the file has been modified. + /// + [JsonProperty] + public bool IsForceHotReloadDisabled { get; set; } + [JsonIgnore] - public string Scope => HotReloadConstants.TestingScopeName; + public string Scope => WellKnownScopes.HotReload; [JsonIgnore] string IMessage.Name => Name; @@ -30,3 +43,14 @@ public bool IsValid() OldText is not null && NewText is not null; } + +public enum FileUpdateResult +{ + Success = 200, + NoChanges = 204, + BadRequest = 400, + FileNotFound = 404, + Failed = 500, + FailedToRequestHotReload = 502, + NotAvailable = 503 +} diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/UpdateFileResponse.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/UpdateFileResponse.cs new file mode 100644 index 000000000000..3cfbc3ec4b37 --- /dev/null +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/UpdateFileResponse.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using Newtonsoft.Json; + +namespace Uno.UI.RemoteControl.HotReload.Messages; + +/// +/// In response to a request. +/// +/// of the request. +/// Actual path of the modified file. +/// Result of the edition. +/// Optional correlation ID of pending hot-reload operation. Null if we don't expect this file update to produce any hot-reload result. +public sealed record UpdateFileResponse( + [property: JsonProperty] string RequestId, + [property: JsonProperty] string FilePath, + [property: JsonProperty] FileUpdateResult Result, + [property: JsonProperty] string? Error = null, + [property: JsonProperty] long? HotReloadCorrelationId = null) : IMessage +{ + public const string Name = nameof(UpdateFileResponse); + + [JsonIgnore] + string IMessage.Scope => WellKnownScopes.HotReload; + + [JsonIgnore] + string IMessage.Name => Name; +} diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/XamlLoadError.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/XamlLoadError.cs index ae1938ca707e..2f83b05402fa 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/XamlLoadError.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/XamlLoadError.cs @@ -16,7 +16,7 @@ public XamlLoadError(string filePath, string message, string? stackTrace, string StackTrace = stackTrace; } - public string Scope => HotReloadConstants.HotReload; + public string Scope => WellKnownScopes.HotReload; string IMessage.Name => Name; diff --git a/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/HotReloadAgent.cs b/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/HotReloadAgent.cs index 798a2737735b..0c8f52a03f16 100644 --- a/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/HotReloadAgent.cs +++ b/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/HotReloadAgent.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Reflection; using Uno; +using Uno.UI.Helpers; namespace Uno.UI.RemoteControl.HotReload.MetadataUpdater; @@ -75,22 +76,21 @@ internal UpdateHandlerActions GetMetadataUpdateHandlerActions() { foreach (var attr in assembly.GetCustomAttributesData()) { - // Look up the attribute by name rather than by type. This would allow netstandard targeting libraries to - // define their own copy without having to cross-compile. - if (attr.AttributeType.FullName != "System.Reflection.Metadata.MetadataUpdateHandlerAttribute") + // Look up the attribute by name rather than by type. + // This would allow netstandard targeting libraries to define their own copy without having to cross-compile. + if (attr is not { AttributeType.FullName: "System.Reflection.Metadata.MetadataUpdateHandlerAttribute" }) { continue; } - IList ctorArgs = attr.ConstructorArguments; - if (ctorArgs.Count != 1 || - ctorArgs[0].Value is not Type handlerType) + if (attr is { ConstructorArguments: [{ Value: Type handlerType }] }) + { + GetHandlerActions(handlerActions, handlerType); + } + else { _log($"'{attr}' found with invalid arguments."); - continue; } - - GetHandlerActions(handlerActions, handlerType); } } catch (Exception e) @@ -112,13 +112,13 @@ internal void GetHandlerActions( { bool methodFound = false; - if (GetUpdateMethod(handlerType, "ClearCache") is MethodInfo clearCache) + if (GetMethod(handlerType, "ClearCache") is MethodInfo clearCache) { handlerActions.ClearCache.Add(CreateAction(clearCache)); methodFound = true; } - if (GetUpdateMethod(handlerType, "UpdateApplication") is MethodInfo updateApplication) + if (GetMethod(handlerType, "UpdateApplication") is MethodInfo updateApplication) { handlerActions.UpdateApplication.Add(CreateAction(updateApplication)); methodFound = true; @@ -150,9 +150,7 @@ internal void GetHandlerActions( }; } - MethodInfo? GetUpdateMethod( - [DynamicallyAccessedMembers(HotReloadHandlerLinkerFlags)] - Type handlerType, string name) + MethodInfo? GetMethod([DynamicallyAccessedMembers(HotReloadHandlerLinkerFlags)] Type handlerType, string name) { if (handlerType.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, new[] { typeof(Type[]) }, null) is MethodInfo updateMethod && updateMethod.ReturnType == typeof(void)) @@ -160,7 +158,7 @@ internal void GetHandlerActions( return updateMethod; } - foreach (MethodInfo method in handlerType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)) + foreach (var method in handlerType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)) { if (method.Name == name) { @@ -237,6 +235,8 @@ public void ApplyDeltas(IReadOnlyList deltas) Type[]? updatedTypes = GetMetadataUpdateTypes(deltas); + // TODO: Diag report --> dev-server update + handlerActions.ClearCache.ForEach(a => a(updatedTypes)); handlerActions.UpdateApplication.ForEach(a => a(updatedTypes)); diff --git a/src/Uno.UI.RemoteControl/HotReload/WindowExtensions.cs b/src/Uno.UI.RemoteControl/HotReload/WindowExtensions.cs index 9175bba2e24a..0f8c814e2bc6 100644 --- a/src/Uno.UI.RemoteControl/HotReload/WindowExtensions.cs +++ b/src/Uno.UI.RemoteControl/HotReload/WindowExtensions.cs @@ -1,6 +1,7 @@ using System; using Uno.UI.RemoteControl.HotReload; using Microsoft.UI.Xaml; +using Uno.Diagnostics.UI; namespace Uno.UI; @@ -21,6 +22,6 @@ public static class WindowExtensions /// /// The window of the application to be updated /// Currently this method doesn't use the window instance. However, with the addition of multi-window - /// support it's likely that the instance will be needed to deterine the window where updates will be applied - public static void ForceHotReloadUpdate(this Window window) => ClientHotReloadProcessor.UpdateApplication(Array.Empty()); + /// support it's likely that the instance will be needed to determine the window where updates will be applied + public static void ForceHotReloadUpdate(this Window window) => ClientHotReloadProcessor.ForceHotReloadUpdate(); } diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj index 2435415a4a9d..48fb485f7feb 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj @@ -41,6 +41,7 @@ + @@ -78,4 +79,10 @@ + + $(MSBuildThisFileDirectory)..\SourceGenerators\Uno.UI.Tasks\bin\$(Configuration)_Shadow + + + +