@@ -47,24 +47,26 @@ internal sealed class ExtensionMessageHandlerService(
4747 private readonly SolutionServices _solutionServices = solutionServices ;
4848 private readonly IExtensionMessageHandlerFactory _customMessageHandlerFactory = customMessageHandlerFactory ;
4949
50- // Core design: To make things lightweight, and to avoid locking, all work is computed and cached in simple
51- // immutable dictionaries. These dictionaries are populated on demand, but contain data that can be recomputed
52- // safely if missing.
50+ /// <summary>
51+ /// Lock for <see cref="_folderPathToExtensionFolder"/>, <see cref="_cachedDocumentHandlers"/>, and <see
52+ /// cref="_cachedWorkspaceHandlers"/>.
53+ /// </summary>
54+ private readonly SemaphoreSlim _gate = new ( initialCount : 1 ) ;
5355
5456 /// <summary>
5557 /// Extensions assembly load contexts and loaded handlers, indexed by extension folder path.
5658 /// </summary>
57- private ImmutableDictionary < string , AsyncLazy < IExtensionFolder > > _folderPathToExtensionFolder = ImmutableDictionary < string , AsyncLazy < IExtensionFolder > > . Empty ;
59+ private readonly Dictionary < string , AsyncLazy < ExtensionFolder > > _folderPathToExtensionFolder = new ( ) ;
5860
5961 /// <summary>
6062 /// Cached handlers of document-related messages, indexed by handler message name.
6163 /// </summary>
62- private ImmutableDictionary < string , AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < Document > > > > _cachedDocumentHandlers = ImmutableDictionary < string , AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < Document > > > > . Empty ;
64+ private readonly Dictionary < string , AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < Document > > > > _cachedDocumentHandlers = new ( ) ;
6365
6466 /// <summary>
6567 /// Cached handlers of non-document-related messages, indexed by handler message name.
6668 /// </summary>
67- private ImmutableDictionary < string , AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < Solution > > > > _cachedWorkspaceHandlers = ImmutableDictionary < string , AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < Solution > > > > . Empty ;
69+ private readonly Dictionary < string , AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < Solution > > > > _cachedWorkspaceHandlers = new ( ) ;
6870
6971 private async ValueTask < TResult > ExecuteInRemoteOrCurrentProcessAsync < TResult > (
7072 Solution ? solution ,
@@ -116,18 +118,22 @@ public async ValueTask<RegisterExtensionResponse> RegisterExtensionInCurrentProc
116118 var assemblyFolderPath = Path . GetDirectoryName ( assemblyFilePath )
117119 ?? throw new InvalidOperationException ( $ "Unable to get the directory name for { assemblyFilePath } .") ;
118120
119- var lazy = ImmutableInterlocked . GetOrAdd (
120- ref _folderPathToExtensionFolder ,
121- assemblyFolderPath ,
122- static ( assemblyFolderPath , @this ) => AsyncLazy . Create (
123- cancellationToken => ExtensionFolder . Create ( @this , assemblyFolderPath , cancellationToken ) ) ,
124- this ) ;
121+ AsyncLazy < ExtensionFolder > lazyExtensionFolder ;
122+ using ( await _gate . DisposableWaitAsync ( cancellationToken ) . ConfigureAwait ( false ) )
123+ {
124+ lazyExtensionFolder = _folderPathToExtensionFolder . GetOrAdd (
125+ assemblyFolderPath ,
126+ static ( assemblyFolderPath , @this ) => AsyncLazy . Create (
127+ cancellationToken => ExtensionFolder . Create ( @this , assemblyFolderPath , cancellationToken ) ) ,
128+ this ) ;
125129
126- var extensionFolder = await lazy . GetValueAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
127- var assemblyHandlers = await extensionFolder . RegisterAssemblyAsync ( assemblyFilePath , cancellationToken ) . ConfigureAwait ( false ) ;
130+ // After registering, clear out the cached handler names. They will be recomputed the next time we need them.
131+ ClearCachedHandlers ( ) ;
132+ }
128133
129- // After registering, clear out the cached handler names. They will be recomputed the next time we need them.
130- ClearCachedHandlers ( ) ;
134+ var extensionFolder = await lazyExtensionFolder . GetValueAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
135+ var lazyAssemblyHandlers = extensionFolder . RegisterAssembly ( assemblyFilePath ) ;
136+ var assemblyHandlers = await lazyAssemblyHandlers . GetValueAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
131137
132138 return new (
133139 [ .. assemblyHandlers . WorkspaceMessageHandlers . Keys ] ,
@@ -152,17 +158,23 @@ private async ValueTask<VoidResult> UnregisterExtensionInCurrentProcessAsync(
152158 var assemblyFolderPath = Path . GetDirectoryName ( assemblyFilePath )
153159 ?? throw new InvalidOperationException ( $ "Unable to get the directory name for { assemblyFilePath } .") ;
154160
155- if ( _folderPathToExtensionFolder . TryGetValue ( assemblyFolderPath , out var lazy ) )
161+ // Note: unregistering is slightly expensive as we do everything under a lock, to ensure that we have a
162+ // consistent view of the world. This is fine as we don't expect this to be called very often.
163+ using ( await _gate . DisposableWaitAsync ( cancellationToken ) . ConfigureAwait ( false ) )
156164 {
157- var extensionFolder = await lazy . GetValueAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
158- // Unregister this particular assembly file from teh assembly folder. If it was the last extension within
159- // this folder, we can remove the registration for the extension entirely.
160- if ( await extensionFolder . UnregisterAssemblyAsync ( assemblyFilePath , cancellationToken ) . ConfigureAwait ( false ) )
161- _folderPathToExtensionFolder = _folderPathToExtensionFolder . Remove ( assemblyFolderPath ) ;
165+ if ( _folderPathToExtensionFolder . TryGetValue ( assemblyFolderPath , out var lazyExtensionFolder ) )
166+ {
167+ var extensionFolder = await lazyExtensionFolder . GetValueAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
168+ // Unregister this particular assembly file from teh assembly folder. If it was the last extension within
169+ // this folder, we can remove the registration for the extension entirely.
170+ if ( extensionFolder . UnregisterAssembly ( assemblyFilePath ) )
171+ _folderPathToExtensionFolder . Remove ( assemblyFolderPath ) ;
172+ }
173+
174+ // After unregistering, clear out the cached handler names. They will be recomputed the next time we need them.
175+ ClearCachedHandlers ( ) ;
162176 }
163177
164- // After unregistering, clear out the cached handler names. They will be recomputed the next time we need them.
165- ClearCachedHandlers ( ) ;
166178 return default ;
167179 }
168180
@@ -177,15 +189,19 @@ await ExecuteInRemoteOrCurrentProcessAsync(
177189
178190 private ValueTask < VoidResult > ResetInCurrentProcessAsync ( )
179191 {
180- _folderPathToExtensionFolder = ImmutableDictionary < string , AsyncLazy < IExtensionFolder > > . Empty ;
181- ClearCachedHandlers ( ) ;
182- return default ;
192+ lock ( _gate )
193+ {
194+ _folderPathToExtensionFolder . Clear ( ) ;
195+ ClearCachedHandlers ( ) ;
196+ return default ;
197+ }
183198 }
184199
185200 private void ClearCachedHandlers ( )
186201 {
187- _cachedWorkspaceHandlers = ImmutableDictionary < string , AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < Solution > > > > . Empty ;
188- _cachedDocumentHandlers = ImmutableDictionary < string , AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < Document > > > > . Empty ;
202+ Contract . ThrowIfTrue ( ! Monitor . IsEntered ( _gate ) ) ;
203+ _cachedWorkspaceHandlers . Clear ( ) ;
204+ _cachedDocumentHandlers . Clear ( ) ;
189205 }
190206
191207 public async ValueTask < string > HandleExtensionWorkspaceMessageAsync ( Solution solution , string messageName , string jsonMessage , CancellationToken cancellationToken )
@@ -210,18 +226,21 @@ public async ValueTask<string> HandleExtensionDocumentMessageAsync(Document docu
210226
211227 private async ValueTask < string > HandleExtensionMessageInCurrentProcessAsync < TArgument > (
212228 TArgument executeArgument , bool isSolution , string messageName , string jsonMessage ,
213- ImmutableDictionary < string , AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < TArgument > > > > cachedHandlers ,
229+ Dictionary < string , AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < TArgument > > > > cachedHandlers ,
214230 CancellationToken cancellationToken )
215231 {
216- var lazy = ImmutableInterlocked . GetOrAdd (
217- ref cachedHandlers ,
218- messageName ,
219- static ( messageName , arg ) => AsyncLazy . Create (
220- static ( arg , cancellationToken ) => ComputeHandlersAsync < TArgument > ( arg . @this , arg . messageName , arg . isSolution , cancellationToken ) ,
221- ( messageName , arg . @this , arg . isSolution ) ) ,
222- ( @this : this , executeArgument , isSolution ) ) ;
223-
224- var handlers = await lazy . GetValueAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
232+ AsyncLazy < ImmutableArray < IExtensionMessageHandlerWrapper < TArgument > > > lazyHandlers ;
233+ using ( await _gate . DisposableWaitAsync ( cancellationToken ) . ConfigureAwait ( false ) )
234+ {
235+ lazyHandlers = cachedHandlers . GetOrAdd (
236+ messageName ,
237+ static ( messageName , arg ) => AsyncLazy . Create (
238+ static ( arg , cancellationToken ) => ComputeHandlersAsync < TArgument > ( arg . @this , arg . messageName , arg . isSolution , cancellationToken ) ,
239+ ( messageName , arg . @this , arg . isSolution ) ) ,
240+ ( messageName , @this : this , isSolution ) ) ;
241+ }
242+
243+ var handlers = await lazyHandlers . GetValueAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
225244 if ( handlers . Length == 0 )
226245 throw new InvalidOperationException ( $ "No handler found for message { messageName } .") ;
227246
@@ -269,58 +288,62 @@ private static async Task<ImmutableArray<IExtensionMessageHandlerWrapper<TResult
269288 return result . ToImmutable ( ) ;
270289 }
271290
272- private interface IExtensionFolder
291+ private abstract class ExtensionFolder
273292 {
274- ValueTask < AssemblyHandlers > RegisterAssemblyAsync ( string assemblyFilePath , CancellationToken cancellationToken ) ;
275-
276293 /// <summary>
277- /// Unregisters this assembly path from this extension folder. If this was the last registered path, then this
278- /// will return true so that this folder can be unloaded.
294+ /// Mapping from assembly file path to the handlers it contains. Used as its own lock when mutating.
279295 /// </summary>
280- ValueTask < bool > UnregisterAssemblyAsync ( string assemblyFilePath , CancellationToken cancellationToken ) ;
296+ private readonly Dictionary < string , AsyncLazy < AssemblyHandlers > > _assemblyFilePathToHandlers = new ( ) ;
281297
282- ValueTask AddHandlersAsync < TResult > ( string messageName , bool isSolution , ArrayBuilder < IExtensionMessageHandlerWrapper < TResult > > result , CancellationToken cancellationToken ) ;
283- }
298+ protected abstract AssemblyHandlers CreateAssemblyHandlers ( string assemblyFilePath , CancellationToken cancellationToken ) ;
284299
285- /// <summary>
286- /// Trivial placeholder impl of <see cref="IExtensionFolder"/> when we fail for some reason to even process the
287- /// folder we are told contains extensions.
288- /// </summary>
289- private sealed class TrivialExtensionFolder : IExtensionFolder
290- {
291- public static readonly TrivialExtensionFolder Instance = new ( ) ;
300+ public AsyncLazy < AssemblyHandlers > RegisterAssembly ( string assemblyFilePath )
301+ {
302+ lock ( _assemblyFilePathToHandlers )
303+ {
304+ return _assemblyFilePathToHandlers . GetOrAdd (
305+ assemblyFilePath ,
306+ static ( assemblyFilePath , @this ) => AsyncLazy . Create (
307+ static ( args , cancellationToken ) => args . @this . CreateAssemblyHandlers ( args . assemblyFilePath , cancellationToken ) ,
308+ ( assemblyFilePath , @this ) ) ,
309+ this ) ;
310+ }
311+ }
292312
293313 /// <summary>
294- /// No lock needed as registratin/unregistration must happen serially.
314+ /// Unregisters this assembly path from this extension folder. If this was the last registered path, then this
315+ /// will return true so that this folder can be unloaded.
295316 /// </summary>
296- private readonly List < string > _registeredFilePaths = [ ] ;
297-
298- public ValueTask < AssemblyHandlers > RegisterAssemblyAsync ( string assemblyFilePath , CancellationToken cancellationToken )
317+ public bool UnregisterAssembly ( string assemblyFilePath )
299318 {
300- _registeredFilePaths . Add ( assemblyFilePath ) ;
301- return new ( AssemblyHandlers . Empty ) ;
319+ lock ( _assemblyFilePathToHandlers )
320+ {
321+ _assemblyFilePathToHandlers . Remove ( assemblyFilePath ) ;
322+ return _assemblyFilePathToHandlers . Count == 0 ;
323+ }
302324 }
303325
304- public ValueTask < bool > UnregisterAssemblyAsync ( string assemblyFilePath , CancellationToken cancellationToken )
326+ public async ValueTask AddHandlersAsync < TResult > ( string messageName , bool isSolution , ArrayBuilder < IExtensionMessageHandlerWrapper < TResult > > result , CancellationToken cancellationToken )
305327 {
306- _registeredFilePaths . Remove ( assemblyFilePath ) ;
307- return new ( _registeredFilePaths . Count == 0 ) ;
308- }
309-
310- public ValueTask AddHandlersAsync < TResult > ( string messageName , bool isSolution , ArrayBuilder < IExtensionMessageHandlerWrapper < TResult > > result , CancellationToken cancellationToken )
311- => default ;
312- }
313-
314- private sealed class ExtensionFolder (
315- ExtensionMessageHandlerService extensionMessageHandlerService ,
316- IAnalyzerAssemblyLoaderInternal analyzerAssemblyLoader ) : IExtensionFolder
317- {
318- private readonly ExtensionMessageHandlerService _extensionMessageHandlerService = extensionMessageHandlerService ;
319- private readonly IAnalyzerAssemblyLoaderInternal _analyzerAssemblyLoader = analyzerAssemblyLoader ;
328+ foreach ( var ( _, lazyHandlers ) in _assemblyFilePathToHandlers )
329+ {
330+ cancellationToken . ThrowIfCancellationRequested ( ) ;
320331
321- private ImmutableDictionary < string , AsyncLazy < AssemblyHandlers > > _assemblyFilePathToHandlers = ImmutableDictionary < string , AsyncLazy < AssemblyHandlers > > . Empty ;
332+ var handlers = await lazyHandlers . GetValueAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
333+ if ( isSolution )
334+ {
335+ if ( handlers . WorkspaceMessageHandlers . TryGetValue ( messageName , out var handler ) )
336+ result . Add ( ( IExtensionMessageHandlerWrapper < TResult > ) handler ) ;
337+ }
338+ else
339+ {
340+ if ( handlers . DocumentMessageHandlers . TryGetValue ( messageName , out var handler ) )
341+ result . Add ( ( IExtensionMessageHandlerWrapper < TResult > ) handler ) ;
342+ }
343+ }
344+ }
322345
323- public static IExtensionFolder Create (
346+ public static ExtensionFolder Create (
324347 ExtensionMessageHandlerService extensionMessageHandlerService ,
325348 string assemblyFolderPath ,
326349 CancellationToken cancellationToken )
@@ -348,36 +371,42 @@ public static IExtensionFolder Create(
348371 analyzerAssemblyLoader . AddDependencyLocation ( dll ) ;
349372 }
350373
351- return new ExtensionFolder ( extensionMessageHandlerService , analyzerAssemblyLoader ) ;
374+ return new ShadowCopyExtensionFolder ( extensionMessageHandlerService , analyzerAssemblyLoader ) ;
352375 }
353376 catch ( Exception ex ) when ( FatalError . ReportAndCatch ( ex , ErrorSeverity . Critical ) )
354377 {
355378 // TODO: Log this exception so the client knows something went wrong.
356379 return new TrivialExtensionFolder ( ) ;
357380 }
358381 }
382+ }
359383
360- public async ValueTask < AssemblyHandlers > RegisterAssemblyAsync (
361- string assemblyFilePath , CancellationToken cancellationToken )
362- {
363- var lazy = ImmutableInterlocked . GetOrAdd (
364- ref _assemblyFilePathToHandlers ,
365- assemblyFilePath ,
366- static ( assemblyFilePath , @this ) => AsyncLazy . Create (
367- static ( args , cancellationToken ) => CreateAssemblyHandlers ( args . @this , args . assemblyFilePath , cancellationToken ) ,
368- ( assemblyFilePath , @this ) ) ,
369- this ) ;
384+ /// <summary>
385+ /// Trivial placeholder impl of <see cref="ExtensionFolder"/> when we fail for some reason to even process the
386+ /// folder we are told contains extensions.
387+ /// </summary>
388+ private sealed class TrivialExtensionFolder : ExtensionFolder
389+ {
390+ protected override AssemblyHandlers CreateAssemblyHandlers ( string assemblyFilePath , CancellationToken cancellationToken )
391+ => AssemblyHandlers . Empty ;
392+ }
370393
371- return await lazy . GetValueAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
372- }
394+ /// <summary>
395+ /// Standard impl of <see cref="ExtensionFolder"/> that uses a shadow copy loader to load extensions.
396+ /// </summary>
397+ private sealed class ShadowCopyExtensionFolder (
398+ ExtensionMessageHandlerService extensionMessageHandlerService ,
399+ IAnalyzerAssemblyLoaderInternal analyzerAssemblyLoader ) : ExtensionFolder
400+ {
401+ private readonly ExtensionMessageHandlerService _extensionMessageHandlerService = extensionMessageHandlerService ;
402+ private readonly IAnalyzerAssemblyLoaderInternal _analyzerAssemblyLoader = analyzerAssemblyLoader ;
373403
374- private static AssemblyHandlers CreateAssemblyHandlers (
375- ExtensionFolder @this , string assemblyFilePath , CancellationToken cancellationToken )
404+ protected override AssemblyHandlers CreateAssemblyHandlers ( string assemblyFilePath , CancellationToken cancellationToken )
376405 {
377406 try
378407 {
379- var assembly = @this . _analyzerAssemblyLoader . LoadFromPath ( assemblyFilePath ) ;
380- var factory = @this . _extensionMessageHandlerService . _customMessageHandlerFactory ;
408+ var assembly = _analyzerAssemblyLoader . LoadFromPath ( assemblyFilePath ) ;
409+ var factory = _extensionMessageHandlerService . _customMessageHandlerFactory ;
381410
382411 var messageWorkspaceHandlers = factory
383412 . CreateWorkspaceMessageHandlers ( assembly , extensionIdentifier : assemblyFilePath , cancellationToken )
@@ -401,32 +430,6 @@ private static AssemblyHandlers CreateAssemblyHandlers(
401430 return AssemblyHandlers . Empty ;
402431 }
403432 }
404-
405- public async ValueTask AddHandlersAsync < TResult > ( string messageName , bool isSolution , ArrayBuilder < IExtensionMessageHandlerWrapper < TResult > > result , CancellationToken cancellationToken )
406- {
407- foreach ( var ( _, lazy ) in _assemblyFilePathToHandlers )
408- {
409- cancellationToken . ThrowIfCancellationRequested ( ) ;
410-
411- var handlers = await lazy . GetValueAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
412- if ( isSolution )
413- {
414- if ( handlers . WorkspaceMessageHandlers . TryGetValue ( messageName , out var handler ) )
415- result . Add ( ( IExtensionMessageHandlerWrapper < TResult > ) handler ) ;
416- }
417- else
418- {
419- if ( handlers . DocumentMessageHandlers . TryGetValue ( messageName , out var handler ) )
420- result . Add ( ( IExtensionMessageHandlerWrapper < TResult > ) handler ) ;
421- }
422- }
423- }
424-
425- public ValueTask < bool > UnregisterAssemblyAsync ( string assemblyFilePath , CancellationToken cancellationToken )
426- {
427- _assemblyFilePathToHandlers = _assemblyFilePathToHandlers . Remove ( assemblyFilePath ) ;
428- return new ( _assemblyFilePathToHandlers . IsEmpty ) ;
429- }
430433 }
431434
432435 private sealed class AssemblyHandlers
0 commit comments