update MEF to preview 9
[mcs.git] / class / System.ComponentModel.Composition / src / ComponentModel / System / ComponentModel / Composition / Hosting / DirectoryCatalog.cs
blob0b9a8fb9c8a734e7a808ed63e257c82547326890
1 // -----------------------------------------------------------------------
2 // Copyright (c) Microsoft Corporation. All rights reserved.
3 // -----------------------------------------------------------------------
4 #if !SILVERLIGHT
6 using System;
7 using System.Collections.Generic;
8 using System.Collections.ObjectModel;
9 using System.ComponentModel.Composition.Diagnostics;
10 using System.ComponentModel.Composition.Primitives;
11 using System.Diagnostics;
12 using System.Diagnostics.CodeAnalysis;
13 using System.Globalization;
14 using System.IO;
15 using System.Linq;
16 using System.Reflection;
17 using Microsoft.Internal;
18 using Microsoft.Internal.Collections;
20 using IOPath = System.IO.Path;
22 namespace System.ComponentModel.Composition.Hosting
24 [DebuggerTypeProxy(typeof(DirectoryCatalogDebuggerProxy))]
25 public partial class DirectoryCatalog : ComposablePartCatalog, INotifyComposablePartCatalogChanged, ICompositionElement
27 private readonly Lock _thisLock = new Lock();
28 private ComposablePartCatalogCollection _catalogCollection;
29 private Dictionary<string, AssemblyCatalog> _assemblyCatalogs;
30 private volatile bool _isDisposed = false;
31 private string _path;
32 private string _fullPath;
33 private string _searchPattern;
34 private ReadOnlyCollection<string> _loadedFiles;
35 private IQueryable<ComposablePartDefinition> _partsQuery;
37 /// <summary>
38 /// Creates a catalog of <see cref="ComposablePartDefinition"/>s based on all the *.dll files
39 /// in the given directory path.
40 ///
41 /// Possible exceptions that can be thrown are any that <see cref="Directory.GetFiles(string, string)"/> or
42 /// <see cref="Assembly.Load(AssemblyName)"/> can throw.
43 /// </summary>
44 /// <param name="path">
45 /// Path to the directory to scan for assemblies to add to the catalog.
46 /// The path needs to be absolute or relative to <see cref="AppDomain.BaseDirectory"/>
47 /// </param>
48 /// <exception cref="ArgumentException">
49 /// If <paramref name="path"/> is a zero-length string, contains only white space, or
50 /// contains one or more implementation-specific invalid characters.
51 /// </exception>
52 /// <exception cref="ArgumentNullException">
53 /// <paramref name="path"/> is <see langword="null"/>.
54 /// </exception>
55 /// <exception cref="DirectoryNotFoundException">
56 /// The specified <paramref name="path"/> is invalid (for example, it is on an unmapped drive).
57 /// </exception>
58 /// <exception cref="PathTooLongException">
59 /// The specified <paramref name="path"/>, file name, or both exceed the system-defined maximum length.
60 /// For example, on Windows-based platforms, paths must be less than 248 characters and file names must
61 /// be less than 260 characters.
62 /// </exception>
63 /// <exception cref="UnauthorizedAccessException">
64 /// The caller does not have the required permission.
65 /// </exception>
66 public DirectoryCatalog(string path) : this(path, "*.dll")
70 /// <summary>
71 /// Creates a catalog of <see cref="ComposablePartDefinition"/>s based on all the given searchPattern
72 /// over the files in the given directory path.
73 ///
74 /// Possible exceptions that can be thrown are any that <see cref="Directory.GetFiles(string, string)"/> or
75 /// <see cref="Assembly.Load(AssemblyName)"/> can throw.
76 /// </summary>
77 /// <param name="path">
78 /// Path to the directory to scan for assemblies to add to the catalog.
79 /// The path needs to be absolute or relative to <see cref="AppDomain.BaseDirectory"/>
80 /// </param>
81 /// <param name="searchPattern">
82 /// Any valid searchPattern that <see cref="Directory.GetFiles(string, string)"/> will accept.
83 /// </param>
84 /// <exception cref="ArgumentException">
85 /// If <paramref name="path"/> is a zero-length string, contains only white space, or
86 /// contains one or more implementation-specific invalid characters. Or <paramref name="searchPattern"/>
87 /// does not contain a valid pattern.
88 /// </exception>
89 /// <exception cref="ArgumentNullException">
90 /// <paramref name="path"/> is <see langword="null"/> or <paramref name="searchPattern"/> is <see langword="null"/>.
91 /// </exception>
92 /// <exception cref="DirectoryNotFoundException">
93 /// The specified <paramref name="path"/> is invalid (for example, it is on an unmapped drive).
94 /// </exception>
95 /// <exception cref="PathTooLongException">
96 /// The specified <paramref name="path"/>, file name, or both exceed the system-defined maximum length.
97 /// For example, on Windows-based platforms, paths must be less than 248 characters and file names must
98 /// be less than 260 characters.
99 /// </exception>
100 /// <exception cref="UnauthorizedAccessException">
101 /// The caller does not have the required permission.
102 /// </exception>
103 public DirectoryCatalog(string path, string searchPattern)
105 Requires.NotNullOrEmpty(path, "path");
106 this.Initialize(path, searchPattern);
109 /// <summary>
110 /// Translated absolute path of the path passed into the constructor of <see cref="DirectoryCatalog"/>.
111 /// </summary>
112 public string FullPath
116 return this._fullPath;
120 /// <summary>
121 /// Set of files that have currently been loaded into the catalog.
122 /// </summary>
123 public ReadOnlyCollection<string> LoadedFiles
127 using (new ReadLock(this._thisLock))
129 return this._loadedFiles;
134 /// <summary>
135 /// Path passed into the constructor of <see cref="DirectoryCatalog"/>.
136 /// </summary>
137 public string Path
141 return this._path;
145 /// <summary>
146 /// Gets the part definitions of the directory catalog.
147 /// </summary>
148 /// <value>
149 /// A <see cref="IQueryable{T}"/> of <see cref="ComposablePartDefinition"/> objects of the
150 /// <see cref="DirectoryCatalog"/>.
151 /// </value>
152 /// <exception cref="ObjectDisposedException">
153 /// The <see cref="DirectoryCatalog"/> has been disposed of.
154 /// </exception>
155 public override IQueryable<ComposablePartDefinition> Parts
159 this.ThrowIfDisposed();
160 return this._partsQuery;
164 /// <summary>
165 /// SearchPattern passed into the constructor of <see cref="DirectoryCatalog"/>, or the default *.dll.
166 /// </summary>
167 public string SearchPattern
171 return this._searchPattern;
175 /// <summary>
176 /// Notify when the contents of the Catalog has changed.
177 /// </summary>
178 public event EventHandler<ComposablePartCatalogChangeEventArgs> Changed;
180 /// <summary>
181 /// Notify when the contents of the Catalog has changing.
182 /// </summary>
183 public event EventHandler<ComposablePartCatalogChangeEventArgs> Changing;
185 /// <summary>
186 /// Releases unmanaged and - optionally - managed resources
187 /// </summary>
188 /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
189 protected override void Dispose(bool disposing)
193 if (disposing)
195 if (!this._isDisposed)
197 bool disposeLock = false;
198 ComposablePartCatalogCollection catalogs = null;
202 using (new WriteLock(this._thisLock))
204 if (!this._isDisposed)
206 disposeLock = true;
207 catalogs = this._catalogCollection;
208 this._catalogCollection = null;
209 this._assemblyCatalogs = null;
210 this._isDisposed = true;
214 finally
216 if (catalogs != null)
218 catalogs.Dispose();
221 if (disposeLock)
223 this._thisLock.Dispose();
229 finally
231 base.Dispose(disposing);
235 /// <summary>
236 /// Returns the export definitions that match the constraint defined by the specified definition.
237 /// </summary>
238 /// <param name="definition">
239 /// The <see cref="ImportDefinition"/> that defines the conditions of the
240 /// <see cref="ExportDefinition"/> objects to return.
241 /// </param>
242 /// <returns>
243 /// An <see cref="IEnumerable{T}"/> of <see cref="Tuple{T1, T2}"/> containing the
244 /// <see cref="ExportDefinition"/> objects and their associated
245 /// <see cref="ComposablePartDefinition"/> for objects that match the constraint defined
246 /// by <paramref name="definition"/>.
247 /// </returns>
248 /// <exception cref="ArgumentNullException">
249 /// <paramref name="definition"/> is <see langword="null"/>.
250 /// </exception>
251 /// <exception cref="ObjectDisposedException">
252 /// The <see cref="DirectoryCatalog"/> has been disposed of.
253 /// </exception>
254 public override IEnumerable<Tuple<ComposablePartDefinition, ExportDefinition>> GetExports(ImportDefinition definition)
256 this.ThrowIfDisposed();
258 Requires.NotNull(definition, "definition");
260 return this._catalogCollection.SelectMany(catalog => catalog.GetExports(definition));
263 /// <summary>
264 /// Raises the <see cref="INotifyComposablePartCatalogChanged.Changed"/> event.
265 /// </summary>
266 /// <param name="e">
267 /// An <see cref="ComposablePartCatalogChangeEventArgs"/> containing the data for the event.
268 /// </param>
269 protected virtual void OnChanged(ComposablePartCatalogChangeEventArgs e)
271 EventHandler<ComposablePartCatalogChangeEventArgs> changedEvent = this.Changed;
272 if (changedEvent != null)
274 changedEvent(this, e);
278 /// <summary>
279 /// Raises the <see cref="INotifyComposablePartCatalogChanged.Changing"/> event.
280 /// </summary>
281 /// <param name="e">
282 /// An <see cref="ComposablePartCatalogChangeEventArgs"/> containing the data for the event.
283 /// </param>
284 protected virtual void OnChanging(ComposablePartCatalogChangeEventArgs e)
286 EventHandler<ComposablePartCatalogChangeEventArgs> changingEvent = this.Changing;
287 if (changingEvent != null)
289 changingEvent(this, e);
293 /// <summary>
294 /// Refreshes the <see cref="ComposablePartDefinition"/>s with the latest files in the directory that match
295 /// the searchPattern. If any files have been added they will be added to the catalog and if any files were
296 /// removed they will be removed from the catalog. For files that have been removed keep in mind that the
297 /// assembly cannot be unloaded from the process so <see cref="ComposablePartDefinition"/>s for those files
298 /// will simply be removed from the catalog.
299 ///
300 /// Possible exceptions that can be thrown are any that <see cref="Directory.GetFiles(string, string)"/> or
301 /// <see cref="Assembly.Load(AssemblyName)"/> can throw.
302 /// </summary>
303 /// <exception cref="DirectoryNotFoundException">
304 /// The specified <paramref name="path"/> has been removed since object construction.
305 /// </exception>
306 public void Refresh()
308 this.ThrowIfDisposed();
309 Assumes.NotNull(this._loadedFiles);
311 List<Tuple<string, AssemblyCatalog>> catalogsToAdd;
312 List<Tuple<string, AssemblyCatalog>> catalogsToRemove;
313 ComposablePartDefinition[] addedDefinitions;
314 ComposablePartDefinition[] removedDefinitions;
315 object changeReferenceObject;
316 string[] afterFiles;
317 string[] beforeFiles;
319 while (true)
321 afterFiles = this.GetFiles();
323 using (new ReadLock(this._thisLock))
325 changeReferenceObject = this._loadedFiles;
326 beforeFiles = this._loadedFiles.ToArray();
329 this.DiffChanges(beforeFiles, afterFiles, out catalogsToAdd, out catalogsToRemove);
331 // Don't go any further if there's no work to do
332 if (catalogsToAdd.Count == 0 && catalogsToRemove.Count == 0)
334 return;
337 // Notify listeners to give them a preview before completeting the changes
338 addedDefinitions = catalogsToAdd
339 .SelectMany(cat => cat.Item2.Parts)
340 .ToArray<ComposablePartDefinition>();
342 removedDefinitions = catalogsToRemove
343 .SelectMany(cat => cat.Item2.Parts)
344 .ToArray<ComposablePartDefinition>();
346 using (var atomicComposition = new AtomicComposition())
348 var changingArgs = new ComposablePartCatalogChangeEventArgs(addedDefinitions, removedDefinitions, atomicComposition);
349 this.OnChanging(changingArgs);
351 // if the change went through then write the catalog changes
352 using (new WriteLock(this._thisLock))
354 if (changeReferenceObject != this._loadedFiles)
356 // Someone updated the list while we were diffing so we need to try the diff again
357 continue;
360 foreach (var catalogToAdd in catalogsToAdd)
362 this._assemblyCatalogs.Add(catalogToAdd.Item1, catalogToAdd.Item2);
363 this._catalogCollection.Add(catalogToAdd.Item2);
366 foreach (var catalogToRemove in catalogsToRemove)
368 this._assemblyCatalogs.Remove(catalogToRemove.Item1);
369 this._catalogCollection.Remove(catalogToRemove.Item2);
372 this._partsQuery = this._catalogCollection.AsQueryable().SelectMany(catalog => catalog.Parts);
373 this._loadedFiles = afterFiles.ToReadOnlyCollection();
375 // Lastly complete any changes added to the atomicComposition during the change event
376 atomicComposition.Complete();
378 // Break out of the while(true)
379 break;
380 } // WriteLock
381 } // AtomicComposition
382 } // while (true)
384 var changedArgs = new ComposablePartCatalogChangeEventArgs(addedDefinitions, removedDefinitions, null);
385 this.OnChanged(changedArgs);
388 /// <summary>
389 /// Returns a string representation of the directory catalog.
390 /// </summary>
391 /// <returns>
392 /// A <see cref="String"/> containing the string representation of the <see cref="DirectoryCatalog"/>.
393 /// </returns>
394 public override string ToString()
396 return GetDisplayName();
399 private AssemblyCatalog CreateAssemblyCatalogGuarded(string assemblyFilePath)
401 Exception exception = null;
405 return new AssemblyCatalog(assemblyFilePath, this);
407 catch (FileNotFoundException ex)
408 { // Files should always exists but don't blow up here if they don't
409 exception = ex;
411 catch (FileLoadException ex)
412 { // File was found but could not be loaded
413 exception = ex;
415 catch (BadImageFormatException ex)
416 { // Dlls that contain native code are not loaded, but do not invalidate the Directory
417 exception = ex;
419 catch (ReflectionTypeLoadException ex)
420 { // Dlls that have missing Managed dependencies are not loaded, but do not invalidate the Directory
421 exception = ex;
424 CompositionTrace.AssemblyLoadFailed(this, assemblyFilePath, exception);
426 return null;
429 private void DiffChanges(string[] beforeFiles, string[] afterFiles,
430 out List<Tuple<string, AssemblyCatalog>> catalogsToAdd,
431 out List<Tuple<string, AssemblyCatalog>> catalogsToRemove)
433 catalogsToAdd = new List<Tuple<string, AssemblyCatalog>>();
434 catalogsToRemove = new List<Tuple<string, AssemblyCatalog>>();
436 IEnumerable<string> filesToAdd = afterFiles.Except(beforeFiles);
437 foreach (string file in filesToAdd)
439 AssemblyCatalog catalog = CreateAssemblyCatalogGuarded(file);
441 if (catalog != null)
443 catalogsToAdd.Add(new Tuple<string, AssemblyCatalog>(file, catalog));
447 IEnumerable<string> filesToRemove = beforeFiles.Except(afterFiles);
448 using (new ReadLock(this._thisLock))
450 foreach (string file in filesToRemove)
452 AssemblyCatalog catalog;
453 if (this._assemblyCatalogs.TryGetValue(file, out catalog))
455 catalogsToRemove.Add(new Tuple<string, AssemblyCatalog>(file, catalog));
461 private string GetDisplayName()
463 return string.Format(CultureInfo.CurrentCulture,
464 "{0} (Path=\"{1}\")", // NOLOC
465 this.GetType().Name,
466 this._path);
469 private string[] GetFiles()
471 string[] files = Directory.GetFiles(this._fullPath, this._searchPattern);
472 return Array.ConvertAll<string, string>(files, (file) => file.ToUpperInvariant());
475 private static string GetFullPath(string path)
477 if (!IOPath.IsPathRooted(path) && AppDomain.CurrentDomain.BaseDirectory != null)
479 path = IOPath.Combine(AppDomain.CurrentDomain.BaseDirectory, path);
482 return IOPath.GetFullPath(path).ToUpperInvariant();
485 private void Initialize(string path, string searchPattern)
487 this._path = path;
488 this._fullPath = GetFullPath(path);
489 this._searchPattern = searchPattern;
490 this._assemblyCatalogs = new Dictionary<string, AssemblyCatalog>();
491 this._catalogCollection = new ComposablePartCatalogCollection(null);
493 this._loadedFiles = GetFiles().ToReadOnlyCollection();
495 foreach (string file in this._loadedFiles)
497 AssemblyCatalog assemblyCatalog = null;
498 assemblyCatalog = CreateAssemblyCatalogGuarded(file);
500 if (assemblyCatalog != null)
502 this._assemblyCatalogs.Add(file, assemblyCatalog);
503 this._catalogCollection.Add(assemblyCatalog);
506 this._partsQuery = this._catalogCollection.AsQueryable().SelectMany(catalog => catalog.Parts);
509 private void ThrowIfDisposed()
511 if (this._isDisposed)
513 throw ExceptionBuilder.CreateObjectDisposed(this);
517 /// <summary>
518 /// Gets the display name of the directory catalog.
519 /// </summary>
520 /// <value>
521 /// A <see cref="String"/> containing a human-readable display name of the <see cref="DirectoryCatalog"/>.
522 /// </value>
523 [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
524 string ICompositionElement.DisplayName
526 get { return this.GetDisplayName(); }
529 /// <summary>
530 /// Gets the composition element from which the directory catalog originated.
531 /// </summary>
532 /// <value>
533 /// This property always returns <see langword="null"/>.
534 /// </value>
535 [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
536 ICompositionElement ICompositionElement.Origin
538 get { return null; }
543 #endif