1 // -----------------------------------------------------------------------
\r
2 // Copyright (c) Microsoft Corporation. All rights reserved.
\r
3 // -----------------------------------------------------------------------
\r
7 using System.Collections.Generic;
\r
8 using System.Collections.ObjectModel;
\r
9 using System.ComponentModel.Composition.Diagnostics;
\r
10 using System.ComponentModel.Composition.Primitives;
\r
11 using System.Diagnostics;
\r
12 using System.Globalization;
\r
15 using System.Reflection;
\r
16 using Microsoft.Internal;
\r
17 using Microsoft.Internal.Collections;
\r
19 using IOPath = System.IO.Path;
\r
21 namespace System.ComponentModel.Composition.Hosting
\r
23 [DebuggerTypeProxy(typeof(DirectoryCatalogDebuggerProxy))]
\r
24 public partial class DirectoryCatalog : ComposablePartCatalog, INotifyComposablePartCatalogChanged, ICompositionElement
\r
26 private readonly Lock _thisLock = new Lock();
\r
27 private ComposablePartCatalogCollection _catalogCollection;
\r
28 private Dictionary<string, AssemblyCatalog> _assemblyCatalogs;
\r
29 private volatile bool _isDisposed = false;
\r
30 private string _path;
\r
31 private string _fullPath;
\r
32 private string _searchPattern;
\r
33 private ReadOnlyCollection<string> _loadedFiles;
\r
34 private IQueryable<ComposablePartDefinition> _partsQuery;
\r
37 /// Creates a catalog of <see cref="ComposablePartDefinition"/>s based on all the *.dll files
\r
38 /// in the given directory path.
\r
40 /// Possible exceptions that can be thrown are any that <see cref="Directory.GetFiles(string, string)"/> or
\r
41 /// <see cref="Assembly.Load(AssemblyName)"/> can throw.
\r
43 /// <param name="path">
\r
44 /// Path to the directory to scan for assemblies to add to the catalog.
\r
45 /// The path needs to be absolute or relative to <see cref="AppDomain.BaseDirectory"/>
\r
47 /// <exception cref="ArgumentException">
\r
48 /// If <paramref name="path"/> is a zero-length string, contains only white space, or
\r
49 /// contains one or more implementation-specific invalid characters.
\r
51 /// <exception cref="ArgumentNullException">
\r
52 /// <paramref name="path"/> is <see langword="null"/>.
\r
54 /// <exception cref="DirectoryNotFoundException">
\r
55 /// The specified <paramref name="path"/> is invalid (for example, it is on an unmapped drive).
\r
57 /// <exception cref="PathTooLongException">
\r
58 /// The specified <paramref name="path"/>, file name, or both exceed the system-defined maximum length.
\r
59 /// For example, on Windows-based platforms, paths must be less than 248 characters and file names must
\r
60 /// be less than 260 characters.
\r
62 /// <exception cref="UnauthorizedAccessException">
\r
63 /// The caller does not have the required permission.
\r
65 public DirectoryCatalog(string path) : this(path, "*.dll")
\r
70 /// Creates a catalog of <see cref="ComposablePartDefinition"/>s based on all the given searchPattern
\r
71 /// over the files in the given directory path.
\r
73 /// Possible exceptions that can be thrown are any that <see cref="Directory.GetFiles(string, string)"/> or
\r
74 /// <see cref="Assembly.Load(AssemblyName)"/> can throw.
\r
76 /// <param name="path">
\r
77 /// Path to the directory to scan for assemblies to add to the catalog.
\r
78 /// The path needs to be absolute or relative to <see cref="AppDomain.BaseDirectory"/>
\r
80 /// <param name="searchPattern">
\r
81 /// Any valid searchPattern that <see cref="Directory.GetFiles(string, string)"/> will accept.
\r
83 /// <exception cref="ArgumentException">
\r
84 /// If <paramref name="path"/> is a zero-length string, contains only white space, or
\r
85 /// contains one or more implementation-specific invalid characters. Or <paramref name="searchPattern"/>
\r
86 /// does not contain a valid pattern.
\r
88 /// <exception cref="ArgumentNullException">
\r
89 /// <paramref name="path"/> is <see langword="null"/> or <paramref name="searchPattern"/> is <see langword="null"/>.
\r
91 /// <exception cref="DirectoryNotFoundException">
\r
92 /// The specified <paramref name="path"/> is invalid (for example, it is on an unmapped drive).
\r
94 /// <exception cref="PathTooLongException">
\r
95 /// The specified <paramref name="path"/>, file name, or both exceed the system-defined maximum length.
\r
96 /// For example, on Windows-based platforms, paths must be less than 248 characters and file names must
\r
97 /// be less than 260 characters.
\r
99 /// <exception cref="UnauthorizedAccessException">
\r
100 /// The caller does not have the required permission.
\r
102 public DirectoryCatalog(string path, string searchPattern)
\r
104 Requires.NotNullOrEmpty(path, "path");
\r
105 this.Initialize(path, searchPattern);
\r
109 /// Translated absolute path of the path passed into the constructor of <see cref="DirectoryCatalog"/>.
\r
111 public string FullPath
\r
115 return this._fullPath;
\r
120 /// Set of files that have currently been loaded into the catalog.
\r
122 public ReadOnlyCollection<string> LoadedFiles
\r
126 using (new ReadLock(this._thisLock))
\r
128 return this._loadedFiles;
\r
134 /// Path passed into the constructor of <see cref="DirectoryCatalog"/>.
\r
145 /// Gets the part definitions of the directory catalog.
\r
148 /// A <see cref="IQueryable{T}"/> of <see cref="ComposablePartDefinition"/> objects of the
\r
149 /// <see cref="DirectoryCatalog"/>.
\r
151 /// <exception cref="ObjectDisposedException">
\r
152 /// The <see cref="DirectoryCatalog"/> has been disposed of.
\r
154 public override IQueryable<ComposablePartDefinition> Parts
\r
158 this.ThrowIfDisposed();
\r
159 return this._partsQuery;
\r
164 /// SearchPattern passed into the constructor of <see cref="DirectoryCatalog"/>, or the default *.dll.
\r
166 public string SearchPattern
\r
170 return this._searchPattern;
\r
175 /// Notify when the contents of the Catalog has changed.
\r
177 public event EventHandler<ComposablePartCatalogChangeEventArgs> Changed;
\r
180 /// Notify when the contents of the Catalog has changing.
\r
182 public event EventHandler<ComposablePartCatalogChangeEventArgs> Changing;
\r
185 /// Releases unmanaged and - optionally - managed resources
\r
187 /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
\r
188 protected override void Dispose(bool disposing)
\r
194 if (!this._isDisposed)
\r
196 bool disposeLock = false;
\r
197 ComposablePartCatalogCollection catalogs = null;
\r
201 using (new WriteLock(this._thisLock))
\r
203 if (!this._isDisposed)
\r
205 disposeLock = true;
\r
206 catalogs = this._catalogCollection;
\r
207 this._catalogCollection = null;
\r
208 this._assemblyCatalogs = null;
\r
209 this._isDisposed = true;
\r
215 if (catalogs != null)
\r
217 catalogs.Dispose();
\r
222 this._thisLock.Dispose();
\r
230 base.Dispose(disposing);
\r
235 /// Returns the export definitions that match the constraint defined by the specified definition.
\r
237 /// <param name="definition">
\r
238 /// The <see cref="ImportDefinition"/> that defines the conditions of the
\r
239 /// <see cref="ExportDefinition"/> objects to return.
\r
242 /// An <see cref="IEnumerable{T}"/> of <see cref="Tuple{T1, T2}"/> containing the
\r
243 /// <see cref="ExportDefinition"/> objects and their associated
\r
244 /// <see cref="ComposablePartDefinition"/> for objects that match the constraint defined
\r
245 /// by <paramref name="definition"/>.
\r
247 /// <exception cref="ArgumentNullException">
\r
248 /// <paramref name="definition"/> is <see langword="null"/>.
\r
250 /// <exception cref="ObjectDisposedException">
\r
251 /// The <see cref="DirectoryCatalog"/> has been disposed of.
\r
253 public override IEnumerable<Tuple<ComposablePartDefinition, ExportDefinition>> GetExports(ImportDefinition definition)
\r
255 this.ThrowIfDisposed();
\r
257 Requires.NotNull(definition, "definition");
\r
259 return this._catalogCollection.SelectMany(catalog => catalog.GetExports(definition));
\r
263 /// Raises the <see cref="INotifyComposablePartCatalogChanged.Changed"/> event.
\r
265 /// <param name="e">
\r
266 /// An <see cref="ComposablePartCatalogChangeEventArgs"/> containing the data for the event.
\r
268 protected virtual void OnChanged(ComposablePartCatalogChangeEventArgs e)
\r
270 EventHandler<ComposablePartCatalogChangeEventArgs> changedEvent = this.Changed;
\r
271 if (changedEvent != null)
\r
273 changedEvent(this, e);
\r
278 /// Raises the <see cref="INotifyComposablePartCatalogChanged.Changing"/> event.
\r
280 /// <param name="e">
\r
281 /// An <see cref="ComposablePartCatalogChangeEventArgs"/> containing the data for the event.
\r
283 protected virtual void OnChanging(ComposablePartCatalogChangeEventArgs e)
\r
285 EventHandler<ComposablePartCatalogChangeEventArgs> changingEvent = this.Changing;
\r
286 if (changingEvent != null)
\r
288 changingEvent(this, e);
\r
293 /// Refreshes the <see cref="ComposablePartDefinition"/>s with the latest files in the directory that match
\r
294 /// the searchPattern. If any files have been added they will be added to the catalog and if any files were
\r
295 /// removed they will be removed from the catalog. For files that have been removed keep in mind that the
\r
296 /// assembly cannot be unloaded from the process so <see cref="ComposablePartDefinition"/>s for those files
\r
297 /// will simply be removed from the catalog.
\r
299 /// Possible exceptions that can be thrown are any that <see cref="Directory.GetFiles(string, string)"/> or
\r
300 /// <see cref="Assembly.Load(AssemblyName)"/> can throw.
\r
302 /// <exception cref="DirectoryNotFoundException">
\r
303 /// The specified <paramref name="path"/> has been removed since object construction.
\r
305 public void Refresh()
\r
307 this.ThrowIfDisposed();
\r
308 Assumes.NotNull(this._loadedFiles);
\r
310 List<Tuple<string, AssemblyCatalog>> catalogsToAdd;
\r
311 List<Tuple<string, AssemblyCatalog>> catalogsToRemove;
\r
312 ComposablePartDefinition[] addedDefinitions;
\r
313 ComposablePartDefinition[] removedDefinitions;
\r
314 object changeReferenceObject;
\r
315 string[] afterFiles;
\r
316 string[] beforeFiles;
\r
320 afterFiles = this.GetFiles();
\r
322 using (new ReadLock(this._thisLock))
\r
324 changeReferenceObject = this._loadedFiles;
\r
325 beforeFiles = this._loadedFiles.ToArray();
\r
328 this.DiffChanges(beforeFiles, afterFiles, out catalogsToAdd, out catalogsToRemove);
\r
330 // Don't go any further if there's no work to do
\r
331 if (catalogsToAdd.Count == 0 && catalogsToRemove.Count == 0)
\r
336 // Notify listeners to give them a preview before completeting the changes
\r
337 addedDefinitions = catalogsToAdd
\r
338 .SelectMany(cat => cat.Item2.Parts)
\r
339 .ToArray<ComposablePartDefinition>();
\r
341 removedDefinitions = catalogsToRemove
\r
342 .SelectMany(cat => cat.Item2.Parts)
\r
343 .ToArray<ComposablePartDefinition>();
\r
345 using (var atomicComposition = new AtomicComposition())
\r
347 var changingArgs = new ComposablePartCatalogChangeEventArgs(addedDefinitions, removedDefinitions, atomicComposition);
\r
348 this.OnChanging(changingArgs);
\r
350 // if the change went through then write the catalog changes
\r
351 using (new WriteLock(this._thisLock))
\r
353 if (changeReferenceObject != this._loadedFiles)
\r
355 // Someone updated the list while we were diffing so we need to try the diff again
\r
359 foreach (var catalogToAdd in catalogsToAdd)
\r
361 this._assemblyCatalogs.Add(catalogToAdd.Item1, catalogToAdd.Item2);
\r
362 this._catalogCollection.Add(catalogToAdd.Item2);
\r
365 foreach (var catalogToRemove in catalogsToRemove)
\r
367 this._assemblyCatalogs.Remove(catalogToRemove.Item1);
\r
368 this._catalogCollection.Remove(catalogToRemove.Item2);
\r
371 this._partsQuery = this._catalogCollection.AsQueryable().SelectMany(catalog => catalog.Parts);
\r
372 this._loadedFiles = afterFiles.ToReadOnlyCollection();
\r
374 // Lastly complete any changes added to the atomicComposition during the change event
\r
375 atomicComposition.Complete();
\r
377 // Break out of the while(true)
\r
380 } // AtomicComposition
\r
383 var changedArgs = new ComposablePartCatalogChangeEventArgs(addedDefinitions, removedDefinitions, null);
\r
384 this.OnChanged(changedArgs);
\r
388 /// Returns a string representation of the directory catalog.
\r
391 /// A <see cref="String"/> containing the string representation of the <see cref="DirectoryCatalog"/>.
\r
393 public override string ToString()
\r
395 return GetDisplayName();
\r
398 private AssemblyCatalog CreateAssemblyCatalogGuarded(string assemblyFilePath)
\r
400 Exception exception = null;
\r
404 return new AssemblyCatalog(assemblyFilePath, this);
\r
406 catch (FileNotFoundException ex)
\r
407 { // Files should always exists but don't blow up here if they don't
\r
410 catch (FileLoadException ex)
\r
411 { // File was found but could not be loaded
\r
414 catch (BadImageFormatException ex)
\r
415 { // Dlls that contain native code are not loaded, but do not invalidate the Directory
\r
418 catch (ReflectionTypeLoadException ex)
\r
419 { // Dlls that have missing Managed dependencies are not loaded, but do not invalidate the Directory
\r
423 CompositionTrace.AssemblyLoadFailed(this, assemblyFilePath, exception);
\r
428 private void DiffChanges(string[] beforeFiles, string[] afterFiles,
\r
429 out List<Tuple<string, AssemblyCatalog>> catalogsToAdd,
\r
430 out List<Tuple<string, AssemblyCatalog>> catalogsToRemove)
\r
432 catalogsToAdd = new List<Tuple<string, AssemblyCatalog>>();
\r
433 catalogsToRemove = new List<Tuple<string, AssemblyCatalog>>();
\r
435 IEnumerable<string> filesToAdd = afterFiles.Except(beforeFiles);
\r
436 foreach (string file in filesToAdd)
\r
438 AssemblyCatalog catalog = CreateAssemblyCatalogGuarded(file);
\r
440 if (catalog != null)
\r
442 catalogsToAdd.Add(new Tuple<string, AssemblyCatalog>(file, catalog));
\r
446 IEnumerable<string> filesToRemove = beforeFiles.Except(afterFiles);
\r
447 using (new ReadLock(this._thisLock))
\r
449 foreach (string file in filesToRemove)
\r
451 AssemblyCatalog catalog;
\r
452 if (this._assemblyCatalogs.TryGetValue(file, out catalog))
\r
454 catalogsToRemove.Add(new Tuple<string, AssemblyCatalog>(file, catalog));
\r
460 private string GetDisplayName()
\r
462 return string.Format(CultureInfo.CurrentCulture,
\r
463 "{0} (Path=\"{1}\")", // NOLOC
\r
464 this.GetType().Name,
\r
468 private string[] GetFiles()
\r
470 string[] files = Directory.GetFiles(this._fullPath, this._searchPattern);
\r
471 return Array.ConvertAll<string, string>(files, (file) => file);
\r
474 private static string GetFullPath(string path)
\r
476 if (!IOPath.IsPathRooted(path) && AppDomain.CurrentDomain.BaseDirectory != null)
\r
478 path = IOPath.Combine(AppDomain.CurrentDomain.BaseDirectory, path);
\r
481 return IOPath.GetFullPath(path);
\r
484 private void Initialize(string path, string searchPattern)
\r
487 this._fullPath = GetFullPath(path);
\r
488 this._searchPattern = searchPattern;
\r
489 this._assemblyCatalogs = new Dictionary<string, AssemblyCatalog>();
\r
490 this._catalogCollection = new ComposablePartCatalogCollection(null);
\r
492 this._loadedFiles = GetFiles().ToReadOnlyCollection();
\r
494 foreach (string file in this._loadedFiles)
\r
496 AssemblyCatalog assemblyCatalog = null;
\r
497 assemblyCatalog = CreateAssemblyCatalogGuarded(file);
\r
499 if (assemblyCatalog != null)
\r
501 this._assemblyCatalogs.Add(file, assemblyCatalog);
\r
502 this._catalogCollection.Add(assemblyCatalog);
\r
505 this._partsQuery = this._catalogCollection.AsQueryable().SelectMany(catalog => catalog.Parts);
\r
508 private void ThrowIfDisposed()
\r
510 if (this._isDisposed)
\r
512 throw ExceptionBuilder.CreateObjectDisposed(this);
\r
517 /// Gets the display name of the directory catalog.
\r
520 /// A <see cref="String"/> containing a human-readable display name of the <see cref="DirectoryCatalog"/>.
\r
522 string ICompositionElement.DisplayName
\r
524 get { return this.GetDisplayName(); }
\r
528 /// Gets the composition element from which the directory catalog originated.
\r
531 /// This property always returns <see langword="null"/>.
\r
533 ICompositionElement ICompositionElement.Origin
\r
535 get { return null; }
\r