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..38cfab4f27c8 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/AddIns.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; +using Uno.UI.RemoteControl.Helpers; + +namespace Uno.UI.RemoteControl.Host.Extensibility; + +public class AddIns +{ + public static IImmutableList Discover(string solutionFile) + => ProcessHelper.RunProcess("dotnet", $"build \"{solutionFile}\" /t:GetRemoteControlAddIns /nowarn:MSB4057") switch // Ignore missing target + { + // 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) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToImmutableList() ?? ImmutableList.Empty, + }; + + private static string? GetConfigurationValue(string msbuildResult, string nodeName) + => Regex.Match(msbuildResult, $"<{nodeName}>(?.*)") is { Success: true } match + ? match.Groups["value"].Value + : null; +} 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..461340b9480e --- /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.Extensions.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.Extensions.DependencyInjection/AttributeDataExtensions.cs b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/AttributeDataExtensions.cs new file mode 100644 index 000000000000..42438b49fecd --- /dev/null +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/AttributeDataExtensions.cs @@ -0,0 +1,101 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace Uno.Extensions.DependencyInjection; + +internal static class AttributeDataExtensions +{ + public static TAttribute? TryCreate(this CustomAttributeData data) + => (TAttribute?)TryCreate(data, typeof(TAttribute)); + + public static object? TryCreate(this CustomAttributeData data, Type attribute) + { + if (!data.AttributeType.FullName?.Equals(attribute.FullName, StringComparison.Ordinal) ?? false) + { + 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 { } + } + + if (instance is null) + { + return null; + } + + 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.Extensions.DependencyInjection/ServiceAttribute.cs b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/ServiceAttribute.cs new file mode 100644 index 000000000000..83ad1d6ea2a4 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/ServiceAttribute.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; + +namespace Uno.Extensions.DependencyInjection; + +[AttributeUsage(AttributeTargets.Assembly)] +public class ServiceAttribute(Type contract, Type implementation) : Attribute +{ + public ServiceAttribute(Type implementation) + : this(implementation, implementation) + { + } + + public Type Contract { get; } = contract; + + public Type Implementation { get; } = implementation; + + public ServiceLifetime LifeTime { get; set; } = ServiceLifetime.Singleton; + + 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 new file mode 100644 index 000000000000..5dd32185dc12 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Host/Extensibility/Uno.Extensions.DependencyInjection/ServiceCollectionServiceExtensions.cs @@ -0,0 +1,63 @@ +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.UI.RemoteControl.Host.Extensibility; + +namespace Uno.Extensions.DependencyInjection; + +public static class ServiceCollectionServiceExtensions +{ + 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} (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}."); + } + } + } + + 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.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..546b6781c8a4 100644 --- a/src/Uno.UI.RemoteControl.VS/EntryPoint.cs +++ b/src/Uno.UI.RemoteControl.VS/EntryPoint.cs @@ -313,7 +313,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,