Skip to content

Commit

Permalink
feat: Add extensiblity support in dev-server
Browse files Browse the repository at this point in the history
  • Loading branch information
dr1rrb committed Oct 4, 2024
1 parent 7474c70 commit 753965d
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 4 deletions.
26 changes: 26 additions & 0 deletions src/Uno.UI.RemoteControl.Host/Extensibility/AddIns.cs
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 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.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());
}
}
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;
}
}
}
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; }
}
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;
}
}
}
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();
}
}
22 changes: 21 additions & 1 deletion src/Uno.UI.RemoteControl.Host/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Expand All @@ -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;
}
}
};

Expand Down Expand Up @@ -66,6 +80,12 @@ static async Task Main(string[] args)
services.AddSingleton<IIdeChannelServerProvider, IdeChannelServerProvider>();
});

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<IIdeChannelServerProvider>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/Uno.UI.RemoteControl.VS/EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 753965d

Please sign in to comment.