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}>(?.*){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 @@
+
+
+
+