/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Collection.Generic; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Windows.Forms; using System.Xml; using System35; using JetBrains.Annotations; using JetBrains.Omea.OpenAPI; using JetBrains.Omea.ResourceStore; using JetBrains.UI.Util; using JetBrains.Util; using OmniaMea; namespace JetBrains.Omea.Plugins { /// /// Loads and starts the plugins. /// Controls the detection, disabling and enabling of the plugin DLLs. /// public class PluginLoader // TODO: make sure the assembly/file name is case-sensitive { #region Data /// /// A regex to check whether the file name is recognized as an Omea Plugin file. /// [NotNull] public static readonly Regex RegexOmeaPluginFile = new Regex(@"^.*\bOmeaPlugin\b.*(\.dll|\.exe)$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Singleline); private static readonly Regex _regexAssemblyNameToPluginDisplayName = new Regex(@"^(?.*)\.?OmeaPlugin\.?(?.*)$"); /// /// The list of default plugin folders. /// Note: cannot be made static, as used for one of the folders might not be yet available when doing static init. /// [NotNull] public readonly PluginFolder[] PluginFolders = PluginFolder.CreateDefaultFolders(); /// /// The list of plugin assemblies for which the XML config hasn't been loaded yet. /// [NotNull] private readonly List _arAssembliesToLoadXmlConfigFrom = new List(); /// /// Full names of types of the loaded instances. /// Prevent duplicate class names in separate assemblies. /// private readonly HashSet _hashLoadedPluginTypes = new HashSet(); /// /// For the loaded plugins (), tracks the files they were loaded from, along with the possible load-time comments from the plugin loader. /// [NotNull] private readonly Dictionary _mapPluginFileInfo = new Dictionary(); /// /// Tracks errors on loading those assemblies that were considered valid Omea plugins and were attempted to be loaded (so their holds a clean record when enumerating the folders), but later failed the runtime checks. /// Key: file full name. /// Value: error text. /// private readonly Dictionary _mapPluginLoadRuntimeErrors = new Dictionary(); /// /// The set of handlers for reading and applying the declarative XML config. /// [NotNull] private readonly Dictionary> _mapXmlConfigHandlers = new Dictionary>(); /// /// Loaded plugins. /// [NotNull] private readonly List _plugins = new List(); /// /// Service provider storage. /// [NotNull] private readonly List _pluginServices = new List(); #endregion #region Operations /// /// Makes a user-friendly name out of the assembly name by removing the “OmeaPlugin” infix. /// [NotNull] public static string AssemblyNameToPluginDisplayName([NotNull] string sAssemblyName) { if(sAssemblyName == null) throw new ArgumentNullException("sAssemblyName"); Match match = _regexAssemblyNameToPluginDisplayName.Match(sAssemblyName); if(!match.Success) return sAssemblyName; string before = match.Groups["Before"].Value.Trim('.').Trim().Replace('.', ' ').Replace('_', ' '); string after = match.Groups["After"].Value.Trim('.').Trim().Replace('.', ' ').Replace('_', ' '); if((before.IsEmpty()) && (after.IsEmpty())) // No extra text could be taken return sAssemblyName; // Choose the longest return before.Length >= after.Length ? before : after; } /// /// Makes a user-friendly name out of the assembly name by removing the “OmeaPlugin” infix. /// [NotNull] public static string AssemblyNameToPluginDisplayName([NotNull] FileInfo pluginfile) { if(pluginfile == null) throw new ArgumentNullException("pluginfile"); return AssemblyNameToPluginDisplayName(Path.GetFileNameWithoutExtension(pluginfile.FullName)); } /// /// Checks if the given file is an Omea plugin DLL. /// Does not check whether it resides in one of the Omea Plugins folders or not. /// For plugins in the Primary Plugins folder, makes sure they qualify as primary plugins. /// Warning! Loads the DLL with CLR! Locks the file! /// public static bool IsOmeaPluginDll([NotNull] FileInfo file, [NotNull] out string sError) { if(file == null) throw new ArgumentNullException("file"); // Exists? if(!file.Exists) { Trace.WriteLine(sError = string.Format("The plugin file “{0}” does not exist.", file.FullName), "Plugins.Loader"); return false; } if(file.Directory == null) { Trace.WriteLine(sError = string.Format("The plugin file “{0}” does not have a parent directory.", file.FullName), "Plugins.Loader"); return false; } // File name? if(!RegexOmeaPluginFile.IsMatch(file.Name)) { Trace.WriteLine(sError = string.Format("The file “{0}” does not match the Omea plugin name pattern, {1}.", file.FullName, RegexOmeaPluginFile), "Plugins.Loader"); return false; } // Try loading the assembly // TODO: don't lock the plugin DLL if we're not loading it yet Type[] assemblytypes; Assembly assembly; try { // Load assembly: Load Context for Primary Plugins, LoadFrom for all others // Primary plugins must have its file name equal to the assembly name plus an extension assembly = LoadPluginAssembly(file); assemblytypes = assembly.GetExportedTypes(); } catch(Exception ex) { Trace.WriteLine(string.Format("Could not load the plugin file “{1}”. {0}", ex.Message, file.FullName), "Plugins.Loader"); sError = ex.Message; return false; } // Primary plugin: strong name check if((PluginFolder.PrimaryPluginFolder.IsPluginUnderFolder(file)) && (!IsPrimaryAssemblyStrongNameOk(assembly.GetName(), file.FullName, out sError))) return false; // Search for IPlugin instances foreach(Type type in assemblytypes) { if(IsOmeaPluginType(type)) { sError = ""; // No error return true; } } // No IPlugin Trace.WriteLine(sError = string.Format("The plugin file “{0}” does not have any plugins inside. Could not find any classes implementing “{1}”.", file.FullName, typeof(IPlugin).AssemblyQualifiedName), "Plugin.Loader"); return false; } /// /// Checks whether the given type implements an Omea plugin. /// public static bool IsOmeaPluginType([NotNull] Type type) { if(type == null) throw new ArgumentNullException("type"); return typeof(IPlugin).IsAssignableFrom(type); } /// /// For a candidate Primary Plugin Assembly, checks that its strong name is OK. /// That is, equal to the strong name of the current assembly, including the no-strong-name case. /// /// The assembly name of the candidate assembly. /// Filename, used for error reporting only. /// On failure, contains the error message. /// Success flag. public static bool IsPrimaryAssemblyStrongNameOk(AssemblyName assnameCandidate, string sFileDisplayName, out string sError) { byte[] tokenMy = Assembly.GetExecutingAssembly().GetName().GetPublicKeyToken(); byte[] tokenCandidate = assnameCandidate.GetPublicKeyToken(); if((tokenMy != null) && (tokenCandidate == null)) { Trace.WriteLine(sError = string.Format("The plugin file “{0}” is in the primary plugins folder, but does not have the required strong name.", sFileDisplayName), "Plugin.Loader"); return false; } if((tokenMy == null) && (tokenCandidate != null)) { Trace.WriteLine(sError = string.Format("The plugin file “{0}” in the primary plugins folder has a strong name, but Omea is built without strong names.", sFileDisplayName), "Plugin.Loader"); return false; } if(tokenMy != null) // Both not Null { bool match = false; if(tokenMy.Length == tokenCandidate.Length) { match = true; for(int a = 0; (a < tokenMy.Length) && (match); a++) match &= tokenMy[a] == tokenCandidate[a]; } if(!match) { Trace.WriteLine(string.Format(sError = "The plugin file “{0}” is in the primary plugins folder, but its strong name is wrong.", sFileDisplayName), "Plugin.Loader"); return false; } } // The remaining case — neither has strong names — is OK (Debug configuration) sError = ""; return true; } /// /// Loads the plugin assembly, applying either Load or LoadFrom context (see ). /// public static Assembly LoadPluginAssembly([NotNull] FileInfo file) { if(file == null) throw new ArgumentNullException("file"); string sPluginName = Path.GetFileNameWithoutExtension(file.FullName); // Primary plugin: Load context, we assume that file name agrees to the assembly name if(PluginFolder.PrimaryPluginFolder.IsPluginUnderFolder(file)) return Assembly.Load(sPluginName); // Non-primary plugin: LoadFrom context, ensure file name agrees to the assembly name Assembly assembly = Assembly.LoadFrom(file.FullName); if(assembly.GetName().Name != sPluginName) throw new InvalidOperationException(StringEx.FormatQuoted("The plugin assembly file {0} has an assembly name {1} that does not agree to the plugin name {2}.", file.FullName, assembly.GetName().Name, sPluginName)); return assembly; } /// /// Collects all of the assembly files eligible for loading plugins from. /// Duplicates are resolved so that high-priorities go first. /// [NotNull] public List GetAllPluginFiles() { var arAllPlugins = new List(); var pluginnames = new HashSet(); // Discover plugin files, skip disabled foreach(PluginFolder folder in PluginFolders) { foreach(PossiblyPluginFileInfo file in folder.GetPluginFiles()) { // Higher priority goes first, ignore subsequent duplicates (applies to plugins only) // Non-plugins are always added if((!file.IsPlugin) || (pluginnames.Add(Path.GetFileNameWithoutExtension(file.File.FullName)))) arAllPlugins.Add(file); else { // Duplicate PossiblyPluginFileInfo fileNot = PossiblyPluginFileInfo.CreateNo(file.Folder, file.File, "There was another plugin with the same assembly name in the same or a higher-priority folder."); arAllPlugins.Add(fileNot); Trace.WriteLine(fileNot.ToString(), "Plugin.Loader"); } } } return arAllPlugins; } /// /// Gets the list of plugins currently loaded in the application. /// public IPlugin[] GetLoadedPlugins() { return _plugins.ToArray(); } /// /// For a currently loaded plugin (see ), gets its file of origin information. /// [NotNull] public PossiblyPluginFileInfo GetPluginFileInfo([NotNull] IPlugin plugin) { if(plugin == null) throw new ArgumentNullException("plugin"); PossiblyPluginFileInfo retval; if(!_mapPluginFileInfo.TryGetValue(plugin, out retval)) throw new InvalidOperationException(string.Format("There is no information available for the plugin {0}, as it has not been loaded by PluginLoader.", plugin.GetType().AssemblyQualifiedName.QuoteIfNeeded())); return retval; } /// /// For those plugins whose says True when enumerating folders, but who had a runtime error at load time, returns that runtime error. /// Otherwise, returns "" (for both successful plugins and random non-plugin ). /// [NotNull] public string GetPluginLoadRuntimeError([NotNull] FileInfo fi) { if(fi == null) throw new ArgumentNullException("fi"); return _mapPluginLoadRuntimeErrors.TryGetValue(fi.FullName) ?? ""; } public object GetPluginService(Type serviceType) { // scan last registered services first for(int i = _pluginServices.Count - 1; i >= 0; i--) { object service = _pluginServices[i]; if(serviceType.IsInstanceOfType(service)) return service; } return null; } /// /// Looks up Omea Plugin assembly files, loads them, instantiates the plugin classes, calls their , all on the calling thread. /// public void LoadPlugins() { var disabledplugins = new DisabledPlugins(); // Collect to get the full count for the progress before we start loading the DLLs/types var plugins = new List(); foreach(PossiblyPluginFileInfo file in GetAllPluginFiles()) { if(!file.IsPlugin) continue; // Need plugins only // Disabled plugin? Check by the DLL name if(!disabledplugins.Contains(Path.GetFileNameWithoutExtension(file.File.FullName))) plugins.Add(file); } // Load! var progressWindow = (SplashScreen)Core.ProgressWindow; for( int nFile = 0; nFile < plugins.Count; nFile++ ) { if( progressWindow != null ) progressWindow.UpdateProgress((nFile + 1) * 100 / plugins.Count, Stringtable.LoadingPluginsProgressMessage, null); // Progress after the current file before we process it (at the beginning, accomodate for collecting the files; at the and, let see the 100% fill for some time) // Try loading string sError; if( !LoadPlugins_LoadFile( plugins[ nFile ], out sError )) { // Failed, report the error _mapPluginLoadRuntimeErrors[ plugins[nFile].File.FullName ] = sError; // Overwrite old Core.ReportBackgroundException( new ApplicationException( sError ) ); // Disable the failed plugin? LoadPlugins_SuggestDisable(plugins[nFile].File, disabledplugins, sError, progressWindow); } else _mapPluginLoadRuntimeErrors.Remove(plugins[nFile].File.FullName); // Clean the error record } // Apply declarative plugin settings LoadPlugins_XmlConfiguration(); // Types without supporting plugins LoadPlugins_MarkUnloadedResourceTypes(); } public void RegisterPluginService(object pluginService) { _pluginServices.Add(pluginService); } public void RegisterXmlConfigurationHandler(string section, Action configDelegate) { _mapXmlConfigHandlers[section] = configDelegate; } /// /// Calls on each of the plugins, on the calling thread. /// /// Whether to report to the progress window. public void ShutdownPlugins(bool bShowProgress) { IProgressWindow window = Core.ProgressWindow; foreach(IPlugin plugin in _plugins) { if((bShowProgress) && (window != null)) window.UpdateProgress(0, string.Format(Stringtable.StoppingPluginProgressMessage, plugin.GetType().Name), null); try { plugin.Shutdown(); } catch(Exception ex) { Core.ReportException(ex, false); } } } /// /// Invokes for each of the loaded plugins, on the Resource thread, sync. /// public void StartupPlugins() { IProgressWindow window = Core.ProgressWindow; for(int a = 0; a < _plugins.Count; a++) { IPlugin plugin = _plugins[a]; Trace.WriteLine("Starting plugin " + plugin.GetType().Name); if(window != null) window.UpdateProgress((a + 1) * 100 / _plugins.Count, Stringtable.StartingPluginsProgressMessage, null); // Run on another thread string sStartupError = null; bool bCancelStartup = false; IPlugin pluginConst = plugin; // Precaution: don't access modified closure Core.ResourceAP.RunJob(string.Format(Stringtable.JobStartingPlugin, plugin.GetType().Name), delegate { try { pluginConst.Startup(); } catch(CancelStartupException) { Trace.WriteLine(string.Format("Starting Plugin “{0}”: CancelStartupException.", pluginConst.GetType().AssemblyQualifiedName), "Plugin.Loader"); bCancelStartup = true; } catch(Exception ex) { sStartupError = ex.Message; } }); // Process results on home thread if( bCancelStartup ) throw new CancelStartupException(); if( sStartupError != null ) Trace.WriteLine(string.Format("Error Starting Plugin “{0}”. {1}", plugin.GetType().AssemblyQualifiedName, sStartupError)); } } #endregion #region Implementation /// /// Whenever a plugin fails to load, suggests disabling it. /// private static void LoadPlugins_SuggestDisable([NotNull] FileInfo file, [NotNull] DisabledPlugins disabledplugins, [NotNull] string sError, [CanBeNull] IWin32Window owner) { if(file == null) throw new ArgumentNullException("file"); if(disabledplugins == null) throw new ArgumentNullException("disabledplugins"); if(sError.IsEmpty()) throw new ArgumentNullException("sError"); // Plugin name, error text, question var sb = new StringBuilder(); string displayname = AssemblyNameToPluginDisplayName(file); sb.AppendFormat(Stringtable.MessageBoxPluginCouldNotBeLoaded, displayname); sb.AppendLine(); sb.AppendLine(); sb.AppendLine(sError); sb.AppendLine(); sb.AppendFormat(Stringtable.MessageBoxDisableFailedPlugin, displayname); // Confirm if(MessageBox.Show(owner, sb.ToString(), Stringtable.MessageBoxFailedPluginTitle, MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation) == DialogResult.Yes) disabledplugins.Add(Path.GetFileNameWithoutExtension(file.FullName)); // Disable } /// /// Looks for “Plugin.xml” resources in the assembly, loads the XML configuration from them. /// internal void LoadXmlConfiguration(Assembly pluginAssembly) { string[] manifestResources = pluginAssembly.GetManifestResourceNames(); foreach(string filename in manifestResources) { if(filename.ToLower().EndsWith("plugin.xml")) { try { var doc = new XmlDocument(); doc.Load(pluginAssembly.GetManifestResourceStream(filename)); XmlNode node = doc.SelectSingleNode("/omniamea-plugin"); if(node != null) LoadXmlConfiguration_Document(pluginAssembly, node); } catch(Exception ex) { Core.ReportException(ex, false); } } } } /// /// Loads the plugin assembly and creates one or more types from it. /// private bool LoadPlugins_LoadFile([NotNull] PossiblyPluginFileInfo file, [NotNull] out string sError) { if(file.File == null) throw new ArgumentNullException("file"); // Here the plugin DLL is loaded by CLR // IPlugin instances are searched for, primary plugins checked for authenticity if(!IsOmeaPluginDll(file.File, out sError)) return false; Trace.WriteLine(string.Format("Loading plugin file “{0}”.", file.File.FullName), "Plugin.Loader"); // Get the types from the assembly, to look for plugins Assembly assembly; Type[] types; try { assembly = LoadPluginAssembly(file.File); types = assembly.GetExportedTypes(); } catch(Exception ex) { sError = ex.Message; return false; } // Load each IPlugin int nLoadedTypes = 0; foreach(Type type in types) { if(IsOmeaPluginType(type)) { string sTypeError; if(LoadPlugins_LoadFile_LoadType(type, file, out sTypeError)) nLoadedTypes++; else sError += sTypeError + " "; } } // Any plugins loaded? Read declarative XML config if(nLoadedTypes > 0) _arAssembliesToLoadXmlConfigFrom.Add(assembly); // If we have picked the DLL as a plugin, then it must have at least one IPlugin type return nLoadedTypes > 0; } /// /// Tries creating and registering the plugin instance from the given class. /// private bool LoadPlugins_LoadFile_LoadType([NotNull] Type type, PossiblyPluginFileInfo file, out string sError) { if(type == null) throw new ArgumentNullException("type"); // Duplicate with someone? if(!_hashLoadedPluginTypes.Add(type.FullName)) { sError = string.Format("The plugin class “{0}” has already been loaded from another assembly.", type.FullName); return false; } // Create and register try { var plugin = (IPlugin)Activator.CreateInstance(type); plugin.Register(); _plugins.Add(plugin); _mapPluginFileInfo.Add(plugin, file); } catch(CancelStartupException) // Special exception to abort the startup { throw; } catch(Exception ex) { #if DEBUG Core.ReportException(ex, false); #endif sError = "The plugin failed to register itself. " + ex.Message; return false; } sError = ""; return true; } private void LoadPlugins_MarkUnloadedResourceTypes() { var pluginNames = new ArrayList(); foreach(IPlugin plugin in _plugins) pluginNames.Add(plugin.GetType().FullName); MyPalStorage.Storage.MarkHiddenResourceTypes((string[])pluginNames.ToArray(typeof(string))); } private void LoadPlugins_XmlConfiguration() { foreach(Assembly assembly in _arAssembliesToLoadXmlConfigFrom) LoadXmlConfiguration(assembly); _arAssembliesToLoadXmlConfigFrom.Clear(); } private void LoadXmlConfiguration_Document(Assembly pluginAssembly, XmlNode rootNode) { foreach(XmlElement xmlElement in rootNode.ChildNodes) { Action handler; if(!_mapXmlConfigHandlers.TryGetValue(xmlElement.Name, out handler)) continue; handler(pluginAssembly, xmlElement); } } #endregion #region CancelStartupException Type /// /// A service exception that aborts the plugins startup sequence. /// public class CancelStartupException : Exception { #region Init public CancelStartupException() : base("Omea startup has been cancelled.") { } #endregion } #endregion #region DisabledPlugins Type /// /// Maintains the colletion of disabled plugin DLL assembly names. Supports persisting. /// public class DisabledPlugins : ICollection { #region Data /// /// Separates serialized assembly names in the settings entry. /// Safe to use, as we require that assembly name math its file name, and this char is illegal in a file name. /// private static readonly char EntrySeparator = '|'; private static readonly string SettingsSectionName = "Plugins"; private static readonly string SettingsValueName = "DisabledPlugins"; private readonly HashSet _storage = Load(); #endregion #region Operations /// /// Checks if the plugins from this assembly are disabled. /// It's assumed that assembly name and file name agree. /// public bool Contains(Assembly assembly) { return Contains(assembly.GetName().Name); } /// /// Checks if the plugins from this file are disabled. /// It's assumed that assembly name and file name agree. /// public bool Contains(FileInfo file) { return Contains(Path.GetFileNameWithoutExtension(file.FullName)); } #endregion #region Implementation /// /// Loads the collection from app settings. /// private static HashSet Load() { var result = new HashSet(); string list = Core.SettingStore.ReadString(SettingsSectionName, SettingsValueName, ""); foreach(string entry in list.Split(EntrySeparator)) result.Add(entry); return result; } /// /// Saves the current collection into the app settings. /// private void Save() { Core.SettingStore.WriteString(SettingsSectionName, SettingsValueName, string.Join(EntrySeparator.ToString(), new List(_storage).ToArray())); } #endregion #region ICollection Members public void Add(string item) { _storage.Add(item); Save(); } public void Clear() { _storage.Clear(); Save(); } /// /// Checks if the plugins from this assembly/file are disabled. /// It's assumed that assembly name and file name agree. /// public bool Contains(string item) { return _storage.Contains(item); } public void CopyTo(string[] array, int arrayIndex) { _storage.CopyTo(array, arrayIndex); } public IEnumerator GetEnumerator() { return ((ICollection)_storage).GetEnumerator(); } public bool Remove(string item) { if(!_storage.Remove(item)) return false; Save(); return true; } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)this).GetEnumerator(); } public int Count { get { return _storage.Count; } } public bool IsReadOnly { get { return false; } } #endregion } #endregion #region PluginFolder Type /// /// Describes a folder from which plugins are collected. /// public class PluginFolder { #region Data /// /// Gets the folder with primary Omea plugins. /// [NotNull] public static readonly PluginFolder PrimaryPluginFolder = new PluginFolder("Omea Core Plugins", "", new FileInfo(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath).Directory, true); /// /// Actual folder path on the local file system. /// [NotNull] public readonly DirectoryInfo Directory; /// /// Gets whether this folder contains primary plugins only. /// Primary plugins are stongly-named with the same key as this DLL, and are loaded using the Load Context, instead of the LoadFrom context. /// In non-primary folders, each subfolder should also be checked for plugins. /// public readonly bool IsPrimary; /// /// Logical machine-independent representation of the location, eg “RoamingAppData” instead of the exact path. /// [NotNull] public readonly string Location; /// /// Human-readable name of the folder. /// [NotNull] public readonly string Name; /// /// Valid extensions for plugin assemblies (without a leading dot). /// [NotNull] private readonly IList _arPluginAssemblyExtensions = new[] {"dll", "exe"}; #endregion #region Init /// /// Fills in the entry. Use to access the existing entries. /// protected PluginFolder([NotNull] string name, [NotNull] string location, [NotNull] DirectoryInfo directory, bool isprimary) { if(name == null) throw new ArgumentNullException("name"); if(location == null) throw new ArgumentNullException("location"); if(directory == null) throw new ArgumentNullException("directory"); Name = name; Location = location; Directory = directory; IsPrimary = isprimary; } #endregion #region Operations /// /// Fills the list of default folders to look for plugins. /// public static PluginFolder[] CreateDefaultFolders() { var folders = new List(); // 1. (P.JB) Primary folders.Add(PrimaryPluginFolder); // 2. (P.DB) Database CreateDefaultFolders_DbPath(folders.Add); // 3. (P.LA) Per-user local CreateDefaultFolders_SpecialFolder("User Local Plugins", "LocalAppData", Environment.SpecialFolder.LocalApplicationData, folders.Add); // 4. (P.RA) Per-user roaming CreateDefaultFolders_SpecialFolder("User Roaming Plugins", "RoamingAppData", Environment.SpecialFolder.ApplicationData, folders.Add); // 5. (P.AU) Per-machine user CreateDefaultFolders_SpecialFolder("All Users Plugins", "AllUsersAppData", Environment.SpecialFolder.CommonApplicationData, folders.Add); // 6. (P.OB) Plugins under Omea Binaries folders.Add(new PluginFolder("Administrative Plugins", "/Plugins[/*]", new DirectoryInfo(Path.Combine(PrimaryPluginFolder.Directory.FullName, "Plugins")), false)); return folders.ToArray(); } /// /// Gets the potential plugin assemblies, as collected from the , if it exists, and its subdirectories, if applicable for this folder type (see ). /// The files are recolleted on each call. /// [NotNull] public IEnumerable GetPluginFiles() { if(!Directory.Exists) return new PossiblyPluginFileInfo[] {}; // List of folders to check var directories = new List {Directory}; // In non-primary folders, also check subfolders: complex plugins might not want to mix their files up with others if(!IsPrimary) { foreach(DirectoryInfo directory in Directory.GetDirectories()) { if((directory.Attributes & FileAttributes.Hidden) == 0) directories.Add(directory); } } var files = new List(); PossiblyPluginFileInfo pluginfileinfo; // Look for the regex-matching files foreach(DirectoryInfo directory in directories) { foreach(FileInfo file in directory.GetFiles()) { if(!_arPluginAssemblyExtensions.Contains(file.Extension.Trim('.'))) { // Note: do not report such items at all, to avoid noise Trace.WriteLine(PossiblyPluginFileInfo.CreateNo(this, file, "File extension mismatch, assembly expected."), "Plugin.Loader"); continue; } if((file.Attributes & FileAttributes.Hidden) != 0) // Skip hidden files pluginfileinfo = PossiblyPluginFileInfo.CreateNo(this, file, "The file is hidden."); else if(!RegexOmeaPluginFile.IsMatch(file.Name)) // Apply file name filter pluginfileinfo = PossiblyPluginFileInfo.CreateNo(this, file, "The file name does not include “OmeaPlugin”."); else pluginfileinfo = PossiblyPluginFileInfo.CreateYes(this, file); // Note: here we do not check for IPlugin types and such, as it will cause loading the DLLs without the progress (we don't know the total count yet) // Pick files.Add(pluginfileinfo); Trace.WriteLine(pluginfileinfo.ToString(), "Plugin.Loader"); } } return files; } /// /// Checks whether the plugin file is under the given plugin folder. /// public bool IsPluginUnderFolder([NotNull] FileInfo pluginfile) { if(pluginfile == null) throw new ArgumentNullException("pluginfile"); DirectoryInfo plugindir = pluginfile.Directory; if(plugindir == null) return false; // Directly under the folder (the only option for primaries) if(plugindir.FullName == Directory.FullName) return true; // In a subfolder (non-primaries only) if(!IsPrimary) { DirectoryInfo plugindirparent = plugindir.Parent; if((plugindirparent != null) && (plugindirparent.FullName == Directory.FullName)) return true; } return false; } #endregion #region Implementation /// /// Helper for creating the plugin folder record for the in-database-path plugins. /// /// Executed to acknowledge the result on success, skipped on failure. private static void CreateDefaultFolders_DbPath(Action funcOnSuccess) { string sDbFolder = MyPalStorage.DBPath; if(string.IsNullOrEmpty(sDbFolder)) return; var pluginfolder = new PluginFolder("Database Plugins", "/Plugins[/*]", new DirectoryInfo(Path.Combine(Path.GetFullPath(sDbFolder), "Plugins")), false); // “Return” the result funcOnSuccess(pluginfolder); } /// /// Helper for creating plugin folder records under system special folders. /// Paths: {SpecialFolder}/JetBrains/Omea/Plugins. /// /// . /// Part of the that identifies the special folder. /// The system folder to look under. /// Executed to acknowledge the result on success, skipped on failure. private static void CreateDefaultFolders_SpecialFolder(string sFriendlyName, string sSpecialFolderTitle, Environment.SpecialFolder specialfolder, Action funcOnSuccess) { // Folder exists on disk? string sSpecialFolder = Environment.GetFolderPath(specialfolder); if(string.IsNullOrEmpty(sSpecialFolder)) return; // No such folder // Create entry var pluginfolder = new PluginFolder(sFriendlyName, string.Format("<{0}>/JetBrains/Omea/Plugins[/*]", sSpecialFolderTitle), new DirectoryInfo(Path.Combine(Path.Combine(Path.Combine(sSpecialFolder, "JetBrains"), "Omea"), "Plugins")), false); // “Return” the result funcOnSuccess(pluginfolder); } #endregion } #endregion #region PossiblyPluginFileInfo Type /// /// Lists assemblies whose location makes them eligible for plugin probing, specifies whether the assembly qualifies as a plugin. /// public struct PossiblyPluginFileInfo { #region Data /// /// Path to the file. /// public FileInfo File; /// /// The folder under which the plugin file was collected. /// public PluginFolder Folder; /// /// Whether the file qualifies as an Omea plugin. /// public bool IsPlugin; /// /// A reason for the flag. Optional. /// Mainly needed to tell why the DLL was not taken as a plugin. /// public string Reason; #endregion #region Init public PossiblyPluginFileInfo([NotNull] PluginFolder folder, [NotNull] FileInfo file, bool bIsPlugin, [CanBeNull] string sReason) { if(folder == null) throw new ArgumentNullException("folder"); if(file == null) throw new ArgumentNullException("file"); Folder = folder; File = file; IsPlugin = bIsPlugin; Reason = sReason; } #endregion #region Operations /// /// Creates an item that is NOT an Omea Plugin. /// public static PossiblyPluginFileInfo CreateNo([NotNull] PluginFolder folder, [NotNull] FileInfo file, [CanBeNull] string sReason) { return new PossiblyPluginFileInfo(folder, file, false, sReason); } /// /// Creates an item that qualifies as an Omea Plugin. /// public static PossiblyPluginFileInfo CreateYes([NotNull] PluginFolder folder, [NotNull] FileInfo file) { return new PossiblyPluginFileInfo(folder, file, true, null); } #endregion #region Overrides public override string ToString() { if(File == null) return ""; var sb = new StringBuilder(); sb.AppendFormat("The file {0}", File.FullName.QuoteIfNeeded()); sb.Append(IsPlugin ? " is an Omea Plugin" : " is not an Omea Plugin"); sb.Append('.'); if(!Reason.IsEmpty()) { sb.Append(' '); sb.Append(Reason); } return sb.ToString(); } #endregion } #endregion } }