1919using Microsoft . CodeAnalysis . CSharp ;
2020using Microsoft . CodeAnalysis . Text ;
2121using Uno . Disposables ;
22+ using Uno . Extensions ;
2223using Uno . Threading ;
2324using Uno . UI . RemoteControl . Helpers ;
2425using 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 )
0 commit comments