-
Notifications
You must be signed in to change notification settings - Fork 742
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #18355 from unoplatform/dev/dr/devSrvAddIn
feat: Add extensiblity support in dev-server
- Loading branch information
Showing
13 changed files
with
401 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string> 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<string>.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<string>.Empty; | ||
} | ||
|
||
private static IEnumerable<string> GetConfigurationValue(string msbuildResult, string nodeName) | ||
=> Regex | ||
.Matches(msbuildResult, $"<{nodeName}>(?<value>.*)</{nodeName}>") | ||
.Where(match => match.Success) | ||
.Select(match => match.Groups["value"].Value); | ||
} |
17 changes: 17 additions & 0 deletions
17
src/Uno.UI.RemoteControl.Host/Extensibility/AddInsExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()); | ||
} | ||
} |
114 changes: 114 additions & 0 deletions
114
...RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/AttributeDataExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
using System; | ||
using System.Linq; | ||
using System.Reflection; | ||
|
||
namespace Uno.Utils.DependencyInjection; | ||
|
||
internal static class AttributeDataExtensions | ||
{ | ||
/// <summary> | ||
/// Attempts to create an instance of the specified <typeparamref name="TAttribute"/> type from the provided <see cref="CustomAttributeData"/>. | ||
/// </summary> | ||
/// <remarks>This offers the ability to a project to implements their own compatible version of the given <typeparamref name="TAttribute"/> type to reduce dependencies.</remarks> | ||
/// <param name="data">Data of an attribute.</param> | ||
/// <returns>An instance of <typeparamref name="TAttribute"/> if the provided <paramref name="data"/> was compatible, `null` otherwise.</returns> | ||
public static TAttribute? TryCreate<TAttribute>(this CustomAttributeData data) | ||
=> (TAttribute?)TryCreate(data, typeof(TAttribute)); | ||
|
||
/// <summary> | ||
/// Attempts to create an instance of the specified <paramref name="attribute"/> type from the provided <see cref="CustomAttributeData"/>. | ||
/// </summary> | ||
/// <remarks>This offers the ability to a project to implements their own compatible version of the given <paramref name="attribute"/> type to reduce dependencies.</remarks> | ||
/// <param name="data">Data of an attribute.</param> | ||
/// <param name="attribute">Type of the attribute to try to instantiate.</param> | ||
/// <returns>An instance of <paramref name="attribute"/> if the provided <paramref name="data"/> was compatible, `null` otherwise.</returns> | ||
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; | ||
} | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
...Uno.UI.RemoteControl.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceAttribute.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
using System; | ||
using System.Linq; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace Uno.Utils.DependencyInjection; | ||
|
||
/// <summary> | ||
/// Attribute to define a service registration in the service collection. | ||
/// </summary> | ||
/// <param name="contract">Type of the contract (i.e. interface) implemented by the concrete <see cref="Implementation"/> type.</param> | ||
/// <param name="implementation">Concrete type to register in the service collection.</param> | ||
[AttributeUsage(AttributeTargets.Assembly)] | ||
public class ServiceAttribute(Type contract, Type implementation) : Attribute | ||
{ | ||
/// <summary> | ||
/// Creates a new instance of the <see cref="ServiceAttribute"/> class with only a concrete type (used as contract and implementation). | ||
/// </summary> | ||
/// <param name="implementation">Concrete type to register in the service collection.</param> | ||
public ServiceAttribute(Type implementation) | ||
: this(implementation, implementation) | ||
{ | ||
} | ||
|
||
/// <summary> | ||
/// Type of the contract (i.e. interface) implemented by the concrete <see cref="Implementation"/> type. | ||
/// </summary> | ||
public Type Contract { get; } = contract; | ||
|
||
/// <summary> | ||
/// Concrete type to register in the service collection. | ||
/// </summary> | ||
public Type Implementation { get; } = implementation; | ||
|
||
/// <summary> | ||
/// The lifetime of the service. | ||
/// </summary> | ||
public ServiceLifetime LifeTime { get; set; } = ServiceLifetime.Singleton; | ||
|
||
/// <summary> | ||
/// Indicates if the service should be automatically initialized at startup. | ||
/// </summary> | ||
public bool IsAutoInit { get; set; } | ||
} |
68 changes: 68 additions & 0 deletions
68
...ol.Host/Extensibility/Uno.Utils.DependencyInjection/ServiceCollectionServiceExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
{ | ||
/// <summary> | ||
/// Register services configured with the <see cref="ServiceAttribute"/> attribute from all loaded assemblies. | ||
/// </summary> | ||
/// <param name="svc">The service collection on which services should be registered.</param> | ||
/// <returns>The service collection for fluent usage.</returns> | ||
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<ServiceAttribute> types) : BackgroundService, IHostedService | ||
{ | ||
/// <inheritdoc /> | ||
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Assembly> Load(IImmutableList<string> dllFiles, bool throwIfLoadFailed = false) | ||
{ | ||
var assemblies = ImmutableList.CreateBuilder<Assembly>(); | ||
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(); | ||
} | ||
} |
Oops, something went wrong.