Skip to content

Commit

Permalink
Merge pull request #18355 from unoplatform/dev/dr/devSrvAddIn
Browse files Browse the repository at this point in the history
feat: Add extensiblity support in dev-server
  • Loading branch information
dr1rrb authored Oct 8, 2024
2 parents 969e990 + 8904cbd commit bec15ba
Show file tree
Hide file tree
Showing 13 changed files with 401 additions and 9 deletions.
8 changes: 8 additions & 0 deletions src/Uno.Sdk/targets/Uno.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@

</Target>

<Target Name="UnoDumpTargetFrameworks">
<!--
This target is used to dump the target frameworks to the output window.
It is useful to determine all target frameworks used by all projects of a solution.
-->
<Message Text="&lt;TargetFrameworks&gt;$(TargetFramework);$(TargetFrameworks)&lt;/TargetFrameworks&gt;" Importance="High" />
</Target>

<Import Project="Uno.SingleProject.VS.Build.targets"
Condition=" '$(UnoSingleProject)' == 'true' AND '$(BuildingInsideVisualStudio)' == 'true' " />
</Project>
75 changes: 75 additions & 0 deletions src/Uno.UI.RemoteControl.Host/Extensibility/AddIns.cs
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 src/Uno.UI.RemoteControl.Host/Extensibility/AddInsExtensions.cs
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());
}
}
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;
}
}
}
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; }
}
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;
}
}
}
36 changes: 36 additions & 0 deletions src/Uno.UI.RemoteControl.Host/Helpers/AssemblyHelper.cs
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();
}
}
Loading

0 comments on commit bec15ba

Please sign in to comment.