Skip to content

Commit a7050dd

Browse files
authored
Merge pull request #22188 from unoplatform/dev/dr/miscHr
fix(hr): HR builds all targets
2 parents b0545c5 + 5d6baaf commit a7050dd

File tree

5 files changed

+90
-45
lines changed

5 files changed

+90
-45
lines changed

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,7 @@ private static async Task HandleWebSocketConnectionRequest(WebApplication app, H
104104
throw new InvalidOperationException("IIdeChannel is required"),
105105
context.RequestServices);
106106

107-
await server.RunAsync(await context.WebSockets.AcceptWebSocketAsync(),
108-
CancellationToken.None);
107+
await server.RunAsync(await context.WebSockets.AcceptWebSocketAsync(), context.RequestAborted);
109108
}
110109
else
111110
{

src/Uno.UI.RemoteControl.Server.Processors/HotReload/MetadataUpdates/CompilationWorkspaceProvider.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ internal static class CompilationWorkspaceProvider
2121
{
2222
private static string MSBuildBasePath = "";
2323

24-
public static async Task<(Solution, WatchHotReloadService)> CreateWorkspaceAsync(
24+
public static async Task<(Workspace, WatchHotReloadService)> CreateWorkspaceAsync(
2525
string projectPath,
2626
IReporter reporter,
2727
string[] metadataUpdateCapabilities,
2828
Dictionary<string, string> properties,
2929
CancellationToken ct)
3030
{
31-
if (properties.TryGetValue("UnoHotReloadDiagnosticsLogPath", out var logPath))
31+
if (properties.TryGetValue("UnoHotReloadDiagnosticsLogPath", out var logPath) && logPath is { Length: > 0 })
3232
{
3333
// Sets Roslyn's environment variable for troubleshooting HR, see:
3434
// https://github.com/dotnet/roslyn/blob/fc6e0c25277ff440ca7ded842ac60278ee6c9695/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs#L72
@@ -102,7 +102,7 @@ static bool IsValidProperty(string property)
102102
await project.GetCompilationAsync(ct);
103103
}
104104

105-
return (currentSolution, hotReloadService);
105+
return (workspace, hotReloadService);
106106
}
107107

108108
public static void InitializeRoslyn(string? workDir)

src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.MetadataUpdate.cs

Lines changed: 83 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Microsoft.CodeAnalysis.CSharp;
2020
using Microsoft.CodeAnalysis.Text;
2121
using Uno.Disposables;
22+
using Uno.Extensions;
2223
using Uno.Threading;
2324
using Uno.UI.RemoteControl.Helpers;
2425
using Uno.UI.RemoteControl.Host.HotReload.MetadataUpdates;
@@ -33,14 +34,11 @@ partial class ServerHotReloadProcessor : IServerProcessor, IDisposable
3334
private static readonly StringComparer _pathsComparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
3435
private static readonly StringComparison _pathsComparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
3536

36-
private IDisposable? _solutionSubscriptions;
3737
private readonly FastAsyncLock _solutionUpdateGate = new();
3838
private readonly BufferGate _solutionWatchersGate = new();
3939

40-
private Task<(Solution, WatchHotReloadService)>? _initializeTask;
41-
private Solution? _currentSolution;
42-
private WatchHotReloadService? _hotReloadService;
43-
private IReporter _reporter = new Reporter();
40+
private (Task<HotReloadWorkspace> GetAsync, CancellationTokenSource Ct)? _workspace;
41+
private readonly IReporter _reporter = new Reporter();
4442

4543
private bool _useRoslynHotReload;
4644
private bool _useHotReloadThruDebugger;
@@ -77,6 +75,12 @@ private void InitializeInner(ConfigureServer configureServer)
7775
{
7876
try
7977
{
78+
if (_workspace is not null)
79+
{
80+
_reporter.Warn("Hot-reload workspace is already initialized.");
81+
return;
82+
}
83+
8084
if (Assembly.Load("Microsoft.CodeAnalysis.Workspaces") is { } wsAsm)
8185
{
8286
// If this assembly was loaded from a stream, it will not have a location.
@@ -90,7 +94,8 @@ private void InitializeInner(ConfigureServer configureServer)
9094

9195
CompilationWorkspaceProvider.InitializeRoslyn(Path.GetDirectoryName(configureServer.ProjectPath));
9296

93-
_initializeTask = InitializeAsync(CancellationToken.None);
97+
var ct = new CancellationTokenSource();
98+
_workspace = (InitializeAsync(ct.Token), ct);
9499
}
95100
catch (Exception e)
96101
{
@@ -101,20 +106,22 @@ private void InitializeInner(ConfigureServer configureServer)
101106

102107
throw;
103108
}
104-
async Task<(Solution, WatchHotReloadService)> InitializeAsync(CancellationToken ct)
109+
async Task<HotReloadWorkspace> InitializeAsync(CancellationToken ct)
105110
{
106111
try
107112
{
108113
await Notify(HotReloadEvent.Initializing);
109114

110-
var (outputPath, intermediateOutputPath, solution, watch) = await CreateCompilation(configureServer, ct);
115+
var workspace = await CreateCompilation(configureServer, ct);
116+
ct.Register(() => workspace.Dispose());
111117

112-
ObserveSolutionPaths(solution, intermediateOutputPath, outputPath);
118+
var fileSystemWatch = ObserveSolutionPaths(workspace.CurrentSolution, workspace.OutputPaths);
119+
ct.Register(() => fileSystemWatch.Dispose());
113120

114121
await _remoteControlServer.SendFrame(new HotReloadWorkspaceLoadResult { WorkspaceInitialized = true });
115122
await Notify(HotReloadEvent.Ready);
116123

117-
return (solution, watch);
124+
return workspace;
118125
}
119126
catch (Exception e)
120127
{
@@ -128,7 +135,19 @@ private void InitializeInner(ConfigureServer configureServer)
128135
}
129136
}
130137

131-
private async Task<(string? outputPath, string? intermediateOutputPath, Solution solution, WatchHotReloadService watch)> CreateCompilation(ConfigureServer configureServer, CancellationToken ct)
138+
private record HotReloadWorkspace(Workspace InnerWorkspace, WatchHotReloadService WatchService, string?[] OutputPaths) : IDisposable
139+
{
140+
public Solution CurrentSolution { get; set; } = InnerWorkspace.CurrentSolution;
141+
142+
/// <inheritdoc />
143+
public void Dispose()
144+
{
145+
WatchService.EndSession();
146+
InnerWorkspace.Dispose();
147+
}
148+
}
149+
150+
private async Task<HotReloadWorkspace> CreateCompilation(ConfigureServer configureServer, CancellationToken ct)
132151
{
133152
// Clone the properties from the ConfigureServer
134153
var properties = configureServer.MSBuildProperties.ToDictionary();
@@ -166,19 +185,50 @@ private void InitializeInner(ConfigureServer configureServer)
166185
if (properties.Remove("TargetFramework", out var targetFramework))
167186
{
168187
properties["UnoHotReloadTargetFramework"] = targetFramework;
188+
properties["TargetFrameworks"] = targetFramework;
169189
}
170190

171-
var (solution, watch) = await CompilationWorkspaceProvider.CreateWorkspaceAsync(
191+
var (workspace, watch) = await CompilationWorkspaceProvider.CreateWorkspaceAsync(
172192
configureServer.ProjectPath,
173193
_reporter,
174194
configureServer.MetadataUpdateCapabilities,
175195
properties,
176196
ct);
177-
return (outputPath, intermediateOutputPath, solution, watch);
197+
198+
return new HotReloadWorkspace(workspace, watch, [Trim(outputPath), Trim(intermediateOutputPath)]);
199+
200+
// We make sure to trim the output path from any TFM / RID / Configuration suffixes
201+
// This is to make sure that if we have multiple active HR workspace (like an old Android emulator reconnecting while a desktop app is running),
202+
// we will not consider the files of the other targets.
203+
string? Trim(string? outDir)
204+
{
205+
var result = outDir;
206+
while (!string.IsNullOrWhiteSpace(result))
207+
{
208+
var updated = result
209+
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
210+
.TrimEnd(targetFramework, _pathsComparison)
211+
.TrimEnd(runtimeIdentifier, _pathsComparison)
212+
.TrimEnd(properties.GetValueOrDefault("Configuration"), _pathsComparison);
213+
if (updated == result)
214+
{
215+
return result + Path.DirectorySeparatorChar; // We make sure to restore the dir separator at the end to make sure filters applies only on folders!
216+
}
217+
else
218+
{
219+
result = updated;
220+
}
221+
}
222+
223+
return null;
224+
}
178225
}
179226

180-
private void ObserveSolutionPaths(Solution solution, params string?[] excludedDirPattern)
227+
private IDisposable ObserveSolutionPaths(Solution solution, params string?[] excludedDirPattern)
181228
{
229+
// TODO: Resolve the bin and obj folders from the project (instead of assuming same config for all projects)
230+
// e.g.: projectDir.First().AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.intermediateoutputpath", out string value)
231+
// Not implemented yet: for a netstd2.0 project, we don't have properties such intermediateoutputpath available!
182232
ImmutableArray<string> excludedDir =
183233
[
184234
.. from pattern in excludedDirPattern
@@ -215,7 +265,7 @@ select Path.Combine(projectDir, pattern).TrimEnd(Path.DirectorySeparatorChar, Pa
215265
filePaths => _ = ProcessFileChanges(filePaths, processing.Token),
216266
e => Console.WriteLine($"Error {e}"));
217267

218-
_solutionSubscriptions = new CompositeDisposable([watchersSubscription, Disposable.Create(processing.Cancel), processing, .. watchers]);
268+
return new CompositeDisposable([watchersSubscription, Disposable.Create(processing.Cancel), processing, .. watchers]);
219269

220270
bool HasInterest(string path)
221271
{
@@ -276,7 +326,7 @@ private async Task ProcessFileChanges(Task<ImmutableHashSet<string>> filesAsync,
276326

277327
private async ValueTask ProcessSolutionChanged(HotReloadServerOperation hotReload, ImmutableHashSet<string> files, CancellationToken ct)
278328
{
279-
if (!await EnsureSolutionInitializedAsync() || _currentSolution is null || _hotReloadService is null)
329+
if (await GetWorkspaceAsync() is not { } workspace)
280330
{
281331
await hotReload.Complete(HotReloadServerResult.Failed); // Failed to init the workspace
282332
return;
@@ -285,11 +335,11 @@ private async ValueTask ProcessSolutionChanged(HotReloadServerOperation hotReloa
285335
var sw = Stopwatch.StartNew();
286336

287337
// Detects the changes and try to update the solution
288-
var originalSolution = _currentSolution;
338+
var originalSolution = workspace.CurrentSolution;
289339
var changeSet = await DiscoverChangesAsync(originalSolution, files, ct);
290340
var solution = await Apply(originalSolution, changeSet, hotReload, ct);
291341

292-
if (solution == _currentSolution)
342+
if (solution == originalSolution)
293343
{
294344
_reporter.Output($"No changes found in {string.Join(",", files.Select(Path.GetFileName))}");
295345

@@ -299,10 +349,10 @@ private async ValueTask ProcessSolutionChanged(HotReloadServerOperation hotReloa
299349

300350
// No matter if the build will succeed or not, we update the _currentSolution.
301351
// Files needs to be updated again to fix the compilation errors.
302-
_currentSolution = solution;
352+
workspace.CurrentSolution = solution;
303353

304354
// Compile the solution and get deltas
305-
var (updates, hotReloadDiagnostics) = await _hotReloadService.EmitSolutionUpdateAsync(solution, ct);
355+
var (updates, hotReloadDiagnostics) = await workspace.WatchService.EmitSolutionUpdateAsync(solution, ct);
306356
// hotReloadDiagnostics currently includes semantic Warnings and Errors for types being updated. We want to limit rude edits to the class
307357
// of unrecoverable errors that a user cannot fix and requires an app rebuild.
308358
var rudeEdits = hotReloadDiagnostics.RemoveAll(d => d.Severity == DiagnosticSeverity.Warning || !d.Descriptor.Id.StartsWith("ENC", StringComparison.Ordinal));
@@ -457,28 +507,21 @@ private ImmutableArray<string> GetCompilationErrors(Solution solution, Cancellat
457507
return builder;
458508
}
459509

460-
[MemberNotNullWhen(true, nameof(_currentSolution), nameof(_hotReloadService))]
461-
private async ValueTask<bool> EnsureSolutionInitializedAsync()
510+
private async ValueTask<HotReloadWorkspace?> GetWorkspaceAsync()
462511
{
463-
if (_currentSolution is not null && _hotReloadService is not null)
512+
if (_workspace is null)
464513
{
465-
return true;
466-
}
467-
468-
if (_initializeTask is null)
469-
{
470-
return false;
514+
return null;
471515
}
472516

473517
try
474518
{
475-
(_currentSolution, _hotReloadService) = await _initializeTask;
476-
return true;
519+
return await _workspace.Value.GetAsync;
477520
}
478521
catch (Exception ex)
479522
{
480523
_reporter.Warn(ex.Message);
481-
return false;
524+
return null;
482525
}
483526
}
484527

@@ -560,7 +603,7 @@ private async ValueTask<ChangeSet> DiscoverChangesAsync(Solution solution, Immut
560603
ImmutableHashSet<string> newFiles,
561604
CancellationToken ct)
562605
{
563-
if (_configureServer is null || _currentSolution is null)
606+
if (_configureServer is null)
564607
{
565608
_reporter.Warn("Cannot handle new files: configuration not available.");
566609
return ([], [], newFiles);
@@ -571,12 +614,13 @@ private async ValueTask<ChangeSet> DiscoverChangesAsync(Solution solution, Immut
571614
return ([], [], newFiles);
572615
}
573616

617+
HotReloadWorkspace? tempWorkspace = null;
574618
try
575619
{
576620
_reporter.Output($"Detected {newFiles.Count} potentially new file(s). Creating temporary workspace to discover them...");
577621

578622
// Create a temporary workspace to discover the new files
579-
var (_, _, tempSolution, _) = await CreateCompilation(_configureServer, ct);
623+
tempWorkspace = await CreateCompilation(_configureServer, ct);
580624

581625
var discoveredDocuments = ImmutableArray.CreateBuilder<AddedDocumentInfo>();
582626
var discoveredAdditionalDocuments = ImmutableArray.CreateBuilder<AddedDocumentInfo>();
@@ -587,7 +631,7 @@ private async ValueTask<ChangeSet> DiscoverChangesAsync(Solution solution, Immut
587631
// Search for the file in the temp workspace's projects
588632
// Note: Here again we assume that document can appear in more than one project (same project loaded with different TFM)
589633
var found = false;
590-
foreach (var project in tempSolution.Projects)
634+
foreach (var project in tempWorkspace.CurrentSolution.Projects)
591635
{
592636
if (project.Documents.FirstOrDefault(d => string.Equals(d.FilePath, file, _pathsComparison)) is { } document)
593637
{
@@ -614,6 +658,10 @@ private async ValueTask<ChangeSet> DiscoverChangesAsync(Solution solution, Immut
614658
_reporter.Warn($"Error while discovering new files: {ex.Message}");
615659
return ([], [], newFiles);
616660
}
661+
finally
662+
{
663+
tempWorkspace?.Dispose();
664+
}
617665
}
618666

619667
private static async ValueTask<Solution> Apply(Solution solution, ChangeSet changeSet, HotReloadServerOperation hotReload, CancellationToken ct)

src/Uno.UI.RemoteControl.Server.Processors/HotReload/ServerHotReloadProcessor.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -854,10 +854,7 @@ private async Task<bool> RequestHotReloadToIde()
854854
#endregion
855855

856856
public void Dispose()
857-
{
858-
_solutionSubscriptions?.Dispose();
859-
_hotReloadService?.EndSession();
860-
}
857+
=> _workspace?.Ct.Cancel();
861858

862859
#region Helpers
863860
private class BufferGate

src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs

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

33
using System;
44
using System.Collections.Generic;
5+
using System.Diagnostics;
56
using System.Linq;
67
using System.Threading.Tasks;
78
using Uno.Foundation.Logging;
@@ -93,7 +94,7 @@ private async Task ConfigureServer()
9394
_status.ReportInvalidRuntime();
9495
}
9596

96-
var hrDebug = Environment.GetEnvironmentVariable("__UNO_SUPPORT_DEBUG_HOT_RELOAD__") == "true";
97+
var hrDebug = Debugger.IsAttached && Environment.GetEnvironmentVariable("__UNO_SUPPORT_DEBUG_HOT_RELOAD__") == "true";
9798
var message = new ConfigureServer(
9899
_projectPath,
99100
GetMetadataUpdateCapabilities(),

0 commit comments

Comments
 (0)