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 new file mode 100644 index 000000000000..9d170b757ef2 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/AddIns.cs @@ -0,0 +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 readonly ILogger _log = typeof(AddIns).Log(); + + public static IImmutableList Discover(string solutionFile) + { + // 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 ImmutableArray.Empty; + } + + + foreach (var targetFramework in targetFrameworks) + { + 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(); + + if (!addIns.IsEmpty) + { + return addIns; + } + } + + if (_log.IsEnabled(LogLevel.Information)) + { + _log.Log(LogLevel.Information, $"Didn't find any add-ins for solution '{solutionFile}'."); + } + + return ImmutableArray.Empty; + } + + 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/AddInsExtensions.cs b/src/Uno.UI.RemoteControl.Host/Extensibility/AddInsExtensions.cs new file mode 100644 index 000000000000..80491025e1b3 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/AddInsExtensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Hosting; +using Uno.Utils.DependencyInjection; +using Uno.UI.RemoteControl.Helpers; + +namespace Uno.UI.RemoteControl.Host.Extensibility; + +public static class AddInsExtensions +{ + public static IWebHostBuilder ConfigureAddIns(this IWebHostBuilder builder, string solutionFile) + { + AssemblyHelper.Load(AddIns.Discover(solutionFile), throwIfLoadFailed: true); + + return builder.ConfigureServices(svc => svc.AddFromAttribute()); + } +} diff --git a/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/AttributeDataExtensions.cs b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/AttributeDataExtensions.cs new file mode 100644 index 000000000000..b6c737f40eca --- /dev/null +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/AttributeDataExtensions.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace Uno.Utils.DependencyInjection; + +internal static class AttributeDataExtensions +{ + /// + /// Attempts to create an instance of the specified type from the provided . + /// + /// This offers the ability to a project to implements their own compatible version of the given type to reduce dependencies. + /// Data of an attribute. + /// An instance of if the provided was compatible, `null` otherwise. + public static TAttribute? TryCreate(this CustomAttributeData data) + => (TAttribute?)TryCreate(data, typeof(TAttribute)); + + /// + /// Attempts to create an instance of the specified type from the provided . + /// + /// This offers the ability to a project to implements their own compatible version of the given type to reduce dependencies. + /// Data of an attribute. + /// Type of the attribute to try to instantiate. + /// An instance of if the provided was compatible, `null` otherwise. + public static object? TryCreate(this CustomAttributeData data, Type attribute) + { + if ((!data.AttributeType.FullName?.Equals(attribute.FullName, StringComparison.Ordinal)) ?? true) + { + return null; + } + + var instance = default(object); + foreach (var ctor in attribute.GetConstructors()) + { + var parameters = ctor.GetParameters(); + var arguments = data.ConstructorArguments; + if (arguments.Count > parameters.Length + || arguments.Count < parameters.Count(p => !p.IsOptional)) + { + continue; + } + + var argumentsCompatible = true; + var args = new object?[parameters.Length]; + for (var i = 0; argumentsCompatible && i < arguments.Count; i++) + { + argumentsCompatible &= parameters[i].ParameterType == arguments[i].ArgumentType; + args[i] = arguments[i].Value; + } + + if (!argumentsCompatible) + { + continue; + } + + try + { + instance = ctor.Invoke(args); + break; + } + catch { /* Nothing to do, lets try another constructor */ } + } + + if (instance is null) + { + return null; // Failed to find a valid constructor. + } + + try + { + var properties = attribute + .GetProperties() + .Where(prop => prop.CanWrite) + .ToDictionary(prop => prop.Name, StringComparer.Ordinal); + var fields = attribute + .GetFields() + .Where(field => !field.IsInitOnly) + .ToDictionary(field => field.Name, StringComparer.Ordinal); + foreach (var member in data.NamedArguments) + { + if (member.IsField) + { + if (fields.TryGetValue(member.MemberName, out var field) + && field.FieldType.IsAssignableFrom(member.TypedValue.ArgumentType)) + { + field.SetValue(instance, member.TypedValue.Value); + } + else + { + return null; + } + } + else + { + if (properties.TryGetValue(member.MemberName, out var prop) + && prop.PropertyType.IsAssignableFrom(member.TypedValue.ArgumentType)) + { + prop.SetValue(instance, member.TypedValue.Value); + } + else + { + return null; + } + } + } + + return instance; + } + catch + { + return null; + } + } +} diff --git a/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceAttribute.cs b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceAttribute.cs new file mode 100644 index 000000000000..7c99d5d2f9c8 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceAttribute.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; + +namespace Uno.Utils.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.Utils.DependencyInjection/ServiceCollectionServiceExtensions.cs b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceCollectionServiceExtensions.cs new file mode 100644 index 000000000000..f0b44f9b539e --- /dev/null +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceCollectionServiceExtensions.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Uno.Extensions; + +namespace Uno.Utils.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); + var services = AppDomain + .CurrentDomain + .GetAssemblies() + .SelectMany(assembly => assembly.GetCustomAttributesData()) + .Select(attrData => attrData.TryCreate(attribute) as ServiceAttribute) + .Where(attr => attr is not null) + .ToImmutableList(); + + foreach (var service in services) + { + svc.Add(new ServiceDescriptor(service!.Contract, service.Implementation, service.LifeTime)); + } + svc.AddHostedService(s => new AutoInitService(s, services!)); + + return svc; + } + + private class AutoInitService(IServiceProvider services, IImmutableList types) : BackgroundService, IHostedService + { + /// + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + foreach (var attr in types.Where(attr => attr.IsAutoInit)) + { + try + { + var svc = services.GetService(attr.Contract); + + if (this.Log().IsEnabled(LogLevel.Information)) + { + 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} for auto-init."); + } + } + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Uno.UI.RemoteControl.Host/Helpers/AssemblyHelper.cs b/src/Uno.UI.RemoteControl.Host/Helpers/AssemblyHelper.cs new file mode 100644 index 000000000000..be3d40bf1acd --- /dev/null +++ b/src/Uno.UI.RemoteControl.Host/Helpers/AssemblyHelper.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.Logging; +using Uno.Extensions; + +namespace Uno.UI.RemoteControl.Helpers; + +public class AssemblyHelper +{ + private static readonly ILogger _log = typeof(AssemblyHelper).Log(); + + public static IImmutableList Load(IImmutableList dllFiles, bool throwIfLoadFailed = false) + { + var assemblies = ImmutableList.CreateBuilder(); + foreach (var dll in dllFiles.Distinct(StringComparer.OrdinalIgnoreCase)) + { + try + { + assemblies.Add(Assembly.LoadFrom(dll)); + } + catch (Exception err) + { + _log.Log(LogLevel.Error, $"Failed to load assembly '{dll}'.", err); + + if (throwIfLoadFailed) + { + throw; + } + } + } + + return assemblies.ToImmutable(); + } +} diff --git a/src/Uno.UI.RemoteControl.Host/Program.cs b/src/Uno.UI.RemoteControl.Host/Program.cs index eed62ebe6890..73cceab1804a 100644 --- a/src/Uno.UI.RemoteControl.Host/Program.cs +++ b/src/Uno.UI.RemoteControl.Host/Program.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.ComponentModel; using System.Threading.Tasks; +using Uno.UI.RemoteControl.Host.Extensibility; using Uno.UI.RemoteControl.Host.IdeChannel; namespace Uno.UI.RemoteControl.Host @@ -19,8 +20,10 @@ static async Task Main(string[] args) { var httpPort = 0; var parentPID = 0; + var solution = default(string); - var p = new OptionSet() { + var p = new OptionSet + { { "httpPort=", s => { if(!int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out httpPort)) @@ -36,6 +39,17 @@ static async Task Main(string[] args) throw new ArgumentException($"The parent process id parameter is invalid {s}"); } } + }, + { + "solution=", s => + { + if (string.IsNullOrWhiteSpace(s) || !File.Exists(s)) + { + throw new ArgumentException($"The provided solution path '{s}' does not exists"); + } + + solution = s; + } } }; @@ -66,6 +80,12 @@ static async Task Main(string[] args) services.AddSingleton(); }); + if (solution is not null) + { + // For backward compatibility, we allow to not have a solution file specified. + builder.ConfigureAddIns(solution); + } + var host = builder.Build(); host.Services.GetService(); 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.Server.Processors/Helpers/ProcessHelper.cs b/src/Uno.UI.RemoteControl.Server.Processors/Helpers/ProcessHelper.cs index 12829378af97..40ff4c45d8c0 100644 --- a/src/Uno.UI.RemoteControl.Server.Processors/Helpers/ProcessHelper.cs +++ b/src/Uno.UI.RemoteControl.Server.Processors/Helpers/ProcessHelper.cs @@ -7,7 +7,7 @@ using System.Runtime.InteropServices; using System.Text; -namespace Uno.UI.RemoteControl.Server.Processors.Helpers +namespace Uno.UI.RemoteControl.Helpers { internal class ProcessHelper { @@ -24,6 +24,8 @@ public static (int exitCode, string output, string error) RunProcess(string exec StartInfo = { UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, RedirectStandardOutput = true, RedirectStandardError = true, FileName = executable, 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 96703f88b4bd..67b25cace569 100644 --- a/src/Uno.UI.RemoteControl.Server.Processors/HotReload/MetadataUpdates/CompilationWorkspaceProvider.cs +++ b/src/Uno.UI.RemoteControl.Server.Processors/HotReload/MetadataUpdates/CompilationWorkspaceProvider.cs @@ -7,7 +7,7 @@ using System.IO; using System.Reflection; using Uno.Extensions; -using Uno.UI.RemoteControl.Server.Processors.Helpers; +using Uno.UI.RemoteControl.Helpers; using System.Collections.Generic; using Microsoft.Extensions.Logging; diff --git a/src/Uno.UI.RemoteControl.VS/EntryPoint.cs b/src/Uno.UI.RemoteControl.VS/EntryPoint.cs index 7ffec4817251..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; @@ -313,7 +314,7 @@ private async Task EnsureServerAsync() var pipeGuid = Guid.NewGuid(); var hostBinPath = Path.Combine(_toolsPath, "host", $"net{version}.0", "Uno.UI.RemoteControl.Host.dll"); - var 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}\" --solution \"{_dte.Solution.FullName}\""; var pi = new ProcessStartInfo("dotnet", arguments) { UseShellExecute = false, @@ -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 @@ + + + +