From d73066ddf0741661e4e34970ded8c4303dd631f3 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 2 Oct 2024 18:10:54 -0400 Subject: [PATCH] feat: Add discovery of dev-server add-ins --- src/Uno.Sdk/targets/Uno.Build.targets | 8 +++ .../Extensibility/AddIns.cs | 71 ++++++++++++++++--- .../AttributeDataExtensions.cs | 6 +- .../ServiceAttribute.cs | 21 ++++++ .../ServiceCollectionServiceExtensions.cs | 10 ++- .../Uno.UI.RemoteControl.Host.csproj | 1 + src/Uno.UI.RemoteControl.VS/EntryPoint.cs | 14 ++-- .../Uno.WinUI.DevServer.targets | 4 ++ 8 files changed, 113 insertions(+), 22 deletions(-) diff --git a/src/Uno.Sdk/targets/Uno.Build.targets b/src/Uno.Sdk/targets/Uno.Build.targets index f3d5e34b4cea..4d7bdf5629dd 100644 --- a/src/Uno.Sdk/targets/Uno.Build.targets +++ b/src/Uno.Sdk/targets/Uno.Build.targets @@ -141,6 +141,14 @@ + + + + + diff --git a/src/Uno.UI.RemoteControl.Host/Extensibility/AddIns.cs b/src/Uno.UI.RemoteControl.Host/Extensibility/AddIns.cs index 38cfab4f27c8..0c80c868ede0 100644 --- a/src/Uno.UI.RemoteControl.Host/Extensibility/AddIns.cs +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/AddIns.cs @@ -1,26 +1,75 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Uno.Extensions; using Uno.UI.RemoteControl.Helpers; namespace Uno.UI.RemoteControl.Host.Extensibility; public class AddIns { + private static ILogger _log = typeof(AddIns).Log(); + public static IImmutableList Discover(string solutionFile) - => ProcessHelper.RunProcess("dotnet", $"build \"{solutionFile}\" /t:GetRemoteControlAddIns /nowarn:MSB4057") switch // Ignore missing target + { + // Note: With .net 9 we need to specify --verbosity detailed to get messages with High importance. + var result = ProcessHelper.RunProcess("dotnet", $"build \"{solutionFile}\" --target:UnoDumpTargetFrameworks --verbosity detailed"); + var targetFrameworks = GetConfigurationValue(result.output ?? "", "TargetFrameworks") + .SelectMany(tfms => tfms.Split(['\r', '\n', ';', ','], StringSplitOptions.RemoveEmptyEntries)) + .Select(tfm => tfm.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToImmutableList(); + + if (targetFrameworks.IsEmpty) + { + if (_log.IsEnabled(LogLevel.Warning)) + { + _log.Log(LogLevel.Warning, new Exception(result.error), $"Failed to get target frameworks of solution '{solutionFile}' (cf. inner exception for details)."); + } + + return new ImmutableArray(); + } + + + foreach (var targetFramework in targetFrameworks) { - // Note: We ignore the exitCode not being 0: even if flagged as nowarn, we can still get MSB4057 for project that does not have the target GetRemoteControlAddIns - { error: { Length: > 0 } err } => throw new InvalidOperationException($"Failed to get add-ins for solution '{solutionFile}' (cf. inner exception for details).", new Exception(err)), - var result => GetConfigurationValue(result.output, "RemoteControlAddIns") - ?.Split(['\r', '\n', ';', ','], StringSplitOptions.RemoveEmptyEntries) + result = ProcessHelper.RunProcess("dotnet", $"build \"{solutionFile}\" --target:UnoDumpRemoteControlAddIns --verbosity detailed --framework \"{targetFramework}\" -nowarn:MSB4057"); + if (!string.IsNullOrWhiteSpace(result.error)) + { + if (_log.IsEnabled(LogLevel.Warning)) + { + _log.Log(LogLevel.Warning, new Exception(result.error), $"Failed to get add-ins for solution '{solutionFile}' for tfm {targetFramework} (cf. inner exception for details)."); + } + + continue; + } + + var addIns = GetConfigurationValue(result.output, "RemoteControlAddIns") + .SelectMany(tfms => tfms.Split(['\r', '\n', ';', ','], StringSplitOptions.RemoveEmptyEntries)) + .Select(tfm => tfm.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) - .ToImmutableList() ?? ImmutableList.Empty, - }; + .ToImmutableList(); + + if (!addIns.IsEmpty) + { + return addIns; + } + } + + if (_log.IsEnabled(LogLevel.Information)) + { + _log.Log(LogLevel.Information, $"Didn't find any add-ins for solution '{solutionFile}'."); + } + + return ImmutableList.Empty; + } - private static string? GetConfigurationValue(string msbuildResult, string nodeName) - => Regex.Match(msbuildResult, $"<{nodeName}>(?.*)") is { Success: true } match - ? match.Groups["value"].Value - : null; + private static IEnumerable GetConfigurationValue(string msbuildResult, string nodeName) + => Regex + .Matches(msbuildResult, $"<{nodeName}>(?.*)") + .Where(match => match.Success) + .Select(match => match.Groups["value"].Value); } diff --git a/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/AttributeDataExtensions.cs b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/AttributeDataExtensions.cs index 42438b49fecd..ccf139ef3fa2 100644 --- a/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/AttributeDataExtensions.cs +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/AttributeDataExtensions.cs @@ -11,7 +11,7 @@ internal static class AttributeDataExtensions public static object? TryCreate(this CustomAttributeData data, Type attribute) { - if (!data.AttributeType.FullName?.Equals(attribute.FullName, StringComparison.Ordinal) ?? false) + if ((!data.AttributeType.FullName?.Equals(attribute.FullName, StringComparison.Ordinal)) ?? true) { return null; } @@ -45,12 +45,12 @@ internal static class AttributeDataExtensions instance = ctor.Invoke(args); break; } - catch { } + catch { /* Nothing to do, lets try another constructor */ } } if (instance is null) { - return null; + return null; // Failed to find a valid constructor. } try diff --git a/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/ServiceAttribute.cs b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/ServiceAttribute.cs index 83ad1d6ea2a4..bddb48a9b226 100644 --- a/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/ServiceAttribute.cs +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/ServiceAttribute.cs @@ -4,19 +4,40 @@ namespace Uno.Extensions.DependencyInjection; +/// +/// Attribute to define a service registration in the service collection. +/// +/// Type of the contract (i.e. interface) implemented by the concrete type. +/// Concrete type to register in the service collection. [AttributeUsage(AttributeTargets.Assembly)] public class ServiceAttribute(Type contract, Type implementation) : Attribute { + /// + /// Creates a new instance of the class with only a concrete type (used as contract and implementation). + /// + /// Concrete type to register in the service collection. public ServiceAttribute(Type implementation) : this(implementation, implementation) { } + /// + /// Type of the contract (i.e. interface) implemented by the concrete type. + /// public Type Contract { get; } = contract; + /// + /// Concrete type to register in the service collection. + /// public Type Implementation { get; } = implementation; + /// + /// The lifetime of the service. + /// public ServiceLifetime LifeTime { get; set; } = ServiceLifetime.Singleton; + /// + /// Indicates if the service should be automatically initialized at startup. + /// public bool IsAutoInit { get; set; } } diff --git a/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/ServiceCollectionServiceExtensions.cs b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/ServiceCollectionServiceExtensions.cs index 5dd32185dc12..56b9b5bc04fa 100644 --- a/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/ServiceCollectionServiceExtensions.cs +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/ServiceCollectionServiceExtensions.cs @@ -6,12 +6,16 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Uno.UI.RemoteControl.Host.Extensibility; namespace Uno.Extensions.DependencyInjection; public static class ServiceCollectionServiceExtensions { + /// + /// Register services configured with the attribute from all loaded assemblies. + /// + /// The service collection on which services should be registered. + /// The service collection for fluent usage. public static IServiceCollection AddFromAttribute(this IServiceCollection svc) { var attribute = typeof(ServiceAttribute); @@ -45,14 +49,14 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken) if (this.Log().IsEnabled(LogLevel.Information)) { - this.Log().Log(LogLevel.Information, $"Successfully created an instance of {attr.Contract} (impl: {svc?.GetType()})"); + this.Log().Log(LogLevel.Information, $"Successfully created an instance of {attr.Contract} for auto-init (impl: {svc?.GetType()})"); } } catch (Exception error) { if (this.Log().IsEnabled(LogLevel.Error)) { - this.Log().Log(LogLevel.Error, error, $"Failed to create an instance of {attr.Contract}."); + this.Log().Log(LogLevel.Error, error, $"Failed to create an instance of {attr.Contract} for auto-init."); } } } diff --git a/src/Uno.UI.RemoteControl.Host/Uno.UI.RemoteControl.Host.csproj b/src/Uno.UI.RemoteControl.Host/Uno.UI.RemoteControl.Host.csproj index 7c8812c8d2a9..98062995f183 100644 --- a/src/Uno.UI.RemoteControl.Host/Uno.UI.RemoteControl.Host.csproj +++ b/src/Uno.UI.RemoteControl.Host/Uno.UI.RemoteControl.Host.csproj @@ -31,6 +31,7 @@ + diff --git a/src/Uno.UI.RemoteControl.VS/EntryPoint.cs b/src/Uno.UI.RemoteControl.VS/EntryPoint.cs index 546b6781c8a4..104bf01b1c7a 100644 --- a/src/Uno.UI.RemoteControl.VS/EntryPoint.cs +++ b/src/Uno.UI.RemoteControl.VS/EntryPoint.cs @@ -16,6 +16,7 @@ using Microsoft.Build.Evaluation; using Microsoft.Internal.VisualStudio.Shell; using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Threading; using Microsoft.VisualStudio.Shell.Interop; using Uno.UI.RemoteControl.Messaging.IdeChannel; using Uno.UI.RemoteControl.VS.DebuggerHelper; @@ -348,17 +349,20 @@ private async Task EnsureServerAsync() // Set the port to the projects // This LEGACY as port should be set through the global properties (cf. OnProvideGlobalPropertiesAsync) var portString = _remoteControlServerPort.ToString(CultureInfo.InvariantCulture); - foreach (var p in await _dte.GetProjectsAsync()) + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var projects = (await _dte.GetProjectsAsync()).ToArray(); // EnumerateProjects must be called on the UI thread. + await TaskScheduler.Default; + foreach (var project in projects) { var filename = string.Empty; try { - filename = p.FileName; + filename = project.FileName; } catch (Exception ex) { - _debugAction?.Invoke($"Exception on retrieving {p.UniqueName} details. Err: {ex}."); - _warningAction?.Invoke($"Cannot read {p.UniqueName} project details (It may be unloaded)."); + _debugAction?.Invoke($"Exception on retrieving {project.UniqueName} details. Err: {ex}."); + _warningAction?.Invoke($"Cannot read {project.UniqueName} project details (It may be unloaded)."); } if (string.IsNullOrWhiteSpace(filename) == false && GetMsbuildProject(filename) is Microsoft.Build.Evaluation.Project msbProject @@ -478,7 +482,7 @@ public void SetGlobalProperty(string projectFullName, string propertyName, strin } } - private static Microsoft.Build.Evaluation.Project GetMsbuildProject(string projectFullName) + private static Microsoft.Build.Evaluation.Project? GetMsbuildProject(string projectFullName) => ProjectCollection.GlobalProjectCollection.GetLoadedProjects(projectFullName).FirstOrDefault(); public void SetGlobalProperties(string projectFullName, IDictionary properties) diff --git a/src/Uno.UI.RemoteControl/buildTransitive/Uno.WinUI.DevServer.targets b/src/Uno.UI.RemoteControl/buildTransitive/Uno.WinUI.DevServer.targets index aa05b2b2aaa8..5a315ec583f6 100644 --- a/src/Uno.UI.RemoteControl/buildTransitive/Uno.WinUI.DevServer.targets +++ b/src/Uno.UI.RemoteControl/buildTransitive/Uno.WinUI.DevServer.targets @@ -31,6 +31,10 @@ + + + +