@@ -43,13 +43,9 @@ internal abstract class LanguageServerProjectLoader
4343 private readonly BinlogNamer _binlogNamer ;
4444 protected readonly ImmutableDictionary < string , string > AdditionalProperties ;
4545
46- /// <summary>
47- /// The list of loaded projects in the workspace, keyed by project file path. The outer dictionary is a concurrent dictionary since we may be loading
48- /// multiple projects at once; the key is a single List we just have a single thread processing any given project file. This is only to be used
49- /// in <see cref="LoadOrReloadProjectsAsync" /> and downstream calls; any other updating of this (like unloading projects) should be achieved by adding
50- /// things to the <see cref="ProjectsToLoadAndReload" />.
51- /// </summary>
52- private readonly ConcurrentDictionary < string , ImmutableArray < LoadedProject > > _loadedProjects = [ ] ;
46+ protected record LoadedProjectSet ( List < LoadedProject > LoadedProjects , SemaphoreSlim Semaphore , CancellationTokenSource CancellationTokenSource ) ;
47+
48+ private readonly ConcurrentDictionary < string , LoadedProjectSet > _loadedProjects = [ ] ;
5349
5450 protected LanguageServerProjectLoader (
5551 ProjectSystemProjectFactory projectFactory ,
@@ -157,103 +153,113 @@ private async Task<bool> LoadOrReloadProjectAsync(ProjectToLoad projectToLoad, T
157153
158154 try
159155 {
160- var ( loadedFile , hasAllInformation , preferredBuildHostKind , actualBuildHostKind ) = await TryLoadProjectAsync ( buildHostProcessManager , projectPath , cancellationToken ) ;
161- if ( preferredBuildHostKind != actualBuildHostKind )
162- preferredBuildHostKindThatWeDidNotGet = preferredBuildHostKind ;
163-
164- if ( loadedFile is null )
165- {
166- _logger . LogWarning ( $ "Unable to load project '{ projectPath } '.") ;
167- return false ;
168- }
169-
170- var diagnosticLogItems = await loadedFile . GetDiagnosticLogItemsAsync ( cancellationToken ) ;
171- if ( diagnosticLogItems . Any ( item => item . Kind is DiagnosticLogItemKind . Error ) )
156+ if ( ! _loadedProjects . TryGetValue ( projectPath , out var loadedProjectSet ) || loadedProjectSet . CancellationTokenSource . IsCancellationRequested )
172157 {
173- await LogDiagnosticsAsync ( diagnosticLogItems ) ;
174- // We have total failures in evaluation, no point in continuing.
158+ // project was already unloaded or in process of unloading.
175159 return false ;
176160 }
177161
178- var loadedProjectInfos = await loadedFile . GetProjectFileInfosAsync ( cancellationToken ) ;
179-
180- // The out-of-proc build host supports more languages than we may actually have Workspace binaries for, so ensure we can actually process that
181- // language in-process.
182- var projectLanguage = loadedProjectInfos . FirstOrDefault ( ) ? . Language ;
183- if ( projectLanguage != null && ProjectFactory . Workspace . Services . GetLanguageService < ICommandLineParserService > ( projectLanguage ) == null )
162+ var linkedTokenSource = CancellationTokenSource . CreateLinkedTokenSource ( loadedProjectSet . CancellationTokenSource . Token , cancellationToken ) ;
163+ try
184164 {
185- return false ;
186- }
165+ var ( loadedFile , hasAllInformation , preferredBuildHostKind , actualBuildHostKind ) = await TryLoadProjectAsync ( buildHostProcessManager , projectPath , cancellationToken ) ;
166+ if ( preferredBuildHostKind != actualBuildHostKind )
167+ preferredBuildHostKindThatWeDidNotGet = preferredBuildHostKind ;
187168
188- if ( ! _loadedProjects . TryGetValue ( projectPath , out var existingProjects ) )
189- existingProjects = [ ] ;
169+ if ( loadedFile is null )
170+ {
171+ _logger . LogWarning ( $ "Unable to load project '{ projectPath } '.") ;
172+ return false ;
173+ }
190174
191- Dictionary < ProjectFileInfo , ProjectLoadTelemetryReporter . TelemetryInfo > telemetryInfos = [ ] ;
192- var needsRestore = false ;
175+ var diagnosticLogItems = await loadedFile . GetDiagnosticLogItemsAsync ( cancellationToken ) ;
176+ if ( diagnosticLogItems . Any ( item => item . Kind is DiagnosticLogItemKind . Error ) )
177+ {
178+ await LogDiagnosticsAsync ( diagnosticLogItems ) ;
179+ // We have total failures in evaluation, no point in continuing.
180+ return false ;
181+ }
193182
194- // We want to remove projects for targets that don't exist anymore; if we update projects we'll remove them from
195- // this list -- what's left we can then remove.
196- HashSet < LoadedProject > projectsToRemove = [ .. existingProjects ] ;
197- foreach ( var loadedProjectInfo in loadedProjectInfos )
198- {
199- var existingProject = existingProjects . FirstOrDefault ( p => p . GetTargetFramework ( ) == loadedProjectInfo . TargetFramework ) ;
200- bool targetNeedsRestore ;
201- ProjectLoadTelemetryReporter . TelemetryInfo targetTelemetryInfo ;
183+ var loadedProjectInfos = await loadedFile . GetProjectFileInfosAsync ( cancellationToken ) ;
202184
203- if ( existingProject != null )
185+ // The out-of-proc build host supports more languages than we may actually have Workspace binaries for, so ensure we can actually process that
186+ // language in-process.
187+ var projectLanguage = loadedProjectInfos . FirstOrDefault ( ) ? . Language ;
188+ if ( projectLanguage != null && ProjectFactory . Workspace . Services . GetLanguageService < ICommandLineParserService > ( projectLanguage ) == null )
204189 {
205- projectsToRemove . Remove ( existingProject ) ;
206- ( targetTelemetryInfo , targetNeedsRestore ) = await existingProject . UpdateWithNewProjectInfoAsync ( loadedProjectInfo , hasAllInformation , _logger ) ;
190+ return false ;
207191 }
208- else
209- {
210- var loadedProject = await CreateAndTrackInitialProjectAsync (
211- projectPath ,
212- loadedProjectInfo . Language ,
213- loadedProjectInfo . TargetFramework ,
214- loadedProjectInfo . IntermediateOutputFilePath ) ;
215- loadedProject . NeedsReload += ( _ , _ ) => ProjectsToLoadAndReload . AddWork ( projectToLoad with { ReportTelemetry = false } ) ;
216192
217- ( targetTelemetryInfo , targetNeedsRestore ) = await loadedProject . UpdateWithNewProjectInfoAsync ( loadedProjectInfo , hasAllInformation , _logger ) ;
193+ Dictionary < ProjectFileInfo , ProjectLoadTelemetryReporter . TelemetryInfo > telemetryInfos = [ ] ;
194+ var needsRestore = false ;
218195
219- needsRestore |= targetNeedsRestore ;
220- telemetryInfos [ loadedProjectInfo ] = targetTelemetryInfo with { IsSdkStyle = preferredBuildHostKind == BuildHostProcessKind . NetCore } ;
196+ using var _ = await loadedProjectSet . Semaphore . DisposableWaitAsync ( cancellationToken ) ;
197+ // last chance for someone to cancel out from under us.
198+ if ( loadedProjectSet . CancellationTokenSource . IsCancellationRequested )
199+ {
200+ return false ;
221201 }
222- }
223202
224- if ( projectsToRemove . Any ( ) )
225- {
226- foreach ( var project in existingProjects )
203+ var existingProjects = loadedProjectSet . LoadedProjects ;
204+
205+ // We want to remove projects for targets that don't exist anymore; if we update projects we'll remove them from
206+ // this list -- what's left we can then remove.
207+ HashSet < LoadedProject > projectsToRemove = [ .. existingProjects ] ;
208+ foreach ( var loadedProjectInfo in loadedProjectInfos )
227209 {
228- if ( projectsToRemove . Contains ( project ) )
210+ var existingProject = existingProjects . FirstOrDefault ( p => p . GetTargetFramework ( ) == loadedProjectInfo . TargetFramework ) ;
211+ bool targetNeedsRestore ;
212+ ProjectLoadTelemetryReporter . TelemetryInfo targetTelemetryInfo ;
213+
214+ if ( existingProject != null )
215+ {
216+ projectsToRemove . Remove ( existingProject ) ;
217+ ( targetTelemetryInfo , targetNeedsRestore ) = await existingProject . UpdateWithNewProjectInfoAsync ( loadedProjectInfo , hasAllInformation , _logger ) ;
218+ }
219+ else
229220 {
230- project . Dispose ( ) ;
221+ var loadedProject = await CreateAndTrackInitialProjectAsync_NoLock (
222+ loadedProjectSet ,
223+ projectPath ,
224+ loadedProjectInfo . Language ,
225+ loadedProjectInfo . TargetFramework ,
226+ loadedProjectInfo . IntermediateOutputFilePath ) ;
227+ loadedProject . NeedsReload += ( _ , _ ) => ProjectsToLoadAndReload . AddWork ( projectToLoad with { ReportTelemetry = false } ) ;
228+
229+ ( targetTelemetryInfo , targetNeedsRestore ) = await loadedProject . UpdateWithNewProjectInfoAsync ( loadedProjectInfo , hasAllInformation , _logger ) ;
230+
231+ needsRestore |= targetNeedsRestore ;
232+ telemetryInfos [ loadedProjectInfo ] = targetTelemetryInfo with { IsSdkStyle = preferredBuildHostKind == BuildHostProcessKind . NetCore } ;
231233 }
232234 }
233235
234- _loadedProjects . AddOrUpdate ( projectPath ,
235- // We expect the key always continues to be present in the dictionary during this operation.
236- addValueFactory : ( _ , _ ) => throw new InvalidOperationException ( ) ,
237- updateValueFactory : static ( _ , existingProjects , projectsToRemove ) => existingProjects . RemoveRange ( projectsToRemove ) ,
238- factoryArgument : projectsToRemove ) ;
239- }
236+ foreach ( var project in projectsToRemove )
237+ {
238+ project . Dispose ( ) ;
239+ existingProjects . Remove ( project ) ;
240+ }
240241
241- if ( projectToLoad . ReportTelemetry )
242- {
243- await _projectLoadTelemetryReporter . ReportProjectLoadTelemetryAsync ( telemetryInfos , projectToLoad , cancellationToken ) ;
244- }
242+ if ( projectToLoad . ReportTelemetry )
243+ {
244+ await _projectLoadTelemetryReporter . ReportProjectLoadTelemetryAsync ( telemetryInfos , projectToLoad , cancellationToken ) ;
245+ }
245246
246- diagnosticLogItems = await loadedFile . GetDiagnosticLogItemsAsync ( cancellationToken ) ;
247- if ( diagnosticLogItems . Any ( ) )
248- {
249- await LogDiagnosticsAsync ( diagnosticLogItems ) ;
247+ diagnosticLogItems = await loadedFile . GetDiagnosticLogItemsAsync ( cancellationToken ) ;
248+ if ( diagnosticLogItems . Any ( ) )
249+ {
250+ await LogDiagnosticsAsync ( diagnosticLogItems ) ;
251+ }
252+ else
253+ {
254+ _logger . LogInformation ( string . Format ( LanguageServerResources . Successfully_completed_load_of_0 , projectPath ) ) ;
255+ }
256+
257+ return needsRestore ;
250258 }
251- else
259+ catch ( OperationCanceledException e ) when ( e . CancellationToken == linkedTokenSource . Token )
252260 {
253- _logger . LogInformation ( string . Format ( LanguageServerResources . Successfully_completed_load_of_0 , projectPath ) ) ;
261+ return false ;
254262 }
255-
256- return needsRestore ;
257263 }
258264 catch ( Exception e )
259265 {
@@ -291,11 +297,32 @@ async Task LogDiagnosticsAsync(ImmutableArray<DiagnosticLogItem> diagnosticLogIt
291297 /// <summary>
292298 /// Creates a <see cref="LoadedProject"/> which has bare minimum information, but, which documents can be added to and obtained from.
293299 /// </summary>
294- protected async Task < LoadedProject > CreateAndTrackInitialProjectAsync (
295- string projectPath ,
296- string language ,
297- string ? targetFramework = null ,
298- string ? intermediateOutputFilePath = null )
300+ protected LoadedProjectSet AddLoadedProjectSet ( string projectPath )
301+ {
302+ var projectSet = new LoadedProjectSet ( [ ] , new SemaphoreSlim ( 1 ) , new CancellationTokenSource ( ) ) ;
303+ return _loadedProjects . GetOrAdd ( projectPath , projectSet ) ;
304+ }
305+
306+ protected async ValueTask TryUnloadProjectSetAsync ( string projectPath )
307+ {
308+ if ( _loadedProjects . TryRemove ( projectPath , out var loadedProjectSet ) )
309+ {
310+ using var _ = await loadedProjectSet . Semaphore . DisposableWaitAsync ( ) ;
311+ if ( loadedProjectSet . CancellationTokenSource . IsCancellationRequested )
312+ {
313+ // don't need to cancel again.
314+ return ;
315+ }
316+
317+ loadedProjectSet . CancellationTokenSource . Cancel ( ) ;
318+ foreach ( var project in loadedProjectSet . LoadedProjects )
319+ {
320+ project . Dispose ( ) ;
321+ }
322+ }
323+ }
324+
325+ protected async Task < LoadedProject > CreateAndTrackInitialProjectAsync_NoLock ( LoadedProjectSet projectSet , string projectPath , string language , string ? targetFramework = null , string ? intermediateOutputFilePath = null )
299326 {
300327 var projectSystemName = targetFramework is null ? projectPath : $ "{ projectPath } (${ targetFramework } )";
301328
@@ -313,7 +340,7 @@ protected async Task<LoadedProject> CreateAndTrackInitialProjectAsync(
313340 _projectSystemHostInfo ) ;
314341
315342 var loadedProject = new LoadedProject ( projectSystemProject , ProjectFactory . Workspace . Services . SolutionServices , _fileChangeWatcher , _targetFrameworkManager ) ;
316- _loadedProjects . AddOrUpdate ( projectPath , addValue : [ loadedProject ] , updateValueFactory : ( _ , arr ) => arr . Add ( loadedProject ) ) ;
343+ projectSet . LoadedProjects . Add ( loadedProject ) ;
317344 return loadedProject ;
318345 }
319346}
0 commit comments