// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// -----------------------------------------------------------------------
#if !SILVERLIGHT
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition.Diagnostics;
using System.ComponentModel.Composition.Primitives;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.Internal;
using Microsoft.Internal.Collections;
using IOPathSystem.IO.Path;
namespace System.ComponentModel.Composition.Hosting{
[DebuggerTypeProxy(typeof(DirectoryCatalogDebuggerProxy))]
public partial class DirectoryCatalog : ComposablePartCatalog, INotifyComposablePartCatalogChanged, ICompositionElement
{
private readonly Lock _thisLock = new Lock();
private ComposablePartCatalogCollection _catalogCollection;
private Dictionary<string, AssemblyCatalog> _assemblyCatalogs;
private volatile bool _isDisposed = false;
private string _path;
private string _fullPath;
private string _searchPattern;
private ReadOnlyCollection<string> _loadedFiles;
private IQueryable<ComposablePartDefinition> _partsQuery;
/// <summary>
/// Creates a catalog of <see cref="ComposablePartDefinition"/>s based on all the *.dll files
/// in the given directory path.
///
/// Possible exceptions that can be thrown are any that <see cref="Directory.GetFiles(string, string)"/> or
/// <see cref="Assembly.Load(AssemblyName)"/> can throw.
/// </summary>
/// <param name="path">
/// Path to the directory to scan for assemblies to add to the catalog.
/// The path needs to be absolute or relative to <see cref="AppDomain.BaseDirectory"/>
/// </param>
/// <exception cref="ArgumentException">
/// If <paramref name="path"/> is a zero-length string, contains only white space, or
/// contains one or more implementation-specific invalid characters.
/// </exception>
/// <exception cref="ArgumentNullException">
/// <paramref name="path"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="DirectoryNotFoundException">
/// The specified <paramref name="path"/> is invalid (for example, it is on an unmapped drive).
/// </exception>
/// <exception cref="PathTooLongException">
/// The specified <paramref name="path"/>, file name, or both exceed the system-defined maximum length.
/// For example, on Windows-based platforms, paths must be less than 248 characters and file names must
/// be less than 260 characters.
/// </exception>
/// <exception cref="UnauthorizedAccessException">
/// The caller does not have the required permission.
/// </exception>
public DirectoryCatalog(string path) : this(path, "*.dll")
{
}
/// <summary>
/// Creates a catalog of <see cref="ComposablePartDefinition"/>s based on all the given searchPattern
/// over the files in the given directory path.
///
/// Possible exceptions that can be thrown are any that <see cref="Directory.GetFiles(string, string)"/> or
/// <see cref="Assembly.Load(AssemblyName)"/> can throw.
/// </summary>
/// <param name="path">
/// Path to the directory to scan for assemblies to add to the catalog.
/// The path needs to be absolute or relative to <see cref="AppDomain.BaseDirectory"/>
/// </param>
/// <param name="searchPattern">
/// Any valid searchPattern that <see cref="Directory.GetFiles(string, string)"/> will accept.
/// </param>
/// <exception cref="ArgumentException">
/// If <paramref name="path"/> is a zero-length string, contains only white space, or
/// contains one or more implementation-specific invalid characters. Or <paramref name="searchPattern"/>
/// does not contain a valid pattern.
/// </exception>
/// <exception cref="ArgumentNullException">
/// <paramref name="path"/> is <see langword="null"/> or <paramref name="searchPattern"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="DirectoryNotFoundException">
/// The specified <paramref name="path"/> is invalid (for example, it is on an unmapped drive).
/// </exception>
/// <exception cref="PathTooLongException">
/// The specified <paramref name="path"/>, file name, or both exceed the system-defined maximum length.
/// For example, on Windows-based platforms, paths must be less than 248 characters and file names must
/// be less than 260 characters.
/// </exception>
/// <exception cref="UnauthorizedAccessException">
/// The caller does not have the required permission.
/// </exception>
public DirectoryCatalog(string path, string searchPattern)
{
Requires.NotNullOrEmpty(path, "path");
this.Initialize(path, searchPattern);
}
/// <summary>
/// Translated absolute path of the path passed into the constructor of <see cref="DirectoryCatalog"/>.
/// </summary>
public string FullPath
{
get
{
return this._fullPath;
}
}
/// <summary>
/// Set of files that have currently been loaded into the catalog.
/// </summary>
public ReadOnlyCollection<string> LoadedFiles
{
get
{
using (new ReadLock(this._thisLock))
{
return this._loadedFiles;
}
}
}
/// <summary>
/// Path passed into the constructor of <see cref="DirectoryCatalog"/>.
/// </summary>
public string Path
{
get
{
return this._path;
}
}
/// <summary>
/// Gets the part definitions of the directory catalog.
/// </summary>
/// <value>
/// A <see cref="IQueryable{T}"/> of <see cref="ComposablePartDefinition"/> objects of the
/// <see cref="DirectoryCatalog"/>.
/// </value>
/// <exception cref="ObjectDisposedException">
/// The <see cref="DirectoryCatalog"/> has been disposed of.
/// </exception>
public override IQueryable<ComposablePartDefinition> Parts
{
get
{
this.ThrowIfDisposed();
return this._partsQuery;
}
}
/// <summary>
/// SearchPattern passed into the constructor of <see cref="DirectoryCatalog"/>, or the default *.dll.
/// </summary>
public string SearchPattern
{
get
{
return this._searchPattern;
}
}
/// <summary>
/// Notify when the contents of the Catalog has changed.
/// </summary>
public event EventHandler<ComposablePartCatalogChangeEventArgs> Changed;
/// <summary>
/// Notify when the contents of the Catalog has changing.
/// </summary>
public event EventHandler<ComposablePartCatalogChangeEventArgs> Changing;
/// <summary>
/// Releases unmanaged and - optionally - managed resources
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected override void Dispose(bool disposing)
{
try
{
if (disposing)
{
if (!this._isDisposed)
{
bool disposeLock = false;
ComposablePartCatalogCollection catalogs = null;
try
{
using (new WriteLock(this._thisLock))
{
if (!this._isDisposed)
{
disposeLock = true;
catalogs = this._catalogCollection;
this._catalogCollection = null;
this._assemblyCatalogs = null;
this._isDisposed = true;
}
}
}
finally
{
if (catalogs != null)
{
catalogs.Dispose();
}
if (disposeLock)
{
this._thisLock.Dispose();
}
}
}
}
}
finally
{
base.Dispose(disposing);
}
}
/// <summary>
/// Returns the export definitions that match the constraint defined by the specified definition.
/// </summary>
/// <param name="definition">
/// The <see cref="ImportDefinition"/> that defines the conditions of the
/// <see cref="ExportDefinition"/> objects to return.
/// </param>
/// <returns>
/// An <see cref="IEnumerable{T}"/> of <see cref="Tuple{T1, T2}"/> containing the
/// <see cref="ExportDefinition"/> objects and their associated
/// <see cref="ComposablePartDefinition"/> for objects that match the constraint defined
/// by <paramref name="definition"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="definition"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// The <see cref="DirectoryCatalog"/> has been disposed of.
/// </exception>
public override IEnumerable<Tuple<ComposablePartDefinition, ExportDefinition>> GetExports(ImportDefinition definition)
{
this.ThrowIfDisposed();
Requires.NotNull(definition, "definition");
return this._catalogCollection.SelectMany(catalog => catalog.GetExports(definition));
}
/// <summary>
/// Raises the <see cref="INotifyComposablePartCatalogChanged.Changed"/> event.
/// </summary>
/// <param name="e">
/// An <see cref="ComposablePartCatalogChangeEventArgs"/> containing the data for the event.
/// </param>
protected virtual void OnChanged(ComposablePartCatalogChangeEventArgs e)
{
EventHandler<ComposablePartCatalogChangeEventArgs> changedEvent = this.Changed;
if (changedEvent != null)
{
changedEvent(this, e);
}
}
/// <summary>
/// Raises the <see cref="INotifyComposablePartCatalogChanged.Changing"/> event.
/// </summary>
/// <param name="e">
/// An <see cref="ComposablePartCatalogChangeEventArgs"/> containing the data for the event.
/// </param>
protected virtual void OnChanging(ComposablePartCatalogChangeEventArgs e)
{
EventHandler<ComposablePartCatalogChangeEventArgs> changingEvent = this.Changing;
if (changingEvent != null)
{
changingEvent(this, e);
}
}
/// <summary>
/// Refreshes the <see cref="ComposablePartDefinition"/>s with the latest files in the directory that match
/// the searchPattern. If any files have been added they will be added to the catalog and if any files were
/// removed they will be removed from the catalog. For files that have been removed keep in mind that the
/// assembly cannot be unloaded from the process so <see cref="ComposablePartDefinition"/>s for those files
/// will simply be removed from the catalog.
///
/// Possible exceptions that can be thrown are any that <see cref="Directory.GetFiles(string, string)"/> or
/// <see cref="Assembly.Load(AssemblyName)"/> can throw.
/// </summary>
/// <exception cref="DirectoryNotFoundException">
/// The specified <paramref name="path"/> has been removed since object construction.
/// </exception>
public void Refresh()
{
this.ThrowIfDisposed();
Assumes.NotNull(this._loadedFiles);
List<Tuple<string, AssemblyCatalog>> catalogsToAdd;
List<Tuple<string, AssemblyCatalog>> catalogsToRemove;
ComposablePartDefinition[] addedDefinitions;
ComposablePartDefinition[] removedDefinitions;
object changeReferenceObject;
string[] afterFiles;
string[] beforeFiles;
while (true)
{
afterFiles = this.GetFiles();
using (new ReadLock(this._thisLock))
{
changeReferenceObject = this._loadedFiles;
beforeFiles = this._loadedFiles.ToArray();
}
this.DiffChanges(beforeFiles, afterFiles, out catalogsToAdd, out catalogsToRemove);
// Don't go any further if there's no work to do
if (catalogsToAdd.Count == 0 && catalogsToRemove.Count == 0)
{
return;
}
// Notify listeners to give them a preview before completeting the changes
addedDefinitions = catalogsToAdd
.SelectMany(cat => cat.Item2.Parts)
.ToArray<ComposablePartDefinition>();
removedDefinitions = catalogsToRemove
.SelectMany(cat => cat.Item2.Parts)
.ToArray<ComposablePartDefinition>();
using (var atomicComposition = new AtomicComposition())
{
var changingArgs = new ComposablePartCatalogChangeEventArgs(addedDefinitions, removedDefinitions, atomicComposition);
this.OnChanging(changingArgs);
// if the change went through then write the catalog changes
using (new WriteLock(this._thisLock))
{
if (changeReferenceObject != this._loadedFiles)
{
// Someone updated the list while we were diffing so we need to try the diff again
continue;
}
foreach (var catalogToAdd in catalogsToAdd)
{
this._assemblyCatalogs.Add(catalogToAdd.Item1, catalogToAdd.Item2);
this._catalogCollection.Add(catalogToAdd.Item2);
}
foreach (var catalogToRemove in catalogsToRemove)
{
this._assemblyCatalogs.Remove(catalogToRemove.Item1);
this._catalogCollection.Remove(catalogToRemove.Item2);
}
this._partsQuery = this._catalogCollection.AsQueryable().SelectMany(catalog => catalog.Parts);
this._loadedFiles = afterFiles.ToReadOnlyCollection();
// Lastly complete any changes added to the atomicComposition during the change event
atomicComposition.Complete();
// Break out of the while(true)
break;
} // WriteLock
} // AtomicComposition
} // while (true)
var changedArgs = new ComposablePartCatalogChangeEventArgs(addedDefinitions, removedDefinitions, null);
this.OnChanged(changedArgs);
}
/// <summary>
/// Returns a string representation of the directory catalog.
/// </summary>
/// <returns>
/// A <see cref="String"/> containing the string representation of the <see cref="DirectoryCatalog"/>.
/// </returns>
public override string ToString()
{
return GetDisplayName();
}
private AssemblyCatalog CreateAssemblyCatalogGuarded(string assemblyFilePath)
{
Exception exception = null;
try
{
return new AssemblyCatalog(assemblyFilePath, this);
}
catch (FileNotFoundException ex)
{ // Files should always exists but don't blow up here if they don't
exception = ex;
}
catch (FileLoadException ex)
{ // File was found but could not be loaded
exception = ex;
}
catch (BadImageFormatException ex)
{ // Dlls that contain native code are not loaded, but do not invalidate the Directory
exception = ex;
}
catch (ReflectionTypeLoadException ex)
{ // Dlls that have missing Managed dependencies are not loaded, but do not invalidate the Directory
exception = ex;
}
CompositionTrace.AssemblyLoadFailed(this, assemblyFilePath, exception);
return null;
}
private void DiffChanges(string[] beforeFiles, string[] afterFiles,
out List<Tuple<string, AssemblyCatalog>> catalogsToAdd,
out List<Tuple<string, AssemblyCatalog>> catalogsToRemove)
{
catalogsToAdd = new List<Tuple<string, AssemblyCatalog>>();
catalogsToRemove = new List<Tuple<string, AssemblyCatalog>>();
IEnumerable<string> filesToAdd = afterFiles.Except(beforeFiles);
foreach (string file in filesToAdd)
{
AssemblyCatalog catalog = CreateAssemblyCatalogGuarded(file);
if (catalog != null)
{
catalogsToAdd.Add(new Tuple<string, AssemblyCatalog>(file, catalog));
}
}
IEnumerable<string> filesToRemove = beforeFiles.Except(afterFiles);
using (new ReadLock(this._thisLock))
{
foreach (string file in filesToRemove)
{
AssemblyCatalog catalog;
if (this._assemblyCatalogs.TryGetValue(file, out catalog))
{
catalogsToRemove.Add(new Tuple<string, AssemblyCatalog>(file, catalog));
}
}
}
}
private string GetDisplayName()
{
return string.Format(CultureInfo.CurrentCulture,
"{0} (Path=\"{1}\")", // NOLOC
this.GetType().Name,
this._path);
}
private string[] GetFiles()
{
string[] files = Directory.GetFiles(this._fullPath, this._searchPattern);
return Array.ConvertAll<string, string>(files, (file) => file);
}
private static string GetFullPath(string path)
{
if (!IOPath.IsPathRooted(path) && AppDomain.CurrentDomain.BaseDirectory != null)
{
path = IOPath.Combine(AppDomain.CurrentDomain.BaseDirectory, path);
}
return IOPath.GetFullPath(path);
}
private void Initialize(string path, string searchPattern)
{
this._path = path;
this._fullPath = GetFullPath(path);
this._searchPattern = searchPattern;
this._assemblyCatalogs = new Dictionary<string, AssemblyCatalog>();
this._catalogCollection = new ComposablePartCatalogCollection(null);
this._loadedFiles = GetFiles().ToReadOnlyCollection();
foreach (string file in this._loadedFiles)
{
AssemblyCatalog assemblyCatalog = null;
assemblyCatalog = CreateAssemblyCatalogGuarded(file);
if (assemblyCatalog != null)
{
this._assemblyCatalogs.Add(file, assemblyCatalog);
this._catalogCollection.Add(assemblyCatalog);
}
}
this._partsQuery = this._catalogCollection.AsQueryable().SelectMany(catalog => catalog.Parts);
}
private void ThrowIfDisposed()
{
if (this._isDisposed)
{
throw ExceptionBuilder.CreateObjectDisposed(this);
}
}
/// <summary>
/// Gets the display name of the directory catalog.
/// </summary>
/// <value>
/// A <see cref="String"/> containing a human-readable display name of the <see cref="DirectoryCatalog"/>.
/// </value>
string ICompositionElement.DisplayName
{
get { return this.GetDisplayName(); }
}
/// <summary>
/// Gets the composition element from which the directory catalog originated.
/// </summary>
/// <value>
/// This property always returns <see langword="null"/>.
/// </value>
ICompositionElement ICompositionElement.Origin
{
get { return null; }
}
}
}
#endif
|