New test.
[mono.git] / mcs / class / System.ComponentModel.Composition / src / ComponentModel / System / ComponentModel / Composition / Hosting / DirectoryCatalog.cs
1 // -----------------------------------------------------------------------\r
2 // Copyright (c) Microsoft Corporation.  All rights reserved.\r
3 // -----------------------------------------------------------------------\r
4 #if !SILVERLIGHT\r
5 \r
6 using System;\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
13 using System.IO;\r
14 using System.Linq;\r
15 using System.Reflection;\r
16 using Microsoft.Internal;\r
17 using Microsoft.Internal.Collections;\r
18 \r
19 using IOPath = System.IO.Path;\r
20 \r
21 namespace System.ComponentModel.Composition.Hosting\r
22 {\r
23     [DebuggerTypeProxy(typeof(DirectoryCatalogDebuggerProxy))]\r
24     public partial class DirectoryCatalog : ComposablePartCatalog, INotifyComposablePartCatalogChanged, ICompositionElement\r
25     {\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
35 \r
36         /// <summary>\r
37         ///     Creates a catalog of <see cref="ComposablePartDefinition"/>s based on all the *.dll files \r
38         ///     in the given directory path.\r
39         ///     \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
42         /// </summary>\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
46         /// </param>\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
50         /// </exception>\r
51         /// <exception cref="ArgumentNullException">\r
52         ///     <paramref name="path"/> is <see langword="null"/>.\r
53         /// </exception>\r
54         /// <exception cref="DirectoryNotFoundException">\r
55         ///     The specified <paramref name="path"/> is invalid (for example, it is on an unmapped drive). \r
56         /// </exception>\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
61         /// </exception>\r
62         /// <exception cref="UnauthorizedAccessException">\r
63         ///     The caller does not have the required permission. \r
64         /// </exception>\r
65         public DirectoryCatalog(string path) : this(path, "*.dll")\r
66         {\r
67         }\r
68 \r
69         /// <summary>\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
72         ///     \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
75         /// </summary>\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
79         /// </param>\r
80         /// <param name="searchPattern">\r
81         ///     Any valid searchPattern that <see cref="Directory.GetFiles(string, string)"/> will accept.\r
82         /// </param>\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
87         /// </exception>\r
88         /// <exception cref="ArgumentNullException">\r
89         ///     <paramref name="path"/> is <see langword="null"/> or <paramref name="searchPattern"/> is <see langword="null"/>.\r
90         /// </exception>\r
91         /// <exception cref="DirectoryNotFoundException">\r
92         ///     The specified <paramref name="path"/> is invalid (for example, it is on an unmapped drive). \r
93         /// </exception>\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
98         /// </exception>\r
99         /// <exception cref="UnauthorizedAccessException">\r
100         ///     The caller does not have the required permission. \r
101         /// </exception>\r
102         public DirectoryCatalog(string path, string searchPattern)\r
103         {\r
104             Requires.NotNullOrEmpty(path, "path");\r
105             this.Initialize(path, searchPattern);\r
106         }\r
107 \r
108         /// <summary>\r
109         ///     Translated absolute path of the path passed into the constructor of <see cref="DirectoryCatalog"/>. \r
110         /// </summary>\r
111         public string FullPath\r
112         {\r
113             get\r
114             {\r
115                 return this._fullPath;\r
116             }\r
117         }\r
118 \r
119         /// <summary>\r
120         ///     Set of files that have currently been loaded into the catalog.\r
121         /// </summary>\r
122         public ReadOnlyCollection<string> LoadedFiles\r
123         {\r
124             get\r
125             {\r
126                 using (new ReadLock(this._thisLock))\r
127                 {\r
128                     return this._loadedFiles;\r
129                 }\r
130             }\r
131         }\r
132 \r
133         /// <summary>\r
134         ///     Path passed into the constructor of <see cref="DirectoryCatalog"/>.\r
135         /// </summary>\r
136         public string Path\r
137         {\r
138             get\r
139             {\r
140                 return this._path;\r
141             }\r
142         }\r
143 \r
144         /// <summary>\r
145         ///     Gets the part definitions of the directory catalog.\r
146         /// </summary>\r
147         /// <value>\r
148         ///     A <see cref="IQueryable{T}"/> of <see cref="ComposablePartDefinition"/> objects of the \r
149         ///     <see cref="DirectoryCatalog"/>.\r
150         /// </value>\r
151         /// <exception cref="ObjectDisposedException">\r
152         ///     The <see cref="DirectoryCatalog"/> has been disposed of.\r
153         /// </exception>\r
154         public override IQueryable<ComposablePartDefinition> Parts\r
155         {\r
156             get\r
157             {\r
158                 this.ThrowIfDisposed();\r
159                 return this._partsQuery;\r
160             }\r
161         }\r
162 \r
163         /// <summary>\r
164         ///   SearchPattern passed into the constructor of <see cref="DirectoryCatalog"/>, or the default *.dll.\r
165         /// </summary>\r
166         public string SearchPattern\r
167         {\r
168             get\r
169             {\r
170                 return this._searchPattern;\r
171             }\r
172         }\r
173 \r
174         /// <summary>\r
175         /// Notify when the contents of the Catalog has changed.\r
176         /// </summary>\r
177         public event EventHandler<ComposablePartCatalogChangeEventArgs> Changed;\r
178 \r
179         /// <summary>\r
180         /// Notify when the contents of the Catalog has changing.\r
181         /// </summary>\r
182         public event EventHandler<ComposablePartCatalogChangeEventArgs> Changing;\r
183 \r
184         /// <summary>\r
185         /// Releases unmanaged and - optionally - managed resources\r
186         /// </summary>\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
189         {\r
190             try\r
191             {\r
192                 if (disposing)\r
193                 {\r
194                     if (!this._isDisposed)\r
195                     {\r
196                         bool disposeLock = false;\r
197                         ComposablePartCatalogCollection catalogs = null;\r
198 \r
199                         try\r
200                         {\r
201                             using (new WriteLock(this._thisLock))\r
202                             {\r
203                                 if (!this._isDisposed)\r
204                                 {\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
210                                 }\r
211                             }\r
212                         }\r
213                         finally\r
214                         {\r
215                             if (catalogs != null)\r
216                             {\r
217                                 catalogs.Dispose();\r
218                             }\r
219 \r
220                             if (disposeLock)\r
221                             {\r
222                                 this._thisLock.Dispose();\r
223                             }\r
224                         }\r
225                     }\r
226                 }\r
227             }\r
228             finally\r
229             {\r
230                 base.Dispose(disposing);\r
231             }\r
232         }\r
233 \r
234         /// <summary>\r
235         ///     Returns the export definitions that match the constraint defined by the specified definition.\r
236         /// </summary>\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
240         /// </param>\r
241         /// <returns>\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
246         /// </returns>\r
247         /// <exception cref="ArgumentNullException">\r
248         ///     <paramref name="definition"/> is <see langword="null"/>.\r
249         /// </exception>\r
250         /// <exception cref="ObjectDisposedException">\r
251         ///     The <see cref="DirectoryCatalog"/> has been disposed of.\r
252         /// </exception>\r
253         public override IEnumerable<Tuple<ComposablePartDefinition, ExportDefinition>> GetExports(ImportDefinition definition)\r
254         {\r
255             this.ThrowIfDisposed();\r
256 \r
257             Requires.NotNull(definition, "definition");\r
258 \r
259             return this._catalogCollection.SelectMany(catalog => catalog.GetExports(definition));\r
260         }\r
261 \r
262         /// <summary>\r
263         ///     Raises the <see cref="INotifyComposablePartCatalogChanged.Changed"/> event.\r
264         /// </summary>\r
265         /// <param name="e">\r
266         ///     An <see cref="ComposablePartCatalogChangeEventArgs"/> containing the data for the event.\r
267         /// </param>\r
268         protected virtual void OnChanged(ComposablePartCatalogChangeEventArgs e)\r
269         {\r
270             EventHandler<ComposablePartCatalogChangeEventArgs> changedEvent = this.Changed;\r
271             if (changedEvent != null)\r
272             {\r
273                 changedEvent(this, e);\r
274             }\r
275         }\r
276 \r
277         /// <summary>\r
278         ///     Raises the <see cref="INotifyComposablePartCatalogChanged.Changing"/> event.\r
279         /// </summary>\r
280         /// <param name="e">\r
281         ///     An <see cref="ComposablePartCatalogChangeEventArgs"/> containing the data for the event.\r
282         /// </param>\r
283         protected virtual void OnChanging(ComposablePartCatalogChangeEventArgs e)\r
284         {\r
285             EventHandler<ComposablePartCatalogChangeEventArgs> changingEvent = this.Changing;\r
286             if (changingEvent != null)\r
287             {\r
288                 changingEvent(this, e);\r
289             }\r
290         }\r
291 \r
292         /// <summary>\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
298         /// \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
301         /// </summary>\r
302         /// <exception cref="DirectoryNotFoundException">\r
303         ///     The specified <paramref name="path"/> has been removed since object construction.\r
304         /// </exception>\r
305         public void Refresh()\r
306         {\r
307             this.ThrowIfDisposed();\r
308             Assumes.NotNull(this._loadedFiles);\r
309 \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
317 \r
318             while (true)\r
319             {\r
320                 afterFiles = this.GetFiles();\r
321 \r
322                 using (new ReadLock(this._thisLock))\r
323                 {\r
324                     changeReferenceObject = this._loadedFiles;\r
325                     beforeFiles = this._loadedFiles.ToArray();\r
326                 }\r
327 \r
328                 this.DiffChanges(beforeFiles, afterFiles, out catalogsToAdd, out catalogsToRemove);\r
329 \r
330                 // Don't go any further if there's no work to do\r
331                 if (catalogsToAdd.Count == 0 && catalogsToRemove.Count == 0)\r
332                 {\r
333                     return;\r
334                 }\r
335 \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
340 \r
341                 removedDefinitions = catalogsToRemove\r
342                     .SelectMany(cat => cat.Item2.Parts)\r
343                     .ToArray<ComposablePartDefinition>();\r
344 \r
345                 using (var atomicComposition = new AtomicComposition())\r
346                 {\r
347                     var changingArgs = new ComposablePartCatalogChangeEventArgs(addedDefinitions, removedDefinitions, atomicComposition);\r
348                     this.OnChanging(changingArgs);\r
349        \r
350                     // if the change went through then write the catalog changes\r
351                     using (new WriteLock(this._thisLock))\r
352                     {\r
353                         if (changeReferenceObject != this._loadedFiles)\r
354                         {\r
355                             // Someone updated the list while we were diffing so we need to try the diff again\r
356                             continue;\r
357                         }\r
358 \r
359                         foreach (var catalogToAdd in catalogsToAdd)\r
360                         {\r
361                             this._assemblyCatalogs.Add(catalogToAdd.Item1, catalogToAdd.Item2);\r
362                             this._catalogCollection.Add(catalogToAdd.Item2);\r
363                         }\r
364 \r
365                         foreach (var catalogToRemove in catalogsToRemove)\r
366                         {\r
367                             this._assemblyCatalogs.Remove(catalogToRemove.Item1);\r
368                             this._catalogCollection.Remove(catalogToRemove.Item2);\r
369                         }\r
370 \r
371                         this._partsQuery = this._catalogCollection.AsQueryable().SelectMany(catalog => catalog.Parts);\r
372                         this._loadedFiles = afterFiles.ToReadOnlyCollection();\r
373 \r
374                         // Lastly complete any changes added to the atomicComposition during the change event\r
375                         atomicComposition.Complete();\r
376 \r
377                         // Break out of the while(true)\r
378                         break;\r
379                     } // WriteLock\r
380                 } // AtomicComposition\r
381             }   // while (true)\r
382 \r
383             var changedArgs = new ComposablePartCatalogChangeEventArgs(addedDefinitions, removedDefinitions, null);\r
384             this.OnChanged(changedArgs);\r
385         }\r
386 \r
387         /// <summary>\r
388         ///     Returns a string representation of the directory catalog.\r
389         /// </summary>\r
390         /// <returns>\r
391         ///     A <see cref="String"/> containing the string representation of the <see cref="DirectoryCatalog"/>.\r
392         /// </returns>\r
393         public override string ToString()\r
394         {\r
395             return GetDisplayName();\r
396         }\r
397 \r
398         private AssemblyCatalog CreateAssemblyCatalogGuarded(string assemblyFilePath)\r
399         {\r
400             Exception exception = null;\r
401 \r
402             try\r
403             {\r
404                 return new AssemblyCatalog(assemblyFilePath, this);\r
405             }\r
406             catch (FileNotFoundException ex)\r
407             {   // Files should always exists but don't blow up here if they don't\r
408                 exception = ex;\r
409             }\r
410             catch (FileLoadException ex)\r
411             {   // File was found but could not be loaded\r
412                 exception = ex;\r
413             }\r
414             catch (BadImageFormatException ex)\r
415             {   // Dlls that contain native code are not loaded, but do not invalidate the Directory\r
416                 exception = ex;\r
417             }\r
418             catch (ReflectionTypeLoadException ex)\r
419             {   // Dlls that have missing Managed dependencies are not loaded, but do not invalidate the Directory \r
420                 exception = ex;\r
421             }\r
422 \r
423             CompositionTrace.AssemblyLoadFailed(this, assemblyFilePath, exception);\r
424 \r
425             return null;\r
426         }\r
427 \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
431         {\r
432             catalogsToAdd = new List<Tuple<string, AssemblyCatalog>>();\r
433             catalogsToRemove = new List<Tuple<string, AssemblyCatalog>>();\r
434 \r
435             IEnumerable<string> filesToAdd = afterFiles.Except(beforeFiles);\r
436             foreach (string file in filesToAdd)\r
437             {\r
438                 AssemblyCatalog catalog = CreateAssemblyCatalogGuarded(file);\r
439 \r
440                 if (catalog != null)\r
441                 {\r
442                     catalogsToAdd.Add(new Tuple<string, AssemblyCatalog>(file, catalog));\r
443                 }\r
444             }\r
445 \r
446             IEnumerable<string> filesToRemove = beforeFiles.Except(afterFiles);\r
447             using (new ReadLock(this._thisLock))\r
448             {\r
449                 foreach (string file in filesToRemove)\r
450                 {\r
451                     AssemblyCatalog catalog;\r
452                     if (this._assemblyCatalogs.TryGetValue(file, out catalog))\r
453                     {\r
454                         catalogsToRemove.Add(new Tuple<string, AssemblyCatalog>(file, catalog));\r
455                     }\r
456                 }\r
457             }\r
458         }\r
459 \r
460         private string GetDisplayName()\r
461         {\r
462             return string.Format(CultureInfo.CurrentCulture,\r
463                                 "{0} (Path=\"{1}\")",   // NOLOC\r
464                                 this.GetType().Name,\r
465                                 this._path);\r
466         }\r
467 \r
468         private string[] GetFiles()\r
469         {\r
470             string[] files = Directory.GetFiles(this._fullPath, this._searchPattern);\r
471             return Array.ConvertAll<string, string>(files, (file) => file);\r
472         }\r
473 \r
474         private static string GetFullPath(string path)\r
475         {\r
476             if (!IOPath.IsPathRooted(path) && AppDomain.CurrentDomain.BaseDirectory != null)\r
477             {\r
478                 path = IOPath.Combine(AppDomain.CurrentDomain.BaseDirectory, path);\r
479             }\r
480 \r
481             return IOPath.GetFullPath(path);\r
482         }\r
483 \r
484         private void Initialize(string path, string searchPattern)\r
485         {\r
486             this._path = path;\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
491 \r
492             this._loadedFiles = GetFiles().ToReadOnlyCollection();\r
493 \r
494             foreach (string file in this._loadedFiles)\r
495             {\r
496                 AssemblyCatalog assemblyCatalog = null;\r
497                 assemblyCatalog = CreateAssemblyCatalogGuarded(file);\r
498 \r
499                 if (assemblyCatalog != null)\r
500                 {\r
501                     this._assemblyCatalogs.Add(file, assemblyCatalog);\r
502                     this._catalogCollection.Add(assemblyCatalog);\r
503                 }\r
504             }\r
505             this._partsQuery = this._catalogCollection.AsQueryable().SelectMany(catalog => catalog.Parts);\r
506         }\r
507 \r
508         private void ThrowIfDisposed()\r
509         {\r
510             if (this._isDisposed)\r
511             {\r
512                 throw ExceptionBuilder.CreateObjectDisposed(this);\r
513             }\r
514         }\r
515        \r
516         /// <summary>\r
517         ///     Gets the display name of the directory catalog.\r
518         /// </summary>\r
519         /// <value>\r
520         ///     A <see cref="String"/> containing a human-readable display name of the <see cref="DirectoryCatalog"/>.\r
521         /// </value>\r
522         string ICompositionElement.DisplayName\r
523         {\r
524             get { return this.GetDisplayName(); }\r
525         }\r
526 \r
527         /// <summary>\r
528         ///     Gets the composition element from which the directory catalog originated.\r
529         /// </summary>\r
530         /// <value>\r
531         ///     This property always returns <see langword="null"/>.\r
532         /// </value>\r
533         ICompositionElement ICompositionElement.Origin\r
534         {\r
535             get { return null; }\r
536         }\r
537     }\r
538 }\r
539 \r
540 #endif\r