From 9b5b00f9ebcc2692b43aa87f150966caa3f6b116 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 14 May 2024 11:18:37 -0400 Subject: [PATCH 01/27] fix: Fix possible exception when implementing custom version of HR attributes --- ...ClientHotReloadProcessor.MetadataUpdate.cs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs index ee2cfeda1694..3c3b5b1aced2 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs @@ -384,11 +384,28 @@ private static void ReplaceViewInstance(UIElement instance, Type replacementType public static void UpdateApplication(Type[] types) { - foreach (var t in types) + foreach (var type in types) { - if (t.GetCustomAttribute() is { } update) + try { - TypeMappings.RegisterMapping(t, update.OriginalType); + // 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. + var attr = type.GetCustomAttributesData().FirstOrDefault(data => data is { AttributeType.FullName: "System.Runtime.CompilerServices.MetadataUpdateOriginalTypeAttribute" }); + if (attr is { ConstructorArguments: [{ Value: Type originalType }] }) + { + TypeMappings.RegisterMapping(type, originalType); + } + else if (attr is not null && _log.IsEnabled(LogLevel.Warning)) + { + _log.Warn($"Found invalid MetadataUpdateOriginalTypeAttribute for {type}"); + } + } + catch (Exception error) + { + if (_log.IsEnabled(LogLevel.Error)) + { + _log.Error($"Error while processing MetadataUpdateOriginalTypeAttribute for {type}", error); + } } } From 2e266651e05840460e6abcf097e42076b433a63e Mon Sep 17 00:00:00 2001 From: David Date: Wed, 15 May 2024 14:09:22 -0400 Subject: [PATCH 02/27] test: Test the WekEventManager --- .../Uno_UI_Helpers/Given_WeakEventManager.cs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/Uno.UI.RuntimeTests/Tests/Uno_UI_Helpers/Given_WeakEventManager.cs diff --git a/src/Uno.UI.RuntimeTests/Tests/Uno_UI_Helpers/Given_WeakEventManager.cs b/src/Uno.UI.RuntimeTests/Tests/Uno_UI_Helpers/Given_WeakEventManager.cs new file mode 100644 index 000000000000..aa1c15eda0e0 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Uno_UI_Helpers/Given_WeakEventManager.cs @@ -0,0 +1,103 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Uno.UI.Helpers; + +namespace Uno.UI.RuntimeTests.Tests.Uno_UI_Helpers; + +[TestClass] +public class Given_WeakEventManager +{ + private sealed class EventPublisher + { + private readonly WeakEventManager _manager = new(); + + internal event Action Event + { + add => _manager.AddEventHandler(value); + remove => _manager.RemoveEventHandler(value); + } + } + + private sealed class EventSubscriber + { + public void M() { } + } + + [TestMethod] + public void When_ManySubscriptions_Then_DoesNotLeak() + { + var publisher = new EventPublisher(); + var weakRefs = new List>(); + Subscribe(publisher, weakRefs); + + for (var i = 0; i < 10; i++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + + foreach (var x in weakRefs) + { + Assert.IsFalse(x.TryGetTarget(out _)); + } + } + + [TestMethod] + public void When_ReEnter_Then_AllHandlersInvokedProperly() + { + var sut = new WeakEventManager(); + int handler1Count = 0, handler2Count = 0; + + sut.AddEventHandler(Handler1, "Event1"); + sut.AddEventHandler(Handler2, "Event1"); + sut.AddEventHandler(Handler1_2, "Event2"); + sut.AddEventHandler(Handler2_2, "Event2"); + + sut.HandleEvent("Event1"); + + Assert.AreEqual(3, handler1Count); + Assert.AreEqual(3, handler2Count); + + sut.HandleEvent("Event2"); + + Assert.AreEqual(4, handler1Count); + Assert.AreEqual(4, handler2Count); + + void Handler1() + { + handler1Count++; + sut.HandleEvent("Event2"); + } + + void Handler1_2() + { + handler1Count++; + } + + void Handler2() + { + handler2Count++; + sut.HandleEvent("Event2"); + } + + void Handler2_2() + { + handler2Count++; + } + } + + private void Subscribe(EventPublisher publisher, List> weakRefs) + { + for (var i = 0; i < 1000; i++) + { + var subscriber = new EventSubscriber(); + publisher.Event += subscriber.M; + weakRefs.Add(new WeakReference(subscriber)); + } + } +} From adc1172ca6ae5b194c4ef9c409018d7c369e1711 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 15 May 2024 14:26:26 -0400 Subject: [PATCH 03/27] test: Add test for the remove handler while raising --- .../Uno_UI_Helpers/Given_WeakEventManager.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/Uno.UI.RuntimeTests/Tests/Uno_UI_Helpers/Given_WeakEventManager.cs b/src/Uno.UI.RuntimeTests/Tests/Uno_UI_Helpers/Given_WeakEventManager.cs index aa1c15eda0e0..468ad9517e66 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Uno_UI_Helpers/Given_WeakEventManager.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Uno_UI_Helpers/Given_WeakEventManager.cs @@ -21,6 +21,8 @@ internal event Action Event add => _manager.AddEventHandler(value); remove => _manager.RemoveEventHandler(value); } + + public void RaiseEvent() => _manager.HandleEvent("Event"); } private sealed class EventSubscriber @@ -91,6 +93,60 @@ void Handler2_2() } } + [TestMethod] + public void When_UnSubscribeInHandler() + { + var sut = new WeakEventManager(); + int handler1Count = 0, handler2Count = 0; + + sut.AddEventHandler(Handler1, "Event1"); + sut.AddEventHandler(Handler2, "Event1"); + + sut.HandleEvent("Event1"); + sut.HandleEvent("Event1"); + + Assert.AreEqual(1, handler1Count); + Assert.AreEqual(2, handler2Count); + + void Handler1() + { + handler1Count++; + sut.RemoveEventHandler(Handler1, "Event1"); + } + + void Handler2() + { + handler2Count++; + } + } + + [TestMethod] + public void When_UnSubscribeInHandler2() + { + var pub = new EventPublisher(); + int handler1Count = 0, handler2Count = 0; + + pub.Event += Handler1; + pub.Event += Handler2; + + pub.RaiseEvent(); + pub.RaiseEvent(); + + Assert.AreEqual(1, handler1Count); + Assert.AreEqual(2, handler2Count); + + void Handler1() + { + handler1Count++; + pub.Event -= Handler1; + } + + void Handler2() + { + handler2Count++; + } + } + private void Subscribe(EventPublisher publisher, List> weakRefs) { for (var i = 0; i < 1000; i++) From fdfe38b72d6a73b5511302a724c9d2573e1d5f40 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 3 Jun 2024 13:05:38 -0400 Subject: [PATCH 04/27] feat: Add DiagnoscticsOverlay utility --- .../Diagnostics/DiagnosticViewRegistry.cs | 56 +++++ .../Diagnostics/IDiagnosticViewContext.cs | 22 ++ .../Diagnostics/IDiagnosticViewProvider.cs | 35 +++ .../Diagnostics/DiagnosticCounter.cs | 39 +++ .../DiagnosticsOverlay.DiagnosticElement.cs | 109 +++++++++ ...iagnosticsOverlay.DiagnosticViewContext.cs | 162 +++++++++++++ .../Diagnostics/DiagnosticsOverlay.cs | 225 ++++++++++++++++++ .../Diagnostics/DiagnosticView.Factories.cs | 25 ++ src/Uno.UI/Diagnostics/DiagnosticView.cs | 37 +++ .../DiagnosticViewHelper.TView.TState.cs | 44 ++++ .../Diagnostics/DiagnosticViewHelper.TView.cs | 39 +++ .../Diagnostics/DiagnosticViewHelper.cs | 30 +++ .../DiagnosticViewProvider.TView.TState.cs | 33 +++ .../DiagnosticViewProvider.TView.cs | 37 +++ 14 files changed, 893 insertions(+) create mode 100644 src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs create mode 100644 src/Uno.Foundation/Diagnostics/IDiagnosticViewContext.cs create mode 100644 src/Uno.Foundation/Diagnostics/IDiagnosticViewProvider.cs create mode 100644 src/Uno.UI.Toolkit/Diagnostics/DiagnosticCounter.cs create mode 100644 src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs create mode 100644 src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticViewContext.cs create mode 100644 src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs create mode 100644 src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs create mode 100644 src/Uno.UI/Diagnostics/DiagnosticView.cs create mode 100644 src/Uno.UI/Diagnostics/DiagnosticViewHelper.TView.TState.cs create mode 100644 src/Uno.UI/Diagnostics/DiagnosticViewHelper.TView.cs create mode 100644 src/Uno.UI/Diagnostics/DiagnosticViewHelper.cs create mode 100644 src/Uno.UI/Diagnostics/DiagnosticViewProvider.TView.TState.cs create mode 100644 src/Uno.UI/Diagnostics/DiagnosticViewProvider.TView.cs diff --git a/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs b/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs new file mode 100644 index 000000000000..038b93760426 --- /dev/null +++ b/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs @@ -0,0 +1,56 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using System.Linq; + +namespace Uno.Diagnostics.UI; + +/// +/// Registry for view that can be displayed by the DiagnosticOverlay. +/// +internal static class DiagnosticViewRegistry +{ + internal static EventHandler>? Added; + + private static ImmutableList _registrations = ImmutableList.Empty; + + /// + /// Gets the list of registered diagnostic providers. + /// + internal static ImmutableList Registrations => _registrations; + + /// + /// Register a global diagnostic provider that can be displayed on any window. + /// + /// A diagnostic provider to display. + public static void Register(IDiagnosticViewProvider provider) + { + ImmutableInterlocked.Update( + ref _registrations, + static (providers, provider) => providers.Add(provider), + new DiagnosticViewRegistration(GlobalProviderMode.One, provider)); + + Added?.Invoke(null, _registrations); + } +} + +internal record DiagnosticViewRegistration(GlobalProviderMode Mode, IDiagnosticViewProvider Provider); + +internal enum GlobalProviderMode +{ + /// + /// Diagnostic is being rendered as overlay on each window. + /// + All, + + /// + /// Diagnostic is being display on at least one window. + /// I.e. only the main/first opened but move to the next one if the current window is closed. + /// + One, + + /// + /// Only registers the diagnostic provider but does not display it. + /// + OnDemand +} diff --git a/src/Uno.Foundation/Diagnostics/IDiagnosticViewContext.cs b/src/Uno.Foundation/Diagnostics/IDiagnosticViewContext.cs new file mode 100644 index 000000000000..2c230e278ade --- /dev/null +++ b/src/Uno.Foundation/Diagnostics/IDiagnosticViewContext.cs @@ -0,0 +1,22 @@ +#nullable enable +using System; +using System.Linq; + +namespace Uno.Diagnostics.UI; + +/// +/// Some information on the context where the diagnostic view is being rendered. +/// +/// This is mainly an abstraction of the dispatcher that ensure to not overload the UI thread for diagnostics. +public interface IDiagnosticViewContext +{ + /// + /// Schedule an update of the diagnostic. + /// + /// Update action. + void Schedule(Action action); + + void ScheduleRecurrent(Action action); + + void AbortRecurrent(Action action); +} diff --git a/src/Uno.Foundation/Diagnostics/IDiagnosticViewProvider.cs b/src/Uno.Foundation/Diagnostics/IDiagnosticViewProvider.cs new file mode 100644 index 000000000000..f795b13d246d --- /dev/null +++ b/src/Uno.Foundation/Diagnostics/IDiagnosticViewProvider.cs @@ -0,0 +1,35 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Uno.Diagnostics.UI; + +/// +/// A diagnostic entry that can be displayed by the DiagnosticsOverlay. +/// +public interface IDiagnosticViewProvider +{ + /// + /// Friendly name of the diagnostic. + /// + string Name { get; } + + /// + /// Get a preview of the diagnostic, usually a value or an icon. + /// + /// An update coordinator that can be used to push updates on the preview. + /// Either a UIElement to be displayed by the diagnostic overlay or a plain object (rendered as text). + /// This is expected to be invoked on the dispatcher used to render the preview. + object GetPreview(IDiagnosticViewContext context); + + /// + /// Show details of the diagnostic. + /// + /// An update coordinator that can be used to push updates on the preview. + /// Token to cancel the async operation. + /// Either a UIElement to be displayed by the diagnostic overlay, a ContentDialog to show, or a simple object to show in a content dialog. + /// This is expected to be invoked on the dispatcher used to render the preview. + ValueTask GetDetailsAsync(IDiagnosticViewContext context, CancellationToken ct); +} diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticCounter.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticCounter.cs new file mode 100644 index 000000000000..169f6389a1b9 --- /dev/null +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticCounter.cs @@ -0,0 +1,39 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Uno.Diagnostics.UI; + +internal sealed class DiagnosticCounter(string name, string description) : IDiagnosticViewProvider +{ + private DiagnosticViewHelper? _preview; + private long _value; + + public string Name => name; + + public string Description => description; + + public long Value => _value; + + public void Increment() + { + Interlocked.Increment(ref _value); + _preview?.NotifyChanged(); + } + + public void Decrement() + { + Interlocked.Decrement(ref _value); + _preview?.NotifyChanged(); + } + + object IDiagnosticViewProvider.GetPreview(IDiagnosticViewContext context) + => (_preview ??= DiagnosticViewHelper.CreateText(() => _value)).GetView(context); + + ValueTask IDiagnosticViewProvider.GetDetailsAsync(IDiagnosticViewContext context, CancellationToken ct) + => new($"current: {_value}\r\n\r\n{Description}"); +} diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs new file mode 100644 index 000000000000..ce50db0e3f56 --- /dev/null +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs @@ -0,0 +1,109 @@ +#nullable enable +#pragma warning disable IDE0055 // Invalid suggestion for switch statement +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Uno.Foundation.Logging; + +namespace Uno.Diagnostics.UI; + +public sealed partial class DiagnosticsOverlay +{ + private record DiagnosticElement(DiagnosticsOverlay Overlay, IDiagnosticViewProvider Provider, IDiagnosticViewContext Coordinator) : IDisposable + { + private UIElement? _preview; + private CancellationTokenSource? _details; + + public UIElement Preview => _preview ??= CreatePreview(); + + private UIElement CreatePreview() + { + try + { + var preview = Provider.GetPreview(Coordinator); + var element = preview as UIElement ?? DiagnosticViewHelper.CreateText(preview.ToString()); + + if (ToolTipService.GetToolTip(element) is null) + { + ToolTipService.SetToolTip(element, Provider.Name); + } + + if (element is not ButtonBase) + { + element.Tapped += (snd, e) => ShowDetails(); + } + + return element; + } + catch (Exception e) + { + this.Log().Error($"Failed to get preview for {Provider.Name}.", e); + + var element = DiagnosticViewHelper.CreateText("**"); + ToolTipService.SetToolTip(element, $"Failed to get preview for {Provider.Name}."); + return element; + } + } + + private void ShowDetails() + { + _ = Do(); + + async ValueTask Do() + { + try + { + var ct = new CancellationTokenSource(); + Interlocked.Exchange(ref _details, ct)?.Cancel(); + + var details = await Provider.GetDetailsAsync(Coordinator, ct.Token); + switch (details) + { + case null: + break; + + case ContentDialog dialog: + dialog.XamlRoot = Overlay._root; + await dialog.ShowAsync().AsTask(ct.Token); + break; + + case UIElement element: + { + var flyout = new Flyout { Content = element }; + flyout.ShowAt(Preview, new FlyoutShowOptions()); + ct.Token.Register(flyout.Hide); + break; + } + + default: + { + var dialog = new ContentDialog + { + XamlRoot = Overlay._root, + Title = Provider.Name, + Content = details.ToString(), + CloseButtonText = "Close", + }; + await dialog.ShowAsync().AsTask(ct.Token); + break; + } + } + } + catch (Exception e) + { + this.Log().Error($"Failed to show details for {Provider.Name}.", e); + } + } + } + + public void HideDetails() + => Interlocked.Exchange(ref _details, new CancellationTokenSource())?.Cancel(); + + public void Dispose() + => Interlocked.Exchange(ref _details, new CancellationTokenSource())?.Cancel(); + } +} diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticViewContext.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticViewContext.cs new file mode 100644 index 000000000000..9333586f0f8c --- /dev/null +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticViewContext.cs @@ -0,0 +1,162 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.UI.Dispatching; +using Uno.Foundation.Logging; + +namespace Uno.Diagnostics.UI; + +public sealed partial class DiagnosticsOverlay +{ + private sealed class Context : IDiagnosticViewContext, IDisposable + { + private Queue _pending = new(); + private Queue _pending2 = new(); + private List? _recurrents; + + private readonly DispatcherQueue _dispatcher; + private DispatcherQueueTimer? _timer; + private int _updatesScheduled; + + public Context(DispatcherQueue dispatcher) + { + _dispatcher = dispatcher; + } + + public void Schedule(Action action) + { + lock (_pending) + { + _pending.Enqueue(action); + } + + if (Interlocked.Increment(ref _updatesScheduled) is 1) + { + _dispatcher.TryEnqueue(DispatcherQueuePriority.Low, DoUpdates); + } + } + + public void ScheduleRecurrent(Action action) + { + Interlocked.Increment(ref _updatesScheduled); + + if (_recurrents is null) + { + Interlocked.CompareExchange(ref _recurrents, new List(), null); + } + + lock (_recurrents) + { + _recurrents.Add(action); + if (_recurrents.Count == 1) + { + if (_timer is null) + { + _timer = _dispatcher.CreateTimer(); + _timer.Interval = TimeSpan.FromMilliseconds(1000); + _timer.Tick += (snd, e) => DoUpdates(); + } + + _timer?.Start(); + } + } + } + + public void AbortRecurrent(Action action) + { + if (_recurrents is null) + { + return; + } + + lock (_recurrents) + { + if (_recurrents.Remove(action)) + { + if (_recurrents.Count == 0) + { + _timer?.Stop(); + } + + if (Interlocked.Decrement(ref _updatesScheduled) > 0) + { + _dispatcher.TryEnqueue(DispatcherQueuePriority.Low, DoUpdates); + } + } + } + } + + private void DoUpdates() + { + DoPending(); + DoRecurrent(); + } + + private void DoPending() + { + Queue pending; + lock (_pending) + { + pending = _pending; + var pendingCount = pending.Count; + if (pendingCount is 0) + { + return; + } + + // Swap collections to avoid locking the first one for too long. + var pending2 = _pending2; + _pending = pending2; + _pending2 = pending; + + // We need to decrement the pending count before releasing the lock so if someone request another one (pushed now in the pending2) + // it will still be able to schedule an update if needed (i.e. if _updatesScheduled is 0). + Interlocked.Add(ref _updatesScheduled, -pendingCount); + } + + foreach (var update in pending) + { + try + { + update(); + } + catch (Exception e) + { + this.Log().Error("Failed to execute update.", e); + } + } + + pending.Clear(); + } + + private void DoRecurrent() + { + if (_recurrents is { Count: > 0 } recurrent) + { + lock (recurrent) + { + foreach (var update in recurrent) + { + try + { + update(); + } + catch (Exception e) + { + this.Log().Error("Failed to execute update.", e); + } + } + } + } + } + + /// + public void Dispose() + { + _updatesScheduled = -4096; + _timer?.Stop(); + } + } +} diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs new file mode 100644 index 000000000000..615b385995da --- /dev/null +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs @@ -0,0 +1,225 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using Microsoft.UI; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; + +namespace Uno.Diagnostics.UI; + +public sealed partial class DiagnosticsOverlay +{ + private static readonly ConditionalWeakTable _overlays = new(); + + /// + /// Gets the for the specified . + /// + /// The root onto which the overlay is being rendered. + /// + public static DiagnosticsOverlay Get(XamlRoot root) + => _overlays.GetValue(root, static r => new DiagnosticsOverlay(r)); + + private readonly XamlRoot _root; + private readonly object _updateGate = new(); + private readonly List _localProviders = new(); + private readonly Dictionary _elements = new(); + + private DispatcherQueue? _dispatcher; + private Context? _updateCoordinator; + private Popup? _overlayHost; + private StackPanel? _overlayPanel; + private bool _isVisible; + private int _updateEnqueued; + + static DiagnosticsOverlay() + { + DiagnosticViewRegistry.Added += static (snd, e) => + { + foreach (var overlay in _overlays) + { + overlay.Value.EnqueueUpdate(); + } + }; + } + + private DiagnosticsOverlay(XamlRoot root) + { + _root = root; + _dispatcher = root.Content?.DispatcherQueue; + _updateCoordinator = _dispatcher is null ? null : new Context(_dispatcher); + + root.Changed += static (snd, e) => + { + var overlay = Get(snd); + var dispatcher = snd.Content?.DispatcherQueue; + if (dispatcher != overlay._dispatcher) // Is this even possible ??? + { + lock (overlay._updateGate) + { + overlay._dispatcher = dispatcher; + overlay._updateCoordinator = dispatcher is null ? null : new Context(dispatcher); + + // Clean all dispatcher bound state + overlay._overlayHost = null; + overlay._overlayPanel = null; + foreach (var element in overlay._elements.Values) + { + element.Dispose(); + } + overlay._elements.Clear(); + } + } + overlay.EnqueueUpdate(); + }; + } + + public bool IsVisible + { + get => _isVisible; + set + { + _isVisible = value; + EnqueueUpdate(forceUpdate: !value); // For update when hiding. + } + } + + /// + /// Add a UI diagnostic element to this overlay. + /// + /// This will make this overlay = true. + public void Add(string name, UIElement preview, Func? details = null) + => Add(new DiagnosticView(name, _ => preview, (_, ct) => new(details?.Invoke()))); + + /// + /// Add a UI diagnostic element to this overlay. + /// + /// This will make this overlay = true. + /// The provider to add. + public void Add(IDiagnosticViewProvider provider) + { + lock (_updateGate) + { + _localProviders.Add(provider); + } + + EnqueueUpdate(); // Making IsVisible = true wil (try) to re-enqueue the update, but safer to keep it here anyway. + IsVisible = true; + } + + private void EnqueueUpdate(bool forceUpdate = false) + { + var dispatcher = _dispatcher; + if ((!_isVisible && !forceUpdate) || dispatcher is null || Interlocked.CompareExchange(ref _updateEnqueued, 1, 0) is not 0) + { + return; + } + + dispatcher.TryEnqueue(() => + { + _updateEnqueued = 0; + + if (!_isVisible) + { + if (_overlayHost is { } host) + { + ShowHost(host, false); + } + + return; + } + + lock (_updateGate) + { + var providers = DiagnosticViewRegistry + .Registrations + .Where(ShouldMaterialize) + .Select(reg => reg.Provider) + .Concat(_localProviders) + .Distinct() + .ToList(); + + var panel = _overlayPanel ??= CreatePanel(); + var host = _overlayHost ??= CreateHost(_root, panel); + + foreach (var provider in providers) + { + if (!_elements.ContainsKey(provider)) + { + var element = new DiagnosticElement(this, provider, _updateCoordinator!); + _elements[provider] = element; + + panel.Children.Add(element.Preview); + } + } + + ShowHost(host, true); + } + }); + } + + private static StackPanel CreatePanel() + { + var panel = new StackPanel + { + BorderThickness = new Thickness(1), + BorderBrush = new SolidColorBrush(Colors.Black), + Background = new SolidColorBrush(Colors.DarkGray), + Orientation = Orientation.Horizontal, + Padding = new Thickness(3), + Spacing = 3, + ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY + }; + panel.ManipulationDelta += static (snd, e) => + { + var panel = (Panel)snd; + var transform = panel.RenderTransform as TranslateTransform; + if (transform is null) + { + panel.RenderTransform = transform = new TranslateTransform(); + } + + transform.X += e.Delta.Translation.X; + transform.Y += e.Delta.Translation.Y; + }; + + return panel; + } + + private static Popup CreateHost(XamlRoot root, StackPanel panel) + => new() + { + XamlRoot = root, + Child = panel, + IsLightDismissEnabled = false, + LightDismissOverlayMode = LightDismissOverlayMode.Off, + }; + + private static void ShowHost(Popup host, bool isVisible) + => host.IsOpen = isVisible; + + private bool ShouldMaterialize(DiagnosticViewRegistration registration) + => registration.Mode switch + { + GlobalProviderMode.All => true, + GlobalProviderMode.OnDemand => false, + _ => _overlays.Count(overlay => overlay.Value.IsMaterialized(registration.Provider)) is 0 + }; + + private bool IsMaterialized(IDiagnosticViewProvider provider) + { + lock (_updateGate) + { + return _elements.ContainsKey(provider); + } + } +} diff --git a/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs b/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs new file mode 100644 index 000000000000..8c3a6c334014 --- /dev/null +++ b/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs @@ -0,0 +1,25 @@ +#nullable enable +using System; +using System.Linq; +using Microsoft.UI.Xaml; + +namespace Uno.Diagnostics.UI; + +internal partial class DiagnosticView +{ + public static DiagnosticView Register(string name, Action update) + where TView : FrameworkElement, new() + { + var provider = new DiagnosticView(name, () => new TView()); + DiagnosticViewRegistry.Register(provider); + return provider; + } + + public static DiagnosticView Register(string name, Action update) + where TView : FrameworkElement, new() + { + var provider = new DiagnosticView(name, () => new TView(), update); + DiagnosticViewRegistry.Register(provider); + return provider; + } +} diff --git a/src/Uno.UI/Diagnostics/DiagnosticView.cs b/src/Uno.UI/Diagnostics/DiagnosticView.cs new file mode 100644 index 000000000000..c7e80cacda56 --- /dev/null +++ b/src/Uno.UI/Diagnostics/DiagnosticView.cs @@ -0,0 +1,37 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; + +namespace Uno.Diagnostics.UI; + +/// +/// A generic diagnostic view provider. +/// +internal partial class DiagnosticView( + string name, + Func preview, + Func>? details = null) + : IDiagnosticViewProvider +{ + public DiagnosticView( + string name, + Func preview, + Func>? details = null) + : this(name, _ => preview(), async (_, ct) => details is null ? null : await details(ct)) + { + } + + /// + string IDiagnosticViewProvider.Name => name; + + /// + public object GetPreview(IDiagnosticViewContext context) + => preview(context); + + /// + public async ValueTask GetDetailsAsync(IDiagnosticViewContext context, CancellationToken ct) + => details is null ? null : await details(context, ct); +} diff --git a/src/Uno.UI/Diagnostics/DiagnosticViewHelper.TView.TState.cs b/src/Uno.UI/Diagnostics/DiagnosticViewHelper.TView.TState.cs new file mode 100644 index 000000000000..41f1d3facbbc --- /dev/null +++ b/src/Uno.UI/Diagnostics/DiagnosticViewHelper.TView.TState.cs @@ -0,0 +1,44 @@ +#nullable enable +using System; +using System.Linq; +using Microsoft.UI.Xaml; + +namespace Uno.Diagnostics.UI; + +internal class DiagnosticViewHelper(Func factory, Action update) + where TView : FrameworkElement +{ + private event EventHandler? _changed; + private bool _hasState; + private TState? _state; + + public DiagnosticViewHelper(Func factory, Action update) + : this(_ => factory(), (_, view, state) => update(view, state)) + { + } + + public void NotifyChanged(TState state) + { + _state = state; + _hasState = true; + _changed?.Invoke(this, state); + } + + public UIElement GetView(IDiagnosticViewContext context) + { + var view = factory(context); + EventHandler requestUpdate = (_, state) => context.Schedule(() => update(context, view, state)); + + view.Loaded += (snd, e) => + { + _changed += requestUpdate; + if (_hasState) + { + requestUpdate(null, _state!); + } + }; + view.Unloaded += (snd, e) => _changed -= requestUpdate; + + return view; + } +} diff --git a/src/Uno.UI/Diagnostics/DiagnosticViewHelper.TView.cs b/src/Uno.UI/Diagnostics/DiagnosticViewHelper.TView.cs new file mode 100644 index 000000000000..a001db1a571b --- /dev/null +++ b/src/Uno.UI/Diagnostics/DiagnosticViewHelper.TView.cs @@ -0,0 +1,39 @@ +#nullable enable + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.UI.Xaml; + +namespace Uno.Diagnostics.UI; + +internal class DiagnosticViewHelper(Func factory, Action update) + where TView : FrameworkElement +{ + private event EventHandler? _changed; + + public DiagnosticViewHelper(Func factory, Action update) + : this(_ => factory(), (_, t) => update(t)) + { + } + + public void NotifyChanged() + { + _changed?.Invoke(this, EventArgs.Empty); + } + + public UIElement GetView(IDiagnosticViewContext context) + { + var view = factory(context); + EventHandler requestUpdate = (_, __) => context.Schedule(() => update(context, view)); + + view.Loaded += (snd, e) => + { + _changed += requestUpdate; + requestUpdate(null, EventArgs.Empty); + }; + view.Unloaded += (snd, e) => _changed -= requestUpdate; + + return view; + } +} diff --git a/src/Uno.UI/Diagnostics/DiagnosticViewHelper.cs b/src/Uno.UI/Diagnostics/DiagnosticViewHelper.cs new file mode 100644 index 000000000000..f3c6e36cc9e8 --- /dev/null +++ b/src/Uno.UI/Diagnostics/DiagnosticViewHelper.cs @@ -0,0 +1,30 @@ +#nullable enable + +using System; +using System.Linq; +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; + +namespace Uno.Diagnostics.UI; + +internal class DiagnosticViewHelper +{ + public static TextBlock CreateText(string? text) + => new() + { + Text = text ?? "--", + VerticalAlignment = VerticalAlignment.Center, + FontSize = 10, + Foreground = new SolidColorBrush(Colors.White) + }; + + public static DiagnosticViewHelper CreateText(Func value) + where T : struct + => new(() => CreateText(value().ToString()), tb => tb.Text = value().ToString()); + + public static DiagnosticViewHelper CreateText(Func value) + where T : struct + => new(() => CreateText(value()), tb => tb.Text = value() ?? "--"); +} diff --git a/src/Uno.UI/Diagnostics/DiagnosticViewProvider.TView.TState.cs b/src/Uno.UI/Diagnostics/DiagnosticViewProvider.TView.TState.cs new file mode 100644 index 000000000000..bef37ee07022 --- /dev/null +++ b/src/Uno.UI/Diagnostics/DiagnosticViewProvider.TView.TState.cs @@ -0,0 +1,33 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; + +namespace Uno.Diagnostics.UI; + +internal class DiagnosticView( + string name, + Func preview, + Action update, + Func>? details = null) + : IDiagnosticViewProvider + where TView : FrameworkElement +{ + private readonly DiagnosticViewHelper _statusView = new(preview, update); + + public void Update(TState status) + => _statusView.NotifyChanged(status); + + /// + string IDiagnosticViewProvider.Name => name; + + /// + object IDiagnosticViewProvider.GetPreview(IDiagnosticViewContext context) + => _statusView.GetView(context); + + /// + async ValueTask IDiagnosticViewProvider.GetDetailsAsync(IDiagnosticViewContext context, CancellationToken ct) + => details is null ? null : await details(context, ct); +} diff --git a/src/Uno.UI/Diagnostics/DiagnosticViewProvider.TView.cs b/src/Uno.UI/Diagnostics/DiagnosticViewProvider.TView.cs new file mode 100644 index 000000000000..29a4b78e6c6a --- /dev/null +++ b/src/Uno.UI/Diagnostics/DiagnosticViewProvider.TView.cs @@ -0,0 +1,37 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; + +namespace Uno.Diagnostics.UI; + +/// +/// A generic diagnostic view provider. +/// +internal class DiagnosticView( + string name, + Func preview, + Func>? details = null) + : IDiagnosticViewProvider + where TView : UIElement +{ + public DiagnosticView( + string name, + Func preview, + Func>? details = null) + : this(name, _ => preview(), async (_, ct) => details is null ? null : await details(ct)) + { + } + + /// + string IDiagnosticViewProvider.Name => name; + + /// + object IDiagnosticViewProvider.GetPreview(IDiagnosticViewContext context) => preview(context); + + /// + async ValueTask IDiagnosticViewProvider.GetDetailsAsync(IDiagnosticViewContext context, CancellationToken ct) + => details is null ? null : await details(context, ct); +} From 96d8c63c1fdb6f8612f7e85fefdd32ef1cd039e6 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 3 Jun 2024 13:23:55 -0400 Subject: [PATCH 05/27] feat: Make VS publish hot-reload events to dev-server --- .../IDEChannel/IdeChannelServer.cs | 3 +- .../IDEChannel/ForceHotReloadIdeMessage.cs | 2 +- .../IDEChannel/HotReloadEvent.cs | 55 ++++++++++++++++ .../IDEChannel/HotReloadEventIdeMessage.cs | 11 ++++ .../HotReloadRequestedIdeMessage.cs | 2 +- .../IDEChannel/IIdeChannelServer.cs | 3 +- .../IDEChannel/IdeMessage.cs | 3 +- .../IDEChannel/KeepAliveIdeMessage.cs | 3 +- .../IDEChannel/Result.cs | 16 +++++ .../EntryPoint.ActiveProfileSync.cs | 23 ------- .../EntryPoint.TelemetryEventListener.cs | 65 +++++++++++++++++++ src/Uno.UI.RemoteControl.VS/EntryPoint.cs | 59 +++++++---------- .../IDEChannel/IDEChannelClient.cs | 52 ++++++++++----- 13 files changed, 212 insertions(+), 85 deletions(-) create mode 100644 src/Uno.UI.RemoteControl.Messaging/IDEChannel/HotReloadEvent.cs create mode 100644 src/Uno.UI.RemoteControl.Messaging/IDEChannel/HotReloadEventIdeMessage.cs create mode 100644 src/Uno.UI.RemoteControl.Messaging/IDEChannel/Result.cs create mode 100644 src/Uno.UI.RemoteControl.VS/EntryPoint.TelemetryEventListener.cs diff --git a/src/Uno.UI.RemoteControl.Host/IDEChannel/IdeChannelServer.cs b/src/Uno.UI.RemoteControl.Host/IDEChannel/IdeChannelServer.cs index b9da19be2ac1..3a4db7f1a214 100644 --- a/src/Uno.UI.RemoteControl.Host/IDEChannel/IdeChannelServer.cs +++ b/src/Uno.UI.RemoteControl.Host/IDEChannel/IdeChannelServer.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Uno.UI.RemoteControl.Host.IDEChannel; using Uno.UI.RemoteControl.Messaging.IdeChannel; @@ -22,7 +23,7 @@ public async Task SendToIdeAsync(IdeMessage message) await Task.Yield(); } - public async Task SendToDevServerAsync(IdeMessageEnvelope message) + public async Task SendToDevServerAsync(IdeMessageEnvelope message, CancellationToken ct) { MessageFromIDE?.Invoke(this, IdeMessageSerializer.Deserialize(message)); diff --git a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/ForceHotReloadIdeMessage.cs b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/ForceHotReloadIdeMessage.cs index 2f560bfe2bf7..2d135a11714f 100644 --- a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/ForceHotReloadIdeMessage.cs +++ b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/ForceHotReloadIdeMessage.cs @@ -2,4 +2,4 @@ namespace Uno.UI.RemoteControl.Messaging.IdeChannel; -public record ForceHotReloadIdeMessage : IdeMessage; +public record ForceHotReloadIdeMessage(long CorrelationId) : IdeMessage(WellKnownScopes.HotReload); diff --git a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/HotReloadEvent.cs b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/HotReloadEvent.cs new file mode 100644 index 000000000000..095006c1d5a0 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/HotReloadEvent.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; + +namespace Uno.UI.RemoteControl.Messaging.HotReload; + +public enum HotReloadEvent +{ + /// + /// Hot-reload is not available (and will not be). + /// WARNING This is raised only by dev-server, not by the IDE + /// + /// This can be the case if initialization failed. + Disabled, + + /// + /// Initializing hot-reload processor (e.g. loading workspace for dev-server based HR). + /// WARNING This is raised only by dev-server, not by the IDE. + /// + Initializing, + + /// + /// Processor is ready, waiting for files changes to trigger. + /// WARNING This is raised only by dev-server, not by the IDE. + /// + Ready, + + /// + /// Processor is processing files changes. + /// WARNING This is raised only by dev-server, not by the IDE (unless we request a force hot-reload from the client). + /// + ProcessingFiles, + + /// + /// Hot-reload completed (errors might come after!) + /// + Completed, + + /// + /// Hot-reload completed with no changes + /// + NoChanges, + + /// + /// Hot-reload failed (usually due to compilation errors) + /// + Failed, + + RudeEdit, + + /// + /// Hot-reload cannot be applied (rude edit), a dialog has been prompt to the user ... and he just gave a response! + /// WARNING This is raised only by IDE, not by the dev-server. + /// + RudeEditDialogButton +} diff --git a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/HotReloadEventIdeMessage.cs b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/HotReloadEventIdeMessage.cs new file mode 100644 index 000000000000..5d1d185dc820 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/HotReloadEventIdeMessage.cs @@ -0,0 +1,11 @@ +using System; +using System.Linq; +using Uno.UI.RemoteControl.Messaging.HotReload; + +namespace Uno.UI.RemoteControl.Messaging.IdeChannel; + +/// +/// A message sent by the IDE to the dev-server regarding hot-reload operations. +/// +/// The kind of hot-reload message. +public record HotReloadEventIdeMessage(HotReloadEvent Event) : IdeMessage(WellKnownScopes.HotReload); diff --git a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/HotReloadRequestedIdeMessage.cs b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/HotReloadRequestedIdeMessage.cs index ed86ff042534..9d2be13a721e 100644 --- a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/HotReloadRequestedIdeMessage.cs +++ b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/HotReloadRequestedIdeMessage.cs @@ -2,4 +2,4 @@ namespace Uno.UI.RemoteControl.Messaging.IdeChannel; -public record HotReloadRequestedIdeMessage : IdeMessage; +public record HotReloadRequestedIdeMessage(long RequestId, Result Result) : IdeMessage(WellKnownScopes.HotReload); diff --git a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/IIdeChannelServer.cs b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/IIdeChannelServer.cs index 8c0b9ccb960d..3752e200c6ce 100644 --- a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/IIdeChannelServer.cs +++ b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/IIdeChannelServer.cs @@ -3,13 +3,14 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Uno.UI.RemoteControl.Messaging.IdeChannel; internal interface IIdeChannelServer { - Task SendToDevServerAsync(IdeMessageEnvelope message); + Task SendToDevServerAsync(IdeMessageEnvelope message, CancellationToken ct); event EventHandler? MessageFromDevServer; } diff --git a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/IdeMessage.cs b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/IdeMessage.cs index 37c3b1e93d04..07dffbd1ec87 100644 --- a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/IdeMessage.cs +++ b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/IdeMessage.cs @@ -2,5 +2,4 @@ namespace Uno.UI.RemoteControl.Messaging.IdeChannel; -public record IdeMessage; - +public record IdeMessage(string Scope); diff --git a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/KeepAliveIdeMessage.cs b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/KeepAliveIdeMessage.cs index 42235f040d69..95ff8e943f4b 100644 --- a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/KeepAliveIdeMessage.cs +++ b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/KeepAliveIdeMessage.cs @@ -2,5 +2,4 @@ namespace Uno.UI.RemoteControl.Messaging.IdeChannel; -public record KeepAliveIdeMessage : IdeMessage; - +public record KeepAliveIdeMessage() : IdeMessage(WellKnownScopes.IdeChannel); diff --git a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/Result.cs b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/Result.cs new file mode 100644 index 000000000000..de650bbe52af --- /dev/null +++ b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/Result.cs @@ -0,0 +1,16 @@ +#nullable enable +using System; +using System.Linq; + +namespace Uno.UI.RemoteControl.Messaging.IdeChannel; + +public record Result(string? Error) +{ + public static Result Success() => new(default(string)); + + public static Result Fail(Exception error) => new(error.ToString()); + + public bool IsSuccess => Error is null; + + public string? Error { get; } = Error; +} diff --git a/src/Uno.UI.RemoteControl.VS/EntryPoint.ActiveProfileSync.cs b/src/Uno.UI.RemoteControl.VS/EntryPoint.ActiveProfileSync.cs index d3bf09dd5b6c..c6e06ffce1e9 100644 --- a/src/Uno.UI.RemoteControl.VS/EntryPoint.ActiveProfileSync.cs +++ b/src/Uno.UI.RemoteControl.VS/EntryPoint.ActiveProfileSync.cs @@ -1,42 +1,19 @@ using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.ComponentModel.Composition; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.IO; -using System.IO.Pipes; using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Reflection; -using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; -using System.Xml; -using EnvDTE; using EnvDTE80; using Microsoft; using Microsoft.Build.Evaluation; using Microsoft.Build.Framework; using Microsoft.VisualStudio; -using Microsoft.VisualStudio.OLE.Interop; -using Microsoft.VisualStudio.ProjectSystem; -using Microsoft.VisualStudio.ProjectSystem.Build; using Microsoft.VisualStudio.ProjectSystem.Debug; -using Microsoft.VisualStudio.ProjectSystem.Properties; -using Microsoft.VisualStudio.ProjectSystem.VS; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; -using StreamJsonRpc; -using Uno.UI.RemoteControl.Messaging.IdeChannel; -using Uno.UI.RemoteControl.VS.DebuggerHelper; using Uno.UI.RemoteControl.VS.Helpers; -using Uno.UI.RemoteControl.VS.IdeChannel; -using ILogger = Uno.UI.RemoteControl.VS.Helpers.ILogger; using Task = System.Threading.Tasks.Task; #pragma warning disable VSTHRD010 diff --git a/src/Uno.UI.RemoteControl.VS/EntryPoint.TelemetryEventListener.cs b/src/Uno.UI.RemoteControl.VS/EntryPoint.TelemetryEventListener.cs new file mode 100644 index 000000000000..562b0ecbd07a --- /dev/null +++ b/src/Uno.UI.RemoteControl.VS/EntryPoint.TelemetryEventListener.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.Telemetry; +using Microsoft.VisualStudio.Telemetry.SessionChannel; +using Uno.UI.RemoteControl.Messaging.HotReload; +using Uno.UI.RemoteControl.Messaging.IdeChannel; + +namespace Uno.UI.RemoteControl.VS; + +public partial class EntryPoint +{ + private class TelemetryEventListener(EntryPoint ide) : ISessionChannel + { + /// + public string ChannelId => "Uno platform hot-reload client application"; + + /// + public string TransportUsed => "Local_TCP"; + + /// + public ChannelProperties Properties { get; set; } = ChannelProperties.DevChannel; + + /// + public bool IsStarted => true; + + /// + public void Start(string sessionId) { } + + /// + public void PostEvent(TelemetryEvent telemetryEvent) + => TryForward(telemetryEvent); + + /// + public void PostEvent(TelemetryEvent telemetryEvent, IEnumerable args) + => TryForward(telemetryEvent); + + private void TryForward(TelemetryEvent telemetryEvent) + { + if (ide is not { _ideChannelClient: { } client, _isDisposed: false, _ct: { IsCancellationRequested: false } ct }) + { + return; + } + + switch (telemetryEvent.Name) + { + case "vs/diagnostics/debugger/enccomplete": + _ = client.SendToDevServerAsync(new HotReloadEventIdeMessage(HotReloadEvent.Completed), ct.Token); + break; + + case "vs/diagnostics/debugger/enc/nochanges": + _ = client.SendToDevServerAsync(new HotReloadEventIdeMessage(HotReloadEvent.NoChanges), ct.Token); + break; + + case "vs/diagnostics/debugger/enc/error": + _ = client.SendToDevServerAsync(new HotReloadEventIdeMessage(HotReloadEvent.Failed), ct.Token); + break; + + case "vs/diagnostics/debugger/hotreloaddialog/buttonclick": + _ = client.SendToDevServerAsync(new HotReloadEventIdeMessage(HotReloadEvent.RudeEditDialogButton), ct.Token); + break; + } + } + } +} diff --git a/src/Uno.UI.RemoteControl.VS/EntryPoint.cs b/src/Uno.UI.RemoteControl.VS/EntryPoint.cs index f8e6e3095c10..30bf2b2a469f 100644 --- a/src/Uno.UI.RemoteControl.VS/EntryPoint.cs +++ b/src/Uno.UI.RemoteControl.VS/EntryPoint.cs @@ -1,35 +1,20 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; -using System.ComponentModel.Composition; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.IO.Pipes; using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; using EnvDTE; using EnvDTE80; using Microsoft.Build.Evaluation; -using Microsoft.Build.Framework; -using Microsoft.VisualStudio; -using Microsoft.VisualStudio.OLE.Interop; -using Microsoft.VisualStudio.ProjectSystem; -using Microsoft.VisualStudio.ProjectSystem.Build; -using Microsoft.VisualStudio.ProjectSystem.Debug; -using Microsoft.VisualStudio.ProjectSystem.Properties; -using Microsoft.VisualStudio.ProjectSystem.VS; +using Microsoft.Internal.VisualStudio.Shell; using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Shell.Interop; -using StreamJsonRpc; using Uno.UI.RemoteControl.Messaging.IdeChannel; using Uno.UI.RemoteControl.VS.DebuggerHelper; using Uno.UI.RemoteControl.VS.Helpers; @@ -48,6 +33,7 @@ public partial class EntryPoint : IDisposable private const string RemoteControlServerPortProperty = "UnoRemoteControlPort"; private const string UnoVSExtensionLoadedProperty = "_UnoVSExtensionLoaded"; + private readonly CancellationTokenSource _ct = new(); private readonly DTE _dte; private readonly DTE2 _dte2; private readonly string _toolsPath; @@ -112,6 +98,8 @@ DTE2 dte2 _ = _debuggerObserver.ObserveProfilesAsync(); _ = _globalPropertiesChanged(); + + TelemetryHelper.DataModelTelemetrySession.AddSessionChannel(new TelemetryEventListener(this)); } private async Task> OnProvideGlobalPropertiesAsync() @@ -319,23 +307,21 @@ private void StartServer() } var runtimeVersionPath = $"net{version}.0"; - var sb = new StringBuilder(); - var pipeGuid = Guid.NewGuid(); var hostBinPath = Path.Combine(_toolsPath, "host", runtimeVersionPath, "Uno.UI.RemoteControl.Host.dll"); - string arguments = $"\"{hostBinPath}\" --httpPort {RemoteControlServerPort} --ppid {System.Diagnostics.Process.GetCurrentProcess().Id} --ideChannel \"{pipeGuid}\""; + var arguments = $"\"{hostBinPath}\" --httpPort {RemoteControlServerPort} --ppid {System.Diagnostics.Process.GetCurrentProcess().Id} --ideChannel \"{pipeGuid}\""; var pi = new ProcessStartInfo("dotnet", arguments) { UseShellExecute = false, CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden, WorkingDirectory = Path.Combine(_toolsPath, "host"), - }; - // redirect the output - pi.RedirectStandardOutput = true; - pi.RedirectStandardError = true; + // redirect the output + RedirectStandardOutput = true, + RedirectStandardError = true + }; _process = new System.Diagnostics.Process(); @@ -351,16 +337,14 @@ private void StartServer() _process.BeginErrorReadLine(); _ideChannelClient = new IdeChannelClient(pipeGuid, new Logger(this)); - _ideChannelClient.ForceHotReloadRequested += IdeChannelClient_ForceHotReloadRequested; + _ideChannelClient.ForceHotReloadRequested += OnForceHotReloadRequestedAsync; _ideChannelClient.ConnectToHost(); _ = _globalPropertiesChanged(); } } -#pragma warning disable VSTHRD100 // Avoid async void methods - private async void IdeChannelClient_ForceHotReloadRequested(object sender, ForceHotReloadIdeMessage message) -#pragma warning restore VSTHRD100 // Avoid async void methods + private async Task OnForceHotReloadRequestedAsync(object? sender, ForceHotReloadIdeMessage request) { try { @@ -369,12 +353,14 @@ private async void IdeChannelClient_ForceHotReloadRequested(object sender, Force // Send a message back to indicate that the request has been received and acted upon. if (_ideChannelClient is not null) { - await _ideChannelClient.SendToDevServerAsync(new HotReloadRequestedIdeMessage()); + await _ideChannelClient.SendToDevServerAsync(new HotReloadRequestedIdeMessage(request.CorrelationId, Result.Success()), _ct.Token); } } - catch (Exception e) + catch (Exception e) when (_ideChannelClient is not null) { - _debugAction?.Invoke($"Failed to execute command to ForceHotReload: {e}"); + await _ideChannelClient.SendToDevServerAsync(new HotReloadRequestedIdeMessage(request.CorrelationId, Result.Fail(e)), _ct.Token); + + throw; } } @@ -442,6 +428,7 @@ public void Dispose() try { + _ct.Cancel(false); _dte.Events.BuildEvents.OnBuildDone -= _onBuildDoneHandler; _dte.Events.BuildEvents.OnBuildProjConfigBegin -= _onBuildProjConfigBeginHandler; } @@ -453,12 +440,10 @@ public void Dispose() private class Logger(EntryPoint entryPoint) : ILogger { - private readonly EntryPoint _entryPoint = entryPoint; - - public void Debug(string message) => _entryPoint._debugAction?.Invoke(message); - public void Error(string message) => _entryPoint._errorAction?.Invoke(message); - public void Info(string message) => _entryPoint._infoAction?.Invoke(message); - public void Warn(string message) => _entryPoint._warningAction?.Invoke(message); - public void Verbose(string message) => _entryPoint._verboseAction?.Invoke(message); + public void Debug(string message) => entryPoint._debugAction?.Invoke(message); + public void Error(string message) => entryPoint._errorAction?.Invoke(message); + public void Info(string message) => entryPoint._infoAction?.Invoke(message); + public void Warn(string message) => entryPoint._warningAction?.Invoke(message); + public void Verbose(string message) => entryPoint._verboseAction?.Invoke(message); } } diff --git a/src/Uno.UI.RemoteControl.VS/IDEChannel/IDEChannelClient.cs b/src/Uno.UI.RemoteControl.VS/IDEChannel/IDEChannelClient.cs index 87227103e80c..138e5b7a9b93 100644 --- a/src/Uno.UI.RemoteControl.VS/IDEChannel/IDEChannelClient.cs +++ b/src/Uno.UI.RemoteControl.VS/IDEChannel/IDEChannelClient.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.VisualStudio.Threading; using StreamJsonRpc; using Uno.UI.RemoteControl.Host.IDEChannel; using Uno.UI.RemoteControl.Messaging.IdeChannel; @@ -23,7 +24,7 @@ internal class IdeChannelClient private IIdeChannelServer? _roslynServer; private readonly ILogger _logger; - public event EventHandler? ForceHotReloadRequested; + public event AsyncEventHandler? ForceHotReloadRequested; public IdeChannelClient(Guid pipeGuid, ILogger logger) { @@ -65,11 +66,14 @@ public void ConnectToHost() }, _IDEChannelCancellation.Token); } - public async Task SendToDevServerAsync(IdeMessage message) + public async Task SendToDevServerAsync(IdeMessage message, CancellationToken ct) { if (_roslynServer is not null) { - await _roslynServer.SendToDevServerAsync(IdeMessageSerializer.Serialize(message)); + ct = ct.CanBeCanceled && ct != _IDEChannelCancellation!.Token + ? CancellationTokenSource.CreateLinkedTokenSource(ct, _IDEChannelCancellation!.Token).Token + : _IDEChannelCancellation!.Token; + await _roslynServer.SendToDevServerAsync(IdeMessageSerializer.Serialize(message), ct); } } @@ -77,30 +81,44 @@ private async Task StartKeepaliveAsync() { while (_IDEChannelCancellation is { IsCancellationRequested: false }) { - _roslynServer?.SendToDevServerAsync(IdeMessageSerializer.Serialize(new KeepAliveIdeMessage())); + _roslynServer?.SendToDevServerAsync(IdeMessageSerializer.Serialize(new KeepAliveIdeMessage()), _IDEChannelCancellation.Token); - await Task.Delay(5000); + await Task.Delay(5000, _IDEChannelCancellation.Token); } } private void ProcessDevServerMessage(object sender, IdeMessageEnvelope devServerMessageEnvelope) { - var devServerMessage = IdeMessageSerializer.Deserialize(devServerMessageEnvelope); + try + { + var devServerMessage = IdeMessageSerializer.Deserialize(devServerMessageEnvelope); - _logger.Verbose($"IDE: IDEChannel message received {devServerMessage}"); + _logger.Verbose($"IDE: IDEChannel message received {devServerMessage}"); - if (devServerMessage is ForceHotReloadIdeMessage forceHotReloadMessage) - { - _logger.Debug($"Hot reload requested"); - ForceHotReloadRequested?.Invoke(this, forceHotReloadMessage); - } - else if (devServerMessage is KeepAliveIdeMessage) - { - _logger.Verbose($"Keep alive from Dev Server"); + var process = Task.CompletedTask; + switch (devServerMessage) + { + case ForceHotReloadIdeMessage forceHotReloadMessage when ForceHotReloadRequested is { } hrRequested: + _logger.Debug("Hot reload requested"); + process = hrRequested.InvokeAsync(this, forceHotReloadMessage); + break; + case KeepAliveIdeMessage: + _logger.Verbose($"Keep alive from Dev Server"); + break; + default: + _logger.Verbose($"Unknown message type {devServerMessage?.GetType()} from DevServer"); + break; + } + + _ = process.ContinueWith( + t => _logger.Error($"Failed to process message {devServerMessage}: {t.Exception?.Flatten()}"), + _IDEChannelCancellation!.Token, + TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent, + TaskScheduler.Default); } - else + catch (Exception e) { - _logger.Verbose($"Unknown message type {devServerMessage?.GetType()} from DevServer"); + _logger.Error($"Error processing message from DevServer: {e}"); } } From 8d2600bf92ca280bd3197acfebcf16a69d9bcf7a Mon Sep 17 00:00:00 2001 From: David Date: Mon, 3 Jun 2024 13:36:28 -0400 Subject: [PATCH 06/27] feat: Instrument dev-server to expose in diagnostics in client --- .../DevServerDiagnostics.cs | 35 ++ .../IRemoteControlServer.cs | 2 +- .../Messages/Frame.cs | 104 +++-- .../WellKnownScopes.cs | 28 ++ .../_Compat/IsExternalInit.cs | 9 + .../_Compat/NotNullWhenAttribute.cs | 25 ++ .../Uno.UI.RemoteControl.Server.csproj | 1 + .../Helpers/VersionHelper.cs | 32 ++ .../HotReload/ClientHotReloadProcessor.cs | 2 +- ...ontrolProcessor.cs => IClientProcessor.cs} | 2 +- .../Messages/KeepAliveMessage.cs | 33 +- .../Messages/ProcessorsDiscovery.cs | 29 +- .../Messages/ProcessorsDiscoveryResponse.cs | 16 + .../RemoteControlClient.Diagnostics.cs | 377 ++++++++++++++++++ .../RemoteControlClient.cs | 108 ++++- 15 files changed, 721 insertions(+), 82 deletions(-) create mode 100644 src/Uno.UI.RemoteControl.Messaging/DevServerDiagnostics.cs create mode 100644 src/Uno.UI.RemoteControl.Messaging/WellKnownScopes.cs create mode 100644 src/Uno.UI.RemoteControl.Messaging/_Compat/IsExternalInit.cs create mode 100644 src/Uno.UI.RemoteControl.Messaging/_Compat/NotNullWhenAttribute.cs create mode 100644 src/Uno.UI.RemoteControl/Helpers/VersionHelper.cs rename src/Uno.UI.RemoteControl/{IRemoteControlProcessor.cs => IClientProcessor.cs} (85%) create mode 100644 src/Uno.UI.RemoteControl/Messages/ProcessorsDiscoveryResponse.cs create mode 100644 src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs diff --git a/src/Uno.UI.RemoteControl.Messaging/DevServerDiagnostics.cs b/src/Uno.UI.RemoteControl.Messaging/DevServerDiagnostics.cs new file mode 100644 index 000000000000..cc9169b20335 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Messaging/DevServerDiagnostics.cs @@ -0,0 +1,35 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading; +using Uno.UI.RemoteControl.HotReload.Messages; + +namespace Uno.UI.RemoteControl; + +public static class DevServerDiagnostics +{ + private static readonly AsyncLocal _current = new(); + + /// + /// Gets the current (async-local) sink for dev-server diagnostics. + /// + public static ISink Current + { + get => _current.Value ?? NullSink.Instance; + set => _current.Value = value; + } + + public interface ISink + { + void ReportInvalidFrame(Frame frame); + } + + private class NullSink : ISink + { + public static NullSink Instance { get; } = new(); + + public void ReportInvalidFrame(Frame frame) + { + } + } +} diff --git a/src/Uno.UI.RemoteControl.Messaging/IRemoteControlServer.cs b/src/Uno.UI.RemoteControl.Messaging/IRemoteControlServer.cs index 87c0ff3ef7f1..2c44f55d49c5 100644 --- a/src/Uno.UI.RemoteControl.Messaging/IRemoteControlServer.cs +++ b/src/Uno.UI.RemoteControl.Messaging/IRemoteControlServer.cs @@ -9,7 +9,7 @@ public interface IRemoteControlServer { string GetServerConfiguration(string key); - Task SendFrame(IMessage fileReload); + Task SendFrame(IMessage message); Task SendMessageToIDEAsync(IdeMessage message); } diff --git a/src/Uno.UI.RemoteControl.Messaging/Messages/Frame.cs b/src/Uno.UI.RemoteControl.Messaging/Messages/Frame.cs index 35e17da32ce8..832e0df32b6d 100644 --- a/src/Uno.UI.RemoteControl.Messaging/Messages/Frame.cs +++ b/src/Uno.UI.RemoteControl.Messaging/Messages/Frame.cs @@ -1,60 +1,88 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; using Newtonsoft.Json; -namespace Uno.UI.RemoteControl.HotReload.Messages +namespace Uno.UI.RemoteControl.HotReload.Messages; + +[DebuggerDisplay("{Name}-{Scope}")] +public class Frame { - [DebuggerDisplay("{Name}-{Scope}")] - public class Frame + public Frame(short version, string scope, string name, string content) { - public Frame(short version, string scope, string name, string content) - { - Version = version; - Scope = scope; - Name = name; - Content = content; - } + Version = version; + Scope = scope; + Name = name; + Content = content; + } - public int Version { get; } + public int Version { get; } - public string Scope { get; } + public string Scope { get; } - public string Name { get; } + public string Name { get; } - public string Content { get; } + public string Content { get; } - public static Frame Read(Stream stream) + public static Frame Read(Stream stream) + { + using (var reader = new BinaryReader(stream, Encoding.UTF8)) { - using (var reader = new BinaryReader(stream, Encoding.UTF8)) - { - var version = reader.ReadInt16(); - var scope = reader.ReadString(); - var name = reader.ReadString(); - var content = reader.ReadString(); + var version = reader.ReadInt16(); + var scope = reader.ReadString(); + var name = reader.ReadString(); + var content = reader.ReadString(); - return new Frame(version, scope, name, content); - } + return new Frame(version, scope, name, content); } + } - public static Frame Create(short version, string scope, string name, T content) - => new Frame( - version, - scope, - name, - JsonConvert.SerializeObject(content) - ); + public static Frame Create(short version, string scope, string name, T content) + => new Frame( + version, + scope, + name, + JsonConvert.SerializeObject(content) + ); - public void WriteTo(Stream stream) - { - var writer = new BinaryWriter(stream, Encoding.UTF8); + public T GetContent() + => TryGetContent(out var content) ? content : throw new InvalidOperationException("Invalid frame"); - writer.Write((short)Version); - writer.Write(Scope); - writer.Write(Name); - writer.Write(Content); + public bool TryGetContent([NotNullWhen(true)] out T? content) + { + try + { + content = JsonConvert.DeserializeObject(Content); + if (content is not null) + { + return true; + } + else + { + DevServerDiagnostics.Current.ReportInvalidFrame(this); + return false; + } } + catch (Exception) + { + DevServerDiagnostics.Current.ReportInvalidFrame(this); + content = default; + return false; + } + } + + public void WriteTo(Stream stream) + { + var writer = new BinaryWriter(stream, Encoding.UTF8); + + writer.Write((short)Version); + writer.Write(Scope); + writer.Write(Name); + writer.Write(Content); } } diff --git a/src/Uno.UI.RemoteControl.Messaging/WellKnownScopes.cs b/src/Uno.UI.RemoteControl.Messaging/WellKnownScopes.cs new file mode 100644 index 000000000000..d15bb2065190 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Messaging/WellKnownScopes.cs @@ -0,0 +1,28 @@ +#nullable enable +using System; +using System.Linq; + +namespace Uno.UI.RemoteControl; + +public static class WellKnownScopes +{ + /// + /// Reserved for internal usage of communication channel between dev-server and IDE (i.e. KeepAliveIdeMessage). + /// + public const string IdeChannel = "IdeChannel"; + + /// + /// Reserved for internal usage of communication channel between dev-server and client (i.e. KeepAliveMessage). + /// + public const string DevServerChannel = "RemoteControlServer"; + + /// + /// For hot-reload messages, client, server and IDE. + /// + public const string HotReload = "HotReload"; + + /// + /// For messages used for testing purpose (e.g. UpdateFile) + /// + public const string Testing = "UnoRuntimeTests"; +} diff --git a/src/Uno.UI.RemoteControl.Messaging/_Compat/IsExternalInit.cs b/src/Uno.UI.RemoteControl.Messaging/_Compat/IsExternalInit.cs new file mode 100644 index 000000000000..8b968152a272 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Messaging/_Compat/IsExternalInit.cs @@ -0,0 +1,9 @@ +#nullable enable +using System; +using System.Linq; + +namespace System.Runtime.CompilerServices; + +internal static class IsExternalInit +{ +} diff --git a/src/Uno.UI.RemoteControl.Messaging/_Compat/NotNullWhenAttribute.cs b/src/Uno.UI.RemoteControl.Messaging/_Compat/NotNullWhenAttribute.cs new file mode 100644 index 000000000000..78cdb75bfd3c --- /dev/null +++ b/src/Uno.UI.RemoteControl.Messaging/_Compat/NotNullWhenAttribute.cs @@ -0,0 +1,25 @@ +#nullable enable +using System; +using System.Linq; + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Parameter, Inherited = false)] +[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +internal sealed class NotNullWhenAttribute : global::System.Attribute +{ + /// + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + public NotNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } +} diff --git a/src/Uno.UI.RemoteControl.Server/Uno.UI.RemoteControl.Server.csproj b/src/Uno.UI.RemoteControl.Server/Uno.UI.RemoteControl.Server.csproj index ad19dadf18b2..091ea02be366 100644 --- a/src/Uno.UI.RemoteControl.Server/Uno.UI.RemoteControl.Server.csproj +++ b/src/Uno.UI.RemoteControl.Server/Uno.UI.RemoteControl.Server.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Uno.UI.RemoteControl/Helpers/VersionHelper.cs b/src/Uno.UI.RemoteControl/Helpers/VersionHelper.cs new file mode 100644 index 000000000000..afa13a2edf0f --- /dev/null +++ b/src/Uno.UI.RemoteControl/Helpers/VersionHelper.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace Uno.UI.RemoteControl.Helpers; + +internal class VersionHelper +{ + public static string GetVersion(Type type) + => GetVersion(type.Assembly); + + public static string GetVersion(Assembly assembly) + { + if (assembly + .GetCustomAttributesData() + .FirstOrDefault(data => data.AttributeType.Name.Contains("AssemblyInformationalVersion", StringComparison.OrdinalIgnoreCase)) + ?.ConstructorArguments + .FirstOrDefault() + .Value + ?.ToString() is { Length: > 0 } informationalVersion) + { + return informationalVersion; + } + + if (assembly.GetName().Version is { } assemblyVersion) + { + return assemblyVersion.ToString(); + } + + return "--unknown--"; + } +} diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs index 35fecc723720..c5b00452cb1b 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs @@ -15,7 +15,7 @@ namespace Uno.UI.RemoteControl.HotReload; -public partial class ClientHotReloadProcessor : IRemoteControlProcessor +public partial class ClientHotReloadProcessor : IClientProcessor { private string? _projectPath; private string[]? _xamlPaths; diff --git a/src/Uno.UI.RemoteControl/IRemoteControlProcessor.cs b/src/Uno.UI.RemoteControl/IClientProcessor.cs similarity index 85% rename from src/Uno.UI.RemoteControl/IRemoteControlProcessor.cs rename to src/Uno.UI.RemoteControl/IClientProcessor.cs index 9cd55a20d7b8..728374cd031b 100644 --- a/src/Uno.UI.RemoteControl/IRemoteControlProcessor.cs +++ b/src/Uno.UI.RemoteControl/IClientProcessor.cs @@ -6,7 +6,7 @@ namespace Uno.UI.RemoteControl; -internal interface IRemoteControlProcessor +internal interface IClientProcessor { string Scope { get; } diff --git a/src/Uno.UI.RemoteControl/Messages/KeepAliveMessage.cs b/src/Uno.UI.RemoteControl/Messages/KeepAliveMessage.cs index ae0a2ae67752..0e417173cffa 100644 --- a/src/Uno.UI.RemoteControl/Messages/KeepAliveMessage.cs +++ b/src/Uno.UI.RemoteControl/Messages/KeepAliveMessage.cs @@ -1,15 +1,28 @@ -namespace Uno.UI.RemoteControl.Messages +using System; +using Uno.UI.RemoteControl.Helpers; + +namespace Uno.UI.RemoteControl.Messages; + +public record KeepAliveMessage : IMessage { - public class KeepAliveMessage : IMessage - { - public const string Name = nameof(KeepAliveMessage); + private static readonly string _localVersion = VersionHelper.GetVersion(typeof(KeepAliveMessage)); + + public const string Name = nameof(KeepAliveMessage); + + public string Scope => WellKnownScopes.DevServerChannel; + + string IMessage.Name => Name; - public KeepAliveMessage() - { - } + /// + /// The version of the dev-server version of the sender. + /// + public string? AssemblyVersion { get; init; } = _localVersion; - public string Scope => "RemoteControlServer"; + /// + /// Sequence ID of the ping. + /// + public ulong SequenceId { get; init; } - string IMessage.Name => Name; - } + public KeepAliveMessage Next() + => this with { SequenceId = SequenceId + 1 }; } diff --git a/src/Uno.UI.RemoteControl/Messages/ProcessorsDiscovery.cs b/src/Uno.UI.RemoteControl/Messages/ProcessorsDiscovery.cs index 3922d5e7e463..e1900eacb521 100644 --- a/src/Uno.UI.RemoteControl/Messages/ProcessorsDiscovery.cs +++ b/src/Uno.UI.RemoteControl/Messages/ProcessorsDiscovery.cs @@ -1,21 +1,22 @@ -namespace Uno.UI.RemoteControl.Messages +using static Uno.UI.RemoteControl.Messages.ProcessorsDiscoveryResponse; + +namespace Uno.UI.RemoteControl.Messages; + +public class ProcessorsDiscovery : IMessage { - public class ProcessorsDiscovery : IMessage - { - public const string Name = nameof(ProcessorsDiscovery); + public const string Name = nameof(ProcessorsDiscovery); - public ProcessorsDiscovery(string basePath, string appInstanceId = "") - { - BasePath = basePath; - AppInstanceId = appInstanceId; - } + public ProcessorsDiscovery(string basePath, string appInstanceId = "") + { + BasePath = basePath; + AppInstanceId = appInstanceId; + } - public string Scope => "RemoteControlServer"; + public string Scope => WellKnownScopes.DevServerChannel; - string IMessage.Name => Name; + string IMessage.Name => Name; - public string BasePath { get; } + public string BasePath { get; } - public string AppInstanceId { get; } - } + public string AppInstanceId { get; } } diff --git a/src/Uno.UI.RemoteControl/Messages/ProcessorsDiscoveryResponse.cs b/src/Uno.UI.RemoteControl/Messages/ProcessorsDiscoveryResponse.cs new file mode 100644 index 000000000000..4c8bf3d4cc08 --- /dev/null +++ b/src/Uno.UI.RemoteControl/Messages/ProcessorsDiscoveryResponse.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Immutable; +using System.Linq; + +namespace Uno.UI.RemoteControl.Messages; + +public record ProcessorsDiscoveryResponse(IImmutableList Assemblies, IImmutableList Processors) : IMessage +{ + public const string Name = nameof(ProcessorsDiscoveryResponse); + + public string Scope => WellKnownScopes.DevServerChannel; + + string IMessage.Name => Name; +} + +public record DiscoveredProcessor(string AssemblyPath, string Type, string Version, bool IsLoaded, string? LoadError = null); diff --git a/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs b/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs new file mode 100644 index 000000000000..2d221944b1e9 --- /dev/null +++ b/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Windows.UI; +using Microsoft.UI; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Shapes; +using Uno.Diagnostics.UI; +using Uno.UI.RemoteControl.Messages; +using Frame = Uno.UI.RemoteControl.HotReload.Messages.Frame; + +namespace Uno.UI.RemoteControl; + +public partial class RemoteControlClient +{ + private interface IDiagnosticsSink : DevServerDiagnostics.ISink + { + void ReportActiveConnection(Connection? connection); + + void Report(ConnectionStatus status); + + void ReportPing(KeepAliveMessage ping); + + void ReportPong(KeepAliveMessage? pong); + + void ReportKeepAliveAborted(Exception error); + + void RegisterRequiredServerProcessor(string typeFullName, string version); + + void ReportServerProcessors(ProcessorsDiscoveryResponse response); + } + + private enum ConnectionStatus + { + /// + /// Client as not been started yet + /// + Idle, + + /// + /// No server information to connect to. + /// + NoServer, + + /// + /// Attempting to connect to the server. + /// + Connecting, + + /// + /// Reach timeout while trying to connect to the server. + /// Connection HAS NOT been established. + /// + ConnectionTimeout, + + /// + /// Connection to the server failed. + /// Connection HAS NOT been established. + /// + ConnectionFailed, + + /// + /// Connection to the server has been established. + /// + Connected, + + /// + /// Reconnecting to the server. + /// Connection has been established once but lost since then, reconnecting to the SAME server. + /// + Reconnecting, + + /// + /// Disconnected from the server. + /// Connection has been established once but lost since then and cannot be restored for now but will be retried later. + /// + Disconnected + } + + private enum KeepAliveStatus + { + Idle, + Ok, // Got ping/pong in expected delays + Late, // Sent ping without response within delay + Lost, // Got an invalid pong response + Aborted // KeepAlive was aborted + } + + private class DiagnosticsView : IDiagnosticsSink, IDiagnosticViewProvider + { + private ConnectionStatus _status = ConnectionStatus.Idle; + private readonly DiagnosticViewHelper _statusView; + + public DiagnosticsView() + { + _statusView = new( + () => new Ellipse { Width = 16, Height = 16, Fill = new SolidColorBrush(Colors.Gray) }, + ellipse => + { + ((SolidColorBrush)ellipse.Fill).Color = GetStatusColor(); + ToolTipService.SetToolTip(ellipse, GetStatusSummary()); + }); + + DiagnosticViewRegistry.Register(this); // Only register, do not make visible + } + + #region Connection status + public void ReportActiveConnection(Connection? connection) + => Report(connection switch + { + null when _status < ConnectionStatus.Connected => ConnectionStatus.ConnectionFailed, + null => ConnectionStatus.Disconnected, + _ => ConnectionStatus.Connected, + }); + + public void Report(ConnectionStatus status) + { + _status = status; + _statusView.NotifyChanged(); + } + #endregion + + #region KeepAlive (ping / pong) + private const int _pongLateDelay = 100; + private const int _pongTimeoutDelay = 1000; + private KeepAliveMessage? _lastPing; + private Stopwatch? _sinceLastPing; + private KeepAliveStatus _keepAliveStatus = KeepAliveStatus.Ok; // We assume Ok as startup to not wait for the first ping to turn green. + private bool? _isVersionValid; + private Timer? _pongTimeout; + private long _roundTrip = -1; + + public void ReportPing(KeepAliveMessage ping) + { + (_sinceLastPing ??= Stopwatch.StartNew()).Restart(); + _lastPing = ping; + + if (_pongTimeout is null) + { + Interlocked.CompareExchange(ref _pongTimeout, new Timer(OnPongLateOrTimeout, this, _pongLateDelay, _pongTimeoutDelay), null); + } + + _pongTimeout.Change(_pongLateDelay, _pongTimeoutDelay); + } + + private static void OnPongLateOrTimeout(object? state) + { + var that = (DiagnosticsView)state!; + + if (that._keepAliveStatus is KeepAliveStatus.Late) + { + that.ReportPong(null); + } + else + { + that._keepAliveStatus = KeepAliveStatus.Late; + that._statusView.NotifyChanged(); + } + } + + public void ReportPong(KeepAliveMessage? pong) + { + var ping = _lastPing; + if (ping is null || pong is null) + { + _sinceLastPing?.Stop(); + _pongTimeout?.Change(Timeout.Infinite, Timeout.Infinite); + _keepAliveStatus = KeepAliveStatus.Lost; + _statusView.NotifyChanged(); + return; + } + + if (pong.SequenceId < ping.SequenceId) + { + // Late pong, ignore it + return; + } + + _pongTimeout?.Change(Timeout.Infinite, Timeout.Infinite); + _roundTrip = _sinceLastPing!.ElapsedMilliseconds; + var isVersionValid = pong.AssemblyVersion == ping.AssemblyVersion; + if (_isVersionValid != isVersionValid) + { + _isVersionValid = isVersionValid; + _statusView.NotifyChanged(); + } + + if (_keepAliveStatus != KeepAliveStatus.Ok) + { + _keepAliveStatus = KeepAliveStatus.Ok; + _statusView.NotifyChanged(); + } + } + + public void ReportKeepAliveAborted(Exception error) + { + Interlocked.Exchange(ref _pongTimeout, null)?.Dispose(); + _sinceLastPing?.Stop(); + _keepAliveStatus = KeepAliveStatus.Aborted; + _statusView.NotifyChanged(); + } + #endregion + + #region Server status + private record ProcessorInfo(string TypeFullName, string Version); + private record struct MissingProcessor(string TypeFullName, string Version, string Details, string? Error = null); + private ImmutableHashSet _requiredProcessors = ImmutableHashSet.Empty; + private ProcessorsDiscoveryResponse? _loadedProcessors; + private bool? _isMissingRequiredProcessor; + + public void RegisterRequiredServerProcessor(string typeFullName, string version) + => ImmutableInterlocked.Update(ref _requiredProcessors, static (set, info) => set.Add(info), new ProcessorInfo(typeFullName, version)); + + public void ReportServerProcessors(ProcessorsDiscoveryResponse response) + { + _loadedProcessors = response; + + var isMissing = GetMissingServerProcessors().Any(); + if (_isMissingRequiredProcessor != isMissing) + { + _isMissingRequiredProcessor = isMissing; + _statusView.NotifyChanged(); + } + } + + private IEnumerable GetMissingServerProcessors() + { + var response = _loadedProcessors; + if (response is null) + { + yield break; + } + + var loaded = response.Processors.ToDictionary(p => p.Type, StringComparer.OrdinalIgnoreCase); + foreach (var required in _requiredProcessors) + { + if (!loaded.TryGetValue(required.TypeFullName, out var actual)) + { + yield return new MissingProcessor(required.TypeFullName, required.Version, "Processor not found by dev-server."); + continue; + } + + if (actual.LoadError is not null) + { + yield return new MissingProcessor(required.TypeFullName, required.Version, "Dev-server failed to create an instance.", actual.LoadError); + continue; + } + + if (!actual.IsLoaded) + { + yield return new MissingProcessor(required.TypeFullName, required.Version, "Type is not a valid server processor."); + continue; + } + + if (actual.Version != required.Version) + { + yield return new MissingProcessor(required.TypeFullName, required.Version, $"Version mismatch, client expected it to be {required.Version} but server loaded version {actual.Version}."); + } + } + } + #endregion + + #region Processors status + private long _invalidFrames; + private ImmutableHashSet _invalidFrameTypes = ImmutableHashSet.Empty; + + public void ReportInvalidFrame(Frame frame) + { + Interlocked.Increment(ref _invalidFrames); + ImmutableInterlocked.Update(ref _invalidFrameTypes, static (set, type) => set.Add(type), typeof(TContent)); + } + #endregion + + /// + string IDiagnosticViewProvider.Name => "Dev-server"; + + /// + object IDiagnosticViewProvider.GetPreview(IDiagnosticViewContext context) + => _statusView.GetView(context); + + /// + ValueTask IDiagnosticViewProvider.GetDetailsAsync(IDiagnosticViewContext context, CancellationToken ct) + => ValueTask.FromResult(GetStatusDetails()); + + private Color GetStatusColor() + => _status switch + { + ConnectionStatus.Idle => Colors.Gray, + ConnectionStatus.NoServer => Colors.Red, + ConnectionStatus.Connecting => Colors.Yellow, + ConnectionStatus.ConnectionTimeout => Colors.Red, + ConnectionStatus.ConnectionFailed => Colors.Red, + ConnectionStatus.Reconnecting => Colors.Yellow, + ConnectionStatus.Disconnected => Colors.Red, + + ConnectionStatus.Connected when _isVersionValid is false => Colors.Orange, + ConnectionStatus.Connected when _invalidFrames is not 0 => Colors.Orange, + ConnectionStatus.Connected when _keepAliveStatus is not KeepAliveStatus.Ok => Colors.Yellow, + ConnectionStatus.Connected => Colors.Green, + + _ => Colors.Gray + }; + + private string GetStatusSummary() + { + var status = _status switch + { + ConnectionStatus.Idle => "Initializing...", + ConnectionStatus.NoServer => "No server configured, cannot initialize connection.", + ConnectionStatus.Connecting => "Connecting to dev-server.", + ConnectionStatus.ConnectionTimeout => "Failed to connect to dev-server (timeout).", + ConnectionStatus.ConnectionFailed => "Failed to connect to dev-server (error).", + ConnectionStatus.Reconnecting => "Connection to dev-server has been lost, reconnecting.", + ConnectionStatus.Disconnected => "Connection to dev-server has been lost, will retry later.", + + ConnectionStatus.Connected when _isVersionValid is false => "Connected to dev-server, but version mis-match with client.", + ConnectionStatus.Connected when _invalidFrames is not 0 => $"Connected to dev-server, but received {_invalidFrames} invalid frames from the server.", + ConnectionStatus.Connected when _isMissingRequiredProcessor is true => "Connected to dev-server, but some required processors are missing on server.", + ConnectionStatus.Connected when _keepAliveStatus is KeepAliveStatus.Late => "Connected to dev-server, but keep-alive messages are taking longer than expected.", + ConnectionStatus.Connected when _keepAliveStatus is KeepAliveStatus.Lost => "Connected to dev-server, but last keep-alive messages has been lost.", + ConnectionStatus.Connected when _keepAliveStatus is KeepAliveStatus.Aborted => "Connected to dev-server, but keep-alive has been aborted.", + ConnectionStatus.Connected => "Connected to dev-server.", + + _ => _status.ToString() + }; + + if (_roundTrip >= 0) + { + status += $" (ping {_roundTrip}ms)"; + } + + return status; + } + + private string GetStatusDetails() + { + var details = new StringBuilder(GetStatusSummary()); + + if (GetMissingServerProcessors().ToList() is { Count: > 0 } missing) + { + details.AppendLine(); + details.AppendLine(); + details.AppendLine("Some processor requested by the client are missing on the server:"); + + foreach (var m in missing) + { + details.AppendLine($"- {m.TypeFullName} v{m.Version}: {m.Details}"); + if (m.Error is not null) + { + details.AppendLine($" {m.Error}"); + } + } + } + + if (_invalidFrameTypes is { Count: > 0 } invalidFrameTypes) + { + details.AppendLine(); + details.AppendLine(); + details.AppendLine($"Received {_invalidFrames} invalid frames from the server. Failing frame types ({invalidFrameTypes.Count}):"); + + foreach (var type in invalidFrameTypes) + { + details.AppendLine($"- {type.FullName}"); + } + } + + return details.ToString(); + } + } +} diff --git a/src/Uno.UI.RemoteControl/RemoteControlClient.cs b/src/Uno.UI.RemoteControl/RemoteControlClient.cs index 83fab0e9d4ac..65febb7d4275 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlClient.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlClient.cs @@ -24,7 +24,7 @@ namespace Uno.UI.RemoteControl; -public class RemoteControlClient : IRemoteControlClient +public partial class RemoteControlClient : IRemoteControlClient { public delegate void RemoteControlFrameReceivedEventHandler(object sender, ReceivedFrameEventArgs args); public delegate void RemoteControlClientEventEventHandler(object sender, ClientEventEventArgs args); @@ -38,7 +38,6 @@ public static RemoteControlClient Initialize(Type appType) internal static RemoteControlClient Initialize(Type appType, ServerEndpointAttribute[]? endpoints) => Instance = new RemoteControlClient(appType, endpoints); - public event RemoteControlFrameReceivedEventHandler? FrameReceived; public event RemoteControlClientEventEventHandler? ClientEvent; public event SendMessageFailedEventHandler? SendMessageFailed; @@ -55,12 +54,15 @@ internal static RemoteControlClient Initialize(Type appType, ServerEndpointAttri public TimeSpan ConnectionRetryInterval { get; } = TimeSpan.FromMilliseconds(_connectionRetryInterval); private const int _connectionRetryInterval = 5_000; + private readonly IDiagnosticsSink _diagnostics = new DiagnosticsView(); + private static readonly TimeSpan _keepAliveInterval = TimeSpan.FromSeconds(30); private readonly (string endpoint, int port)[]? _serverAddresses; - private readonly Dictionary _processors = new(); + private readonly Dictionary _processors = new(); private readonly List _preprocessors = new(); private readonly object _connectionGate = new(); private Task _connection; // null if no server, socket only null if connection was established once but lost since then private Timer? _keepAliveTimer; + private KeepAliveMessage _ping = new(); private record Connection(Uri EndPoint, Stopwatch Since, WebSocket? Socket); @@ -111,17 +113,21 @@ private RemoteControlClient(Type appType, ServerEndpointAttribute[]? endpoints = this.Log().LogError("Failed to get any remote control server endpoint from the IDE."); _connection = Task.FromResult(null); + _diagnostics.Report(ConnectionStatus.NoServer); return; } + // Enable hot-reload RegisterProcessor(new HotReload.ClientHotReloadProcessor(this)); + _diagnostics.RegisterRequiredServerProcessor("Uno.UI.RemoteControl.Host.HotReload.ServerHotReloadProcessor", VersionHelper.GetVersion(typeof(ClientHotReloadProcessor))); + _connection = StartConnection(); } public IEnumerable Processors => _processors.Values; - internal IRemoteControlProcessor[] RegisteredProcessors + internal IClientProcessor[] RegisteredProcessors => _processors.Values.ToArray(); internal Task WaitForConnection() @@ -130,7 +136,7 @@ internal Task WaitForConnection() public Task WaitForConnection(CancellationToken ct) => _connection; - private void RegisterProcessor(IRemoteControlProcessor processor) + private void RegisterProcessor(IClientProcessor processor) => _processors[processor.Scope] = processor; public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor) @@ -145,6 +151,8 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor) // We have a socket (and uri) but we lost the connection, try to reconnect (only if more than 5 sec since last attempt) lock (_connectionGate) { + _diagnostics.Report(ConnectionStatus.Reconnecting); + if (connectionTask == _connection) { _connection = Connect(connection.EndPoint, CancellationToken.None); @@ -154,6 +162,8 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor) connection = await _connection; } + _diagnostics.ReportActiveConnection(connection); + return connection?.Socket; } @@ -168,6 +178,7 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor) this.Log().LogWarning($"No server addresses provided, skipping."); } + _diagnostics.Report(ConnectionStatus.NoServer); return default; } @@ -177,6 +188,8 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor) const bool isHttps = false; #endif + _diagnostics.Report(ConnectionStatus.Connecting); + var pending = _serverAddresses .Where(adr => adr.port != 0 || Uri.TryCreate(adr.endpoint, UriKind.Absolute, out _)) .Select(s => @@ -204,6 +217,7 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor) this.Log().LogError("Failed to connect to the server (timeout)."); } + _diagnostics.Report(ConnectionStatus.ConnectionTimeout); AbortPending(); return null; @@ -224,6 +238,8 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor) // Abort all other pending connections AbortPending(); + _diagnostics.ReportActiveConnection(connected); + if (connected is null) { if (this.Log().IsEnabled(LogLevel.Error)) @@ -240,6 +256,7 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor) this.Log().LogDebug($"Connected to {connected!.EndPoint}"); } + DevServerDiagnostics.Current = _diagnostics; _ = ProcessMessages(connected!.Socket!); return connected; @@ -387,14 +404,15 @@ private async Task ProcessMessages(WebSocket socket) while (await WebSocketHelper.ReadFrame(socket, CancellationToken.None) is HotReload.Messages.Frame frame) { - if (frame.Scope == "RemoteControlServer") + if (frame.Scope == WellKnownScopes.DevServerChannel) { if (frame.Name == KeepAliveMessage.Name) { - if (this.Log().IsEnabled(LogLevel.Trace)) - { - this.Log().Trace($"Server Keepalive frame"); - } + ProcessPong(frame); + } + else if (frame.Name == ProcessorsDiscoveryResponse.Name) + { + ProcessServerProcessorsDiscovered(frame); } } else @@ -419,7 +437,17 @@ private async Task ProcessMessages(WebSocket socket) if (!skipProcessing) { - await processor.ProcessFrame(frame); + try + { + await processor.ProcessFrame(frame); + } + catch (Exception e) + { + if (this.Log().IsEnabled(LogLevel.Error)) + { + this.Log().LogError($"Error while processing frame [{frame.Scope}/{frame.Name}]", e); + } + } } } else @@ -439,12 +467,57 @@ private async Task ProcessMessages(WebSocket socket) { if (this.Log().IsEnabled(LogLevel.Error)) { - this.Log().LogError($"Error while processing frame {frame.Scope}/{frame.Name}", error); + this.Log().LogError($"Error while notifying frame received {frame.Scope}/{frame.Name}", error); } } } } + private void ProcessPong(Frame frame) + { + if (frame.TryGetContent(out KeepAliveMessage? pong)) + { + _diagnostics.ReportPong(pong); + + if (pong.AssemblyVersion != _ping.AssemblyVersion && this.Log().IsEnabled(LogLevel.Warning)) + { + this.Log().Trace( + $"Server pong frame (a.k.a. KeepAlive), but version differs from client (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."); + } + else if (this.Log().IsEnabled(LogLevel.Trace)) + { + this.Log().Trace($"Server pong frame (a.k.a. KeepAlive) with valid version ({pong.AssemblyVersion})."); + } + } + else + { + _diagnostics.ReportPong(null); + + if (this.Log().IsEnabled(LogLevel.Warning)) + { + this.Log().Trace( + "Server pong frame (a.k.a. KeepAlive), but failed to deserialize it's content. " + + $"This usually indicates a version mismatch between client and server (client: {_ping.AssemblyVersion})." + + "Some feature like hot-reload are most likely to fail. To fix this, you might have to restart Visual Studio."); + } + } + } + + private void ProcessServerProcessorsDiscovered(Frame frame) + { + if (frame.TryGetContent(out ProcessorsDiscoveryResponse? response)) + { + _diagnostics.ReportServerProcessors(response); + + if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().Debug($"Server loaded processors: \r\n{response.Processors.Select(p => $"\t- {p.Type} v {p.Version} (from {p.AssemblyPath})").JoinBy("\r\n")}."); + } + } + } + private void StartKeepAliveTimer() { if (_keepAliveTimer is not null) @@ -452,8 +525,6 @@ private void StartKeepAliveTimer() return; } - KeepAliveMessage keepAlive = new(); - Timer? timer = default; timer = new Timer(async _ => { @@ -464,15 +535,18 @@ private void StartKeepAliveTimer() this.Log().Trace($"Sending Keepalive frame from client"); } - await SendMessage(keepAlive); + _ping = _ping.Next(); + _diagnostics.ReportPing(_ping); + await SendMessage(_ping); } - catch (Exception) + catch (Exception error) { if (this.Log().IsEnabled(LogLevel.Trace)) { this.Log().Trace("Keepalive failed"); } + _diagnostics.ReportKeepAliveAborted(error); Interlocked.CompareExchange(ref _keepAliveTimer, null, timer); timer?.Dispose(); } @@ -480,7 +554,7 @@ private void StartKeepAliveTimer() if (Interlocked.CompareExchange(ref _keepAliveTimer, timer, null) is null) { - timer.Change(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30)); + timer.Change(_keepAliveInterval, _keepAliveInterval); } } From 3d92130b2998833a24d70d7644f843118bb5ccf5 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 3 Jun 2024 14:17:58 -0400 Subject: [PATCH 07/27] 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 + + + + From d1ff74d849b7315317ecf4e310e75820f4fa80c6 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 4 Jun 2024 15:02:19 -0400 Subject: [PATCH 08/27] chore: Apply code review coments --- .../Diagnostics/DiagnosticViewRegistry.cs | 12 +- .../RemoteControlServer.cs | 16 +- .../Uno.UI.RemoteControl.Messaging.csproj | 4 + .../_Compat/IsExternalInit.cs | 9 - .../_Compat/NotNullWhenAttribute.cs | 25 --- ...tusView.xaml.cs => HotReloadStatusView.cs} | 12 +- .../HotReload/HotReloadStatusView.xaml | 161 +++++++++--------- .../HotReload/Messages/FileUpdateResult.cs | 15 ++ .../HotReload/Messages/UpdateFile.cs | 11 -- .../MetadataUpdater/HotReloadAgent.cs | 14 +- src/Uno.UI.RemoteControl/Themes/Generic.xaml | 9 + .../Uno.UI.RemoteControl.Reference.csproj | 3 + .../Uno.UI.RemoteControl.Wasm.csproj | 2 + .../Uno.UI.RemoteControl.netcoremobile.csproj | 2 + .../Diagnostics/DiagnosticsOverlay.cs | 7 +- src/Uno.UI/Diagnostics/DiagnosticView.cs | 4 +- 16 files changed, 155 insertions(+), 151 deletions(-) delete mode 100644 src/Uno.UI.RemoteControl.Messaging/_Compat/IsExternalInit.cs delete mode 100644 src/Uno.UI.RemoteControl.Messaging/_Compat/NotNullWhenAttribute.cs rename src/Uno.UI.RemoteControl/HotReload/{HotReloadStatusView.xaml.cs => HotReloadStatusView.cs} (87%) create mode 100644 src/Uno.UI.RemoteControl/HotReload/Messages/FileUpdateResult.cs create mode 100644 src/Uno.UI.RemoteControl/Themes/Generic.xaml diff --git a/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs b/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs index 038b93760426..bca378697118 100644 --- a/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs +++ b/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs @@ -10,14 +10,14 @@ namespace Uno.Diagnostics.UI; /// internal static class DiagnosticViewRegistry { - internal static EventHandler>? Added; + internal static EventHandler>? Added; - private static ImmutableList _registrations = ImmutableList.Empty; + private static ImmutableArray _registrations = ImmutableArray.Empty; /// /// Gets the list of registered diagnostic providers. /// - internal static ImmutableList Registrations => _registrations; + internal static IImmutableList Registrations => _registrations; /// /// Register a global diagnostic provider that can be displayed on any window. @@ -28,15 +28,15 @@ public static void Register(IDiagnosticViewProvider provider) ImmutableInterlocked.Update( ref _registrations, static (providers, provider) => providers.Add(provider), - new DiagnosticViewRegistration(GlobalProviderMode.One, provider)); + new DiagnosticViewRegistration(DiagnosticViewRegistrationMode.One, provider)); Added?.Invoke(null, _registrations); } } -internal record DiagnosticViewRegistration(GlobalProviderMode Mode, IDiagnosticViewProvider Provider); +internal record DiagnosticViewRegistration(DiagnosticViewRegistrationMode Mode, IDiagnosticViewProvider Provider); -internal enum GlobalProviderMode +internal enum DiagnosticViewRegistrationMode { /// /// Diagnostic is being rendered as overlay on each window. diff --git a/src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs b/src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs index cfd8c5ed6311..e430e8d8b310 100644 --- a/src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs +++ b/src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs @@ -207,9 +207,9 @@ private void ProcessIdeMessage(object? sender, IdeMessage message) { if (_processors.TryGetValue(message.Scope, out var processor)) { - if (this.Log().IsEnabled(LogLevel.Debug)) + if (this.Log().IsEnabled(LogLevel.Trace)) { - this.Log().LogDebug("Received message [{Scope} / {Name}] to be processed by {processor}", message.Scope, message.GetType().Name, processor); + this.Log().LogTrace("Received message [{Scope} / {Name}] to be processed by {processor}", message.Scope, message.GetType().Name, processor); } var process = processor.ProcessIdeMessage(message, _ct.Token); @@ -225,9 +225,9 @@ private void ProcessIdeMessage(object? sender, IdeMessage message) } else { - if (this.Log().IsEnabled(LogLevel.Debug)) + if (this.Log().IsEnabled(LogLevel.Trace)) { - this.Log().LogDebug("Unknown Frame [{Scope} / {Name}]", message.Scope, message.GetType().Name); + this.Log().LogTrace("Unknown Frame [{Scope} / {Name}]", message.Scope, message.GetType().Name); } } } @@ -241,7 +241,7 @@ private async Task ProcessPingFrame(Frame frame) if (ping.AssemblyVersion != pong.AssemblyVersion && this.Log().IsEnabled(LogLevel.Warning)) { - this.Log().LogTrace( + this.Log().LogWarning( $"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."); @@ -257,7 +257,7 @@ private async Task ProcessPingFrame(Frame frame) if (this.Log().IsEnabled(LogLevel.Warning)) { - this.Log().LogTrace( + this.Log().LogWarning( "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."); @@ -309,9 +309,9 @@ private async Task ProcessDiscoveryFrame(Frame frame) var basePath = msg.BasePath.Replace('/', Path.DirectorySeparatorChar); #if NET9_0_OR_GREATER - basePath = Path.Combine(basePath, "net9.0"); + basePath = Path.Combine(basePath, "net9.0"); #elif NET8_0_OR_GREATER - basePath = Path.Combine(basePath, "net8.0"); + basePath = Path.Combine(basePath, "net8.0"); #endif // Additional processors may not need the directory added immediately above. diff --git a/src/Uno.UI.RemoteControl.Messaging/Uno.UI.RemoteControl.Messaging.csproj b/src/Uno.UI.RemoteControl.Messaging/Uno.UI.RemoteControl.Messaging.csproj index c0ff503761ff..e22b416dcf68 100644 --- a/src/Uno.UI.RemoteControl.Messaging/Uno.UI.RemoteControl.Messaging.csproj +++ b/src/Uno.UI.RemoteControl.Messaging/Uno.UI.RemoteControl.Messaging.csproj @@ -10,6 +10,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Uno.UI.RemoteControl.Messaging/_Compat/IsExternalInit.cs b/src/Uno.UI.RemoteControl.Messaging/_Compat/IsExternalInit.cs deleted file mode 100644 index 8b968152a272..000000000000 --- a/src/Uno.UI.RemoteControl.Messaging/_Compat/IsExternalInit.cs +++ /dev/null @@ -1,9 +0,0 @@ -#nullable enable -using System; -using System.Linq; - -namespace System.Runtime.CompilerServices; - -internal static class IsExternalInit -{ -} diff --git a/src/Uno.UI.RemoteControl.Messaging/_Compat/NotNullWhenAttribute.cs b/src/Uno.UI.RemoteControl.Messaging/_Compat/NotNullWhenAttribute.cs deleted file mode 100644 index 78cdb75bfd3c..000000000000 --- a/src/Uno.UI.RemoteControl.Messaging/_Compat/NotNullWhenAttribute.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable enable -using System; -using System.Linq; - -namespace System.Diagnostics.CodeAnalysis; - -/// -/// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. -/// -[global::System.AttributeUsage(global::System.AttributeTargets.Parameter, Inherited = false)] -[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -internal sealed class NotNullWhenAttribute : global::System.Attribute -{ - /// - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - public NotNullWhenAttribute(bool returnValue) - { - ReturnValue = returnValue; - } - - /// Gets the return value condition. - public bool ReturnValue { get; } -} diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml.cs b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs similarity index 87% rename from src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml.cs rename to src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs index 40856caf59d5..f54e2cbc0490 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml.cs +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs @@ -6,18 +6,24 @@ namespace Uno.UI.RemoteControl.HotReload; -internal sealed partial class HotReloadStatusView : UserControl +internal sealed partial class HotReloadStatusView : Control { private (long id, string state) _currentResult = (-1, "None"); public HotReloadStatusView() { - InitializeComponent(); + DefaultStyleKey = typeof(HotReloadStatusView); + } + + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); } public void Update(HotReloadStatusMessage? status) { - ToolTipService.SetToolTip(Root, GetStatusSummary(status)); + ToolTipService.SetToolTip(this, GetStatusSummary(status)); if (status is null) { diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml index 7218a2a4bedd..be7b2db60429 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml @@ -1,5 +1,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/FileUpdateResult.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/FileUpdateResult.cs new file mode 100644 index 000000000000..1c5181c2f1b7 --- /dev/null +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/FileUpdateResult.cs @@ -0,0 +1,15 @@ +using System; +using System.Linq; + +namespace Uno.UI.RemoteControl.HotReload.Messages; + +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/UpdateFile.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/UpdateFile.cs index 61ed1aae3901..51cfb6667c35 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/UpdateFile.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/UpdateFile.cs @@ -43,14 +43,3 @@ 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/MetadataUpdater/HotReloadAgent.cs b/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/HotReloadAgent.cs index 0c8f52a03f16..975433455682 100644 --- a/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/HotReloadAgent.cs +++ b/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/HotReloadAgent.cs @@ -76,8 +76,8 @@ 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. + // 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; @@ -112,13 +112,13 @@ internal void GetHandlerActions( { bool methodFound = false; - if (GetMethod(handlerType, "ClearCache") is MethodInfo clearCache) + if (GetUpdateMethod(handlerType, "ClearCache") is MethodInfo clearCache) { handlerActions.ClearCache.Add(CreateAction(clearCache)); methodFound = true; } - if (GetMethod(handlerType, "UpdateApplication") is MethodInfo updateApplication) + if (GetUpdateMethod(handlerType, "UpdateApplication") is MethodInfo updateApplication) { handlerActions.UpdateApplication.Add(CreateAction(updateApplication)); methodFound = true; @@ -150,7 +150,7 @@ internal void GetHandlerActions( }; } - MethodInfo? GetMethod([DynamicallyAccessedMembers(HotReloadHandlerLinkerFlags)] Type handlerType, string name) + MethodInfo? GetUpdateMethod([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)) @@ -158,7 +158,7 @@ internal void GetHandlerActions( return updateMethod; } - foreach (var method in handlerType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)) + foreach (MethodInfo method in handlerType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)) { if (method.Name == name) { @@ -235,8 +235,6 @@ 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/Themes/Generic.xaml b/src/Uno.UI.RemoteControl/Themes/Generic.xaml new file mode 100644 index 000000000000..aec1899494d2 --- /dev/null +++ b/src/Uno.UI.RemoteControl/Themes/Generic.xaml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj index 8b8d1cd6390b..bdfc5e3d8796 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj @@ -77,4 +77,7 @@ + + + diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj index 5a21ec99b352..7cfd616a719b 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj @@ -74,4 +74,6 @@ + + diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj index 38227301a225..5a1769e77baa 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj @@ -67,4 +67,6 @@ + + diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs index 615b385995da..dead47024a81 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs @@ -17,6 +17,9 @@ namespace Uno.Diagnostics.UI; +/// +/// An overlay layer used to inject analytics and diagnostics indicators into the UI. +/// public sealed partial class DiagnosticsOverlay { private static readonly ConditionalWeakTable _overlays = new(); @@ -210,8 +213,8 @@ private static void ShowHost(Popup host, bool isVisible) private bool ShouldMaterialize(DiagnosticViewRegistration registration) => registration.Mode switch { - GlobalProviderMode.All => true, - GlobalProviderMode.OnDemand => false, + DiagnosticViewRegistrationMode.All => true, + DiagnosticViewRegistrationMode.OnDemand => false, _ => _overlays.Count(overlay => overlay.Value.IsMaterialized(registration.Provider)) is 0 }; diff --git a/src/Uno.UI/Diagnostics/DiagnosticView.cs b/src/Uno.UI/Diagnostics/DiagnosticView.cs index c7e80cacda56..883a652d3c04 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticView.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticView.cs @@ -28,10 +28,10 @@ public DiagnosticView( string IDiagnosticViewProvider.Name => name; /// - public object GetPreview(IDiagnosticViewContext context) + public object GetPreview(IDiagnosticViewContext context) => preview(context); /// - public async ValueTask GetDetailsAsync(IDiagnosticViewContext context, CancellationToken ct) + public async ValueTask GetDetailsAsync(IDiagnosticViewContext context, CancellationToken ct) => details is null ? null : await details(context, ct); } From caba5713ac0a6f6a74cdecf2d8d0c5ce8978cd70 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 4 Jun 2024 15:09:01 -0400 Subject: [PATCH 09/27] chore: Typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérôme Laban --- src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs | 4 ++-- src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs index f54e2cbc0490..e5affa957a1b 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs @@ -40,9 +40,9 @@ public void Update(HotReloadStatusMessage? status) public static string GetStatusSummary(HotReloadStatusMessage? status) => status?.State switch { - HotReloadState.Disabled => "Hot-reload is disable.", + HotReloadState.Disabled => "Hot-reload is disabled.", HotReloadState.Initializing => "Hot-reload is initializing.", - HotReloadState.Idle => "Hot-reload server is ready and listing for file changes.", + HotReloadState.Idle => "Hot-reload server is ready and listening for file changes.", HotReloadState.Processing => "Hot-reload server is processing file changes", _ => "Unable to determine the state of the hot-reload server." }; diff --git a/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs b/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs index 2d221944b1e9..c15d85bbf8fb 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs @@ -324,7 +324,7 @@ private string GetStatusSummary() ConnectionStatus.Connected when _invalidFrames is not 0 => $"Connected to dev-server, but received {_invalidFrames} invalid frames from the server.", ConnectionStatus.Connected when _isMissingRequiredProcessor is true => "Connected to dev-server, but some required processors are missing on server.", ConnectionStatus.Connected when _keepAliveStatus is KeepAliveStatus.Late => "Connected to dev-server, but keep-alive messages are taking longer than expected.", - ConnectionStatus.Connected when _keepAliveStatus is KeepAliveStatus.Lost => "Connected to dev-server, but last keep-alive messages has been lost.", + ConnectionStatus.Connected when _keepAliveStatus is KeepAliveStatus.Lost => "Connected to dev-server, but last keep-alive messages have been lost.", ConnectionStatus.Connected when _keepAliveStatus is KeepAliveStatus.Aborted => "Connected to dev-server, but keep-alive has been aborted.", ConnectionStatus.Connected => "Connected to dev-server.", @@ -347,7 +347,7 @@ private string GetStatusDetails() { details.AppendLine(); details.AppendLine(); - details.AppendLine("Some processor requested by the client are missing on the server:"); + details.AppendLine("Some processor(s) requested by the client are missing on the server:"); foreach (var m in missing) { From f4148e0e6ebf02b0ee22599f108ffa9a1a638655 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 4 Jun 2024 16:38:20 -0400 Subject: [PATCH 10/27] chore: Extract RC.Status into dedicted class to improve re-use --- .../RemoteControlClient.Diagnostics.cs | 247 +++++------------- .../RemoteControlClient.cs | 12 +- .../RemoteControlStatusView.cs | 113 ++++++++ .../RemoteControlStatusView.xaml | 15 ++ src/Uno.UI.RemoteControl/Themes/Generic.xaml | 1 + .../Diagnostics/DiagnosticView.Factories.cs | 11 +- .../DiagnosticViewProvider.TView.TState.cs | 13 +- 7 files changed, 223 insertions(+), 189 deletions(-) create mode 100644 src/Uno.UI.RemoteControl/RemoteControlStatusView.cs create mode 100644 src/Uno.UI.RemoteControl/RemoteControlStatusView.xaml diff --git a/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs b/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs index c15d85bbf8fb..6493a6ff4bcc 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs @@ -2,16 +2,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text; using System.Threading; -using System.Threading.Tasks; -using Windows.UI; -using Microsoft.UI; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Shapes; using Uno.Diagnostics.UI; using Uno.UI.RemoteControl.Messages; using Frame = Uno.UI.RemoteControl.HotReload.Messages.Frame; @@ -24,7 +16,7 @@ private interface IDiagnosticsSink : DevServerDiagnostics.ISink { void ReportActiveConnection(Connection? connection); - void Report(ConnectionStatus status); + void Report(ConnectionState state); void ReportPing(KeepAliveMessage ping); @@ -37,7 +29,7 @@ private interface IDiagnosticsSink : DevServerDiagnostics.ISink void ReportServerProcessors(ProcessorsDiscoveryResponse response); } - private enum ConnectionStatus + internal enum ConnectionState { /// /// Client as not been started yet @@ -84,7 +76,7 @@ private enum ConnectionStatus Disconnected } - private enum KeepAliveStatus + internal enum KeepAliveState { Idle, Ok, // Got ping/pong in expected delays @@ -93,37 +85,42 @@ private enum KeepAliveStatus Aborted // KeepAlive was aborted } - private class DiagnosticsView : IDiagnosticsSink, IDiagnosticViewProvider + internal record Status( + ConnectionState State, + bool? IsVersionValid, + (KeepAliveState State, long RoundTrip) KeepAlive, + ImmutableHashSet MissingRequiredProcessors, + (long Count, ImmutableHashSet Types) InvalidFrames); + + internal record struct MissingProcessor(string TypeFullName, string Version, string Details, string? Error = null); + + private class DiagnosticsSink : IDiagnosticsSink { - private ConnectionStatus _status = ConnectionStatus.Idle; - private readonly DiagnosticViewHelper _statusView; + private ConnectionState _state = ConnectionState.Idle; + private readonly DiagnosticView _view = DiagnosticView.Register( + "Dev-server", + (view, status) => view.Update(status), + RemoteControlStatusView.GetStatusDetails); - public DiagnosticsView() - { - _statusView = new( - () => new Ellipse { Width = 16, Height = 16, Fill = new SolidColorBrush(Colors.Gray) }, - ellipse => - { - ((SolidColorBrush)ellipse.Fill).Color = GetStatusColor(); - ToolTipService.SetToolTip(ellipse, GetStatusSummary()); - }); + public Status BuildStatus() + => new(_state, _isVersionValid, (_keepAliveState, _roundTrip), _missingRequiredProcessors, (_invalidFrames, _invalidFrameTypes)); - DiagnosticViewRegistry.Register(this); // Only register, do not make visible - } + private void NotifyStatusChanged() + => _view.Update(BuildStatus()); #region Connection status public void ReportActiveConnection(Connection? connection) => Report(connection switch { - null when _status < ConnectionStatus.Connected => ConnectionStatus.ConnectionFailed, - null => ConnectionStatus.Disconnected, - _ => ConnectionStatus.Connected, + null when _state < ConnectionState.Connected => ConnectionState.ConnectionFailed, + null => ConnectionState.Disconnected, + _ => ConnectionState.Connected, }); - public void Report(ConnectionStatus status) + public void Report(ConnectionState state) { - _status = status; - _statusView.NotifyChanged(); + _state = state; + NotifyStatusChanged(); } #endregion @@ -132,7 +129,7 @@ public void Report(ConnectionStatus status) private const int _pongTimeoutDelay = 1000; private KeepAliveMessage? _lastPing; private Stopwatch? _sinceLastPing; - private KeepAliveStatus _keepAliveStatus = KeepAliveStatus.Ok; // We assume Ok as startup to not wait for the first ping to turn green. + private KeepAliveState _keepAliveState = KeepAliveState.Ok; // We assume Ok as startup to not wait for the first ping to turn green. private bool? _isVersionValid; private Timer? _pongTimeout; private long _roundTrip = -1; @@ -152,16 +149,16 @@ public void ReportPing(KeepAliveMessage ping) private static void OnPongLateOrTimeout(object? state) { - var that = (DiagnosticsView)state!; + var that = (DiagnosticsSink)state!; - if (that._keepAliveStatus is KeepAliveStatus.Late) + if (that._keepAliveState is KeepAliveState.Late) { that.ReportPong(null); } else { - that._keepAliveStatus = KeepAliveStatus.Late; - that._statusView.NotifyChanged(); + that._keepAliveState = KeepAliveState.Late; + that.NotifyStatusChanged(); } } @@ -172,8 +169,8 @@ public void ReportPong(KeepAliveMessage? pong) { _sinceLastPing?.Stop(); _pongTimeout?.Change(Timeout.Infinite, Timeout.Infinite); - _keepAliveStatus = KeepAliveStatus.Lost; - _statusView.NotifyChanged(); + _keepAliveState = KeepAliveState.Lost; + NotifyStatusChanged(); return; } @@ -189,13 +186,13 @@ public void ReportPong(KeepAliveMessage? pong) if (_isVersionValid != isVersionValid) { _isVersionValid = isVersionValid; - _statusView.NotifyChanged(); + NotifyStatusChanged(); } - if (_keepAliveStatus != KeepAliveStatus.Ok) + if (_keepAliveState != KeepAliveState.Ok) { - _keepAliveStatus = KeepAliveStatus.Ok; - _statusView.NotifyChanged(); + _keepAliveState = KeepAliveState.Ok; + NotifyStatusChanged(); } } @@ -203,65 +200,53 @@ public void ReportKeepAliveAborted(Exception error) { Interlocked.Exchange(ref _pongTimeout, null)?.Dispose(); _sinceLastPing?.Stop(); - _keepAliveStatus = KeepAliveStatus.Aborted; - _statusView.NotifyChanged(); + _keepAliveState = KeepAliveState.Aborted; + NotifyStatusChanged(); } #endregion #region Server status private record ProcessorInfo(string TypeFullName, string Version); - private record struct MissingProcessor(string TypeFullName, string Version, string Details, string? Error = null); + private ImmutableHashSet _requiredProcessors = ImmutableHashSet.Empty; - private ProcessorsDiscoveryResponse? _loadedProcessors; - private bool? _isMissingRequiredProcessor; + private ImmutableHashSet _missingRequiredProcessors = ImmutableHashSet.Empty; public void RegisterRequiredServerProcessor(string typeFullName, string version) => ImmutableInterlocked.Update(ref _requiredProcessors, static (set, info) => set.Add(info), new ProcessorInfo(typeFullName, version)); public void ReportServerProcessors(ProcessorsDiscoveryResponse response) { - _loadedProcessors = response; + _missingRequiredProcessors = GetMissingServerProcessors(_requiredProcessors, response).ToImmutableHashSet(); - var isMissing = GetMissingServerProcessors().Any(); - if (_isMissingRequiredProcessor != isMissing) - { - _isMissingRequiredProcessor = isMissing; - _statusView.NotifyChanged(); - } - } + NotifyStatusChanged(); - private IEnumerable GetMissingServerProcessors() - { - var response = _loadedProcessors; - if (response is null) + static IEnumerable GetMissingServerProcessors(ImmutableHashSet requiredProcessors, ProcessorsDiscoveryResponse response) { - yield break; - } - - var loaded = response.Processors.ToDictionary(p => p.Type, StringComparer.OrdinalIgnoreCase); - foreach (var required in _requiredProcessors) - { - if (!loaded.TryGetValue(required.TypeFullName, out var actual)) + var loaded = response.Processors.ToDictionary(p => p.Type, StringComparer.OrdinalIgnoreCase); + foreach (var required in requiredProcessors) { - yield return new MissingProcessor(required.TypeFullName, required.Version, "Processor not found by dev-server."); - continue; - } + if (!loaded.TryGetValue(required.TypeFullName, out var actual)) + { + yield return new MissingProcessor(required.TypeFullName, required.Version, "Processor not found by dev-server."); + continue; + } - if (actual.LoadError is not null) - { - yield return new MissingProcessor(required.TypeFullName, required.Version, "Dev-server failed to create an instance.", actual.LoadError); - continue; - } + if (actual.LoadError is not null) + { + yield return new MissingProcessor(required.TypeFullName, required.Version, "Dev-server failed to create an instance.", actual.LoadError); + continue; + } - if (!actual.IsLoaded) - { - yield return new MissingProcessor(required.TypeFullName, required.Version, "Type is not a valid server processor."); - continue; - } + if (!actual.IsLoaded) + { + yield return new MissingProcessor(required.TypeFullName, required.Version, "Type is not a valid server processor."); + continue; + } - if (actual.Version != required.Version) - { - yield return new MissingProcessor(required.TypeFullName, required.Version, $"Version mismatch, client expected it to be {required.Version} but server loaded version {actual.Version}."); + if (actual.Version != required.Version) + { + yield return new MissingProcessor(required.TypeFullName, required.Version, $"Version mismatch, client expected it to be {required.Version} but server loaded version {actual.Version}."); + } } } } @@ -275,103 +260,9 @@ public void ReportInvalidFrame(Frame frame) { Interlocked.Increment(ref _invalidFrames); ImmutableInterlocked.Update(ref _invalidFrameTypes, static (set, type) => set.Add(type), typeof(TContent)); - } - #endregion - /// - string IDiagnosticViewProvider.Name => "Dev-server"; - - /// - object IDiagnosticViewProvider.GetPreview(IDiagnosticViewContext context) - => _statusView.GetView(context); - - /// - ValueTask IDiagnosticViewProvider.GetDetailsAsync(IDiagnosticViewContext context, CancellationToken ct) - => ValueTask.FromResult(GetStatusDetails()); - - private Color GetStatusColor() - => _status switch - { - ConnectionStatus.Idle => Colors.Gray, - ConnectionStatus.NoServer => Colors.Red, - ConnectionStatus.Connecting => Colors.Yellow, - ConnectionStatus.ConnectionTimeout => Colors.Red, - ConnectionStatus.ConnectionFailed => Colors.Red, - ConnectionStatus.Reconnecting => Colors.Yellow, - ConnectionStatus.Disconnected => Colors.Red, - - ConnectionStatus.Connected when _isVersionValid is false => Colors.Orange, - ConnectionStatus.Connected when _invalidFrames is not 0 => Colors.Orange, - ConnectionStatus.Connected when _keepAliveStatus is not KeepAliveStatus.Ok => Colors.Yellow, - ConnectionStatus.Connected => Colors.Green, - - _ => Colors.Gray - }; - - private string GetStatusSummary() - { - var status = _status switch - { - ConnectionStatus.Idle => "Initializing...", - ConnectionStatus.NoServer => "No server configured, cannot initialize connection.", - ConnectionStatus.Connecting => "Connecting to dev-server.", - ConnectionStatus.ConnectionTimeout => "Failed to connect to dev-server (timeout).", - ConnectionStatus.ConnectionFailed => "Failed to connect to dev-server (error).", - ConnectionStatus.Reconnecting => "Connection to dev-server has been lost, reconnecting.", - ConnectionStatus.Disconnected => "Connection to dev-server has been lost, will retry later.", - - ConnectionStatus.Connected when _isVersionValid is false => "Connected to dev-server, but version mis-match with client.", - ConnectionStatus.Connected when _invalidFrames is not 0 => $"Connected to dev-server, but received {_invalidFrames} invalid frames from the server.", - ConnectionStatus.Connected when _isMissingRequiredProcessor is true => "Connected to dev-server, but some required processors are missing on server.", - ConnectionStatus.Connected when _keepAliveStatus is KeepAliveStatus.Late => "Connected to dev-server, but keep-alive messages are taking longer than expected.", - ConnectionStatus.Connected when _keepAliveStatus is KeepAliveStatus.Lost => "Connected to dev-server, but last keep-alive messages have been lost.", - ConnectionStatus.Connected when _keepAliveStatus is KeepAliveStatus.Aborted => "Connected to dev-server, but keep-alive has been aborted.", - ConnectionStatus.Connected => "Connected to dev-server.", - - _ => _status.ToString() - }; - - if (_roundTrip >= 0) - { - status += $" (ping {_roundTrip}ms)"; - } - - return status; - } - - private string GetStatusDetails() - { - var details = new StringBuilder(GetStatusSummary()); - - if (GetMissingServerProcessors().ToList() is { Count: > 0 } missing) - { - details.AppendLine(); - details.AppendLine(); - details.AppendLine("Some processor(s) requested by the client are missing on the server:"); - - foreach (var m in missing) - { - details.AppendLine($"- {m.TypeFullName} v{m.Version}: {m.Details}"); - if (m.Error is not null) - { - details.AppendLine($" {m.Error}"); - } - } - } - - if (_invalidFrameTypes is { Count: > 0 } invalidFrameTypes) - { - details.AppendLine(); - details.AppendLine(); - details.AppendLine($"Received {_invalidFrames} invalid frames from the server. Failing frame types ({invalidFrameTypes.Count}):"); - - foreach (var type in invalidFrameTypes) - { - details.AppendLine($"- {type.FullName}"); - } - } - - return details.ToString(); + NotifyStatusChanged(); } + #endregion } } diff --git a/src/Uno.UI.RemoteControl/RemoteControlClient.cs b/src/Uno.UI.RemoteControl/RemoteControlClient.cs index 65febb7d4275..a6ec5aeb86ad 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlClient.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlClient.cs @@ -54,7 +54,7 @@ internal static RemoteControlClient Initialize(Type appType, ServerEndpointAttri public TimeSpan ConnectionRetryInterval { get; } = TimeSpan.FromMilliseconds(_connectionRetryInterval); private const int _connectionRetryInterval = 5_000; - private readonly IDiagnosticsSink _diagnostics = new DiagnosticsView(); + private readonly IDiagnosticsSink _diagnostics = new DiagnosticsSink(); private static readonly TimeSpan _keepAliveInterval = TimeSpan.FromSeconds(30); private readonly (string endpoint, int port)[]? _serverAddresses; private readonly Dictionary _processors = new(); @@ -113,7 +113,7 @@ private RemoteControlClient(Type appType, ServerEndpointAttribute[]? endpoints = this.Log().LogError("Failed to get any remote control server endpoint from the IDE."); _connection = Task.FromResult(null); - _diagnostics.Report(ConnectionStatus.NoServer); + _diagnostics.Report(ConnectionState.NoServer); return; } @@ -151,7 +151,7 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor) // We have a socket (and uri) but we lost the connection, try to reconnect (only if more than 5 sec since last attempt) lock (_connectionGate) { - _diagnostics.Report(ConnectionStatus.Reconnecting); + _diagnostics.Report(ConnectionState.Reconnecting); if (connectionTask == _connection) { @@ -178,7 +178,7 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor) this.Log().LogWarning($"No server addresses provided, skipping."); } - _diagnostics.Report(ConnectionStatus.NoServer); + _diagnostics.Report(ConnectionState.NoServer); return default; } @@ -188,7 +188,7 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor) const bool isHttps = false; #endif - _diagnostics.Report(ConnectionStatus.Connecting); + _diagnostics.Report(ConnectionState.Connecting); var pending = _serverAddresses .Where(adr => adr.port != 0 || Uri.TryCreate(adr.endpoint, UriKind.Absolute, out _)) @@ -217,7 +217,7 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor) this.Log().LogError("Failed to connect to the server (timeout)."); } - _diagnostics.Report(ConnectionStatus.ConnectionTimeout); + _diagnostics.Report(ConnectionState.ConnectionTimeout); AbortPending(); return null; diff --git a/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs b/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs new file mode 100644 index 000000000000..886cbad89df1 --- /dev/null +++ b/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs @@ -0,0 +1,113 @@ +using System; +using System.Linq; +using System.Text; +using Windows.UI; +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Shapes; +using Uno.UI.RemoteControl.HotReload.Messages; + +namespace Uno.UI.RemoteControl; + +internal sealed partial class RemoteControlStatusView : Ellipse +{ + public RemoteControlStatusView() + { + Fill = new SolidColorBrush(Colors.Gray); + Width = 16; + Height = 16; + } + + public void Update(RemoteControlClient.Status status) + { + ((SolidColorBrush)Fill).Color = GetStatusColor(status); + ToolTipService.SetToolTip(this, GetStatusSummary(status)); + } + + private Color GetStatusColor(RemoteControlClient.Status status) + => status.State switch + { + RemoteControlClient.ConnectionState.Idle => Colors.Gray, + RemoteControlClient.ConnectionState.NoServer => Colors.Red, + RemoteControlClient.ConnectionState.Connecting => Colors.Yellow, + RemoteControlClient.ConnectionState.ConnectionTimeout => Colors.Red, + RemoteControlClient.ConnectionState.ConnectionFailed => Colors.Red, + RemoteControlClient.ConnectionState.Reconnecting => Colors.Yellow, + RemoteControlClient.ConnectionState.Disconnected => Colors.Red, + + RemoteControlClient.ConnectionState.Connected when status.IsVersionValid is false => Colors.Orange, + RemoteControlClient.ConnectionState.Connected when status.InvalidFrames.Count is not 0 => Colors.Orange, + RemoteControlClient.ConnectionState.Connected when status.KeepAlive.State is not RemoteControlClient.KeepAliveState.Ok => Colors.Yellow, + RemoteControlClient.ConnectionState.Connected => Colors.Green, + + _ => Colors.Gray + }; + + private static string GetStatusSummary(RemoteControlClient.Status status) + { + var summary = status.State switch + { + RemoteControlClient.ConnectionState.Idle => "Initializing...", + RemoteControlClient.ConnectionState.NoServer => "No server configured, cannot initialize connection.", + RemoteControlClient.ConnectionState.Connecting => "Connecting to dev-server.", + RemoteControlClient.ConnectionState.ConnectionTimeout => "Failed to connect to dev-server (timeout).", + RemoteControlClient.ConnectionState.ConnectionFailed => "Failed to connect to dev-server (error).", + RemoteControlClient.ConnectionState.Reconnecting => "Connection to dev-server has been lost, reconnecting.", + RemoteControlClient.ConnectionState.Disconnected => "Connection to dev-server has been lost, will retry later.", + + RemoteControlClient.ConnectionState.Connected when status.IsVersionValid is false => "Connected to dev-server, but version mis-match with client.", + RemoteControlClient.ConnectionState.Connected when status.InvalidFrames.Count is not 0 => $"Connected to dev-server, but received {status.InvalidFrames.Count} invalid frames from the server.", + RemoteControlClient.ConnectionState.Connected when status.MissingRequiredProcessors is { IsEmpty: false } => "Connected to dev-server, but some required processors are missing on server.", + RemoteControlClient.ConnectionState.Connected when status.KeepAlive.State is RemoteControlClient.KeepAliveState.Late => "Connected to dev-server, but keep-alive messages are taking longer than expected.", + RemoteControlClient.ConnectionState.Connected when status.KeepAlive.State is RemoteControlClient.KeepAliveState.Lost => "Connected to dev-server, but last keep-alive messages have been lost.", + RemoteControlClient.ConnectionState.Connected when status.KeepAlive.State is RemoteControlClient.KeepAliveState.Aborted => "Connected to dev-server, but keep-alive has been aborted.", + RemoteControlClient.ConnectionState.Connected => "Connected to dev-server.", + + _ => status.State.ToString() + }; + + if (status.KeepAlive.RoundTrip >= 0) + { + summary += $" (ping {status.KeepAlive.RoundTrip}ms)"; + } + + return summary; + } + + internal static string GetStatusDetails(RemoteControlClient.Status status) + { + var details = new StringBuilder(GetStatusSummary(status)); + + if (status.MissingRequiredProcessors is { Count: > 0 } missing) + { + details.AppendLine(); + details.AppendLine(); + details.AppendLine("Some processor(s) requested by the client are missing on the server:"); + + foreach (var m in missing) + { + details.AppendLine($"- {m.TypeFullName} v{m.Version}: {m.Details}"); + if (m.Error is not null) + { + details.AppendLine($" {m.Error}"); + } + } + } + + if (status.InvalidFrames.Types is { Count: > 0 } invalidFrameTypes) + { + details.AppendLine(); + details.AppendLine(); + details.AppendLine($"Received {status.InvalidFrames.Count} invalid frames from the server. Failing frame types ({invalidFrameTypes.Count}):"); + + foreach (var type in invalidFrameTypes) + { + details.AppendLine($"- {type.FullName}"); + } + } + + return details.ToString(); + } +} diff --git a/src/Uno.UI.RemoteControl/RemoteControlStatusView.xaml b/src/Uno.UI.RemoteControl/RemoteControlStatusView.xaml new file mode 100644 index 000000000000..c320f7888b0e --- /dev/null +++ b/src/Uno.UI.RemoteControl/RemoteControlStatusView.xaml @@ -0,0 +1,15 @@ + + + + diff --git a/src/Uno.UI.RemoteControl/Themes/Generic.xaml b/src/Uno.UI.RemoteControl/Themes/Generic.xaml index aec1899494d2..f56651e9b43a 100644 --- a/src/Uno.UI.RemoteControl/Themes/Generic.xaml +++ b/src/Uno.UI.RemoteControl/Themes/Generic.xaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + diff --git a/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs b/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs index 8c3a6c334014..d214e8e68a04 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs @@ -1,6 +1,8 @@ #nullable enable using System; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.UI.Xaml; namespace Uno.Diagnostics.UI; @@ -15,10 +17,15 @@ public static DiagnosticView Register(string name, Action u return provider; } - public static DiagnosticView Register(string name, Action update) + public static DiagnosticView Register( + string name, + Action update, + Func? details = null) where TView : FrameworkElement, new() { - var provider = new DiagnosticView(name, () => new TView(), update); + var provider = details is null + ? new DiagnosticView(name, () => new TView(), update) + : new DiagnosticView(name, () => new TView(), update, (ctx, state, ct) => new(details(state)); DiagnosticViewRegistry.Register(provider); return provider; } diff --git a/src/Uno.UI/Diagnostics/DiagnosticViewProvider.TView.TState.cs b/src/Uno.UI/Diagnostics/DiagnosticViewProvider.TView.TState.cs index bef37ee07022..87464a685be2 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticViewProvider.TView.TState.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticViewProvider.TView.TState.cs @@ -11,14 +11,21 @@ internal class DiagnosticView( string name, Func preview, Action update, - Func>? details = null) + Func>? details = null) : IDiagnosticViewProvider where TView : FrameworkElement { private readonly DiagnosticViewHelper _statusView = new(preview, update); + private bool _hasState; + private TState? _state; + public void Update(TState status) - => _statusView.NotifyChanged(status); + { + _state = status; + _hasState = true; + _statusView.NotifyChanged(status); + } /// string IDiagnosticViewProvider.Name => name; @@ -29,5 +36,5 @@ object IDiagnosticViewProvider.GetPreview(IDiagnosticViewContext context) /// async ValueTask IDiagnosticViewProvider.GetDetailsAsync(IDiagnosticViewContext context, CancellationToken ct) - => details is null ? null : await details(context, ct); + => _hasState && details is not null ? await details(context, _state!, ct) : null; } From 18cf1190077e13a3d718f3d2c232a8719e0a79c4 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 6 Jun 2024 09:19:20 -0400 Subject: [PATCH 11/27] feat: Improve HR diag view --- ...ServerHotReloadProcessor.MetadataUpdate.cs | 10 +- .../HotReload/ServerHotReloadProcessor.cs | 67 +++--- .../ClientHotReloadProcessor.Agent.cs | 16 +- .../ClientHotReloadProcessor.Common.Status.cs | 202 ++++++++++++++++++ ...ClientHotReloadProcessor.MetadataUpdate.cs | 77 ++++--- .../HotReload/ClientHotReloadProcessor.cs | 13 +- .../HotReload/HotReloadStatusView.cs | 201 +++++++++++++---- .../HotReload/HotReloadStatusView.xaml | 45 +++- ...loadResult.cs => HotReloadServerResult.cs} | 4 +- .../Messages/HotReloadStatusMessage.cs | 9 +- .../MetadataUpdater/ElementUpdaterAgent.cs | 5 +- .../RemoteControlClient.Diagnostics.cs | 2 +- .../Diagnostics/DiagnosticView.Factories.cs | 4 +- src/Uno.UI/Helpers/TypeMappings.cs | 9 +- 14 files changed, 520 insertions(+), 144 deletions(-) create mode 100644 src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs rename src/Uno.UI.RemoteControl/HotReload/Messages/{HotReloadResult.cs => HotReloadServerResult.cs} (89%) 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 213757f40f49..07318b189bee 100644 --- a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.MetadataUpdate.cs +++ b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.MetadataUpdate.cs @@ -164,7 +164,7 @@ private async Task ProcessMetadataChanges(IEnumerable filePaths) } } - private async Task ProcessSolutionChanged(HotReloadOperation hotReload, string file, CancellationToken cancellationToken) + private async Task ProcessSolutionChanged(HotReloadServerOperation hotReload, string file, CancellationToken cancellationToken) { if (!await EnsureSolutionInitializedAsync() || _currentSolution is null || _hotReloadService is null) { @@ -219,12 +219,12 @@ private async Task ProcessSolutionChanged(HotReloadOperation hotReload, st if (diagnostics.IsDefaultOrEmpty) { await UpdateMetadata(file, updates); - hotReload.NotifyIntermediate(file, HotReloadResult.NoChanges); + hotReload.NotifyIntermediate(file, HotReloadServerResult.NoChanges); } else { _reporter.Output($"Got {diagnostics.Length} errors"); - hotReload.NotifyIntermediate(file, HotReloadResult.Failed); + hotReload.NotifyIntermediate(file, HotReloadServerResult.Failed); } // HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler); @@ -241,7 +241,7 @@ private async Task ProcessSolutionChanged(HotReloadOperation hotReload, st _reporter.Verbose(CSharpDiagnosticFormatter.Instance.Format(diagnostic, CultureInfo.InvariantCulture)); } - hotReload.NotifyIntermediate(file, HotReloadResult.RudeEdit); + hotReload.NotifyIntermediate(file, HotReloadServerResult.RudeEdit); // HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler); return false; @@ -252,7 +252,7 @@ private async Task ProcessSolutionChanged(HotReloadOperation hotReload, st sw.Stop(); await UpdateMetadata(file, updates); - hotReload.NotifyIntermediate(file, HotReloadResult.Success); + hotReload.NotifyIntermediate(file, HotReloadServerResult.Success); // HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler); 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 32c9ba00655e..86306d4cd6d1 100644 --- a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs +++ b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs @@ -73,7 +73,7 @@ public async Task ProcessIdeMessage(IdeMessage message, CancellationToken ct) #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 + private HotReloadServerOperation? _current; // I.e. head of the operation chain list public enum HotReloadEventSource { @@ -89,13 +89,13 @@ private async ValueTask EnsureHotReloadStarted() } } - private async ValueTask StartHotReload(ImmutableHashSet? filesPaths) + private async ValueTask StartHotReload(ImmutableHashSet? filesPaths) { var previous = _current; - HotReloadOperation? current, @new; + HotReloadServerOperation? current, @new; while (true) { - @new = new HotReloadOperation(this, previous, filesPaths); + @new = new HotReloadServerOperation(this, previous, filesPaths); current = Interlocked.CompareExchange(ref _current, @new, previous); if (current == previous) { @@ -113,13 +113,13 @@ private async ValueTask StartHotReload(ImmutableHashSet StartOrContinueHotReload(ImmutableHashSet? filesPaths = null) + 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(); + => _current?.Complete(HotReloadServerResult.Aborted) ?? SendUpdate(); private async ValueTask Notify(HotReloadEvent evt, HotReloadEventSource source = HotReloadEventSource.DevServer) { @@ -147,31 +147,31 @@ private async ValueTask Notify(HotReloadEvent evt, HotReloadEventSource source = break; case HotReloadEvent.Completed: - await (await StartOrContinueHotReload()).DeferComplete(HotReloadResult.Success); + await (await StartOrContinueHotReload()).DeferComplete(HotReloadServerResult.Success); break; case HotReloadEvent.NoChanges: - await (await StartOrContinueHotReload()).Complete(HotReloadResult.NoChanges); + await (await StartOrContinueHotReload()).Complete(HotReloadServerResult.NoChanges); break; case HotReloadEvent.Failed: - await (await StartOrContinueHotReload()).Complete(HotReloadResult.Failed); + await (await StartOrContinueHotReload()).Complete(HotReloadServerResult.Failed); break; case HotReloadEvent.RudeEdit: case HotReloadEvent.RudeEditDialogButton: - await (await StartOrContinueHotReload()).Complete(HotReloadResult.RudeEdit); + await (await StartOrContinueHotReload()).Complete(HotReloadServerResult.RudeEdit); break; } } - private async ValueTask SendUpdate(HotReloadOperation? completing = null) + private async ValueTask SendUpdate(HotReloadServerOperation? completing = null) { var state = _globalState; - var operations = ImmutableList.Empty; + var operations = ImmutableList.Empty; if (state is not HotReloadState.Disabled && (_current ?? completing) is { } current) { - var infos = ImmutableList.CreateBuilder(); + var infos = ImmutableList.CreateBuilder(); var foundCompleting = completing is null; LoadInfos(current); if (!foundCompleting) @@ -181,7 +181,7 @@ private async ValueTask SendUpdate(HotReloadOperation? completing = null) operations = infos.ToImmutable(); - void LoadInfos(HotReloadOperation? operation) + void LoadInfos(HotReloadServerOperation? operation) { while (operation is not null) { @@ -191,7 +191,7 @@ void LoadInfos(HotReloadOperation? operation) } foundCompleting |= operation == completing; - infos.Add(new(operation.Id, operation.FilePaths, operation.Result)); + infos.Add(new(operation.Id, operation.StartTime, operation.FilePaths, operation.CompletionTime, operation.Result)); operation = operation.Previous!; } } @@ -203,7 +203,7 @@ void LoadInfos(HotReloadOperation? operation) /// /// A hot-reload operation that is in progress. /// - private class HotReloadOperation + private class HotReloadServerOperation { // Delay to wait without any update to consider operation was aborted. private static readonly TimeSpan _timeoutDelay = TimeSpan.FromSeconds(30); @@ -212,7 +212,7 @@ private class HotReloadOperation private static long _count; private readonly ServerHotReloadProcessor _owner; - private readonly HotReloadOperation? _previous; + private readonly HotReloadServerOperation? _previous; private readonly Timer _timeout; private ImmutableHashSet _filePaths; @@ -221,21 +221,25 @@ private class HotReloadOperation public long Id { get; } = Interlocked.Increment(ref _count); - public HotReloadOperation? Previous => _previous; + public DateTimeOffset StartTime { get; } = DateTimeOffset.Now; + + public DateTimeOffset? CompletionTime { get; private set; } + + public HotReloadServerOperation? Previous => _previous; public ImmutableHashSet FilePaths => _filePaths; - public HotReloadResult? Result => _result is -1 ? null : (HotReloadResult)_result; + public HotReloadServerResult? Result => _result is -1 ? null : (HotReloadServerResult)_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) + public HotReloadServerOperation(ServerHotReloadProcessor owner, HotReloadServerOperation? previous, ImmutableHashSet? filePaths = null) { _owner = owner; _previous = previous; _filePaths = filePaths ?? _empty; _timeout = new Timer( - static that => _ = ((HotReloadOperation)that!).Complete(HotReloadResult.Aborted), + static that => _ = ((HotReloadServerOperation)that!).Complete(HotReloadServerResult.Aborted), this, _timeoutDelay, Timeout.InfiniteTimeSpan); @@ -286,9 +290,9 @@ public bool TryMerge(ImmutableHashSet filePaths) } // 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 HotReloadServerResult _aggregatedResult = HotReloadServerResult.NoChanges; private int _aggregatedFilesCount; - public void NotifyIntermediate(string file, HotReloadResult result) + public void NotifyIntermediate(string file, HotReloadServerResult result) { if (Interlocked.Increment(ref _aggregatedFilesCount) is 1) { @@ -296,7 +300,7 @@ public void NotifyIntermediate(string file, HotReloadResult result) return; } - _aggregatedResult = (HotReloadResult)Math.Max((int)_aggregatedResult, (int)result); + _aggregatedResult = (HotReloadServerResult)Math.Max((int)_aggregatedResult, (int)result); _timeout.Change(_timeoutDelay, Timeout.InfiniteTimeSpan); } @@ -309,9 +313,9 @@ public async ValueTask CompleteUsingIntermediates() /// /// 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) + public async ValueTask DeferComplete(HotReloadServerResult result, Exception? exception = null) { - Debug.Assert(result != HotReloadResult.InternalError || exception is not null); // For internal error we should always provide an exception! + Debug.Assert(result != HotReloadServerResult.InternalError || exception is not null); // For internal error we should always provide an exception! if (Interlocked.CompareExchange(ref _deferredCompletion, new CancellationTokenSource(), null) is null) { @@ -324,12 +328,12 @@ public async ValueTask DeferComplete(HotReloadResult result, Exception? exceptio } } - public ValueTask Complete(HotReloadResult result, Exception? exception = null) + public ValueTask Complete(HotReloadServerResult result, Exception? exception = null) => Complete(result, exception, isFromNext: false); - private async ValueTask Complete(HotReloadResult result, Exception? exception, bool isFromNext) + private async ValueTask Complete(HotReloadServerResult result, Exception? exception, bool isFromNext) { - Debug.Assert(result != HotReloadResult.InternalError || exception is not null); // For internal error we should always provide an exception! + Debug.Assert(result != HotReloadServerResult.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); @@ -341,13 +345,14 @@ private async ValueTask Complete(HotReloadResult result, Exception? exception, b return; // Already completed } + CompletionTime = DateTimeOffset.Now; 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, + HotReloadServerResult.Aborted, new TimeoutException("An more recent hot-reload operation has completed."), isFromNext: true); } @@ -463,7 +468,7 @@ private async Task ProcessUpdateFile(UpdateFile message) } catch (Exception ex) { - await hotReload.Complete(HotReloadResult.InternalError, ex); + await hotReload.Complete(HotReloadServerResult.InternalError, ex); await _remoteControlServer.SendFrame(new UpdateFileResponse(message.RequestId, message.FilePath, FileUpdateResult.Failed, ex.Message)); } diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs index 71d069a67f1c..2039cb05b1b4 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs @@ -142,17 +142,7 @@ 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) + private void ProcessAssemblyReload(AssemblyDeltaReload assemblyDeltaReload) { try { @@ -185,7 +175,7 @@ private void AssemblyReload(AssemblyDeltaReload assemblyDeltaReload) UpdatedTypes = ReadIntArray(changedTypesReader) }; - _source = HotReloadSource.DevServer; + _status.ConfigureSourceForNextOperation(HotReloadSource.DevServer); _agent?.ApplyDeltas(new[] { delta }); if (this.Log().IsEnabled(LogLevel.Trace)) @@ -210,7 +200,7 @@ private void AssemblyReload(AssemblyDeltaReload assemblyDeltaReload) } finally { - _source = default; // runtime + _status.ConfigureSourceForNextOperation(default); // runtime } } diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs new file mode 100644 index 000000000000..e55e46bb3416 --- /dev/null +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading; +using Uno.Diagnostics.UI; +using Uno.UI.RemoteControl.HotReload.Messages; + +namespace Uno.UI.RemoteControl.HotReload; + +public partial class ClientHotReloadProcessor +{ + private readonly StatusSink _status = new(); + + internal enum HotReloadSource + { + Runtime, + DevServer, + Manual + } + + internal enum HotReloadClientResult + { + /// + /// Successful hot-reload. + /// + Success = 1, + + /// + /// Changes cannot be applied in local app due to handler errors. + /// + Failed = 2, + + /// + /// The changes have been ignored. + /// + Ignored = 256, + } + + internal record Status( + HotReloadState State, + (HotReloadState State, IImmutableList Operations) Server, + (HotReloadState State, IImmutableList Operations) Local); + + private class StatusSink + { + private readonly DiagnosticView _view = DiagnosticView.Register("Hot reload", (view, status) => view.Update(status)); + + private HotReloadState? _serverState; + private ImmutableDictionary _serverOperations = ImmutableDictionary.Empty; + private ImmutableList _localOperations = ImmutableList.Empty; + private HotReloadSource _source; + + public void ReportServerStatus(HotReloadStatusMessage status) + { + _serverState = status.State; + ImmutableInterlocked.Update(ref _serverOperations, UpdateOperations, status.Operations); + NotifyStatusChanged(); + + static ImmutableDictionary UpdateOperations(ImmutableDictionary history, IImmutableList udpated) + { + var updatedHistory = history.ToBuilder(); + foreach (var op in udpated) + { + updatedHistory[op.Id] = op; + } + + return updatedHistory.ToImmutable(); + } + } + + public void ConfigureSourceForNextOperation(HotReloadSource source) + => _source = source; + + public HotReloadClientOperation ReportLocalStarting(Type[] types) + { + var op = new HotReloadClientOperation(_source, types, NotifyStatusChanged); + ImmutableInterlocked.Update(ref _localOperations, static (history, op) => history.Add(op).Sort(Compare), op); + NotifyStatusChanged(); + + return op; + + static int Compare(HotReloadClientOperation left, HotReloadClientOperation right) + => Comparer.Default.Compare(left.Id, right.Id); + } + + private void NotifyStatusChanged() + => _view.Update(GetStatus()); + + private Status GetStatus() + { + var serverState = _serverState ?? (_localOperations.Any() ? HotReloadState.Idle /* no info */ : HotReloadState.Initializing); + var localState = _localOperations.Any(op => op.Result is null) ? HotReloadState.Processing : HotReloadState.Idle; + var globalState = _serverState is HotReloadState.Disabled ? HotReloadState.Disabled : (HotReloadState)Math.Max((int)serverState, (int)localState); + + return new(globalState, (serverState, _serverOperations.Values.ToImmutableArray()), (localState, _localOperations)); + } + } + + internal class HotReloadClientOperation + { + #region Current + [ThreadStatic] + private static HotReloadClientOperation? _opForCurrentUiThread; + + public static HotReloadClientOperation? GetForCurrentThread() + => _opForCurrentUiThread; + + public void SetCurrent() + { + Debug.Assert(_opForCurrentUiThread == null, "Only one operation should be active at once for a given UI thread."); + _opForCurrentUiThread = this; + } + + public void ResignCurrent() + { + Debug.Assert(_opForCurrentUiThread == this, "Another operation has been started for teh current UI thread."); + _opForCurrentUiThread = null; + } + #endregion + + private static int _count; + + private readonly Action _onUpdated; + private string[]? _curatedTypes; + private ImmutableList _exceptions = ImmutableList.Empty; + private int _result = -1; + + public HotReloadClientOperation(HotReloadSource source, Type[] types, Action onUpdated) + { + Source = source; + Types = types; + + _onUpdated = onUpdated; + } + + public int Id { get; } = Interlocked.Increment(ref _count); + + public DateTimeOffset StartTime { get; } = DateTimeOffset.Now; + + public HotReloadSource Source { get; } + + public Type[] Types { get; } + + public string[] CuratedTypes => _curatedTypes ??= Types + .Select(t => + { + var name = t.Name; + var versionIndex = t.Name.IndexOf('#'); + return versionIndex < 0 + ? default! + : $"{name[..versionIndex]} (v{name[(versionIndex + 1)..]})"; + }) + .Where(t => t is not null) + .ToArray(); + + public DateTimeOffset? EndTime { get; private set; } + + public HotReloadClientResult? Result => _result is -1 ? null : (HotReloadClientResult)_result; + + public ImmutableList Exceptions => _exceptions; + + public string? IgnoreReason { get; private set; } + + public void ReportError(MethodInfo source, Exception error) + => ReportError(error); // For now we just ignore the source + + public void ReportError(Exception error) + { + ImmutableInterlocked.Update(ref _exceptions, static (errors, error) => errors.Add(error), error); + _onUpdated(); + } + + public void ReportCompleted() + { + var result = (_exceptions, AbortReason: IgnoreReason) switch + { + ({ Count: > 0 }, _) => HotReloadClientResult.Failed, + (_, not null) => HotReloadClientResult.Ignored, + _ => HotReloadClientResult.Success + }; + + if (Interlocked.CompareExchange(ref _result, (int)result, -1) is -1) + { + EndTime = DateTimeOffset.Now; + _onUpdated(); + } + else + { + Debug.Fail("The result should not have already been set."); + } + } + + public void ReportIgnored(string reason) + { + IgnoreReason = reason; + ReportCompleted(); + } + } +} diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs index a4e86740a634..71ffb50f539b 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs @@ -15,6 +15,7 @@ using Uno.UI.Helpers; using Uno.UI.RemoteControl.HotReload.MetadataUpdater; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation.Text; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; @@ -26,47 +27,43 @@ namespace Uno.UI.RemoteControl.HotReload; partial class ClientHotReloadProcessor { - private static int _isReloading; + private static int _isWaitingForTypeMapping; private static ElementUpdateAgent? _elementAgent; - private static Logger _log = typeof(ClientHotReloadProcessor).Log(); + private static readonly Logger _log = typeof(ClientHotReloadProcessor).Log(); private static Window? _currentWindow; private static ElementUpdateAgent ElementAgent { get { - _elementAgent ??= new ElementUpdateAgent(s => - { - if (_log.IsEnabled(LogLevel.Trace)) - { - _log.Trace(s); - } - }); + var log = _log.IsEnabled(LogLevel.Trace) + ? new Action(_log.Trace) + : static _ => { }; + + _elementAgent ??= new ElementUpdateAgent(log, static (callback, error) => HotReloadClientOperation.GetForCurrentThread()?.ReportError(callback, error)); return _elementAgent; } } - private static async Task ShouldReload() + private static async Task<(bool value, string reason)> ShouldReload() { - if (Interlocked.CompareExchange(ref _isReloading, 1, 0) == 1) + if (Interlocked.CompareExchange(ref _isWaitingForTypeMapping, 1, 0) == 1) { - return false; + return (false, "another reload is already waiting for type mapping to resume"); } try { - var waiter = TypeMappings.WaitForResume(); - if (!waiter.IsCompleted) - { - return false; - } - return await waiter; + var shouldReload = await TypeMappings.WaitForResume(); + return shouldReload + ? (true, string.Empty) + : (false, "type mapping prevent reload"); } finally { - Interlocked.Exchange(ref _isReloading, 0); + Interlocked.Exchange(ref _isWaitingForTypeMapping, 0); } } @@ -98,17 +95,22 @@ private static void ShowDiagnosticsOnFirstActivation(object snd, WindowActivated } } - private static async Task ReloadWithUpdatedTypes(Type[] updatedTypes) + /// + /// Run on UI thread to reload the visual tree with updated types + /// + private static async Task ReloadWithUpdatedTypes(HotReloadClientOperation? hrOp, Window window, Type[] updatedTypes) { var handlerActions = ElementAgent?.ElementHandlerActions; var uiUpdating = true; try { - var window = CurrentWindow; - if (window is null || !await ShouldReload()) + hrOp?.SetCurrent(); + + if (await ShouldReload() is { value: false } prevent) { uiUpdating = false; + hrOp?.ReportIgnored(prevent.reason); return; } @@ -217,6 +219,8 @@ private static async Task ReloadWithUpdatedTypes(Type[] updatedTypes) } catch (Exception ex) { + hrOp?.ReportError(ex); + if (_log.IsEnabled(LogLevel.Error)) { _log.Error($"Error doing UI Update - {ex.Message}", ex); @@ -228,6 +232,9 @@ private static async Task ReloadWithUpdatedTypes(Type[] updatedTypes) { // Action: ReloadCompleted _ = handlerActions?.Do(h => h.Value.ReloadCompleted(updatedTypes, uiUpdating)).ToArray(); + + hrOp?.ResignCurrent(); + hrOp?.ReportCompleted(); } } @@ -420,12 +427,12 @@ public static void ForceHotReloadUpdate() { try { - _source = HotReloadSource.Manual; + _instance?._status.ConfigureSourceForNextOperation(HotReloadSource.Manual); UpdateApplication(Array.Empty()); } finally { - _source = default; + _instance?._status.ConfigureSourceForNextOperation(default); } } @@ -435,7 +442,7 @@ public static void ForceHotReloadUpdate() [EditorBrowsable(EditorBrowsableState.Never)] public static void UpdateApplication(Type[] types) { - // TODO: Diag.Report --> Real handler or force reload + var hr = _instance?._status.ReportLocalStarting(types); foreach (var type in types) { @@ -459,6 +466,7 @@ public static void UpdateApplication(Type[] types) { _log.Error($"Error while processing MetadataUpdateOriginalTypeAttribute for {type}", error); } + hr?.ReportError(error); } } @@ -468,21 +476,24 @@ public static void UpdateApplication(Type[] types) } #if WINUI - var dispatcherQueue = CurrentWindow?.DispatcherQueue; - if (dispatcherQueue is not null) + if (CurrentWindow is { DispatcherQueue: { } dispatcherQueue } window) { - dispatcherQueue.TryEnqueue(async () => await ReloadWithUpdatedTypes(types)); + dispatcherQueue.TryEnqueue(async () => await ReloadWithUpdatedTypes(hr, window, types)); } #else - var dispatcher = CurrentWindow?.Dispatcher; - if (dispatcher is not null) + if (CurrentWindow is { Dispatcher: { } dispatcher } window) { - _ = dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () => await ReloadWithUpdatedTypes(types)); + _ = dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () => await ReloadWithUpdatedTypes(hr, window, types)); } #endif - else if (_log.IsEnabled(LogLevel.Warning)) + else { - _log.Warn($"Unable to access Dispatcher/DispatcherQueue in order to invoke {nameof(ReloadWithUpdatedTypes)}. Make sure you have enabled hot-reload (Window.EnableHotReload()) in app startup. See https://aka.platform.uno/hot-reload"); + var errorMsg = $"Unable to access Dispatcher/DispatcherQueue in order to invoke {nameof(ReloadWithUpdatedTypes)}. Make sure you have enabled hot-reload (Window.EnableHotReload()) in app startup. See https://aka.platform.uno/hot-reload"; + hr?.ReportError(new InvalidOperationException(errorMsg)); + if (_log.IsEnabled(LogLevel.Warning)) + { + _log.Warn(errorMsg); + } } } } diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs index 3c0fe0a86163..c12a29b50e8a 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs @@ -8,6 +8,10 @@ using Uno.Foundation.Logging; using Uno.UI.RemoteControl.HotReload.Messages; using Uno.Diagnostics.UI; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Reflection; +using Uno.UI.Helpers; namespace Uno.UI.RemoteControl.HotReload; @@ -16,7 +20,6 @@ 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; @@ -38,7 +41,7 @@ public async Task ProcessFrame(Messages.Frame frame) switch (frame.Name) { case AssemblyDeltaReload.Name: - AssemblyReload(frame.GetContent()); + ProcessAssemblyReload(frame.GetContent()); break; case FileReload.Name: @@ -50,7 +53,7 @@ public async Task ProcessFrame(Messages.Frame frame) break; case HotReloadStatusMessage.Name: - await ProcessStatus(frame.GetContent()); + await ProcessServerStatus(frame.GetContent()); break; default: @@ -157,8 +160,8 @@ private string GetMSBuildProperty(string property, string defaultValue = "") } #endregion - private async Task ProcessStatus(HotReloadStatusMessage status) + private async Task ProcessServerStatus(HotReloadStatusMessage status) { - _diagView.Update(status); + _status.ReportServerStatus(status); } } diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs index e5affa957a1b..843ec546dd77 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs @@ -1,54 +1,160 @@ using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.IO; using System.Linq; +using Windows.UI; +using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Uno.UI.RemoteControl.HotReload.Messages; +using static Uno.UI.RemoteControl.HotReload.ClientHotReloadProcessor; namespace Uno.UI.RemoteControl.HotReload; internal sealed partial class HotReloadStatusView : Control { - private (long id, string state) _currentResult = (-1, "None"); + #region HeadLine (DP) + public static DependencyProperty HeadLineProperty { get; } = DependencyProperty.Register( + nameof(HeadLine), + typeof(string), + typeof(HotReloadStatusView), + new PropertyMetadata(default(string), (snd, args) => ToolTipService.SetToolTip(snd, args.NewValue?.ToString()))); + + public string HeadLine + { + get => (string)GetValue(HeadLineProperty); + set => SetValue(HeadLineProperty, value); + } + #endregion + + #region History (DP) + public static DependencyProperty HistoryProperty { get; } = DependencyProperty.Register( + nameof(History), + typeof(ObservableCollection), + typeof(HotReloadStatusView), + new PropertyMetadata(default(ObservableCollection))); + + public ObservableCollection History + { + get => (ObservableCollection)GetValue(HistoryProperty); + private init => SetValue(HistoryProperty, value); + } + #endregion public HotReloadStatusView() { DefaultStyleKey = typeof(HotReloadStatusView); + History = []; + + UpdateHeadline(null); } - /// - protected override void OnApplyTemplate() + public void Update(Status status) { - base.OnApplyTemplate(); + SyncOperations(status); + UpdateHeadline(status.State); + + VisualStateManager.GoToState(this, GetStatusVisualState(status.State), true); + VisualStateManager.GoToState(this, GetResultVisualState(), true); } - public void Update(HotReloadStatusMessage? status) + private void SyncOperations(Status status) { - ToolTipService.SetToolTip(this, GetStatusSummary(status)); + var operations = History; + var vms = operations.ToDictionary(op => (op.IsServer, op.Id)); - if (status is null) + foreach (var srvOp in status.Server.Operations) { - return; + if (!vms.TryGetValue((true, srvOp.Id), out var vm)) + { + vm = new HotReloadEntryViewModel(true, srvOp.Id, srvOp.StartTime); + operations.Insert(FindIndex(srvOp.StartTime), vm); + } + + string[] files = srvOp.FilePaths.Select(Path.GetFileName).ToArray()!; + + vm.IsCompleted = srvOp.Result is not null; + vm.IsSuccess = srvOp.Result is HotReloadServerResult.Success or HotReloadServerResult.NoChanges; + vm.Description = srvOp.Result switch + { + null => $"Processing changes{Join(files, "files")}.", + HotReloadServerResult.NoChanges => $"No changes detected by the server{Join(files, "files")}.", + HotReloadServerResult.Success => $"Server successfully detected and compiled changes{Join(files, "files")}.", + HotReloadServerResult.RudeEdit => $"Server detected changes{Join(files, "files")} but is not able to apply them.", + HotReloadServerResult.Failed => $"Server detected changes{Join(files, "files")} but is not able to compile them.", + HotReloadServerResult.Aborted => $"Hot-reload has been aborted (usually because some other changes has been detected).", + HotReloadServerResult.InternalError => "Hot-reload failed for due to an internal error.", + _ => $"Unknown server operation result: {srvOp.Result}." + }; + vm.Duration = srvOp.EndTime is not null ? srvOp.EndTime - srvOp.StartTime : null; + vm.RaiseChanged(); } - VisualStateManager.GoToState(this, GetStatusVisualState(status), true); - if (GetResultVisualState(status) is { } resultState) + foreach (var localOp in status.Local.Operations) { - VisualStateManager.GoToState(this, resultState, true); + if (!vms.TryGetValue((false, localOp.Id), out var vm)) + { + vm = new HotReloadEntryViewModel(false, localOp.Id, localOp.StartTime); + operations.Insert(FindIndex(localOp.StartTime), vm); + } + + var types = localOp.CuratedTypes; + + vm.IsCompleted = localOp.Result is not null; + vm.IsSuccess = localOp.Result is HotReloadClientResult.Success; + vm.Description = localOp.Result switch + { + null => $"Processing changes{Join(types, "types")} (total of {localOp.Types.Length} types updated).", + HotReloadClientResult.Success => $"Application received {localOp.Types.Length} changes{Join(types, "types")} and updated the view (total of {localOp.Types.Length} types updated).", + HotReloadClientResult.Failed => $"Application received {localOp.Types.Length} changes{Join(types, "types")} (total of {localOp.Types.Length} types updated) but failed to update the view ({localOp.Exceptions.FirstOrDefault()?.Message}).", + HotReloadClientResult.Ignored => $"Application received {localOp.Types.Length} changes{Join(types, "types")} (total of {localOp.Types.Length} types updated) but view was not been updated because {localOp.IgnoreReason}.", + _ => $"Unknown application operation result: {localOp.Result}." + }; + vm.Duration = localOp.EndTime is not null ? localOp.EndTime - localOp.StartTime : null; + vm.RaiseChanged(); + } + + string Join(string[] items, string itemType, int maxItems = 5) + => items switch + { + { Length: 0 } => "", + { Length: 1 } => $" in {items[0]}", + { Length: < 3 } => $" in {string.Join(",", items[..^1])} and {items[^1]}", + _ => $" in {string.Join(",", items[..3])} and {items.Length - 3} other {itemType}" + }; + + int FindIndex(DateTimeOffset date) + { + for (var i = 0; i < operations.Count; i++) + { + if (operations[i].Start > date) + { + return i - 1; + } + } + + return 0; } } - public static string GetStatusSummary(HotReloadStatusMessage? status) - => status?.State switch + public string UpdateHeadline(HotReloadState? state) + => HeadLine = state switch { - HotReloadState.Disabled => "Hot-reload is disabled.", - HotReloadState.Initializing => "Hot-reload is initializing.", + null => """ + State of the hot-reload engine is unknown. + This usually indicates that connection to the dev-server failed, but if running within VisualStudio, updates might still be detected. + """, + HotReloadState.Disabled => "Hot-reload server is disabled.", + HotReloadState.Initializing => "Hot-reload engine is initializing.", HotReloadState.Idle => "Hot-reload server is ready and listening for file changes.", - HotReloadState.Processing => "Hot-reload server is processing file changes", - _ => "Unable to determine the state of the hot-reload server." + HotReloadState.Processing => "Hot-reload engine is processing file changes", + _ => "Unable to determine the state of the hot-reload engine." }; - private static string GetStatusVisualState(HotReloadStatusMessage status) - => status.State switch + private static string GetStatusVisualState(HotReloadState state) + => state switch { HotReloadState.Disabled => "Disabled", HotReloadState.Initializing => "Initializing", @@ -57,30 +163,47 @@ private static string GetStatusVisualState(HotReloadStatusMessage status) _ => "Unknown" }; - private string? GetResultVisualState(HotReloadStatusMessage status) + private string GetResultVisualState() { - var op = status.Operations.MaxBy(op => op.Id); - if (op is null) + var operations = History; + if (operations is { Count: 0 } || operations.Any(op => !op.IsCompleted)) { - return null; // No state change + return "None"; // Makes sure to restore to previous None! } - var updated = (op.Id, GetStateName(op)); - if (_currentResult == updated) - { - return null; // No state change - } + return operations[0].IsSuccess ? "Success" : "Failed"; + } +} - _currentResult = updated; - return _currentResult.state; +[Microsoft.UI.Xaml.Data.Bindable] +internal sealed record HotReloadEntryViewModel(bool IsServer, long Id, DateTimeOffset Start) : INotifyPropertyChanged +{ + /// + public event PropertyChangedEventHandler? PropertyChanged; - static string GetStateName(HotReloadOperationInfo op) - => op.Result switch - { - null => "None", - HotReloadResult.NoChanges => "Success", - HotReloadResult.Success => "Success", - _ => "Failed" - }; - } + public bool IsCompleted { get; set; } + public bool IsSuccess { get; set; } + public TimeSpan? Duration { get; set; } + public string? Description { get; set; } + + // Quick patch as we don't have MVUX + public string Title => $"{Start.LocalDateTime:T} - {(IsServer ? "Server" : "Application")}{GetDuration()}".ToString(CultureInfo.CurrentCulture); + public Color Color => (IsCompleted, IsSuccess) switch + { + (false, _) => Colors.Yellow, + (true, false) => Colors.Red, + (true, true) => Colors.Green, + }; + + public void RaiseChanged() + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("")); + + private string GetDuration() + => Duration switch + { + null => string.Empty, + { TotalMilliseconds: < 1000 } ms => $" - {ms.TotalMilliseconds:F0} ms", + { TotalSeconds: < 3 } s => $" - {s.TotalSeconds:N2} s", + { } d => $" - {d:g}" + }; } diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml index be7b2db60429..c35000738736 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml @@ -4,14 +4,28 @@ xmlns:local="using:Uno.UI.RemoteControl.HotReload" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Microsoft.UI.Xaml.Controls;assembly=Uno.UI" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> + + + + + + + + + + + + + diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadResult.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadServerResult.cs similarity index 89% rename from src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadResult.cs rename to src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadServerResult.cs index 1ac7d3736454..8a5f19d1905b 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadResult.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadServerResult.cs @@ -4,9 +4,9 @@ namespace Uno.UI.RemoteControl.HotReload.Messages; /// -/// The result of an hot-reload operation. +/// The result of a hot-reload operation on server. /// -public enum HotReloadResult +public enum HotReloadServerResult { /// /// Hot-reload completed with no changes. diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs index ed3810fd5c31..673a414d1ca0 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs @@ -7,7 +7,7 @@ namespace Uno.UI.RemoteControl.HotReload.Messages; public record HotReloadStatusMessage( [property: JsonProperty] HotReloadState State, - [property: JsonProperty] IImmutableList Operations) + [property: JsonProperty] IImmutableList Operations) : IMessage { public const string Name = nameof(HotReloadStatusMessage); @@ -21,4 +21,9 @@ public record HotReloadStatusMessage( string IMessage.Name => Name; } -public record HotReloadOperationInfo(long Id, ImmutableHashSet FilePaths, HotReloadResult? Result); +public record HotReloadServerOperationData( + long Id, + DateTimeOffset StartTime, + ImmutableHashSet FilePaths, + DateTimeOffset? EndTime, + HotReloadServerResult? Result); diff --git a/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/ElementUpdaterAgent.cs b/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/ElementUpdaterAgent.cs index cc60e4463eb1..d9dff7580042 100644 --- a/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/ElementUpdaterAgent.cs +++ b/src/Uno.UI.RemoteControl/HotReload/MetadataUpdater/ElementUpdaterAgent.cs @@ -23,14 +23,16 @@ internal sealed class ElementUpdateAgent : IDisposable private const DynamicallyAccessedMemberTypes HotReloadHandlerLinkerFlags = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; private readonly Action _log; + private readonly Action _onActionError; private readonly AssemblyLoadEventHandler _assemblyLoad; private readonly ConcurrentDictionary _elementHandlerActions = new(); internal const string MetadataUpdaterType = "System.Reflection.Metadata.MetadataUpdater"; - public ElementUpdateAgent(Action log) + public ElementUpdateAgent(Action log, Action onActionError) { _log = log; + _onActionError = onActionError; _assemblyLoad = OnAssemblyLoad; AppDomain.CurrentDomain.AssemblyLoad += _assemblyLoad; LoadElementUpdateHandlerActions(); @@ -314,6 +316,7 @@ internal void GetElementHandlerActions( } catch (Exception ex) { + _onActionError(update, ex); _log($"Exception from '{update.Name}' on {handlerType.Name}: {ex}"); } }; diff --git a/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs b/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs index 6493a6ff4bcc..62d77e869e98 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs @@ -98,7 +98,7 @@ private class DiagnosticsSink : IDiagnosticsSink { private ConnectionState _state = ConnectionState.Idle; private readonly DiagnosticView _view = DiagnosticView.Register( - "Dev-server", + "Dev-server", (view, status) => view.Update(status), RemoteControlStatusView.GetStatusDetails); diff --git a/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs b/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs index d214e8e68a04..0db66f889fa5 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs @@ -23,9 +23,9 @@ public static DiagnosticView Register( Func? details = null) where TView : FrameworkElement, new() { - var provider = details is null + var provider = details is null ? new DiagnosticView(name, () => new TView(), update) - : new DiagnosticView(name, () => new TView(), update, (ctx, state, ct) => new(details(state)); + : new DiagnosticView(name, () => new TView(), update, (ctx, state, ct) => new(details(state))); DiagnosticViewRegistry.Register(provider); return provider; } diff --git a/src/Uno.UI/Helpers/TypeMappings.cs b/src/Uno.UI/Helpers/TypeMappings.cs index f71df7da3780..9937fe533e51 100644 --- a/src/Uno.UI/Helpers/TypeMappings.cs +++ b/src/Uno.UI/Helpers/TypeMappings.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Uno.UI.Helpers; @@ -165,12 +166,10 @@ public static void Resume() /// Indicates whether the layout should be updated after resuming updates public static void Resume(bool updateLayout) { - var completion = _mappingsPaused; - _mappingsPaused = null; - if (completion is not null) + if (Interlocked.Exchange(ref _mappingsPaused, null) is { } completion) { - MappedTypeToOriginalTypeMappings = AllMappedTypeToOriginalTypeMappings.ToDictionary(x => x.Key, x => x.Value); - OriginalTypeToMappedType = AllOriginalTypeToMappedType.ToDictionary(x => x.Key, x => x.Value); + MappedTypeToOriginalTypeMappings = new Dictionary(AllMappedTypeToOriginalTypeMappings); + OriginalTypeToMappedType = new Dictionary(AllOriginalTypeToMappedType); completion.TrySetResult(updateLayout); } } From 723e48bbcfda11063ffa973c5d4b4298807bfa89 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 6 Jun 2024 09:19:49 -0400 Subject: [PATCH 12/27] fix(dev-server): Improve socket performance --- .../Messages/Frame.cs | 29 ++++++++++--------- .../Helpers/WebSocketHelper.cs | 16 ++++------ 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/Uno.UI.RemoteControl.Messaging/Messages/Frame.cs b/src/Uno.UI.RemoteControl.Messaging/Messages/Frame.cs index 832e0df32b6d..631f46bbfae6 100644 --- a/src/Uno.UI.RemoteControl.Messaging/Messages/Frame.cs +++ b/src/Uno.UI.RemoteControl.Messaging/Messages/Frame.cs @@ -29,19 +29,6 @@ public Frame(short version, string scope, string name, string content) public string Content { get; } - public static Frame Read(Stream stream) - { - using (var reader = new BinaryReader(stream, Encoding.UTF8)) - { - var version = reader.ReadInt16(); - var scope = reader.ReadString(); - var name = reader.ReadString(); - var content = reader.ReadString(); - - return new Frame(version, scope, name, content); - } - } - public static Frame Create(short version, string scope, string name, T content) => new Frame( version, @@ -76,13 +63,27 @@ public bool TryGetContent([NotNullWhen(true)] out T? content) } } + public static Frame Read(Stream stream) + { + using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); + + var version = reader.ReadInt16(); + var scope = reader.ReadString(); + var name = reader.ReadString(); + var content = reader.ReadString(); + + return new Frame(version, scope, name, content); + } + public void WriteTo(Stream stream) { - var writer = new BinaryWriter(stream, Encoding.UTF8); + using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true); writer.Write((short)Version); writer.Write(Scope); writer.Write(Name); writer.Write(Content); + + writer.Flush(); } } diff --git a/src/Uno.UI.RemoteControl/Helpers/WebSocketHelper.cs b/src/Uno.UI.RemoteControl/Helpers/WebSocketHelper.cs index e3be665760ea..b99a027b788a 100644 --- a/src/Uno.UI.RemoteControl/Helpers/WebSocketHelper.cs +++ b/src/Uno.UI.RemoteControl/Helpers/WebSocketHelper.cs @@ -31,21 +31,17 @@ public static class WebSocketHelper return null; } - if (result.EndOfMessage) + if (result.Count != 0) { - if (result.Count != 0) - { - await mem.WriteAsync(buff, 0, result.Count); - } + await mem.WriteAsync(buff, 0, result.Count, token); + } + if (result.EndOfMessage) + { mem.Position = 0; return Frame.Read(mem); } - else - { - await mem.WriteAsync(buff, 0, result.Count); - } } } finally @@ -59,6 +55,6 @@ internal static async Task SendFrame(WebSocket webSocket, Frame frame, Cancellat using var stream = manager.GetStream(); frame.WriteTo(stream); - await webSocket.SendAsync(new ArraySegment(stream.GetBuffer(), 0, (int)stream.Length), WebSocketMessageType.Binary, true, ct); + await webSocket.SendAsync(new ArraySegment(stream.GetBuffer(), 0, (int)stream.Position), WebSocketMessageType.Binary, true, ct); } } From 97d0a1da346d3f1d0aa78a2541a60729a4dc28bb Mon Sep 17 00:00:00 2001 From: David Date: Thu, 6 Jun 2024 09:20:17 -0400 Subject: [PATCH 13/27] chore: Fix UWP build --- .../Uno.UI.RemoteControl.Reference.csproj | 1 + .../Uno.UI.RemoteControl.Skia.csproj | 11 +++++++++++ .../Uno.UI.RemoteControl.Wasm.csproj | 1 + .../Uno.UI.RemoteControl.netcoremobile.csproj | 1 + src/Uno.UI/Diagnostics/DiagnosticViewHelper.cs | 7 +++++-- 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj index bdfc5e3d8796..d84ad8143e68 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj @@ -44,6 +44,7 @@ + diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj index 48fb485f7feb..3f6216e62844 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj @@ -27,6 +27,12 @@ + + + + + + $(AssemblyName).xml @@ -85,4 +91,9 @@ + + + MSBuild:Compile + + diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj index 7cfd616a719b..a96f3f93d4b6 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj @@ -39,6 +39,7 @@ + diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj index 5a1769e77baa..9a0d1760a8d6 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj @@ -49,6 +49,7 @@ + diff --git a/src/Uno.UI/Diagnostics/DiagnosticViewHelper.cs b/src/Uno.UI/Diagnostics/DiagnosticViewHelper.cs index f3c6e36cc9e8..7fc57cbb2de9 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticViewHelper.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticViewHelper.cs @@ -2,22 +2,25 @@ using System; using System.Linq; -using Microsoft.UI; +using Windows.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; +using Colors = Microsoft.UI.Colors; namespace Uno.Diagnostics.UI; internal class DiagnosticViewHelper { + private static readonly Color _white = new(255, 255, 255, 255); + public static TextBlock CreateText(string? text) => new() { Text = text ?? "--", VerticalAlignment = VerticalAlignment.Center, FontSize = 10, - Foreground = new SolidColorBrush(Colors.White) + Foreground = new SolidColorBrush(_white) }; public static DiagnosticViewHelper CreateText(Func value) From 7a1f6ebfe595387e15b39d23c80d3a66243094f6 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 6 Jun 2024 10:41:29 -0400 Subject: [PATCH 14/27] fix: Fix invalid message in HR indicator --- .../HotReload/HotReloadStatusView.cs | 6 +- .../RemoteControlStatusView.cs | 65 +++++++++---------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs index 843ec546dd77..aedb12d5e81b 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs @@ -107,9 +107,9 @@ private void SyncOperations(Status status) vm.Description = localOp.Result switch { null => $"Processing changes{Join(types, "types")} (total of {localOp.Types.Length} types updated).", - HotReloadClientResult.Success => $"Application received {localOp.Types.Length} changes{Join(types, "types")} and updated the view (total of {localOp.Types.Length} types updated).", - HotReloadClientResult.Failed => $"Application received {localOp.Types.Length} changes{Join(types, "types")} (total of {localOp.Types.Length} types updated) but failed to update the view ({localOp.Exceptions.FirstOrDefault()?.Message}).", - HotReloadClientResult.Ignored => $"Application received {localOp.Types.Length} changes{Join(types, "types")} (total of {localOp.Types.Length} types updated) but view was not been updated because {localOp.IgnoreReason}.", + HotReloadClientResult.Success => $"Application received changes{Join(types, "types")} and updated the view (total of {localOp.Types.Length} types updated).", + HotReloadClientResult.Failed => $"Application received changes{Join(types, "types")} (total of {localOp.Types.Length} types updated) but failed to update the view ({localOp.Exceptions.FirstOrDefault()?.Message}).", + HotReloadClientResult.Ignored => $"Application received changes{Join(types, "types")} (total of {localOp.Types.Length} types updated) but view was not been updated because {localOp.IgnoreReason}.", _ => $"Unknown application operation result: {localOp.Result}." }; vm.Duration = localOp.EndTime is not null ? localOp.EndTime - localOp.StartTime : null; diff --git a/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs b/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs index 886cbad89df1..2b2f4cb00aae 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs @@ -3,11 +3,10 @@ using System.Text; using Windows.UI; using Microsoft.UI; -using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Shapes; -using Uno.UI.RemoteControl.HotReload.Messages; +using static Uno.UI.RemoteControl.RemoteControlClient; namespace Uno.UI.RemoteControl; @@ -20,50 +19,50 @@ public RemoteControlStatusView() Height = 16; } - public void Update(RemoteControlClient.Status status) + public void Update(Status status) { ((SolidColorBrush)Fill).Color = GetStatusColor(status); ToolTipService.SetToolTip(this, GetStatusSummary(status)); } - private Color GetStatusColor(RemoteControlClient.Status status) + private static Color GetStatusColor(Status status) => status.State switch { - RemoteControlClient.ConnectionState.Idle => Colors.Gray, - RemoteControlClient.ConnectionState.NoServer => Colors.Red, - RemoteControlClient.ConnectionState.Connecting => Colors.Yellow, - RemoteControlClient.ConnectionState.ConnectionTimeout => Colors.Red, - RemoteControlClient.ConnectionState.ConnectionFailed => Colors.Red, - RemoteControlClient.ConnectionState.Reconnecting => Colors.Yellow, - RemoteControlClient.ConnectionState.Disconnected => Colors.Red, - - RemoteControlClient.ConnectionState.Connected when status.IsVersionValid is false => Colors.Orange, - RemoteControlClient.ConnectionState.Connected when status.InvalidFrames.Count is not 0 => Colors.Orange, - RemoteControlClient.ConnectionState.Connected when status.KeepAlive.State is not RemoteControlClient.KeepAliveState.Ok => Colors.Yellow, - RemoteControlClient.ConnectionState.Connected => Colors.Green, + ConnectionState.Idle => Colors.Gray, + ConnectionState.NoServer => Colors.Red, + ConnectionState.Connecting => Colors.Yellow, + ConnectionState.ConnectionTimeout => Colors.Red, + ConnectionState.ConnectionFailed => Colors.Red, + ConnectionState.Reconnecting => Colors.Yellow, + ConnectionState.Disconnected => Colors.Red, + + ConnectionState.Connected when status.IsVersionValid is false => Colors.Orange, + ConnectionState.Connected when status.InvalidFrames.Count is not 0 => Colors.Orange, + ConnectionState.Connected when status.KeepAlive.State is not KeepAliveState.Ok => Colors.Yellow, + ConnectionState.Connected => Colors.Green, _ => Colors.Gray }; - private static string GetStatusSummary(RemoteControlClient.Status status) + private static string GetStatusSummary(Status status) { var summary = status.State switch { - RemoteControlClient.ConnectionState.Idle => "Initializing...", - RemoteControlClient.ConnectionState.NoServer => "No server configured, cannot initialize connection.", - RemoteControlClient.ConnectionState.Connecting => "Connecting to dev-server.", - RemoteControlClient.ConnectionState.ConnectionTimeout => "Failed to connect to dev-server (timeout).", - RemoteControlClient.ConnectionState.ConnectionFailed => "Failed to connect to dev-server (error).", - RemoteControlClient.ConnectionState.Reconnecting => "Connection to dev-server has been lost, reconnecting.", - RemoteControlClient.ConnectionState.Disconnected => "Connection to dev-server has been lost, will retry later.", - - RemoteControlClient.ConnectionState.Connected when status.IsVersionValid is false => "Connected to dev-server, but version mis-match with client.", - RemoteControlClient.ConnectionState.Connected when status.InvalidFrames.Count is not 0 => $"Connected to dev-server, but received {status.InvalidFrames.Count} invalid frames from the server.", - RemoteControlClient.ConnectionState.Connected when status.MissingRequiredProcessors is { IsEmpty: false } => "Connected to dev-server, but some required processors are missing on server.", - RemoteControlClient.ConnectionState.Connected when status.KeepAlive.State is RemoteControlClient.KeepAliveState.Late => "Connected to dev-server, but keep-alive messages are taking longer than expected.", - RemoteControlClient.ConnectionState.Connected when status.KeepAlive.State is RemoteControlClient.KeepAliveState.Lost => "Connected to dev-server, but last keep-alive messages have been lost.", - RemoteControlClient.ConnectionState.Connected when status.KeepAlive.State is RemoteControlClient.KeepAliveState.Aborted => "Connected to dev-server, but keep-alive has been aborted.", - RemoteControlClient.ConnectionState.Connected => "Connected to dev-server.", + ConnectionState.Idle => "Initializing...", + ConnectionState.NoServer => "No server configured, cannot initialize connection.", + ConnectionState.Connecting => "Connecting to dev-server.", + ConnectionState.ConnectionTimeout => "Failed to connect to dev-server (timeout).", + ConnectionState.ConnectionFailed => "Failed to connect to dev-server (error).", + ConnectionState.Reconnecting => "Connection to dev-server has been lost, reconnecting.", + ConnectionState.Disconnected => "Connection to dev-server has been lost, will retry later.", + + ConnectionState.Connected when status.IsVersionValid is false => "Connected to dev-server, but version mis-match with client.", + ConnectionState.Connected when status.InvalidFrames.Count is not 0 => $"Connected to dev-server, but received {status.InvalidFrames.Count} invalid frames from the server.", + ConnectionState.Connected when status.MissingRequiredProcessors is { IsEmpty: false } => "Connected to dev-server, but some required processors are missing on server.", + ConnectionState.Connected when status.KeepAlive.State is KeepAliveState.Late => "Connected to dev-server, but keep-alive messages are taking longer than expected.", + ConnectionState.Connected when status.KeepAlive.State is KeepAliveState.Lost => "Connected to dev-server, but last keep-alive messages have been lost.", + ConnectionState.Connected when status.KeepAlive.State is KeepAliveState.Aborted => "Connected to dev-server, but keep-alive has been aborted.", + ConnectionState.Connected => "Connected to dev-server.", _ => status.State.ToString() }; @@ -76,7 +75,7 @@ private static string GetStatusSummary(RemoteControlClient.Status status) return summary; } - internal static string GetStatusDetails(RemoteControlClient.Status status) + internal static string GetStatusDetails(Status status) { var details = new StringBuilder(GetStatusSummary(status)); From 93f0f32e951f7ffe4f7f3b059ee5e0c6c92c66dc Mon Sep 17 00:00:00 2001 From: David Date: Thu, 6 Jun 2024 11:30:03 -0400 Subject: [PATCH 15/27] chore: More build fixes --- .../ClientHotReloadProcessor.Common.Status.cs | 4 +++- .../ClientHotReloadProcessor.MetadataUpdate.cs | 3 --- .../HotReload/HotReloadStatusView.xaml | 10 +++++----- src/Uno.UI.Toolkit/Diagnostics/DiagnosticCounter.cs | 2 ++ .../DiagnosticsOverlay.DiagnosticElement.cs | 2 ++ .../DiagnosticsOverlay.DiagnosticViewContext.cs | 2 ++ .../Diagnostics/DiagnosticsOverlay.cs | 3 ++- src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj | 13 ++++++++++++- src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs | 2 +- 9 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs index e55e46bb3416..d95d636eee26 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs index 71ffb50f539b..d158a2e74481 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs @@ -15,13 +15,10 @@ using Uno.UI.Helpers; using Uno.UI.RemoteControl.HotReload.MetadataUpdater; using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Automation.Text; 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; diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml index c35000738736..72c90df9598e 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml @@ -4,13 +4,13 @@ xmlns:local="using:Uno.UI.RemoteControl.HotReload" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:controls="clr-namespace:Microsoft.UI.Xaml.Controls;assembly=Uno.UI" + xmlns:mux="using:Microsoft.UI.Xaml.Controls" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> - + @@ -25,7 +25,7 @@ diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticCounter.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticCounter.cs index 169f6389a1b9..cfafd7269b04 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticCounter.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticCounter.cs @@ -1,4 +1,5 @@ #nullable enable +#if WINAPPSDK || HAS_UNO_WINUI using System; using System.Linq; using System.Threading; @@ -37,3 +38,4 @@ object IDiagnosticViewProvider.GetPreview(IDiagnosticViewContext context) ValueTask IDiagnosticViewProvider.GetDetailsAsync(IDiagnosticViewContext context, CancellationToken ct) => new($"current: {_value}\r\n\r\n{Description}"); } +#endif diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs index ce50db0e3f56..8e804b3056ed 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs @@ -1,5 +1,6 @@ #nullable enable #pragma warning disable IDE0055 // Invalid suggestion for switch statement +#if WINAPPSDK || HAS_UNO_WINUI using System; using System.Linq; using System.Threading; @@ -107,3 +108,4 @@ public void Dispose() => Interlocked.Exchange(ref _details, new CancellationTokenSource())?.Cancel(); } } +#endif diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticViewContext.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticViewContext.cs index 9333586f0f8c..b4202485c495 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticViewContext.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticViewContext.cs @@ -1,4 +1,5 @@ #nullable enable +#if WINUI || HAS_UNO_WINUI using System; using System.Collections.Generic; using System.Linq; @@ -160,3 +161,4 @@ public void Dispose() } } } +#endif diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs index dead47024a81..7b0febc039bc 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs @@ -1,5 +1,5 @@ #nullable enable - +#if WINAPPSDK || HAS_UNO_WINUI using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -226,3 +226,4 @@ private bool IsMaterialized(IDiagnosticViewProvider provider) } } } +#endif diff --git a/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj b/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj index 4d7effa1ca77..4bf6f0d83c91 100644 --- a/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj +++ b/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj @@ -26,6 +26,16 @@ + + + + + + + + + + @@ -39,9 +49,10 @@ - + + diff --git a/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs b/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs index 0db66f889fa5..f2189b4abafc 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticView.Factories.cs @@ -18,7 +18,7 @@ public static DiagnosticView Register(string name, Action u } public static DiagnosticView Register( - string name, + string name, Action update, Func? details = null) where TView : FrameworkElement, new() From 47bfa4debfaf8b5c13eec2ae6acf5ebc0f0bb496 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 11 Jun 2024 18:28:38 -0400 Subject: [PATCH 16/27] feat(diags): Add ability for a diag view to send notifications --- .../Diagnostics/DiagnosticViewNotification.cs | 29 +++ .../Diagnostics/DiagnosticViewRegistry.cs | 10 +- ...sticViewProvider.cs => IDiagnosticView.cs} | 11 +- .../Diagnostics/IDiagnosticViewContext.cs | 2 + .../ClientHotReloadProcessor.Common.Status.cs | 2 +- ...ClientHotReloadProcessor.MetadataUpdate.cs | 2 +- .../HotReload/HotReloadStatusView.cs | 97 ++++++++- .../HotReload/HotReloadStatusView.xaml | 23 +- .../Diagnostics/DiagnosticCounter.cs | 10 +- .../DiagnosticsOverlay.DiagnosticElement.cs | 11 +- ...iagnosticsOverlay.DiagnosticViewContext.cs | 13 +- .../DiagnosticsOverlay.Notification.cs | 60 +++++ .../Diagnostics/DiagnosticsOverlay.cs | 206 ++++++++++++------ .../Diagnostics/DiagnosticsOverlay.xaml | 117 ++++++++++ src/Uno.UI.Toolkit/Themes/Generic.xaml | 4 + .../Diagnostics/DiagnosticView.Factories.cs | 20 +- ...tate.cs => DiagnosticView.TView.TState.cs} | 23 +- ...vider.TView.cs => DiagnosticView.TView.cs} | 19 +- src/Uno.UI/Diagnostics/DiagnosticView.cs | 21 +- .../Diagnostics/DiagnosticViewHelper.cs | 6 +- ... => DiagnosticViewManager.TView.TState.cs} | 4 +- ...View.cs => DiagnosticViewManager.TView.cs} | 4 +- 22 files changed, 558 insertions(+), 136 deletions(-) create mode 100644 src/Uno.Foundation/Diagnostics/DiagnosticViewNotification.cs rename src/Uno.Foundation/Diagnostics/{IDiagnosticViewProvider.cs => IDiagnosticView.cs} (79%) create mode 100644 src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Notification.cs create mode 100644 src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.xaml rename src/Uno.UI/Diagnostics/{DiagnosticViewProvider.TView.TState.cs => DiagnosticView.TView.TState.cs} (50%) rename src/Uno.UI/Diagnostics/{DiagnosticViewProvider.TView.cs => DiagnosticView.TView.cs} (54%) rename src/Uno.UI/Diagnostics/{DiagnosticViewHelper.TView.TState.cs => DiagnosticViewManager.TView.TState.cs} (78%) rename src/Uno.UI/Diagnostics/{DiagnosticViewHelper.TView.cs => DiagnosticViewManager.TView.cs} (77%) diff --git a/src/Uno.Foundation/Diagnostics/DiagnosticViewNotification.cs b/src/Uno.Foundation/Diagnostics/DiagnosticViewNotification.cs new file mode 100644 index 000000000000..555edd398f08 --- /dev/null +++ b/src/Uno.Foundation/Diagnostics/DiagnosticViewNotification.cs @@ -0,0 +1,29 @@ +#nullable enable +using System; +using System.ComponentModel; +using System.Linq; + +namespace Uno.Diagnostics.UI; + +/// +/// A notification sent by a diagnostic view to visually show an important status. +/// +/// The content of the notification. +/// Configures the duration to wait before automatically hide the notification. +public class DiagnosticViewNotification(object content, TimeSpan? duration = null) +{ + [EditorBrowsable(EditorBrowsableState.Never)] // For XAML only + public DiagnosticViewNotification() : this(default!) + { + } + + /// + /// The content of the notification. + /// + public object Content { get; set; } = content; + + /// + /// Configures the duration to wait before automatically hide the notification. + /// + public TimeSpan? Duration { get; set; } = duration; +} diff --git a/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs b/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs index bca378697118..39d7e73049ee 100644 --- a/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs +++ b/src/Uno.Foundation/Diagnostics/DiagnosticViewRegistry.cs @@ -20,21 +20,21 @@ internal static class DiagnosticViewRegistry internal static IImmutableList Registrations => _registrations; /// - /// Register a global diagnostic provider that can be displayed on any window. + /// Register a global diagnostic view that can be displayed on any window. /// - /// A diagnostic provider to display. - public static void Register(IDiagnosticViewProvider provider) + /// A diagnostic view to display. + public static void Register(IDiagnosticView view) { ImmutableInterlocked.Update( ref _registrations, static (providers, provider) => providers.Add(provider), - new DiagnosticViewRegistration(DiagnosticViewRegistrationMode.One, provider)); + new DiagnosticViewRegistration(DiagnosticViewRegistrationMode.One, view)); Added?.Invoke(null, _registrations); } } -internal record DiagnosticViewRegistration(DiagnosticViewRegistrationMode Mode, IDiagnosticViewProvider Provider); +internal record DiagnosticViewRegistration(DiagnosticViewRegistrationMode Mode, IDiagnosticView Provider); internal enum DiagnosticViewRegistrationMode { diff --git a/src/Uno.Foundation/Diagnostics/IDiagnosticViewProvider.cs b/src/Uno.Foundation/Diagnostics/IDiagnosticView.cs similarity index 79% rename from src/Uno.Foundation/Diagnostics/IDiagnosticViewProvider.cs rename to src/Uno.Foundation/Diagnostics/IDiagnosticView.cs index f795b13d246d..34847ce89737 100644 --- a/src/Uno.Foundation/Diagnostics/IDiagnosticViewProvider.cs +++ b/src/Uno.Foundation/Diagnostics/IDiagnosticView.cs @@ -9,20 +9,25 @@ namespace Uno.Diagnostics.UI; /// /// A diagnostic entry that can be displayed by the DiagnosticsOverlay. /// -public interface IDiagnosticViewProvider +public interface IDiagnosticView { + /// + /// Identifier of the diagnostic view than can be used to request to show or remove it from a DiagnosticsOverlay. + /// + string Id { get; } + /// /// Friendly name of the diagnostic. /// string Name { get; } /// - /// Get a preview of the diagnostic, usually a value or an icon. + /// Gets a visual element of the diagnostic, usually a value or an icon. /// /// An update coordinator that can be used to push updates on the preview. /// Either a UIElement to be displayed by the diagnostic overlay or a plain object (rendered as text). /// This is expected to be invoked on the dispatcher used to render the preview. - object GetPreview(IDiagnosticViewContext context); + object GetElement(IDiagnosticViewContext context); /// /// Show details of the diagnostic. diff --git a/src/Uno.Foundation/Diagnostics/IDiagnosticViewContext.cs b/src/Uno.Foundation/Diagnostics/IDiagnosticViewContext.cs index 2c230e278ade..7131afacf882 100644 --- a/src/Uno.Foundation/Diagnostics/IDiagnosticViewContext.cs +++ b/src/Uno.Foundation/Diagnostics/IDiagnosticViewContext.cs @@ -19,4 +19,6 @@ public interface IDiagnosticViewContext void ScheduleRecurrent(Action action); void AbortRecurrent(Action action); + + void Notify(DiagnosticViewNotification notification); } diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs index d95d636eee26..f9f7a5b84d89 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs @@ -48,7 +48,7 @@ internal record Status( private class StatusSink { - private readonly DiagnosticView _view = DiagnosticView.Register("Hot reload", (view, status) => view.Update(status)); + private readonly DiagnosticView _view = DiagnosticView.Register("Hot reload", ctx => new HotReloadStatusView(ctx), static (view, status) => view.Update(status)); private HotReloadState? _serverState; private ImmutableDictionary _serverOperations = ImmutableDictionary.Empty; diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs index d158a2e74481..4ab44b6f6af0 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs @@ -88,7 +88,7 @@ private static void ShowDiagnosticsOnFirstActivation(object snd, WindowActivated if (snd is Window { RootElement.XamlRoot: { } xamlRoot } window) { window.Activated -= ShowDiagnosticsOnFirstActivation; - DiagnosticsOverlay.Get(xamlRoot).IsVisible = true; + DiagnosticsOverlay.Get(xamlRoot).Show(); } } diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs index aedb12d5e81b..63eb4bdb5952 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs @@ -1,20 +1,45 @@ -using System; +#nullable enable + +using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Globalization; -using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Windows.UI; using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Shapes; +using Uno.Diagnostics.UI; using Uno.UI.RemoteControl.HotReload.Messages; using static Uno.UI.RemoteControl.HotReload.ClientHotReloadProcessor; +using Path = System.IO.Path; namespace Uno.UI.RemoteControl.HotReload; +[TemplateVisualState(GroupName = "Status", Name = StatusUnknownVisualStateName)] +[TemplateVisualState(GroupName = "Status", Name = StatusErrorVisualStateName)] +[TemplateVisualState(GroupName = "Status", Name = StatusInitializingVisualStateName)] +[TemplateVisualState(GroupName = "Status", Name = StatusIdleVisualStateName)] +[TemplateVisualState(GroupName = "Status", Name = StatusProcessingVisualStateName)] +[TemplateVisualState(GroupName = "Result", Name = ResultNoneVisualStateName)] +[TemplateVisualState(GroupName = "Result", Name = ResultSuccessVisualStateName)] +[TemplateVisualState(GroupName = "Result", Name = ResultFailedVisualStateName)] internal sealed partial class HotReloadStatusView : Control { + private const string StatusUnknownVisualStateName = "Unknown"; + private const string StatusErrorVisualStateName = "Error"; + private const string StatusInitializingVisualStateName = "Initializing"; + private const string StatusIdleVisualStateName = "Idle"; + private const string StatusProcessingVisualStateName = "Processing"; + + private const string ResultNoneVisualStateName = "None"; + private const string ResultSuccessVisualStateName = "Success"; + private const string ResultFailedVisualStateName = "Failed"; + #region HeadLine (DP) public static DependencyProperty HeadLineProperty { get; } = DependencyProperty.Register( nameof(HeadLine), @@ -43,8 +68,40 @@ public ObservableCollection History } #endregion - public HotReloadStatusView() + #region SuccessNotification (DP) + public static readonly DependencyProperty SuccessNotificationProperty = DependencyProperty.Register( + nameof(SuccessNotification), + typeof(DiagnosticViewNotification), + typeof(HotReloadStatusView), + new PropertyMetadata(default(DiagnosticViewNotification?))); + + public DiagnosticViewNotification? SuccessNotification + { + get => (DiagnosticViewNotification?)GetValue(SuccessNotificationProperty); + set => SetValue(SuccessNotificationProperty, value); + } + #endregion + + #region FailureNotification (DP) + public static readonly DependencyProperty FailureNotificationProperty = DependencyProperty.Register( + nameof(FailureNotification), + typeof(DiagnosticViewNotification), + typeof(HotReloadStatusView), + new PropertyMetadata(default(DiagnosticViewNotification?))); + + public DiagnosticViewNotification? FailureNotification + { + get => (DiagnosticViewNotification?)GetValue(FailureNotificationProperty); + set => SetValue(FailureNotificationProperty, value); + } + #endregion + + private readonly IDiagnosticViewContext _ctx; + private string _resultState = "None"; + + public HotReloadStatusView(IDiagnosticViewContext ctx) { + _ctx = ctx; DefaultStyleKey = typeof(HotReloadStatusView); History = []; @@ -57,7 +114,23 @@ public void Update(Status status) UpdateHeadline(status.State); VisualStateManager.GoToState(this, GetStatusVisualState(status.State), true); - VisualStateManager.GoToState(this, GetResultVisualState(), true); + + var resultState = GetResultVisualState(); + if (resultState != _resultState) + { + _resultState = resultState; + VisualStateManager.GoToState(this, resultState, true); + switch (resultState) + { + case ResultSuccessVisualStateName when SuccessNotification is not null: + _ctx.Notify(SuccessNotification); + break; + + case ResultFailedVisualStateName when FailureNotification is not null: + _ctx.Notify(FailureNotification); + break; + } + } } private void SyncOperations(Status status) @@ -156,11 +229,11 @@ State of the hot-reload engine is unknown. private static string GetStatusVisualState(HotReloadState state) => state switch { - HotReloadState.Disabled => "Disabled", - HotReloadState.Initializing => "Initializing", - HotReloadState.Idle => "Idle", - HotReloadState.Processing => "Processing", - _ => "Unknown" + HotReloadState.Disabled => StatusErrorVisualStateName, + HotReloadState.Initializing => StatusInitializingVisualStateName, + HotReloadState.Idle => StatusIdleVisualStateName, + HotReloadState.Processing => StatusProcessingVisualStateName, + _ => StatusUnknownVisualStateName }; private string GetResultVisualState() @@ -168,10 +241,12 @@ private string GetResultVisualState() var operations = History; if (operations is { Count: 0 } || operations.Any(op => !op.IsCompleted)) { - return "None"; // Makes sure to restore to previous None! + return ResultNoneVisualStateName; // Makes sure to restore to previous None! } - return operations[0].IsSuccess ? "Success" : "Failed"; + return operations[0].IsSuccess + ? ResultSuccessVisualStateName + : ResultFailedVisualStateName; } } diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml index 72c90df9598e..9792720463c5 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml @@ -2,6 +2,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Uno.UI.RemoteControl.HotReload" + xmlns:diag="using:Uno.Diagnostics.UI" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mux="using:Microsoft.UI.Xaml.Controls" @@ -23,6 +24,26 @@ + diff --git a/src/Uno.UI.Toolkit/Themes/Generic.xaml b/src/Uno.UI.Toolkit/Themes/Generic.xaml index 169b96a5c889..2eea4d3f8536 100644 --- a/src/Uno.UI.Toolkit/Themes/Generic.xaml +++ b/src/Uno.UI.Toolkit/Themes/Generic.xaml @@ -2,6 +2,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:toolkit="using:Uno.UI.Toolkit"> + + + + diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs index d6cf1a057747..6a496a48de89 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs @@ -14,23 +14,23 @@ namespace Uno.Diagnostics.UI; public sealed partial class DiagnosticsOverlay { - private record DiagnosticElement(DiagnosticsOverlay Overlay, IDiagnosticView Provider, IDiagnosticViewContext Context) : IDisposable + private record DiagnosticElement(DiagnosticsOverlay Overlay, IDiagnosticView View, IDiagnosticViewContext Context) : IDisposable { - private UIElement? _preview; + private UIElement? _value; private CancellationTokenSource? _details; - public UIElement Preview => _preview ??= CreatePreview(); + public UIElement Value => _value ??= CreateValue(); - private UIElement CreatePreview() + private UIElement CreateValue() { try { - var preview = Provider.GetElement(Context); + var preview = View.GetElement(Context); var element = preview as UIElement ?? DiagnosticViewHelper.CreateText(preview.ToString()); if (ToolTipService.GetToolTip(element) is null) { - ToolTipService.SetToolTip(element, Provider.Name); + ToolTipService.SetToolTip(element, View.Name); } if (element is not ButtonBase) @@ -42,10 +42,10 @@ private UIElement CreatePreview() } catch (Exception e) { - this.Log().Error($"Failed to get preview for {Provider.Name}.", e); + this.Log().Error($"Failed to get preview for {View.Name}.", e); var element = DiagnosticViewHelper.CreateText("**"); - ToolTipService.SetToolTip(element, $"Failed to get preview for {Provider.Name}."); + ToolTipService.SetToolTip(element, $"Failed to get preview for {View.Name}."); return element; } } @@ -64,7 +64,7 @@ async ValueTask Do() await previous.CancelAsync(); } - var details = await Provider.GetDetailsAsync(Context, ct.Token); + var details = await View.GetDetailsAsync(Context, ct.Token); switch (details) { case null: @@ -78,7 +78,7 @@ async ValueTask Do() case UIElement element: { var flyout = new Flyout { Content = element }; - flyout.ShowAt(Preview, new FlyoutShowOptions()); + flyout.ShowAt(Value, new FlyoutShowOptions()); ct.Token.Register(flyout.Hide); break; } @@ -88,7 +88,7 @@ async ValueTask Do() var dialog = new ContentDialog { XamlRoot = Overlay._root, - Title = Provider.Name, + Title = View.Name, Content = details.ToString(), CloseButtonText = "Close", }; @@ -99,7 +99,7 @@ async ValueTask Do() } catch (Exception e) { - this.Log().Error($"Failed to show details for {Provider.Name}.", e); + this.Log().Error($"Failed to show details for {View.Name}.", e); } } } diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs index 5d35845f238f..c1534ee7c783 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs @@ -2,8 +2,10 @@ #if WINAPPSDK || HAS_UNO_WINUI using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.UI.Dispatching; @@ -12,6 +14,9 @@ using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; +using Uno.Extensions.Specialized; +using Uno.Extensions; +using static Microsoft.UI.Xaml.Controls.CollectionChangedOperation; namespace Uno.Diagnostics.UI; @@ -49,8 +54,9 @@ public static DiagnosticsOverlay Get(XamlRoot root) private readonly XamlRoot _root; private readonly object _updateGate = new(); - private readonly List _localProviders = new(); + private readonly List _localRegistrations = new(); private readonly Dictionary _elements = new(); + private readonly Dictionary _configuredElementVisibilities = new(); private DispatcherQueue? _dispatcher; private Context? _context; @@ -110,9 +116,13 @@ private DiagnosticsOverlay(XamlRoot root) /// Make the overlay visible. /// /// This can be invoked from any thread.> - public void Show(bool isExpanded = false) + public void Show(bool? isExpanded = false) { _isVisible = true; + if (isExpanded is not null) + { + _isExpanded = isExpanded.Value; + } EnqueueUpdate(); } @@ -126,29 +136,59 @@ public void Hide() EnqueueUpdate(forceUpdate: true); } + /// + /// Hide the given view from the overlay. + /// + /// of the to hide. + public void Hide(string viewId) + { + lock (_updateGate) + { + _configuredElementVisibilities[viewId] = false; + } + EnqueueUpdate(); + } + + /// + /// Hide the given view from the overlay. + /// + /// of the to hide. + /// This will also make this overlay visible (cf. ). + public void Show(string viewId) + { + lock (_updateGate) + { + _configuredElementVisibilities[viewId] = true; + } + Show(); + } + /// /// Add a UI diagnostic element to this overlay. /// - /// This will also make this overlay visible (cf. ). + /// This will also make this overlay visible (cf. ). public void Add(string id, string name, UIElement preview, Func? details = null) => Add(new DiagnosticView(id, name, _ => preview, (_, ct) => new(details?.Invoke()))); /// /// Add a UI diagnostic element to this overlay. /// - /// This will also make this overlay visible (cf. ). + /// This will also make this overlay visible (cf. ). /// The provider to add. public void Add(IDiagnosticView provider) { lock (_updateGate) { - _localProviders.Add(provider); + _localRegistrations.Add(provider); } EnqueueUpdate(); // Making IsVisible = true wil (try) to re-enqueue the update, but safer to keep it here anyway. Show(); } + public UIElement? Find(string viewId) + => _elements.Values.FirstOrDefault(elt => elt.View.Id == viewId)?.Value; + /// protected override void OnVisibilityChanged(Visibility oldValue, Visibility newValue) { @@ -250,29 +290,52 @@ private void EnqueueUpdate(bool forceUpdate = false) } } + var visibleViews = 0; lock (_updateGate) { - var providers = DiagnosticViewRegistry + var viewsThatShouldBeMaterialized = DiagnosticViewRegistry .Registrations .Where(ShouldMaterialize) - .Select(reg => reg.Provider) - .Concat(_localProviders) + .Select(reg => reg.View) + .Concat(_localRegistrations) .Distinct() .ToList(); - foreach (var provider in providers) + foreach (var view in viewsThatShouldBeMaterialized) { - if (!_elements.ContainsKey(provider)) + ref var element = ref CollectionsMarshal.GetValueRefOrAddDefault(_elements, view, out var hasElement); + if (!hasElement) { - var element = new DiagnosticElement(this, provider, _context!); - _elements[provider] = element; - - _elementsPanel.Children.Add(element.Preview); + element = new DiagnosticElement(this, view, _context!); + _elementsPanel.Children.Add(element.Value); } } - ShowHost(host, true); + foreach (var element in _elements.Values) + { + if (_configuredElementVisibilities.GetValueOrDefault(element.View.Id, true)) + { + var currentIndex = _elementsPanel.Children.IndexOf(element.Value); + if (currentIndex is -1) + { + _elementsPanel.Children.Insert(visibleViews, element.Value); + } + else if (currentIndex != visibleViews) + { + Debug.Fail("Invalid index, patching"); + _elementsPanel.Children.Move((uint)currentIndex, (uint)visibleViews); + } + + visibleViews++; + } + else + { + _elementsPanel.Children.Remove(element.Value); + } + } } + + ShowHost(host, isVisible: visibleViews is not 0); }); } @@ -289,12 +352,20 @@ private static void ShowHost(Popup host, bool isVisible) => host.IsOpen = isVisible; private bool ShouldMaterialize(DiagnosticViewRegistration registration) - => registration.Mode switch + { + if (_configuredElementVisibilities.TryGetValue(registration.View.Id, out var isVisible) && isVisible) + { + // We explicitly requested to show that view, so yes we have to materialize it! + return true; + } + + return registration.Mode switch { DiagnosticViewRegistrationMode.All => true, DiagnosticViewRegistrationMode.OnDemand => false, - _ => _overlays.Count(overlay => overlay.Value.IsMaterialized(registration.Provider)) is 0 + _ => _overlays.Count(overlay => overlay.Value.IsMaterialized(registration.View)) is 0 }; + } private bool IsMaterialized(IDiagnosticView provider) { diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.xaml b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.xaml index 5927c21aa745..fe6b30f1a0a9 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.xaml +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.xaml @@ -15,7 +15,7 @@ - + @@ -48,7 +48,7 @@ - + @@ -101,12 +101,13 @@ + Padding="4,0" + Orientation="Horizontal" + VerticalAlignment="Center" /> - From fb5e56f926397db0706a48879a775d79af8a3a34 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 14 Jun 2024 16:30:17 -0400 Subject: [PATCH 18/27] chore: Fix UWP build --- ...ntext.cs => DiagnosticsOverlay.Context.cs} | 33 ++++++++++++++----- .../Diagnostics/DiagnosticsOverlay.cs | 26 +++++---------- 2 files changed, 33 insertions(+), 26 deletions(-) rename src/Uno.UI.Toolkit/Diagnostics/{DiagnosticsOverlay.DiagnosticViewContext.cs => DiagnosticsOverlay.Context.cs} (75%) diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticViewContext.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Context.cs similarity index 75% rename from src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticViewContext.cs rename to src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Context.cs index 0c11d6cc3f51..586001f6c89f 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticViewContext.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Context.cs @@ -1,21 +1,28 @@ #nullable enable -#if WINUI || HAS_UNO_WINUI using System; -using System.Collections.Generic; using System.Linq; + +#if WINAPPSDK || HAS_UNO_WINUI +using System.Collections.Generic; using System.Threading; -using System.Threading.Tasks; using Microsoft.UI.Dispatching; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; using Uno.Foundation.Logging; +#endif namespace Uno.Diagnostics.UI; public sealed partial class DiagnosticsOverlay { - private sealed class Context : IDiagnosticViewContext, IDisposable + private sealed record Context : IDiagnosticViewContext, IDisposable { +#if !WINAPPSDK && !HAS_UNO_WINUI + public static Context? TryCreate(DiagnosticsOverlay owner) => null; + public void Schedule(Action action) => throw new NotSupportedException("Diag overlay is not supported on UWP"); + void IDiagnosticViewContext.ScheduleRecurrent(Action action) => throw new NotSupportedException("Diag overlay is not supported on UWP"); + void IDiagnosticViewContext.AbortRecurrent(Action action) => throw new NotSupportedException("Diag overlay is not supported on UWP"); + void IDiagnosticViewContext.Notify(DiagnosticViewNotification notification) => throw new NotSupportedException("Diag overlay is not supported on UWP"); + void IDisposable.Dispose() { } +#else private Queue _pending = new(); private Queue _pending2 = new(); private List? _recurrents; @@ -25,7 +32,17 @@ private sealed class Context : IDiagnosticViewContext, IDisposable private DispatcherQueueTimer? _timer; private int _updatesScheduled; - public Context(DiagnosticsOverlay owner, DispatcherQueue dispatcher) + public static Context? TryCreate(DiagnosticsOverlay owner) + { + if (owner._root.Content?.DispatcherQueue is { } dispatcher) + { + return new Context(owner, dispatcher); + } + + return null; + } + + private Context(DiagnosticsOverlay owner, DispatcherQueue dispatcher) { _owner = owner; _dispatcher = dispatcher; @@ -170,6 +187,6 @@ public void Dispose() _updatesScheduled = -4096; _timer?.Stop(); } +#endif } } -#endif diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs index c1534ee7c783..629899c95a16 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs @@ -1,5 +1,4 @@ #nullable enable -#if WINAPPSDK || HAS_UNO_WINUI using System; using System.Collections.Generic; using System.Diagnostics; @@ -7,16 +6,11 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; -using System.Threading.Tasks; -using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; -using Uno.Extensions.Specialized; -using Uno.Extensions; -using static Microsoft.UI.Xaml.Controls.CollectionChangedOperation; namespace Uno.Diagnostics.UI; @@ -58,7 +52,6 @@ public static DiagnosticsOverlay Get(XamlRoot root) private readonly Dictionary _elements = new(); private readonly Dictionary _configuredElementVisibilities = new(); - private DispatcherQueue? _dispatcher; private Context? _context; private Popup? _overlayHost; private bool _isVisible; @@ -82,21 +75,19 @@ static DiagnosticsOverlay() private DiagnosticsOverlay(XamlRoot root) { _root = root; - _dispatcher = root.Content?.DispatcherQueue; - _context = _dispatcher is null ? null : new Context(this, _dispatcher); + _context = Context.TryCreate(this); root.Changed += static (snd, e) => { var overlay = Get(snd); - var dispatcher = snd.Content?.DispatcherQueue; - if (dispatcher != overlay._dispatcher) // Is this even possible ??? + var context = Context.TryCreate(overlay); + if (context != overlay._context) // I.e. dispatcher changed ... is this even possible ??? { lock (overlay._updateGate) { - overlay._dispatcher = dispatcher; - overlay._context = dispatcher is null ? null : new Context(overlay, dispatcher); + overlay._context = context; - // Clean all dispatcher bound state + // Clean all dispatcher bound states overlay._overlayHost = null; overlay._elementsPanel = null; foreach (var element in overlay._elements.Values) @@ -253,16 +244,16 @@ private void OnAnchorManipulated(object sender, ManipulationDeltaRoutedEventArgs private void EnqueueUpdate(bool forceUpdate = false) { - var dispatcher = _dispatcher; + var context = _context; var isHidden = !_isVisible; if ((isHidden && !forceUpdate) - || dispatcher is null + || context is null || Interlocked.CompareExchange(ref _updateEnqueued, 1, 0) is not 0) { return; } - dispatcher.TryEnqueue(() => + context.Schedule(() => { _updateEnqueued = 0; @@ -375,4 +366,3 @@ private bool IsMaterialized(IDiagnosticView provider) } } } -#endif From 937edd7911391bc7a58ced55ca4e57903316adff Mon Sep 17 00:00:00 2001 From: David Date: Fri, 14 Jun 2024 16:43:12 -0400 Subject: [PATCH 19/27] chore: Add some docs --- .../ClientHotReloadProcessor.Common.Status.cs | 32 +++++++++++++------ .../HotReload/ClientHotReloadProcessor.cs | 1 + 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs index f9f7a5b84d89..41c711660b52 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs @@ -14,7 +14,12 @@ namespace Uno.UI.RemoteControl.HotReload; public partial class ClientHotReloadProcessor { - private readonly StatusSink _status = new(); + /// + /// Raised when the status of the hot-reload engine changes. + /// + internal EventHandler? StatusChanged; + + private readonly StatusSink _status; internal enum HotReloadSource { @@ -41,12 +46,18 @@ internal enum HotReloadClientResult Ignored = 256, } + /// + /// The aggregated status of the hot-reload engine. + /// + /// The global state of the hot-reload engine (combining server and client state). + /// State and history of all hot-reload operations detected on the server. + /// State and history of all hot-reload operation received by this client. internal record Status( HotReloadState State, (HotReloadState State, IImmutableList Operations) Server, (HotReloadState State, IImmutableList Operations) Local); - private class StatusSink + private class StatusSink(ClientHotReloadProcessor owner) { private readonly DiagnosticView _view = DiagnosticView.Register("Hot reload", ctx => new HotReloadStatusView(ctx), static (view, status) => view.Update(status)); @@ -89,7 +100,10 @@ static int Compare(HotReloadClientOperation left, HotReloadClientOperation right } private void NotifyStatusChanged() - => _view.Update(GetStatus()); + { + _view.Update(GetStatus()); + owner.StatusChanged?.Invoke(this, GetStatus()); + } private Status GetStatus() { @@ -130,7 +144,7 @@ public void ResignCurrent() private ImmutableList _exceptions = ImmutableList.Empty; private int _result = -1; - public HotReloadClientOperation(HotReloadSource source, Type[] types, Action onUpdated) + internal HotReloadClientOperation(HotReloadSource source, Type[] types, Action onUpdated) { Source = source; Types = types; @@ -146,7 +160,7 @@ public HotReloadClientOperation(HotReloadSource source, Type[] types, Action onU public Type[] Types { get; } - public string[] CuratedTypes => _curatedTypes ??= Types + internal string[] CuratedTypes => _curatedTypes ??= Types .Select(t => { var name = t.Name; @@ -166,16 +180,16 @@ public HotReloadClientOperation(HotReloadSource source, Type[] types, Action onU public string? IgnoreReason { get; private set; } - public void ReportError(MethodInfo source, Exception error) + internal void ReportError(MethodInfo source, Exception error) => ReportError(error); // For now we just ignore the source - public void ReportError(Exception error) + internal void ReportError(Exception error) { ImmutableInterlocked.Update(ref _exceptions, static (errors, error) => errors.Add(error), error); _onUpdated(); } - public void ReportCompleted() + internal void ReportCompleted() { var result = (_exceptions, AbortReason: IgnoreReason) switch { @@ -195,7 +209,7 @@ public void ReportCompleted() } } - public void ReportIgnored(string reason) + internal void ReportIgnored(string reason) { IgnoreReason = reason; ReportCompleted(); diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs index c12a29b50e8a..33d75e94043f 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs @@ -27,6 +27,7 @@ public partial class ClientHotReloadProcessor : IClientProcessor public ClientHotReloadProcessor(IRemoteControlClient rcClient) { _rcClient = rcClient; + _status = new(this); } partial void InitializeMetadataUpdater(); From 154e82238289ad0612bcddfa29011f7d8dd07785 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 17 Jun 2024 09:38:09 -0400 Subject: [PATCH 20/27] chore: Fix build --- src/Uno.CrossTargetting.targets | 2 +- .../HotReload/HotReloadStatusView.xaml | 12 ++--- .../Messages/HotReloadServerResult.cs | 2 +- .../HotReload/Messages/HotReloadState.cs | 2 +- .../Messages/HotReloadStatusMessage.cs | 48 +++++++++++-------- .../Uno.UI.RemoteControl.Skia.csproj | 6 --- .../Diagnostics/DiagnosticsOverlay.Context.cs | 16 +++---- .../DiagnosticsOverlay.DiagnosticElement.cs | 2 - .../DiagnosticsOverlay.Notification.cs | 4 +- .../Diagnostics/DiagnosticsOverlay.cs | 11 +++-- .../Uno.UI.Toolkit.Windows.csproj | 14 ++++-- 11 files changed, 65 insertions(+), 54 deletions(-) diff --git a/src/Uno.CrossTargetting.targets b/src/Uno.CrossTargetting.targets index 5d6b5c1ac7d5..5e50e29c966e 100644 --- a/src/Uno.CrossTargetting.targets +++ b/src/Uno.CrossTargetting.targets @@ -137,7 +137,7 @@ <_OverrideTargetFramework Condition="$(_OverrideTargetFramework.EndsWith('.0-android'))">$(_OverrideTargetFramework)30.0 <_overridePackageId>$(CommonOverridePackageId.ToLowerInvariant()) - <_overridePackageId Condition="'$(CommonOverridePackageId)'==''">$(AssemblyName.ToLowerVariant()) + <_overridePackageId Condition="'$(CommonOverridePackageId)'==''">$(AssemblyName.ToLowerInvariant()) <_overridePackageId Condition="'$(UNO_UWP_BUILD)'=='false'">$(_overridePackageId.Replace('uno.ui','uno.winui')) <_TargetNugetFolder>$(NuGetPackageRoot)\$(_overridePackageId)\$(UnoNugetOverrideVersion)\lib\$(_OverrideTargetFramework) <_TargetNugetRefFolder>$(NuGetPackageRoot)\$(_overridePackageId)\$(UnoNugetOverrideVersion)\ref\$(_OverrideTargetFramework) diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml index c8bf60a55f9b..1361659c8ea5 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.xaml @@ -6,9 +6,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mux="using:Microsoft.UI.Xaml.Controls" - mc:Ignorable="d" - d:DesignHeight="300" - d:DesignWidth="400"> + mc:Ignorable="d"> @@ -125,13 +123,13 @@ - - + - - + + diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadServerResult.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadServerResult.cs index 8a5f19d1905b..f7e0ad2e5f95 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadServerResult.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadServerResult.cs @@ -6,7 +6,7 @@ namespace Uno.UI.RemoteControl.HotReload.Messages; /// /// The result of a hot-reload operation on server. /// -public enum HotReloadServerResult +internal enum HotReloadServerResult { /// /// Hot-reload completed with no changes. diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadState.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadState.cs index fad733d8e646..0361feaad988 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadState.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadState.cs @@ -3,7 +3,7 @@ namespace Uno.UI.RemoteControl.HotReload.Messages; -public enum HotReloadState +internal enum HotReloadState { /// /// Hot reload is disabled. diff --git a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs index 673a414d1ca0..59af405fd427 100644 --- a/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs +++ b/src/Uno.UI.RemoteControl/HotReload/Messages/HotReloadStatusMessage.cs @@ -3,27 +3,37 @@ using System.Linq; using Newtonsoft.Json; -namespace Uno.UI.RemoteControl.HotReload.Messages; +#if !HAS_UNO // We don't want to add a dependency on Newtonsoft.Json in Uno.Toolkit +namespace Newtonsoft.Json +{ + internal class JsonPropertyAttribute : Attribute + { + } +} +#endif -public record HotReloadStatusMessage( - [property: JsonProperty] HotReloadState State, - [property: JsonProperty] IImmutableList Operations) - : IMessage +namespace Uno.UI.RemoteControl.HotReload.Messages { - public const string Name = nameof(HotReloadStatusMessage); + internal record HotReloadStatusMessage( + [property: JsonProperty] HotReloadState State, + [property: JsonProperty] IImmutableList Operations) + : IMessage + { + public const string Name = nameof(HotReloadStatusMessage); - /// - [JsonProperty] - public string Scope => WellKnownScopes.HotReload; + /// + [JsonProperty] + public string Scope => WellKnownScopes.HotReload; - /// - [JsonProperty] - string IMessage.Name => Name; -} + /// + [JsonProperty] + string IMessage.Name => Name; + } -public record HotReloadServerOperationData( - long Id, - DateTimeOffset StartTime, - ImmutableHashSet FilePaths, - DateTimeOffset? EndTime, - HotReloadServerResult? Result); + internal record HotReloadServerOperationData( + long Id, + DateTimeOffset StartTime, + ImmutableHashSet FilePaths, + DateTimeOffset? EndTime, + HotReloadServerResult? Result); +} diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj index 3f6216e62844..a544129cacd9 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj @@ -27,12 +27,6 @@ - - - - - - $(AssemblyName).xml diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Context.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Context.cs index 586001f6c89f..0cc8c44039d6 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Context.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Context.cs @@ -13,14 +13,14 @@ namespace Uno.Diagnostics.UI; public sealed partial class DiagnosticsOverlay { - private sealed record Context : IDiagnosticViewContext, IDisposable + private sealed record ViewContext : IDiagnosticViewContext, IDisposable { #if !WINAPPSDK && !HAS_UNO_WINUI - public static Context? TryCreate(DiagnosticsOverlay owner) => null; + public static ViewContext? TryCreate(DiagnosticsOverlay owner) => null; public void Schedule(Action action) => throw new NotSupportedException("Diag overlay is not supported on UWP"); void IDiagnosticViewContext.ScheduleRecurrent(Action action) => throw new NotSupportedException("Diag overlay is not supported on UWP"); void IDiagnosticViewContext.AbortRecurrent(Action action) => throw new NotSupportedException("Diag overlay is not supported on UWP"); - void IDiagnosticViewContext.Notify(DiagnosticViewNotification notification) => throw new NotSupportedException("Diag overlay is not supported on UWP"); + void IDiagnosticViewContext.Notify(DiagnosticViewNotification notif) { } void IDisposable.Dispose() { } #else private Queue _pending = new(); @@ -32,17 +32,17 @@ void IDisposable.Dispose() { } private DispatcherQueueTimer? _timer; private int _updatesScheduled; - public static Context? TryCreate(DiagnosticsOverlay owner) + public static ViewContext? TryCreate(DiagnosticsOverlay owner) { if (owner._root.Content?.DispatcherQueue is { } dispatcher) { - return new Context(owner, dispatcher); + return new ViewContext(owner, dispatcher); } return null; } - private Context(DiagnosticsOverlay owner, DispatcherQueue dispatcher) + private ViewContext(DiagnosticsOverlay owner, DispatcherQueue dispatcher) { _owner = owner; _dispatcher = dispatcher; @@ -113,9 +113,7 @@ public void AbortRecurrent(Action action) /// public void Notify(DiagnosticViewNotification notif) - { - _owner.Notify(notif, this); - } + => _owner.Notify(notif, this); private void DoUpdates() { diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs index 6a496a48de89..db9f71269acc 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs @@ -1,6 +1,5 @@ #nullable enable #pragma warning disable IDE0055 // Invalid suggestion for switch statement -#if WINAPPSDK || HAS_UNO_WINUI using System; using System.Linq; using System.Threading; @@ -111,4 +110,3 @@ public void Dispose() => Interlocked.Exchange(ref _details, new CancellationTokenSource())?.Cancel(); } } -#endif diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Notification.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Notification.cs index 60a76e817a4b..f87e0298e6d5 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Notification.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Notification.cs @@ -16,7 +16,8 @@ private void OnNotificationTapped(object sender, TappedRoutedEventArgs e) HideNotification(); } - private void Notify(DiagnosticViewNotification notif, Context context) +#pragma warning disable IDE0051 // Not used on windows UWP + private void Notify(DiagnosticViewNotification notif, ViewContext context) { if (!_isVisible || _isExpanded) { @@ -52,6 +53,7 @@ async ValueTask ShowNotification() } } } +#pragma warning restore IDE0051 private void HideNotification() { diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs index 629899c95a16..5945b6c15865 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs @@ -52,7 +52,7 @@ public static DiagnosticsOverlay Get(XamlRoot root) private readonly Dictionary _elements = new(); private readonly Dictionary _configuredElementVisibilities = new(); - private Context? _context; + private ViewContext? _context; private Popup? _overlayHost; private bool _isVisible; private bool _isExpanded; @@ -75,12 +75,12 @@ static DiagnosticsOverlay() private DiagnosticsOverlay(XamlRoot root) { _root = root; - _context = Context.TryCreate(this); + _context = ViewContext.TryCreate(this); root.Changed += static (snd, e) => { var overlay = Get(snd); - var context = Context.TryCreate(overlay); + var context = ViewContext.TryCreate(overlay); if (context != overlay._context) // I.e. dispatcher changed ... is this even possible ??? { lock (overlay._updateGate) @@ -180,6 +180,7 @@ public void Add(IDiagnosticView provider) public UIElement? Find(string viewId) => _elements.Values.FirstOrDefault(elt => elt.View.Id == viewId)?.Value; +#if HAS_UNO /// protected override void OnVisibilityChanged(Visibility oldValue, Visibility newValue) { @@ -187,6 +188,7 @@ protected override void OnVisibilityChanged(Visibility oldValue, Visibility newV EnqueueUpdate(forceUpdate: newValue is not Visibility.Visible); // Force update when hiding. } +#endif /// protected override void OnApplyTemplate() @@ -212,6 +214,7 @@ protected override void OnApplyTemplate() _anchor.Tapped += OnAnchorTapped; _anchor.ManipulationDelta += OnAnchorManipulated; _anchor.ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY | ManipulationModes.TranslateInertia; + RenderTransform = new TranslateTransform { X = 15, Y = 15 }; } if (_notificationPresenter is not null) { @@ -313,7 +316,7 @@ private void EnqueueUpdate(bool forceUpdate = false) } else if (currentIndex != visibleViews) { - Debug.Fail("Invalid index, patching"); + global::System.Diagnostics.Debug.Fail("Invalid index, patching"); _elementsPanel.Children.Move((uint)currentIndex, (uint)visibleViews); } diff --git a/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj b/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj index 4bf6f0d83c91..b57fd803ccff 100644 --- a/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj +++ b/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj @@ -28,14 +28,17 @@ + + - + + - - + + @@ -54,6 +57,11 @@ + + + + + From f42c7ccaeb035f2a32dcc39e01626ce5a6a02894 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 18 Jun 2024 14:48:43 -0400 Subject: [PATCH 21/27] chore: Remove HR diag from windows head --- .../ClientHotReloadProcessor.Common.Status.cs | 11 +- ...ClientHotReloadProcessor.MetadataUpdate.cs | 10 +- .../HotReload/HotReloadStatusView.cs | 32 ++--- ...stics.cs => RemoteControlClient.Status.cs} | 107 +++++++++++++--- .../RemoteControlClient.cs | 2 +- .../RemoteControlStatusView.cs | 121 ++---------------- .../Uno.UI.RemoteControl.Reference.csproj | 10 ++ .../Uno.UI.RemoteControl.Skia.csproj | 10 ++ .../Uno.UI.RemoteControl.Wasm.csproj | 10 ++ .../Uno.UI.RemoteControl.netcoremobile.csproj | 18 ++- 10 files changed, 176 insertions(+), 155 deletions(-) rename src/Uno.UI.RemoteControl/{RemoteControlClient.Diagnostics.cs => RemoteControlClient.Status.cs} (67%) diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs index 41c711660b52..9ab33eacda08 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.Status.cs @@ -59,7 +59,9 @@ internal record Status( private class StatusSink(ClientHotReloadProcessor owner) { +#if HAS_UNO_WINUI private readonly DiagnosticView _view = DiagnosticView.Register("Hot reload", ctx => new HotReloadStatusView(ctx), static (view, status) => view.Update(status)); +#endif private HotReloadState? _serverState; private ImmutableDictionary _serverOperations = ImmutableDictionary.Empty; @@ -101,11 +103,14 @@ static int Compare(HotReloadClientOperation left, HotReloadClientOperation right private void NotifyStatusChanged() { - _view.Update(GetStatus()); - owner.StatusChanged?.Invoke(this, GetStatus()); + var status = BuildStatus(); +#if HAS_UNO_WINUI + _view.Update(status); +#endif + owner.StatusChanged?.Invoke(this, status); } - private Status GetStatus() + private Status BuildStatus() { var serverState = _serverState ?? (_localOperations.Any() ? HotReloadState.Idle /* no info */ : HotReloadState.Initializing); var localState = _localOperations.Any(op => op.Result is null) ? HotReloadState.Processing : HotReloadState.Idle; diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs index 4ab44b6f6af0..ae281fd943c3 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs @@ -20,6 +20,12 @@ using Microsoft.UI.Xaml.Media; using Uno.Diagnostics.UI; +#if HAS_UNO_WINUI +using _WindowActivatedEventArgs = Microsoft.UI.Xaml.WindowActivatedEventArgs; +#else +using _WindowActivatedEventArgs = Windows.UI.Core.WindowActivatedEventArgs; +#endif + namespace Uno.UI.RemoteControl.HotReload; partial class ClientHotReloadProcessor @@ -83,12 +89,14 @@ internal static Window? CurrentWindow } } - private static void ShowDiagnosticsOnFirstActivation(object snd, WindowActivatedEventArgs windowActivatedEventArgs) + private static void ShowDiagnosticsOnFirstActivation(object snd, _WindowActivatedEventArgs windowActivatedEventArgs) { if (snd is Window { RootElement.XamlRoot: { } xamlRoot } window) { window.Activated -= ShowDiagnosticsOnFirstActivation; +#if HAS_UNO_WINUI // No diag to show currently DiagnosticsOverlay.Get(xamlRoot).Show(); +#endif } } diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs index a61bbba6e24b..91cdb1a378f9 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs @@ -50,7 +50,7 @@ internal sealed partial class HotReloadStatusView : Control public string HeadLine { get => (string)GetValue(HeadLineProperty); - set => SetValue(HeadLineProperty, value); + private set => SetValue(HeadLineProperty, value); } #endregion @@ -122,7 +122,7 @@ private static void OnLoaded(object sender, RoutedEventArgs e) public void Update(Status status) { - SyncOperations(status); + UpdateHistory(status); UpdateHeadline(status.State); VisualStateManager.GoToState(this, GetStatusVisualState(status.State), true); @@ -145,17 +145,17 @@ public void Update(Status status) } } - private void SyncOperations(Status status) + private void UpdateHistory(Status status) { - var operations = History; - var vms = operations.ToDictionary(op => (op.IsServer, op.Id)); + var history = History; + var vms = history.ToDictionary(op => (op.IsServer, op.Id)); foreach (var srvOp in status.Server.Operations) { if (!vms.TryGetValue((true, srvOp.Id), out var vm)) { vm = new HotReloadEntryViewModel(true, srvOp.Id, srvOp.StartTime); - operations.Insert(FindIndex(srvOp.StartTime), vm); + history.Insert(FindIndex(srvOp.StartTime), vm); } string[] files = srvOp.FilePaths.Select(Path.GetFileName).ToArray()!; @@ -182,7 +182,7 @@ private void SyncOperations(Status status) if (!vms.TryGetValue((false, localOp.Id), out var vm)) { vm = new HotReloadEntryViewModel(false, localOp.Id, localOp.StartTime); - operations.Insert(FindIndex(localOp.StartTime), vm); + history.Insert(FindIndex(localOp.StartTime), vm); } var types = localOp.CuratedTypes; @@ -201,7 +201,7 @@ private void SyncOperations(Status status) vm.RaiseChanged(); } - string Join(string[] items, string itemType, int maxItems = 5) + static string Join(string[] items, string itemType, int maxItems = 5) => items switch { { Length: 0 } => "", @@ -212,9 +212,9 @@ string Join(string[] items, string itemType, int maxItems = 5) int FindIndex(DateTimeOffset date) { - for (var i = 0; i < operations.Count; i++) + for (var i = 0; i < history.Count; i++) { - if (operations[i].Start > date) + if (history[i].Start > date) { return i - 1; } @@ -224,19 +224,21 @@ int FindIndex(DateTimeOffset date) } } - public string UpdateHeadline(HotReloadState? state) - => HeadLine = state switch + public void UpdateHeadline(HotReloadState? state) + { + HeadLine = state switch { null => """ - State of the hot-reload engine is unknown. - This usually indicates that connection to the dev-server failed, but if running within VisualStudio, updates might still be detected. - """, + State of the hot-reload engine is unknown. + This usually indicates that connection to the dev-server failed, but if running within VisualStudio, updates might still be detected. + """, HotReloadState.Disabled => "Hot-reload server is disabled.", HotReloadState.Initializing => "Hot-reload engine is initializing.", HotReloadState.Idle => "Hot-reload server is ready and listening for file changes.", HotReloadState.Processing => "Hot-reload engine is processing file changes", _ => "Unable to determine the state of the hot-reload engine." }; + } private static string GetStatusVisualState(HotReloadState state) => state switch diff --git a/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs b/src/Uno.UI.RemoteControl/RemoteControlClient.Status.cs similarity index 67% rename from src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs rename to src/Uno.UI.RemoteControl/RemoteControlClient.Status.cs index 1cca943a96dd..65be9d902dd3 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlClient.Diagnostics.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlClient.Status.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Text; using System.Threading; using Uno.Diagnostics.UI; using Uno.UI.RemoteControl.Messages; @@ -14,23 +15,7 @@ public partial class RemoteControlClient { internal event EventHandler? StatusChanged; - private interface IStatusSink : DevServerDiagnostics.ISink - { - void ReportActiveConnection(Connection? connection); - - void Report(ConnectionState state); - - void ReportPing(KeepAliveMessage ping); - - void ReportPong(KeepAliveMessage? pong); - - void ReportKeepAliveAborted(Exception error); - - void RegisterRequiredServerProcessor(string typeFullName, string version); - - void ReportServerProcessors(ProcessorsDiscoveryResponse response); - } - + #region Status object model internal enum ConnectionState { /// @@ -87,22 +72,100 @@ internal enum KeepAliveState Aborted // KeepAlive was aborted } + internal record struct MissingProcessor(string TypeFullName, string Version, string Details, string? Error = null); + internal record Status( ConnectionState State, bool? IsVersionValid, (KeepAliveState State, long RoundTrip) KeepAlive, ImmutableHashSet MissingRequiredProcessors, - (long Count, ImmutableHashSet Types) InvalidFrames); + (long Count, ImmutableHashSet Types) InvalidFrames) + { + public (StatusClassification kind, string message) GetSummary() + { + var (kind, message) = State switch + { + ConnectionState.Idle => (StatusClassification.Info, "Initializing..."), + ConnectionState.NoServer => (StatusClassification.Error, "No server configured, cannot initialize connection."), + ConnectionState.Connecting => (StatusClassification.Info, "Connecting to dev-server."), + ConnectionState.ConnectionTimeout => (StatusClassification.Error, "Failed to connect to dev-server (timeout)."), + ConnectionState.ConnectionFailed => (StatusClassification.Error, "Failed to connect to dev-server (error)."), + ConnectionState.Reconnecting => (StatusClassification.Info, "Connection to dev-server has been lost, reconnecting."), + ConnectionState.Disconnected => (StatusClassification.Error, "Connection to dev-server has been lost, will retry later."), + + ConnectionState.Connected when IsVersionValid is false => (StatusClassification.Warning, "Connected to dev-server, but version mis-match with client."), + ConnectionState.Connected when InvalidFrames.Count is not 0 => (StatusClassification.Warning, $"Connected to dev-server, but received {InvalidFrames.Count} invalid frames from the server."), + ConnectionState.Connected when MissingRequiredProcessors is { IsEmpty: false } => (StatusClassification.Warning, "Connected to dev-server, but some required processors are missing on server."), + ConnectionState.Connected when KeepAlive.State is KeepAliveState.Late => (StatusClassification.Info, "Connected to dev-server, but keep-alive messages are taking longer than expected."), + ConnectionState.Connected when KeepAlive.State is KeepAliveState.Lost => (StatusClassification.Warning, "Connected to dev-server, but last keep-alive messages have been lost."), + ConnectionState.Connected when KeepAlive.State is KeepAliveState.Aborted => (StatusClassification.Warning, "Connected to dev-server, but keep-alive has been aborted."), + ConnectionState.Connected => (StatusClassification.Ok, "Connected to dev-server."), + + _ => (StatusClassification.Warning, State.ToString()), + }; + + if (KeepAlive.RoundTrip >= 0) + { + message += $" (ping {KeepAlive.RoundTrip}ms)"; + } - internal record struct MissingProcessor(string TypeFullName, string Version, string Details, string? Error = null); + return (kind, message); + } + + internal string GetDescription() + { + var details = new StringBuilder(GetSummary().message); + + if (MissingRequiredProcessors is { Count: > 0 } missing) + { + details.AppendLine(); + details.AppendLine(); + details.AppendLine("Some processor(s) requested by the client are missing on the server:"); + + foreach (var m in missing) + { + details.AppendLine($"- {m.TypeFullName} v{m.Version}: {m.Details}"); + if (m.Error is not null) + { + details.AppendLine($" {m.Error}"); + } + } + } + + if (InvalidFrames.Types is { Count: > 0 } invalidFrameTypes) + { + details.AppendLine(); + details.AppendLine(); + details.AppendLine($"Received {InvalidFrames.Count} invalid frames from the server. Failing frame types ({invalidFrameTypes.Count}):"); + + foreach (var type in invalidFrameTypes) + { + details.AppendLine($"- {type.FullName}"); + } + } + + return details.ToString(); + } + } + + internal enum StatusClassification + { + Ok, + Info, + Warning, + Error + } + #endregion - private class StatusSink(RemoteControlClient owner) : IStatusSink + private class StatusSink(RemoteControlClient owner) : DevServerDiagnostics.ISink { private ConnectionState _state = ConnectionState.Idle; +#if HAS_UNO_WINUI private readonly DiagnosticView _view = DiagnosticView.Register( "Dev-server", (view, status) => view.Update(status), - RemoteControlStatusView.GetStatusDetails); + status => status.GetDescription()); +#endif public Status BuildStatus() => new(_state, _isVersionValid, (_keepAliveState, _roundTrip), _missingRequiredProcessors, (_invalidFrames, _invalidFrameTypes)); @@ -110,7 +173,9 @@ public Status BuildStatus() private void NotifyStatusChanged() { var status = BuildStatus(); +#if HAS_UNO_WINUI _view.Update(status); +#endif owner.StatusChanged?.Invoke(owner, status); } diff --git a/src/Uno.UI.RemoteControl/RemoteControlClient.cs b/src/Uno.UI.RemoteControl/RemoteControlClient.cs index 4b1778cc2a14..d84e05b8fbbb 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlClient.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlClient.cs @@ -54,7 +54,7 @@ internal static RemoteControlClient Initialize(Type appType, ServerEndpointAttri public TimeSpan ConnectionRetryInterval { get; } = TimeSpan.FromMilliseconds(_connectionRetryInterval); private const int _connectionRetryInterval = 5_000; - private readonly IStatusSink _status; + private readonly StatusSink _status; private static readonly TimeSpan _keepAliveInterval = TimeSpan.FromSeconds(30); private readonly (string endpoint, int port)[]? _serverAddresses; private readonly Dictionary _processors = new(); diff --git a/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs b/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs index 36a028cf941f..c3aa43b2e1b2 100644 --- a/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs +++ b/src/Uno.UI.RemoteControl/RemoteControlStatusView.cs @@ -1,7 +1,5 @@ using System; using System.Linq; -using System.Text; -using Windows.UI; using Microsoft.UI; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; @@ -23,119 +21,22 @@ public RemoteControlStatusView() public void Update(Status status) { - ((SolidColorBrush)Fill).Color = GetStatusColor(status); - ToolTipService.SetToolTip(this, GetStatusSummary(status)); - - if (IsAutoHideEnabled) + var (kind, message) = status.GetSummary(); + ((SolidColorBrush)Fill).Color = kind switch { - Visibility = IsImportantState(status) - ? Microsoft.UI.Xaml.Visibility.Visible - : Microsoft.UI.Xaml.Visibility.Collapsed; - } - } - - private static bool IsImportantState(Status status) - => status.State switch - { - ConnectionState.NoServer => true, - ConnectionState.ConnectionTimeout => true, - ConnectionState.ConnectionFailed => true, - ConnectionState.Reconnecting => true, - ConnectionState.Disconnected => true, - - ConnectionState.Connected when status.IsVersionValid is false => true, - ConnectionState.Connected when status.InvalidFrames.Count is not 0 => true, - ConnectionState.Connected when status.MissingRequiredProcessors is { IsEmpty: false } => true, - ConnectionState.Connected when status.KeepAlive.State is not KeepAliveState.Ok => true, - - ConnectionState.Connected => false, - - _ => false - }; - - private static Color GetStatusColor(Status status) - => status.State switch - { - ConnectionState.Idle => Colors.Gray, - ConnectionState.NoServer => Colors.Red, - ConnectionState.Connecting => Colors.Yellow, - ConnectionState.ConnectionTimeout => Colors.Red, - ConnectionState.ConnectionFailed => Colors.Red, - ConnectionState.Reconnecting => Colors.Yellow, - ConnectionState.Disconnected => Colors.Red, - - ConnectionState.Connected when status.IsVersionValid is false => Colors.Orange, - ConnectionState.Connected when status.InvalidFrames.Count is not 0 => Colors.Orange, - ConnectionState.Connected when status.MissingRequiredProcessors is { IsEmpty: false } => Colors.Orange, - ConnectionState.Connected when status.KeepAlive.State is not KeepAliveState.Ok => Colors.Yellow, - ConnectionState.Connected => Colors.Green, - + StatusClassification.Ok => Colors.Green, + StatusClassification.Info => Colors.Yellow, + StatusClassification.Warning => Colors.Orange, + StatusClassification.Error => Colors.Red, _ => Colors.Gray }; + ToolTipService.SetToolTip(this, message); - private static string GetStatusSummary(Status status) - { - var summary = status.State switch - { - ConnectionState.Idle => "Initializing...", - ConnectionState.NoServer => "No server configured, cannot initialize connection.", - ConnectionState.Connecting => "Connecting to dev-server.", - ConnectionState.ConnectionTimeout => "Failed to connect to dev-server (timeout).", - ConnectionState.ConnectionFailed => "Failed to connect to dev-server (error).", - ConnectionState.Reconnecting => "Connection to dev-server has been lost, reconnecting.", - ConnectionState.Disconnected => "Connection to dev-server has been lost, will retry later.", - - ConnectionState.Connected when status.IsVersionValid is false => "Connected to dev-server, but version mis-match with client.", - ConnectionState.Connected when status.InvalidFrames.Count is not 0 => $"Connected to dev-server, but received {status.InvalidFrames.Count} invalid frames from the server.", - ConnectionState.Connected when status.MissingRequiredProcessors is { IsEmpty: false } => "Connected to dev-server, but some required processors are missing on server.", - ConnectionState.Connected when status.KeepAlive.State is KeepAliveState.Late => "Connected to dev-server, but keep-alive messages are taking longer than expected.", - ConnectionState.Connected when status.KeepAlive.State is KeepAliveState.Lost => "Connected to dev-server, but last keep-alive messages have been lost.", - ConnectionState.Connected when status.KeepAlive.State is KeepAliveState.Aborted => "Connected to dev-server, but keep-alive has been aborted.", - ConnectionState.Connected => "Connected to dev-server.", - - _ => status.State.ToString() - }; - - if (status.KeepAlive.RoundTrip >= 0) - { - summary += $" (ping {status.KeepAlive.RoundTrip}ms)"; - } - - return summary; - } - - internal static string GetStatusDetails(Status status) - { - var details = new StringBuilder(GetStatusSummary(status)); - - if (status.MissingRequiredProcessors is { Count: > 0 } missing) - { - details.AppendLine(); - details.AppendLine(); - details.AppendLine("Some processor(s) requested by the client are missing on the server:"); - - foreach (var m in missing) - { - details.AppendLine($"- {m.TypeFullName} v{m.Version}: {m.Details}"); - if (m.Error is not null) - { - details.AppendLine($" {m.Error}"); - } - } - } - - if (status.InvalidFrames.Types is { Count: > 0 } invalidFrameTypes) + if (IsAutoHideEnabled) { - details.AppendLine(); - details.AppendLine(); - details.AppendLine($"Received {status.InvalidFrames.Count} invalid frames from the server. Failing frame types ({invalidFrameTypes.Count}):"); - - foreach (var type in invalidFrameTypes) - { - details.AppendLine($"- {type.FullName}"); - } + Visibility = kind is StatusClassification.Ok + ? Microsoft.UI.Xaml.Visibility.Collapsed + : Microsoft.UI.Xaml.Visibility.Visible; } - - return details.ToString(); } } diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj index d84ad8143e68..9ab5de536482 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj @@ -62,6 +62,16 @@ TargetFramework + + + + + + + + + + diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj index a544129cacd9..f94f11fa3150 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj @@ -61,6 +61,16 @@ TargetFramework + + + + + + + + + + diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj index a96f3f93d4b6..f00aafc41a7c 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj @@ -57,6 +57,16 @@ TargetFramework + + + + + + + + + + diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj index 9a0d1760a8d6..821613957f02 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj @@ -33,10 +33,10 @@ - - build - true - + + build + true + @@ -68,6 +68,16 @@ + + + + + + + + + + From cae55188f0678353aafa8ad5ba614147d54030d0 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 18 Jun 2024 17:29:47 -0400 Subject: [PATCH 22/27] chore: Fix WinAppSDK build --- .../ClientHotReloadProcessor.Agent.cs | 1 - .../ClientHotReloadProcessor.Common.cs | 13 ++ ...ClientHotReloadProcessor.MetadataUpdate.cs | 14 +- .../Uno.UI.RemoteControl.Reference.csproj | 5 + .../Uno.UI.RemoteControl.Skia.csproj | 5 + .../Uno.UI.RemoteControl.Wasm.csproj | 5 + .../Uno.UI.RemoteControl.netcoremobile.csproj | 5 + .../Uno_UI_Helpers/Given_WeakEventManager.cs | 159 ------------------ .../Diagnostics/DiagnosticsOverlay.cs | 6 +- .../Diagnostics/DiagnosticViewHelper.cs | 2 +- 10 files changed, 46 insertions(+), 169 deletions(-) delete mode 100644 src/Uno.UI.RuntimeTests/Tests/Uno_UI_Helpers/Given_WeakEventManager.cs diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs index 2039cb05b1b4..7e1e88c14c20 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Agent.cs @@ -25,7 +25,6 @@ partial class ClientHotReloadProcessor private bool _linkerEnabled; private HotReloadAgent? _agent; private bool _serverMetadataUpdatesEnabled; - private static ClientHotReloadProcessor? _instance; private readonly TaskCompletionSource _hotReloadWorkloadSpaceLoaded = new(); private void WorkspaceLoadResult(HotReloadWorkspaceLoadResult hotReloadWorkspaceLoadResult) diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.cs index 10483fc1c830..a2233baf6933 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.Common.cs @@ -32,6 +32,19 @@ namespace Uno.UI.RemoteControl.HotReload { partial class ClientHotReloadProcessor { + private static ClientHotReloadProcessor? _instance; + +#if HAS_UNO + private static ClientHotReloadProcessor? Instance => _instance; +#else + private static ClientHotReloadProcessor? Instance => _instance ??= new(); + + private ClientHotReloadProcessor() + { + _status = new(this); + } +#endif + private static async IAsyncEnumerable EnumerateHotReloadInstances( object? instance, Func> predicate, diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs index ae281fd943c3..690421f1f860 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs @@ -75,30 +75,34 @@ internal static Window? CurrentWindow get => _currentWindow; set { +#if HAS_UNO_WINUI if (_currentWindow is not null) { _currentWindow.Activated -= ShowDiagnosticsOnFirstActivation; } +#endif _currentWindow = value; +#if HAS_UNO_WINUI if (_currentWindow is not null) { _currentWindow.Activated += ShowDiagnosticsOnFirstActivation; } +#endif } } +#if HAS_UNO_WINUI // No diag to show currently private static void ShowDiagnosticsOnFirstActivation(object snd, _WindowActivatedEventArgs windowActivatedEventArgs) { if (snd is Window { RootElement.XamlRoot: { } xamlRoot } window) { window.Activated -= ShowDiagnosticsOnFirstActivation; -#if HAS_UNO_WINUI // No diag to show currently DiagnosticsOverlay.Get(xamlRoot).Show(); -#endif } } +#endif /// /// Run on UI thread to reload the visual tree with updated types @@ -432,12 +436,12 @@ public static void ForceHotReloadUpdate() { try { - _instance?._status.ConfigureSourceForNextOperation(HotReloadSource.Manual); + Instance?._status.ConfigureSourceForNextOperation(HotReloadSource.Manual); UpdateApplication(Array.Empty()); } finally { - _instance?._status.ConfigureSourceForNextOperation(default); + Instance?._status.ConfigureSourceForNextOperation(default); } } @@ -447,7 +451,7 @@ public static void ForceHotReloadUpdate() [EditorBrowsable(EditorBrowsableState.Never)] public static void UpdateApplication(Type[] types) { - var hr = _instance?._status.ReportLocalStarting(types); + var hr = Instance?._status.ReportLocalStarting(types); foreach (var type in types) { diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj index 9ab5de536482..d5c4ba7468b1 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Reference.csproj @@ -68,10 +68,15 @@ + + + + $(MSBuildThisFileDirectory)**\*.xaml + diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj index f94f11fa3150..700f5c7d46bb 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Skia.csproj @@ -67,10 +67,15 @@ + + + + $(MSBuildThisFileDirectory)**\*.xaml + diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj index f00aafc41a7c..cfafc63ebd39 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.Wasm.csproj @@ -63,10 +63,15 @@ + + + + $(MSBuildThisFileDirectory)**\*.xaml + diff --git a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj index 821613957f02..273209e68caa 100644 --- a/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj +++ b/src/Uno.UI.RemoteControl/Uno.UI.RemoteControl.netcoremobile.csproj @@ -73,10 +73,15 @@ + + + + $(MSBuildThisFileDirectory)**\*.xaml + diff --git a/src/Uno.UI.RuntimeTests/Tests/Uno_UI_Helpers/Given_WeakEventManager.cs b/src/Uno.UI.RuntimeTests/Tests/Uno_UI_Helpers/Given_WeakEventManager.cs deleted file mode 100644 index 468ad9517e66..000000000000 --- a/src/Uno.UI.RuntimeTests/Tests/Uno_UI_Helpers/Given_WeakEventManager.cs +++ /dev/null @@ -1,159 +0,0 @@ -#nullable enable - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Uno.UI.Helpers; - -namespace Uno.UI.RuntimeTests.Tests.Uno_UI_Helpers; - -[TestClass] -public class Given_WeakEventManager -{ - private sealed class EventPublisher - { - private readonly WeakEventManager _manager = new(); - - internal event Action Event - { - add => _manager.AddEventHandler(value); - remove => _manager.RemoveEventHandler(value); - } - - public void RaiseEvent() => _manager.HandleEvent("Event"); - } - - private sealed class EventSubscriber - { - public void M() { } - } - - [TestMethod] - public void When_ManySubscriptions_Then_DoesNotLeak() - { - var publisher = new EventPublisher(); - var weakRefs = new List>(); - Subscribe(publisher, weakRefs); - - for (var i = 0; i < 10; i++) - { - GC.Collect(); - GC.WaitForPendingFinalizers(); - } - - foreach (var x in weakRefs) - { - Assert.IsFalse(x.TryGetTarget(out _)); - } - } - - [TestMethod] - public void When_ReEnter_Then_AllHandlersInvokedProperly() - { - var sut = new WeakEventManager(); - int handler1Count = 0, handler2Count = 0; - - sut.AddEventHandler(Handler1, "Event1"); - sut.AddEventHandler(Handler2, "Event1"); - sut.AddEventHandler(Handler1_2, "Event2"); - sut.AddEventHandler(Handler2_2, "Event2"); - - sut.HandleEvent("Event1"); - - Assert.AreEqual(3, handler1Count); - Assert.AreEqual(3, handler2Count); - - sut.HandleEvent("Event2"); - - Assert.AreEqual(4, handler1Count); - Assert.AreEqual(4, handler2Count); - - void Handler1() - { - handler1Count++; - sut.HandleEvent("Event2"); - } - - void Handler1_2() - { - handler1Count++; - } - - void Handler2() - { - handler2Count++; - sut.HandleEvent("Event2"); - } - - void Handler2_2() - { - handler2Count++; - } - } - - [TestMethod] - public void When_UnSubscribeInHandler() - { - var sut = new WeakEventManager(); - int handler1Count = 0, handler2Count = 0; - - sut.AddEventHandler(Handler1, "Event1"); - sut.AddEventHandler(Handler2, "Event1"); - - sut.HandleEvent("Event1"); - sut.HandleEvent("Event1"); - - Assert.AreEqual(1, handler1Count); - Assert.AreEqual(2, handler2Count); - - void Handler1() - { - handler1Count++; - sut.RemoveEventHandler(Handler1, "Event1"); - } - - void Handler2() - { - handler2Count++; - } - } - - [TestMethod] - public void When_UnSubscribeInHandler2() - { - var pub = new EventPublisher(); - int handler1Count = 0, handler2Count = 0; - - pub.Event += Handler1; - pub.Event += Handler2; - - pub.RaiseEvent(); - pub.RaiseEvent(); - - Assert.AreEqual(1, handler1Count); - Assert.AreEqual(2, handler2Count); - - void Handler1() - { - handler1Count++; - pub.Event -= Handler1; - } - - void Handler2() - { - handler2Count++; - } - } - - private void Subscribe(EventPublisher publisher, List> weakRefs) - { - for (var i = 0; i < 1000; i++) - { - var subscriber = new EventSubscriber(); - publisher.Event += subscriber.M; - weakRefs.Add(new WeakReference(subscriber)); - } - } -} diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs index 5945b6c15865..dbe230251f69 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs @@ -205,9 +205,9 @@ protected override void OnApplyTemplate() base.OnApplyTemplate(); - _elementsPanel = GetTemplateChild(ElementsPanelPartName); - _anchor = GetTemplateChild(AnchorPartName); - _notificationPresenter = GetTemplateChild(NotificationPartName); + _elementsPanel = GetTemplateChild(ElementsPanelPartName) as Panel; + _anchor = GetTemplateChild(AnchorPartName) as UIElement; + _notificationPresenter = GetTemplateChild(NotificationPartName) as ContentPresenter; if (_anchor is not null) { diff --git a/src/Uno.UI/Diagnostics/DiagnosticViewHelper.cs b/src/Uno.UI/Diagnostics/DiagnosticViewHelper.cs index 7c3295146c49..5225b0b655a4 100644 --- a/src/Uno.UI/Diagnostics/DiagnosticViewHelper.cs +++ b/src/Uno.UI/Diagnostics/DiagnosticViewHelper.cs @@ -11,7 +11,7 @@ namespace Uno.Diagnostics.UI; internal class DiagnosticViewHelper { - private static readonly Color _white = new(255, 255, 255, 255); + private static readonly Color _white = new Color { A = 255, R = 255, G = 255, B = 255 }; public static TextBlock CreateText(string? text) => new() From 93dc4e8a5ca7d85334ee5195d0bb08bc39705348 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 19 Jun 2024 09:14:14 -0400 Subject: [PATCH 23/27] chore: Fix UWP toolkit build --- src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj b/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj index b57fd803ccff..4b47dd820f47 100644 --- a/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj +++ b/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj @@ -70,11 +70,18 @@ - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - From 4be66f477814a1058e60b2297a59c85361a4ef9b Mon Sep 17 00:00:00 2001 From: David Date: Wed, 19 Jun 2024 14:30:48 -0400 Subject: [PATCH 24/27] chore: Fix HR testing failing due to IDE0055 ... --- .../HotReload/Frame/HRApp/AppHead.xaml.cs | 84 +++++++++---------- .../DiagnosticsOverlay.DiagnosticElement.cs | 6 ++ .../Uno.UI.Toolkit.Windows.csproj | 2 +- 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/AppHead.xaml.cs b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/AppHead.xaml.cs index 05f75130e336..3a23bb9f975f 100644 --- a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/AppHead.xaml.cs +++ b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/AppHead.xaml.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging; using Microsoft.UI.Xaml; using System; @@ -23,65 +23,65 @@ public AppHead() /// private static void InitializeLogging() { - // Logging is disabled by default for release builds, as it incurs a significant - // initialization cost from Microsoft.Extensions.Logging setup. If startup performance - // is a concern for your application, keep this disabled. If you're running on the web or - // desktop targets, you can use URL or command line parameters to enable it. - // - // For more performance documentation: https://platform.uno/docs/articles/Uno-UI-Performance.html + // Logging is disabled by default for release builds, as it incurs a significant + // initialization cost from Microsoft.Extensions.Logging setup. If startup performance + // is a concern for your application, keep this disabled. If you're running on the web or + // desktop targets, you can use URL or command line parameters to enable it. + // + // For more performance documentation: https://platform.uno/docs/articles/Uno-UI-Performance.html - var factory = LoggerFactory.Create(builder => - { + var factory = LoggerFactory.Create(builder => + { #if __WASM__ - builder.AddProvider(new global::Uno.Extensions.Logging.WebAssembly.WebAssemblyConsoleLoggerProvider()); + builder.AddProvider(new global::Uno.Extensions.Logging.WebAssembly.WebAssemblyConsoleLoggerProvider()); #elif __IOS__ || __MACCATALYST__ - builder.AddProvider(new global::Uno.Extensions.Logging.OSLogLoggerProvider()); + builder.AddProvider(new global::Uno.Extensions.Logging.OSLogLoggerProvider()); #elif WINAPPSDK - builder.AddDebug(); + builder.AddDebug(); #else - builder.AddConsole(); + builder.AddConsole(); #endif - // Exclude logs below this level - builder.SetMinimumLevel(LogLevel.Information); + // Exclude logs below this level + builder.SetMinimumLevel(LogLevel.Information); - // Default filters for Uno Platform namespaces - builder.AddFilter("Uno", LogLevel.Warning); - builder.AddFilter("Windows", LogLevel.Warning); - builder.AddFilter("Microsoft", LogLevel.Warning); + // Default filters for Uno Platform namespaces + builder.AddFilter("Uno", LogLevel.Warning); + builder.AddFilter("Windows", LogLevel.Warning); + builder.AddFilter("Microsoft", LogLevel.Warning); - // Generic Xaml events - // builder.AddFilter("Microsoft.UI.Xaml", LogLevel.Debug ); - // builder.AddFilter("Microsoft.UI.Xaml.VisualStateGroup", LogLevel.Debug ); - // builder.AddFilter("Microsoft.UI.Xaml.StateTriggerBase", LogLevel.Debug ); - // builder.AddFilter("Microsoft.UI.Xaml.UIElement", LogLevel.Debug ); - // builder.AddFilter("Microsoft.UI.Xaml.FrameworkElement", LogLevel.Trace ); + // Generic Xaml events + // builder.AddFilter("Microsoft.UI.Xaml", LogLevel.Debug ); + // builder.AddFilter("Microsoft.UI.Xaml.VisualStateGroup", LogLevel.Debug ); + // builder.AddFilter("Microsoft.UI.Xaml.StateTriggerBase", LogLevel.Debug ); + // builder.AddFilter("Microsoft.UI.Xaml.UIElement", LogLevel.Debug ); + // builder.AddFilter("Microsoft.UI.Xaml.FrameworkElement", LogLevel.Trace ); - // Layouter specific messages - // builder.AddFilter("Microsoft.UI.Xaml.Controls", LogLevel.Debug ); - // builder.AddFilter("Microsoft.UI.Xaml.Controls.Layouter", LogLevel.Debug ); - // builder.AddFilter("Microsoft.UI.Xaml.Controls.Panel", LogLevel.Debug ); + // Layouter specific messages + // builder.AddFilter("Microsoft.UI.Xaml.Controls", LogLevel.Debug ); + // builder.AddFilter("Microsoft.UI.Xaml.Controls.Layouter", LogLevel.Debug ); + // builder.AddFilter("Microsoft.UI.Xaml.Controls.Panel", LogLevel.Debug ); - // builder.AddFilter("Windows.Storage", LogLevel.Debug ); + // builder.AddFilter("Windows.Storage", LogLevel.Debug ); - // Binding related messages - // builder.AddFilter("Microsoft.UI.Xaml.Data", LogLevel.Debug ); - // builder.AddFilter("Microsoft.UI.Xaml.Data", LogLevel.Debug ); + // Binding related messages + // builder.AddFilter("Microsoft.UI.Xaml.Data", LogLevel.Debug ); + // builder.AddFilter("Microsoft.UI.Xaml.Data", LogLevel.Debug ); - // Binder memory references tracking - // builder.AddFilter("Uno.UI.DataBinding.BinderReferenceHolder", LogLevel.Debug ); + // Binder memory references tracking + // builder.AddFilter("Uno.UI.DataBinding.BinderReferenceHolder", LogLevel.Debug ); - // RemoteControl and HotReload related - builder.AddFilter("Uno.UI.RemoteControl", LogLevel.Information); + // RemoteControl and HotReload related + builder.AddFilter("Uno.UI.RemoteControl", LogLevel.Information); - // Debug JS interop - // builder.AddFilter("Uno.Foundation.WebAssemblyRuntime", LogLevel.Debug ); - }); + // Debug JS interop + // builder.AddFilter("Uno.Foundation.WebAssemblyRuntime", LogLevel.Debug ); + }); - global::Uno.Extensions.LogExtensionPoint.AmbientLoggerFactory = factory; + global::Uno.Extensions.LogExtensionPoint.AmbientLoggerFactory = factory; #if HAS_UNO - global::Uno.UI.Adapter.Microsoft.Extensions.Logging.LoggingAdapter.Initialize(); + global::Uno.UI.Adapter.Microsoft.Extensions.Logging.LoggingAdapter.Initialize(); #endif } } diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs index db9f71269acc..c5f221c07fb3 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs @@ -7,7 +7,9 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; +#if WINUI || HAS_UNO using Uno.Foundation.Logging; +#endif namespace Uno.Diagnostics.UI; @@ -41,7 +43,9 @@ private UIElement CreateValue() } catch (Exception e) { +#if WINUI || HAS_UNO this.Log().Error($"Failed to get preview for {View.Name}.", e); +#endif var element = DiagnosticViewHelper.CreateText("**"); ToolTipService.SetToolTip(element, $"Failed to get preview for {View.Name}."); @@ -98,7 +102,9 @@ async ValueTask Do() } catch (Exception e) { +#if WINUI || HAS_UNO this.Log().Error($"Failed to show details for {View.Name}.", e); +#endif } } } diff --git a/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj b/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj index 4b47dd820f47..d65b7050b277 100644 --- a/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj +++ b/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj @@ -78,7 +78,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + From 13acb937bd73df632abf3bd47b616c24ebd76b2d Mon Sep 17 00:00:00 2001 From: David Date: Wed, 19 Jun 2024 16:14:30 -0400 Subject: [PATCH 25/27] chore: Completely remove diag overlay form UWP since runtime is too old on those targets --- ...ClientHotReloadProcessor.MetadataUpdate.cs | 2 +- .../Uno.UI.RuntimeTests.HRApp.Skia.Gtk.csproj | 3 +++ .../Diagnostics/DiagnosticCounter.cs | 2 +- .../Diagnostics/DiagnosticsOverlay.Context.cs | 14 ++-------- .../DiagnosticsOverlay.DiagnosticElement.cs | 8 ++---- .../DiagnosticsOverlay.Notification.cs | 3 ++- .../Diagnostics/DiagnosticsOverlay.Uwp.cs | 13 ++++++++++ .../Diagnostics/DiagnosticsOverlay.cs | 2 ++ .../Uno.UI.Toolkit.Windows.csproj | 26 +++++++++---------- 9 files changed, 39 insertions(+), 34 deletions(-) create mode 100644 src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Uwp.cs diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs index 690421f1f860..bd289301c8cc 100644 --- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs +++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.MetadataUpdate.cs @@ -93,7 +93,7 @@ internal static Window? CurrentWindow } } -#if HAS_UNO_WINUI // No diag to show currently +#if HAS_UNO_WINUI // No diag to show currently on windows (so no WINUI) private static void ShowDiagnosticsOnFirstActivation(object snd, _WindowActivatedEventArgs windowActivatedEventArgs) { if (snd is Window { RootElement.XamlRoot: { } xamlRoot } window) diff --git a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Uno.UI.RuntimeTests.HRApp.Skia.Gtk.csproj b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Uno.UI.RuntimeTests.HRApp.Skia.Gtk.csproj index bf2122e4bb42..7a0caf131328 100644 --- a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Uno.UI.RuntimeTests.HRApp.Skia.Gtk.csproj +++ b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Uno.UI.RuntimeTests.HRApp.Skia.Gtk.csproj @@ -14,6 +14,9 @@ true false + + false + $(NoWarn);CS1998;IDE0051;IDE0055 diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticCounter.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticCounter.cs index 43c3052a0a65..b50bfaba4572 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticCounter.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticCounter.cs @@ -1,5 +1,5 @@ #nullable enable -#if WINAPPSDK || HAS_UNO_WINUI +#if WINUI || HAS_UNO_WINUI using System; using System.Linq; using System.Threading; diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Context.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Context.cs index 0cc8c44039d6..3d0d544f6e59 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Context.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Context.cs @@ -1,13 +1,11 @@ #nullable enable +#if WINUI || HAS_UNO_WINUI using System; using System.Linq; - -#if WINAPPSDK || HAS_UNO_WINUI using System.Collections.Generic; using System.Threading; using Microsoft.UI.Dispatching; using Uno.Foundation.Logging; -#endif namespace Uno.Diagnostics.UI; @@ -15,14 +13,6 @@ public sealed partial class DiagnosticsOverlay { private sealed record ViewContext : IDiagnosticViewContext, IDisposable { -#if !WINAPPSDK && !HAS_UNO_WINUI - public static ViewContext? TryCreate(DiagnosticsOverlay owner) => null; - public void Schedule(Action action) => throw new NotSupportedException("Diag overlay is not supported on UWP"); - void IDiagnosticViewContext.ScheduleRecurrent(Action action) => throw new NotSupportedException("Diag overlay is not supported on UWP"); - void IDiagnosticViewContext.AbortRecurrent(Action action) => throw new NotSupportedException("Diag overlay is not supported on UWP"); - void IDiagnosticViewContext.Notify(DiagnosticViewNotification notif) { } - void IDisposable.Dispose() { } -#else private Queue _pending = new(); private Queue _pending2 = new(); private List? _recurrents; @@ -185,6 +175,6 @@ public void Dispose() _updatesScheduled = -4096; _timer?.Stop(); } -#endif } } +#endif diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs index c5f221c07fb3..3e954fbcb68c 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.DiagnosticElement.cs @@ -1,5 +1,6 @@ #nullable enable #pragma warning disable IDE0055 // Invalid suggestion for switch statement +#if WINUI || HAS_UNO_WINUI using System; using System.Linq; using System.Threading; @@ -7,9 +8,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; -#if WINUI || HAS_UNO using Uno.Foundation.Logging; -#endif namespace Uno.Diagnostics.UI; @@ -43,9 +42,7 @@ private UIElement CreateValue() } catch (Exception e) { -#if WINUI || HAS_UNO this.Log().Error($"Failed to get preview for {View.Name}.", e); -#endif var element = DiagnosticViewHelper.CreateText("**"); ToolTipService.SetToolTip(element, $"Failed to get preview for {View.Name}."); @@ -102,9 +99,7 @@ async ValueTask Do() } catch (Exception e) { -#if WINUI || HAS_UNO this.Log().Error($"Failed to show details for {View.Name}.", e); -#endif } } } @@ -116,3 +111,4 @@ public void Dispose() => Interlocked.Exchange(ref _details, new CancellationTokenSource())?.Cancel(); } } +#endif diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Notification.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Notification.cs index f87e0298e6d5..59fbeb73beec 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Notification.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Notification.cs @@ -1,5 +1,5 @@ #nullable enable - +#if WINUI || HAS_UNO_WINUI using System.Threading; using System.Threading.Tasks; using Microsoft.UI.Xaml; @@ -60,3 +60,4 @@ private void HideNotification() VisualStateManager.GoToState(this, NotificationCollapsedStateName, true); } } +#endif diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Uwp.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Uwp.cs new file mode 100644 index 000000000000..c95630a105df --- /dev/null +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.Uwp.cs @@ -0,0 +1,13 @@ +#nullable enable +#if !WINUI && !HAS_UNO_WINUI +using System; +using System.Linq; +using Microsoft.UI.Xaml.Controls; + +namespace Uno.Diagnostics.UI; + +public sealed partial class DiagnosticsOverlay : Control +{ + // Note: This file is only to let the DiagnosticsOverlay.xaml (ref unconditionally in Generic.xaml) to compile properly. +} +#endif diff --git a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs index dbe230251f69..4dba82fe6115 100644 --- a/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs +++ b/src/Uno.UI.Toolkit/Diagnostics/DiagnosticsOverlay.cs @@ -1,4 +1,5 @@ #nullable enable +#if WINUI || HAS_UNO_WINUI using System; using System.Collections.Generic; using System.Diagnostics; @@ -369,3 +370,4 @@ private bool IsMaterialized(IDiagnosticView provider) } } } +#endif diff --git a/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj b/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj index d65b7050b277..16e31206ed1d 100644 --- a/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj +++ b/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Windows.csproj @@ -26,19 +26,6 @@ - - - - - - - - - - - - - @@ -65,6 +52,19 @@ + + + + + + + + + + + + + From 43cae2a5f06238a94364fcb6b0c96d731808f444 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 20 Jun 2024 08:39:44 -0400 Subject: [PATCH 26/27] chore: Fix IDE rules in HRApp --- .../ServerHotReloadProcessor.MetadataUpdate.cs | 10 ++++++---- .../HotReload/ServerHotReloadProcessor.cs | 13 ++++++++++--- .../HotReload/HotReloadStatusView.cs | 2 +- .../Tests/HotReload/Frame/HRApp/AppHead.xaml.cs | 2 +- .../Frame/HRApp/Tests/Given_AppDictionary.cs | 5 ++--- .../Frame/HRApp/Tests/Given_Frame_DataContext.cs | 3 ++- .../HotReload/Frame/HRApp/Tests/Given_TextBox.cs | 2 +- ...HR_DPUpdates_Binding_PageUsingComponent.xaml.cs | 14 +++++++------- 8 files changed, 30 insertions(+), 21 deletions(-) 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 07318b189bee..6d4b0b802cff 100644 --- a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.MetadataUpdate.cs +++ b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.MetadataUpdate.cs @@ -152,14 +152,14 @@ private async Task ProcessMetadataChanges(IEnumerable filePaths) { ProcessSolutionChanged(hotReload, file, CancellationToken.None).Wait(); } + + await hotReload.CompleteUsingIntermediates(); } catch (Exception e) { _reporter.Warn($"Internal error while processing hot-reload ({e.Message})."); - } - finally - { - await hotReload.CompleteUsingIntermediates(); + + await hotReload.CompleteUsingIntermediates(e); } } } @@ -168,6 +168,7 @@ private async Task ProcessSolutionChanged(HotReloadServerOperation hotRelo { if (!await EnsureSolutionInitializedAsync() || _currentSolution is null || _hotReloadService is null) { + hotReload.NotifyIntermediate(file, HotReloadServerResult.NoChanges); return false; } @@ -202,6 +203,7 @@ private async Task ProcessSolutionChanged(HotReloadServerOperation hotRelo { _reporter.Verbose($"Could not find document with path {file} in the workspace."); // HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler); + hotReload.NotifyIntermediate(file, HotReloadServerResult.NoChanges); return false; } diff --git a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs index 86306d4cd6d1..086244ade72b 100644 --- a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs +++ b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs @@ -304,10 +304,17 @@ public void NotifyIntermediate(string file, HotReloadServerResult result) _timeout.Change(_timeoutDelay, Timeout.InfiniteTimeSpan); } - public async ValueTask CompleteUsingIntermediates() + public async ValueTask CompleteUsingIntermediates(Exception? exception = null) { - Debug.Assert(_aggregatedFilesCount == _filePaths.Count); - await Complete(_aggregatedResult); + if (exception is null) + { + //Debug.Assert(_aggregatedFilesCount == _filePaths.Count); + await Complete(_aggregatedResult); + } + else + { + await Complete(HotReloadServerResult.Failed, exception); + } } /// diff --git a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs index 91cdb1a378f9..a5f1b3ccaabb 100644 --- a/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs +++ b/src/Uno.UI.RemoteControl/HotReload/HotReloadStatusView.cs @@ -216,7 +216,7 @@ int FindIndex(DateTimeOffset date) { if (history[i].Start > date) { - return i - 1; + return i; } } diff --git a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/AppHead.xaml.cs b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/AppHead.xaml.cs index 3a23bb9f975f..fe330c0d4733 100644 --- a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/AppHead.xaml.cs +++ b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/AppHead.xaml.cs @@ -1,4 +1,4 @@ - using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.UI.Xaml; using System; diff --git a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_AppDictionary.cs b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_AppDictionary.cs index f5764b0cf6d4..ad67d251d085 100644 --- a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_AppDictionary.cs +++ b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_AppDictionary.cs @@ -1,5 +1,4 @@ - -using System; +using System; using System.Reflection.Metadata; using Microsoft.VisualStudio.TestTools.UnitTesting; using Uno.Extensions; @@ -92,7 +91,7 @@ bool ValidateColor(FrameworkElement root, Color color) { if (root.FindName("tb01") is TextBlock tb) { - if(tb.Foreground is SolidColorBrush scb) + if (tb.Foreground is SolidColorBrush scb) { return scb.Color == color; } diff --git a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_Frame_DataContext.cs b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_Frame_DataContext.cs index 5c6adc8c753a..6070511e81ce 100644 --- a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_Frame_DataContext.cs +++ b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_Frame_DataContext.cs @@ -1,4 +1,5 @@ - #nullable disable +#nullable disable +#pragma warning disable IDE0051 // Members used for testing by reflection using System; using System.Formats.Asn1; diff --git a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_TextBox.cs b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_TextBox.cs index 88b494691738..709370517395 100644 --- a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_TextBox.cs +++ b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_TextBox.cs @@ -22,7 +22,7 @@ public class Given_TextBox : BaseTestClass /// of specific controls (in this case the Text property of a TextBox /// [TestMethod] - [Ignore("This doesn't work on the CI pipeline")] + [Ignore("This doesn't work on the CI pipeline")] public async Task When_Changing_TextBox() { var ct = new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token; diff --git a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Pages/HR_DPUpdates_Binding_PageUsingComponent.xaml.cs b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Pages/HR_DPUpdates_Binding_PageUsingComponent.xaml.cs index dc5f1cc4b534..b0912d378cef 100644 --- a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Pages/HR_DPUpdates_Binding_PageUsingComponent.xaml.cs +++ b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Pages/HR_DPUpdates_Binding_PageUsingComponent.xaml.cs @@ -10,12 +10,12 @@ public HR_DPUpdates_Binding_PageUsingComponent() myComponent.DataContext = this; } - public object Tag2 - { - get => GetValue(Tag2Property); - set => SetValue(Tag2Property, value); - } + public object Tag2 + { + get => GetValue(Tag2Property); + set => SetValue(Tag2Property, value); + } - public static DependencyProperty Tag2Property { get; } = - DependencyProperty.Register(nameof(Tag2), typeof(object), typeof(HR_DPUpdates_Binding_PageUsingComponent), new PropertyMetadata(defaultValue: null)); + public static DependencyProperty Tag2Property { get; } = + DependencyProperty.Register(nameof(Tag2), typeof(object), typeof(HR_DPUpdates_Binding_PageUsingComponent), new PropertyMetadata(defaultValue: null)); } From 92904f17653f9b2ad1733c8fcb8ec36e44508040 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 20 Jun 2024 13:45:21 -0400 Subject: [PATCH 27/27] test: Fix failing test on CI --- .../Frame/HRApp/Tests/Given_AppDictionary.cs | 3 ++- .../Frame/HRApp/Tests/RemoteControlExtensions.cs | 15 ++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_AppDictionary.cs b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_AppDictionary.cs index ad67d251d085..6e6c34777906 100644 --- a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_AppDictionary.cs +++ b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_AppDictionary.cs @@ -148,9 +148,10 @@ await HotReloadHelper.UpdateProjectFileAndRevert( } [TestMethod] + //[Ignore("Failing ramdomly on CI")] public async Task When_Change_AppResource_LotOfTimes() { - var ct = new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token; + var ct = new CancellationTokenSource(TimeSpan.FromSeconds(90)).Token; // We're not storing the instance explicitly, as the HR engine replaces // the top level content of the window. We keep poking at the UnitTestsUIContentHelper.Content diff --git a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/RemoteControlExtensions.cs b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/RemoteControlExtensions.cs index 771dcf76b733..36f46fa549ce 100644 --- a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/RemoteControlExtensions.cs +++ b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/RemoteControlExtensions.cs @@ -95,11 +95,16 @@ public static async Task UpdateProjectFileAndRevert( await RemoteControlClient.Instance.WaitForConnection(); - await UpdateServerFile(filePathInProject, originalText, replacementText, ct); - - await callback(); + try + { + await UpdateServerFile(filePathInProject, originalText, replacementText, ct); - await UpdateServerFile(filePathInProject, replacementText, originalText, ct); + await callback(); + } + finally + { + await UpdateServerFile(filePathInProject, replacementText, originalText, CancellationToken.None); + } } public static async Task UpdateServerFileAndRevert( @@ -124,7 +129,7 @@ public static async Task UpdateServerFileAndRevert( } finally { - await UpdateServerFile(replacementText, originalText, ct); + await UpdateServerFile(replacementText, originalText, CancellationToken.None); } }