-
Notifications
You must be signed in to change notification settings - Fork 730
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add extensiblity support in dev-server
- Loading branch information
Showing
10 changed files
with
291 additions
and
4 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string> 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<string>.Empty, | ||
}; | ||
|
||
private static string? GetConfigurationValue(string msbuildResult, string nodeName) | ||
=> Regex.Match(msbuildResult, $"<{nodeName}>(?<value>.*)</{nodeName}>") is { Success: true } match | ||
? match.Groups["value"].Value | ||
: null; | ||
} |
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.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()); | ||
} | ||
} |
101 changes: 101 additions & 0 deletions
101
...eControl.Host/Extensibility/Uno.Extensions.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,101 @@ | ||
using System; | ||
using System.Linq; | ||
using System.Reflection; | ||
|
||
namespace Uno.Extensions.DependencyInjection; | ||
|
||
internal static class AttributeDataExtensions | ||
{ | ||
public static TAttribute? TryCreate<TAttribute>(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; | ||
} | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
...I.RemoteControl.Host/Extensibility/Uno.Extensions.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,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; } | ||
} |
63 changes: 63 additions & 0 deletions
63
...st/Extensibility/Uno.Extensions.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,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<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} (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; | ||
} | ||
} | ||
} |
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(); | ||
} | ||
} |
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
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