Skip to content

Commit

Permalink
feat(rc): Add support for metadata update
Browse files Browse the repository at this point in the history
  • Loading branch information
jeromelaban committed Nov 28, 2021
1 parent 1991d64 commit 8387262
Show file tree
Hide file tree
Showing 19 changed files with 943 additions and 125 deletions.
4 changes: 3 additions & 1 deletion src/Uno.UI.RemoteControl.Host/RemoteControlExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Uno.Extensions;

Expand Down Expand Up @@ -29,7 +31,7 @@ public static IApplicationBuilder UseRemoteControlServer(

try
{
using (var server = new RemoteControlServer())
using (var server = new RemoteControlServer(context.RequestServices.GetService<IConfiguration>()))
{
await server.Run(await context.WebSockets.AcceptWebSocketAsync(), CancellationToken.None);
}
Expand Down
36 changes: 26 additions & 10 deletions src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Uno.Extensions;
using Uno.UI.RemoteControl.Messages;
using System.Runtime.Loader;
using Microsoft.Extensions.Configuration;

namespace Uno.UI.RemoteControl.Host
{
Expand All @@ -22,10 +23,12 @@ internal class RemoteControlServer : IRemoteControlServer, IDisposable
private readonly Dictionary<string, IServerProcessor> _processors = new Dictionary<string, IServerProcessor>();

private WebSocket _socket;
private AssemblyLoadContext _loadContext;
private readonly IConfiguration _configuration;
private readonly AssemblyLoadContext _loadContext;

public RemoteControlServer()
public RemoteControlServer(IConfiguration configuration)
{
_configuration = configuration;
_loadContext = new AssemblyLoadContext(null, isCollectible: true);
_loadContext.Unloading += (e) => {
if (this.Log().IsEnabled(LogLevel.Debug))
Expand All @@ -40,6 +43,9 @@ public RemoteControlServer()
}
}

string IRemoteControlServer.GetServerConfiguration(string key)
=> _configuration[key];

private void RegisterProcessor(IServerProcessor hotReloadProcessor)
{
_processors[hotReloadProcessor.Scope] = hotReloadProcessor;
Expand Down Expand Up @@ -82,9 +88,11 @@ private void ProcessDiscoveryFrame(Frame frame)

var basePath = msg.BasePath.Replace('/', Path.DirectorySeparatorChar);

foreach (var file in Directory.GetFiles(basePath, "*.dll"))
var assemblies = new List<System.Reflection.Assembly>();

foreach (var file in Directory.GetFiles(basePath, "Uno.*.dll"))
{
if(Path.GetFileNameWithoutExtension(file).Equals(serverAssemblyName, StringComparison.OrdinalIgnoreCase))
if (Path.GetFileNameWithoutExtension(file).Equals(serverAssemblyName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
Expand All @@ -94,16 +102,24 @@ private void ProcessDiscoveryFrame(Frame frame)
this.Log().LogDebug($"Discovery: Loading {file}");
}

var asm = _loadContext.LoadFromAssemblyPath(file);
assemblies.Add(_loadContext.LoadFromAssemblyPath(file));
}

foreach(var processorType in asm.GetTypes().Where(t => t.GetInterfaces().Any(i => i == typeof(IServerProcessor))))
foreach(var asm in assemblies)
{
var attributes = asm.GetCustomAttributes(typeof(ServerProcessorAttribute), false);

foreach (var processorAttribute in attributes)
{
if (this.Log().IsEnabled(LogLevel.Debug))
if (processorAttribute is ServerProcessorAttribute processor)
{
this.Log().LogDebug($"Discovery: Registering {processorType}");
}
if (this.Log().IsEnabled(LogLevel.Debug))
{
this.Log().LogDebug($"Discovery: Registering {processor.ProcessorType}");
}

RegisterProcessor((IServerProcessor)Activator.CreateInstance(processorType, this));
RegisterProcessor((IServerProcessor)Activator.CreateInstance(processor.ProcessorType, this));
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>

Expand Down Expand Up @@ -36,7 +36,7 @@
<PropertyGroup>
<_baseNugetPath Condition="'$(USERPROFILE)'!=''">$(USERPROFILE)</_baseNugetPath>
<_baseNugetPath Condition="'$(HOME)'!=''">$(HOME)</_baseNugetPath>
<_TargetNugetFolder>$(_baseNugetPath)\.nuget\packages\Uno.UI\$(UnoNugetOverrideVersion)\tools\rc\host</_TargetNugetFolder>
<_TargetNugetFolder>$(_baseNugetPath)\.nuget\packages\Uno.UI.RemoteControl\$(UnoNugetOverrideVersion)\tools\rc\host</_TargetNugetFolder>
</PropertyGroup>
<ItemGroup>
<_OutputFiles Include="$(TargetDir)*.*" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.MSBuild;
using System.Linq;
using System.Threading;
using System;
using System.IO;
using System.Reflection;
using Uno.Extensions;

namespace Uno.UI.RemoteControl.Host.HotReload
{
internal static class CompilationWorkspaceProvider
{
private static string MSBuildBasePath;

public static Task<(Solution, WatchHotReloadService)> CreateWorkspaceAsync(string projectPath, IReporter reporter, CancellationToken cancellationToken)
{
var taskCompletionSource = new TaskCompletionSource<(Solution, WatchHotReloadService)>(TaskCreationOptions.RunContinuationsAsynchronously);
CreateProject(taskCompletionSource, projectPath, reporter, cancellationToken);

return taskCompletionSource.Task;
}

static async void CreateProject(TaskCompletionSource<(Solution, WatchHotReloadService)> taskCompletionSource, string projectPath, IReporter reporter, CancellationToken cancellationToken)
{
var workspace = MSBuildWorkspace.Create();

workspace.WorkspaceFailed += (_sender, diag) =>
{
if (diag.Diagnostic.Kind == WorkspaceDiagnosticKind.Warning)
{
reporter.Verbose($"MSBuildWorkspace warning: {diag.Diagnostic}");
}
else
{
if (!diag.Diagnostic.ToString().StartsWith("[Failure] Found invalid data while decoding"))
{
taskCompletionSource.TrySetException(new InvalidOperationException($"Failed to create MSBuildWorkspace: {diag.Diagnostic}"));
}
}
};

await workspace.OpenProjectAsync(projectPath, cancellationToken: cancellationToken);
var currentSolution = workspace.CurrentSolution;
var hotReloadService = new WatchHotReloadService(workspace.Services);
await hotReloadService.StartSessionAsync(currentSolution, cancellationToken);

// Read the documents to memory
await Task.WhenAll(
currentSolution.Projects.SelectMany(p => p.Documents.Concat(p.AdditionalDocuments)).Select(d => d.GetTextAsync(cancellationToken)));

// Warm up the compilation. This would help make the deltas for first edit appear much more quickly
foreach (var project in currentSolution.Projects)
{
await project.GetCompilationAsync(cancellationToken);
}

taskCompletionSource.TrySetResult((currentSolution, hotReloadService));
}

public static void InitializeRoslyn()
{
RegisterAssemblyLoader();

var pi = new System.Diagnostics.ProcessStartInfo(
"cmd.exe",
@"/c ""C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"" -property installationPath"
)
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};

var process = System.Diagnostics.Process.Start(pi);
process.WaitForExit();
var installPath = process.StandardOutput.ReadToEnd().Split('\r').First();

SetupMSBuildLookupPath(installPath);
}

private static void SetupMSBuildLookupPath(string installPath)
{
Environment.SetEnvironmentVariable("VSINSTALLDIR", installPath);
Environment.SetEnvironmentVariable("MSBuildSDKsPath", @"C:\Program Files\dotnet\sdk\6.0.100-rc.2.21505.57\Sdks");

bool MSBuildExists() => File.Exists(Path.Combine(MSBuildBasePath, "Microsoft.Build.dll"));

MSBuildBasePath = @"C:\Program Files\dotnet\sdk\6.0.100-rc.2.21505.57";

if (!MSBuildExists())
{
MSBuildBasePath = Path.Combine(installPath, "MSBuild\\Current\\Bin");
if (!MSBuildExists())
{
throw new InvalidOperationException($"Invalid Visual studio installation (Cannot find Microsoft.Build.dll)");
}
}
}


private static void RegisterAssemblyLoader()
{
// Force assembly loader to consider siblings, when running in a separate appdomain.
ResolveEventHandler localResolve = (s, e) =>
{
if (e.Name == "Mono.Runtime")
{
// Roslyn 2.0 and later checks for the presence of the Mono runtime
// through this check.
return null;
}

var assembly = new AssemblyName(e.Name);
var basePath = Path.GetDirectoryName(new Uri(typeof(CompilationWorkspaceProvider).Assembly.CodeBase).LocalPath);

Console.WriteLine($"Searching for [{assembly}] from [{basePath}]");

// Ignore resource assemblies for now, we'll have to adjust this
// when adding globalization.
if (assembly.Name.EndsWith(".resources"))
{
return null;
}

// Lookup for the highest version matching assembly in the current app domain.
// There may be an existing one that already matches, even though the
// fusion loader did not find an exact match.
var loadedAsm = (
from asm in AppDomain.CurrentDomain.GetAssemblies()
where asm.GetName().Name == assembly.Name
orderby asm.GetName().Version descending
select asm
).ToArray();

if (loadedAsm.Length > 1)
{
var duplicates = loadedAsm
.Skip(1)
.Where(a => a.GetName().Version == loadedAsm[0].GetName().Version)
.ToArray();

if (duplicates.Length != 0)
{
Console.WriteLine($"Selecting first occurrence of assembly [{e.Name}] which can be found at [{duplicates.Select(d => d.CodeBase).JoinBy("; ")}]");
}

return loadedAsm[0];
}
else if (loadedAsm.Length == 1)
{
return loadedAsm[0];
}

Assembly LoadAssembly(string filePath)
{
if (File.Exists(filePath))
{
try
{
var output = Assembly.LoadFrom(filePath);

Console.WriteLine($"Loaded [{output.GetName()}] from [{output.CodeBase}]");

return output;
}
catch (Exception ex)
{
Console.WriteLine($"Failed to load [{assembly}] from [{filePath}]", ex);
return null;
}
}
else
{
return null;
}
}

var paths = new[] {
Path.Combine(basePath, assembly.Name + ".dll"),
Path.Combine(MSBuildBasePath, assembly.Name + ".dll"),
};

return paths
.Select(LoadAssembly)
.Where(p => p != null)
.FirstOrDefault();
};

AppDomain.CurrentDomain.AssemblyResolve += localResolve;
AppDomain.CurrentDomain.TypeResolve += localResolve;
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;

namespace Uno.UI.RemoteControl.Host.HotReload
{
interface IDeltaApplier : IDisposable
{
ValueTask InitializeAsync(CancellationToken cancellationToken);

ValueTask<bool> Apply(string changedFile, ImmutableArray<WatchHotReloadService.Update> solutionUpdate, CancellationToken cancellationToken);

ValueTask ReportDiagnosticsAsync(IEnumerable<string> diagnostics, CancellationToken cancellationToken);
}
}
14 changes: 14 additions & 0 deletions src/Uno.UI.RemoteControl.Server.Processors/HotReload/IReporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Uno.UI.RemoteControl.Host.HotReload
{
/// <summary>
/// This API supports infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public interface IReporter
{
void Verbose(string message);
void Output(string message);
void Warn(string message);
void Error(string message);
}
}
12 changes: 12 additions & 0 deletions src/Uno.UI.RemoteControl.Server.Processors/HotReload/Reporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.Build.Tasks;

namespace Uno.UI.RemoteControl.Host.HotReload
{
internal class Reporter : IReporter
{
public void Error(string message) => System.Console.WriteLine($"[Error] {message}");
public void Output(string message) => System.Console.WriteLine($"[Output] {message}");
public void Verbose(string message) => System.Console.WriteLine($"[Verbose] {message}");
public void Warn(string message) => System.Console.WriteLine($"[Warn] {message}");
}
}
Loading

0 comments on commit 8387262

Please sign in to comment.