Skip to content

Commit 8387262

Browse files
committed
feat(rc): Add support for metadata update
1 parent 1991d64 commit 8387262

19 files changed

+943
-125
lines changed

src/Uno.UI.RemoteControl.Host/RemoteControlExtensions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System.Threading;
22
using Microsoft.AspNetCore.Builder;
33
using Microsoft.AspNetCore.Routing;
4+
using Microsoft.Extensions.Configuration;
5+
using Microsoft.Extensions.DependencyInjection;
46
using Microsoft.Extensions.Logging;
57
using Uno.Extensions;
68

@@ -29,7 +31,7 @@ public static IApplicationBuilder UseRemoteControlServer(
2931

3032
try
3133
{
32-
using (var server = new RemoteControlServer())
34+
using (var server = new RemoteControlServer(context.RequestServices.GetService<IConfiguration>()))
3335
{
3436
await server.Run(await context.WebSockets.AcceptWebSocketAsync(), CancellationToken.None);
3537
}

src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Uno.Extensions;
1515
using Uno.UI.RemoteControl.Messages;
1616
using System.Runtime.Loader;
17+
using Microsoft.Extensions.Configuration;
1718

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

2425
private WebSocket _socket;
25-
private AssemblyLoadContext _loadContext;
26+
private readonly IConfiguration _configuration;
27+
private readonly AssemblyLoadContext _loadContext;
2628

27-
public RemoteControlServer()
29+
public RemoteControlServer(IConfiguration configuration)
2830
{
31+
_configuration = configuration;
2932
_loadContext = new AssemblyLoadContext(null, isCollectible: true);
3033
_loadContext.Unloading += (e) => {
3134
if (this.Log().IsEnabled(LogLevel.Debug))
@@ -40,6 +43,9 @@ public RemoteControlServer()
4043
}
4144
}
4245

46+
string IRemoteControlServer.GetServerConfiguration(string key)
47+
=> _configuration[key];
48+
4349
private void RegisterProcessor(IServerProcessor hotReloadProcessor)
4450
{
4551
_processors[hotReloadProcessor.Scope] = hotReloadProcessor;
@@ -82,9 +88,11 @@ private void ProcessDiscoveryFrame(Frame frame)
8288

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

85-
foreach (var file in Directory.GetFiles(basePath, "*.dll"))
91+
var assemblies = new List<System.Reflection.Assembly>();
92+
93+
foreach (var file in Directory.GetFiles(basePath, "Uno.*.dll"))
8694
{
87-
if(Path.GetFileNameWithoutExtension(file).Equals(serverAssemblyName, StringComparison.OrdinalIgnoreCase))
95+
if (Path.GetFileNameWithoutExtension(file).Equals(serverAssemblyName, StringComparison.OrdinalIgnoreCase))
8896
{
8997
continue;
9098
}
@@ -94,16 +102,24 @@ private void ProcessDiscoveryFrame(Frame frame)
94102
this.Log().LogDebug($"Discovery: Loading {file}");
95103
}
96104

97-
var asm = _loadContext.LoadFromAssemblyPath(file);
105+
assemblies.Add(_loadContext.LoadFromAssemblyPath(file));
106+
}
98107

99-
foreach(var processorType in asm.GetTypes().Where(t => t.GetInterfaces().Any(i => i == typeof(IServerProcessor))))
108+
foreach(var asm in assemblies)
109+
{
110+
var attributes = asm.GetCustomAttributes(typeof(ServerProcessorAttribute), false);
111+
112+
foreach (var processorAttribute in attributes)
100113
{
101-
if (this.Log().IsEnabled(LogLevel.Debug))
114+
if (processorAttribute is ServerProcessorAttribute processor)
102115
{
103-
this.Log().LogDebug($"Discovery: Registering {processorType}");
104-
}
116+
if (this.Log().IsEnabled(LogLevel.Debug))
117+
{
118+
this.Log().LogDebug($"Discovery: Registering {processor.ProcessorType}");
119+
}
105120

106-
RegisterProcessor((IServerProcessor)Activator.CreateInstance(processorType, this));
121+
RegisterProcessor((IServerProcessor)Activator.CreateInstance(processor.ProcessorType, this));
122+
}
107123
}
108124
}
109125
}

src/Uno.UI.RemoteControl.Host/Uno.UI.RemoteControl.Host.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>netcoreapp3.1</TargetFramework>
5+
<TargetFramework>net6.0</TargetFramework>
66
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
77
</PropertyGroup>
88

