Skip to content

Commit

Permalink
feat: Add discovery of dev-server add-ins
Browse files Browse the repository at this point in the history
  • Loading branch information
dr1rrb committed Oct 4, 2024
1 parent 753965d commit d73066d
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 22 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>
71 changes: 60 additions & 11 deletions src/Uno.UI.RemoteControl.Host/Extensibility/AddIns.cs
Original file line number Diff line number Diff line change
@@ -1,26 +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 ILogger _log = typeof(AddIns).Log();

public static IImmutableList<string> Discover(string solutionFile)
=> ProcessHelper.RunProcess("dotnet", $"build \"{solutionFile}\" /t:GetRemoteControlAddIns /nowarn:MSB4057") switch // Ignore missing target
{
// 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 new ImmutableArray<string>();
}


foreach (var targetFramework in targetFrameworks)
{
// 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)
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() ?? ImmutableList<string>.Empty,
};
.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 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;
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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal static class AttributeDataExtensions

public static object? TryCreate(this CustomAttributeData data, Type attribute)
{
if (!data.AttributeType.FullName?.Equals(attribute.FullName, StringComparison.Ordinal) ?? false)
if ((!data.AttributeType.FullName?.Equals(attribute.FullName, StringComparison.Ordinal)) ?? true)
{
return null;
}
Expand Down Expand Up @@ -45,12 +45,12 @@ internal static class AttributeDataExtensions
instance = ctor.Invoke(args);
break;
}
catch { }
catch { /* Nothing to do, lets try another constructor */ }
}

if (instance is null)
{
return null;
return null; // Failed to find a valid constructor.
}

try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,40 @@

namespace Uno.Extensions.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
Expand Up @@ -6,12 +6,16 @@
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
{
/// <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);
Expand Down Expand Up @@ -45,14 +49,14 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken)

if (this.Log().IsEnabled(LogLevel.Information))
{
this.Log().Log(LogLevel.Information, $"Successfully created an instance of {attr.Contract} (impl: {svc?.GetType()})");
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}.");
this.Log().Log(LogLevel.Error, error, $"Failed to create an instance of {attr.Contract} for auto-init.");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

<ItemGroup>
<Compile Include="..\Uno.UI.RemoteControl\Helpers\**\*.cs" Link="Helpers/%(Filename)" />
<Compile Include="..\Uno.UI.RemoteControl.Server.Processors\Helpers\**\*.cs" Link="Helpers/%(Filename)" />
</ItemGroup>

<ItemGroup>
Expand Down
14 changes: 9 additions & 5 deletions src/Uno.UI.RemoteControl.VS/EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<string, string> properties)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
<Message Text="&lt;IsRCClientRemote&gt;$(_IsRCClientRemote)&lt;/IsRCClientRemote&gt;" Importance="High" />
</Target>

<Target Name="GetRemoteControlAddIns">
<Message Text="&lt;RemoteControlAddIns&gt;@(UnoRemoteControlAddIns)&lt;/RemoteControlAddIns&gt;" Importance="High" />
</Target>

<!-- .NET 7 and earlier compatibility for .NET8 msbuild CLI `getProperty` equivalent -->
<Target Name="UnoVSCodeGetProjectProperties">
<Message Text='{%0a "Properties": {%0a "TargetFramework": "$(TargetFramework)",' Importance="High" />
Expand Down

0 comments on commit d73066d

Please sign in to comment.