@@ -36,7 +36,7 @@
3636
<PropertyGroup>
3737
<_baseNugetPath Condition="'$(USERPROFILE)'!=''">$(USERPROFILE)</_baseNugetPath>
3838
<_baseNugetPath Condition="'$(HOME)'!=''">$(HOME)</_baseNugetPath>
39-
<_TargetNugetFolder>$(_baseNugetPath)\.nuget\packages\Uno.UI\$(UnoNugetOverrideVersion)\tools\rc\host</_TargetNugetFolder>
39+
<_TargetNugetFolder>$(_baseNugetPath)\.nuget\packages\Uno.UI.RemoteControl\$(UnoNugetOverrideVersion)\tools\rc\host</_TargetNugetFolder>
4040
</PropertyGroup>
4141
<ItemGroup>
4242
<_OutputFiles Include="$(TargetDir)*.*" />
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
using System.Threading.Tasks;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.MSBuild;
4+
using System.Linq;
5+
using System.Threading;
6+
using System;
7+
using System.IO;
8+
using System.Reflection;
9+
using Uno.Extensions;
10+
11+
namespace Uno.UI.RemoteControl.Host.HotReload
12+
{
13+
internal static class CompilationWorkspaceProvider
14+
{
15+
private static string MSBuildBasePath;
16+
17+
public static Task<(Solution, WatchHotReloadService)> CreateWorkspaceAsync(string projectPath, IReporter reporter, CancellationToken cancellationToken)
18+
{
19+
var taskCompletionSource = new TaskCompletionSource<(Solution, WatchHotReloadService)>(TaskCreationOptions.RunContinuationsAsynchronously);
20+
CreateProject(taskCompletionSource, projectPath, reporter, cancellationToken);
21+
22+
return taskCompletionSource.Task;
23+
}
24+
25+
static async void CreateProject(TaskCompletionSource<(Solution, WatchHotReloadService)> taskCompletionSource, string projectPath, IReporter reporter, CancellationToken cancellationToken)
26+
{
27+
var workspace = MSBuildWorkspace.Create();
28+
29+
workspace.WorkspaceFailed += (_sender, diag) =>
30+
{
31+
if (diag.Diagnostic.Kind == WorkspaceDiagnosticKind.Warning)
32+
{
33+
reporter.Verbose($"MSBuildWorkspace warning: {diag.Diagnostic}");
34+
}
35+
else
36+
{
37+
if (!diag.Diagnostic.ToString().StartsWith("[Failure] Found invalid data while decoding"))
38+
{
39+
taskCompletionSource.TrySetException(new InvalidOperationException($"Failed to create MSBuildWorkspace: {diag.Diagnostic}"));
40+
}
41+
}
42+
};
43+
44+
await workspace.OpenProjectAsync(projectPath, cancellationToken: cancellationToken);
45+
var currentSolution = workspace.CurrentSolution;
46+
var hotReloadService = new WatchHotReloadService(workspace.Services);
47+
await hotReloadService.StartSessionAsync(currentSolution, cancellationToken);
48+
49+
// Read the documents to memory
50+
await Task.WhenAll(
51+
currentSolution.Projects.SelectMany(p => p.Documents.Concat(p.AdditionalDocuments)).Select(d => d.GetTextAsync(cancellationToken)));
52+
53+
// Warm up the compilation. This would help make the deltas for first edit appear much more quickly
54+
foreach (var project in currentSolution.Projects)
55+
{
56+
await project.GetCompilationAsync(cancellationToken);
57+
}
58+
59+
taskCompletionSource.TrySetResult((currentSolution, hotReloadService));
60+
}
61+
62+
public static void InitializeRoslyn()
63+
{
64+
RegisterAssemblyLoader();
65+
66+
var pi = new System.Diagnostics.ProcessStartInfo(
67+
"cmd.exe",
68+
@"/c ""C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"" -property installationPath"
69+
)
70+
{
71+
RedirectStandardOutput = true,
72+
UseShellExecute = false,
73+
CreateNoWindow = true
74+
};
75+
76+
var process = System.Diagnostics.Process.Start(pi);
77+
process.WaitForExit();
78+
var installPath = process.StandardOutput.ReadToEnd().Split('\r').First();
79+
80+
SetupMSBuildLookupPath(installPath);
81+
}
82+
83+
private static void SetupMSBuildLookupPath(string installPath)
84+
{
85+
Environment.SetEnvironmentVariable("VSINSTALLDIR", installPath);
86+
Environment.SetEnvironmentVariable("MSBuildSDKsPath", @"C:\Program Files\dotnet\sdk\6.0.100-rc.2.21505.57\Sdks");
87+
88+
bool MSBuildExists() => File.Exists(Path.Combine(MSBuildBasePath, "Microsoft.Build.dll"));
89+
90+
MSBuildBasePath = @"C:\Program Files\dotnet\sdk\6.0.100-rc.2.21505.57";
91+
92+
if (!MSBuildExists())
93+
{
94+
MSBuildBasePath = Path.Combine(installPath, "MSBuild\\Current\\Bin");
95+
if (!MSBuildExists())
96+
{
97+
throw new InvalidOperationException($"Invalid Visual studio installation (Cannot find Microsoft.Build.dll)");
98+
}
99+
}
100+
}
101+
102+
103+
private static void RegisterAssemblyLoader()
104+
{
105+
// Force assembly loader to consider siblings, when running in a separate appdomain.
106+
ResolveEventHandler localResolve = (s, e) =>
107+
{
108+
if (e.Name == "Mono.Runtime")
109+
{
110+
// Roslyn 2.0 and later checks for the presence of the Mono runtime
111+
// through this check.
112+
return null;
113+
}
114+
115+
var assembly = new AssemblyName(e.Name);
116+
var basePath = Path.GetDirectoryName(new Uri(typeof(CompilationWorkspaceProvider).Assembly.CodeBase).LocalPath);
117+
118+
Console.WriteLine($"Searching for [{assembly}] from [{basePath}]");
119+
120+
// Ignore resource assemblies for now, we'll have to adjust this
121+
// when adding globalization.
122+
if (assembly.Name.EndsWith(".resources"))
123+
{
124+
return null;
125+
}
126+
127+
// Lookup for the highest version matching assembly in the current app domain.
128+
// There may be an existing one that already matches, even though the
129+
// fusion loader did not find an exact match.
130+
var loadedAsm = (
131+
from asm in AppDomain.CurrentDomain.GetAssemblies()
132+
where asm.GetName().Name == assembly.Name
133+
orderby asm.GetName().Version descending
134+
select asm
135+
).ToArray();
136+
137+
if (loadedAsm.Length > 1)
138+
{
139+
var duplicates = loadedAsm
140+
.Skip(1)
141+
.Where(a => a.GetName().Version == loadedAsm[0].GetName().Version)
142+
.ToArray();
143+
144+
if (duplicates.Length != 0)
145+
{
146+
Console.WriteLine($"Selecting first occurrence of assembly [{e.Name}] which can be found at [{duplicates.Select(d => d.CodeBase).JoinBy("; ")}]");
147+
}
148+
149+
return loadedAsm[0];
150+
}
151+
else if (loadedAsm.Length == 1)
152+
{
153+
return loadedAsm[0];
154+
}
155+
156+
Assembly LoadAssembly(string filePath)
157+
{
158+
if (File.Exists(filePath))
159+
{
160+
try
161+
{
162+
var output = Assembly.LoadFrom(filePath);
163+
164+
Console.WriteLine($"Loaded [{output.GetName()}] from [{output.CodeBase}]");
165+
166+
return output;
167+
}
168+
catch (Exception ex)
169+
{
170+
Console.WriteLine($"Failed to load [{assembly}] from [{filePath}]", ex);
171+
return null;
172+
}
173+
}
174+
else
175+
{
176+
return null;
177+
}
178+
}
179+
180+
var paths = new[] {
181+
Path.Combine(basePath, assembly.Name + ".dll"),
182+
Path.Combine(MSBuildBasePath, assembly.Name + ".dll"),
183+
};
184+
185+
return paths
186+
.Select(LoadAssembly)
187+
.Where(p => p != null)
188+
.FirstOrDefault();
189+
};
190+
191+
AppDomain.CurrentDomain.AssemblyResolve += localResolve;
192+
AppDomain.CurrentDomain.TypeResolve += localResolve;
193+
}
194+
195+
}
196+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace Uno.UI.RemoteControl.Host.HotReload
8+
{
9+
interface IDeltaApplier : IDisposable
10+
{
11+
ValueTask InitializeAsync(CancellationToken cancellationToken);
12+
13+
ValueTask<bool> Apply(string changedFile, ImmutableArray<WatchHotReloadService.Update> solutionUpdate, CancellationToken cancellationToken);
14+
15+
ValueTask ReportDiagnosticsAsync(IEnumerable<string> diagnostics, CancellationToken cancellationToken);
16+
}
17+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Uno.UI.RemoteControl.Host.HotReload
2+
{
3+
/// <summary>
4+
/// This API supports infrastructure and is not intended to be used
5+
/// directly from your code. This API may change or be removed in future releases.
6+
/// </summary>
7+
public interface IReporter
8+
{
9+
void Verbose(string message);
10+
void Output(string message);
11+
void Warn(string message);
12+
void Error(string message);
13+
}
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Microsoft.Build.Tasks;
2+
3+
namespace Uno.UI.RemoteControl.Host.HotReload
4+
{
5+
internal class Reporter : IReporter
6+
{
7+
public void Error(string message) => System.Console.WriteLine($"[Error] {message}");
8+
public void Output(string message) => System.Console.WriteLine($"[Output] {message}");
9+
public void Verbose(string message) => System.Console.WriteLine($"[Verbose] {message}");
10+
public void Warn(string message) => System.Console.WriteLine($"[Warn] {message}");
11+
}
12+
}

0 commit comments

Comments
 (0)