using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web; using GreenTree.Nachtragsmanagement.Core.Plugins; using System.Threading; using System.Web.Hosting; using GreenTree.Nachtragsmanagement.Core.ComponentModel; using System.Configuration; using System.Reflection; using System.Web.Compilation; [assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")] namespace GreenTree.Nachtragsmanagement.Core.Plugins { public class PluginManager { #region Const private const string InstalledPluginsFilePath = "~/App_Data/InstalledPlugins.txt"; private const string PluginsPath = "~/Plugins"; private const string ShadowCopyPath = "~/Plugins/bin"; #endregion #region Fields private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); private static DirectoryInfo _shadowCopyFolder; private static bool _clearShadowDirectoryOnStartup; #endregion #region Methods /// /// Returns a collection of all referenced plugin assemblies that have been shadow copied /// public static IEnumerable ReferencedPlugins { get; set; } /// /// Returns a collection of all plugin which are not compatible with the current version /// public static IEnumerable IncompatiblePlugins { get; set; } /// /// Initialize /// public static void Initialize() { using (new WriteLockDisposable(Locker)) { // TODO: Add verbose exception handling / raising here since this is happening on app startup and could // prevent app from starting altogether var pluginFolder = new DirectoryInfo(HostingEnvironment.MapPath(PluginsPath)); _shadowCopyFolder = new DirectoryInfo(HostingEnvironment.MapPath(ShadowCopyPath)); var referencedPlugins = new List(); var incompatiblePlugins = new List(); _clearShadowDirectoryOnStartup = !String.IsNullOrEmpty(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]) && Convert.ToBoolean(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]); try { var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath()); Debug.WriteLine("Creating shadow copy folder and querying for dlls"); //ensure folders are created Directory.CreateDirectory(pluginFolder.FullName); Directory.CreateDirectory(_shadowCopyFolder.FullName); //get list of all files in bin var binFiles = _shadowCopyFolder.GetFiles("*", SearchOption.AllDirectories); if (_clearShadowDirectoryOnStartup) { //clear out shadow copied plugins foreach (var f in binFiles) { Debug.WriteLine("Deleting " + f.Name); try { File.Delete(f.FullName); } catch (Exception exc) { Debug.WriteLine("Error deleting file " + f.Name + ". Exception: " + exc); } } } //load description files foreach (var dfd in GetDescriptionFilesAndDescriptors(pluginFolder)) { var descriptionFile = dfd.Key; var pluginDescriptor = dfd.Value; //ensure that version of plugin is valid if (!pluginDescriptor.SupportedVersions.Contains(AppendixVersion.CurrentVersion, StringComparer.InvariantCultureIgnoreCase)) { incompatiblePlugins.Add(pluginDescriptor.SystemName); continue; } //some validation if (String.IsNullOrWhiteSpace(pluginDescriptor.SystemName)) throw new Exception(string.Format("A plugin '{0}' has no system name. Try assigning the plugin a unique name and recompiling.", descriptionFile.FullName)); if (referencedPlugins.Contains(pluginDescriptor)) throw new Exception(string.Format("A plugin with '{0}' system name is already defined", pluginDescriptor.SystemName)); //set 'Installed' property pluginDescriptor.Installed = installedPluginSystemNames .FirstOrDefault(x => x.Equals(pluginDescriptor.SystemName, StringComparison.InvariantCultureIgnoreCase)) != null; try { if (descriptionFile.Directory == null) throw new Exception(string.Format("Directory cannot be resolved for '{0}' description file", descriptionFile.Name)); //get list of all DLLs in plugins (not in bin!) var pluginFiles = descriptionFile.Directory.GetFiles("*.dll", SearchOption.AllDirectories) //just make sure we're not registering shadow copied plugins .Where(x => !binFiles.Select(q => q.FullName).Contains(x.FullName)) .Where(x => IsPackagePluginFolder(x.Directory)) .ToList(); //other plugin description info var mainPluginFile = pluginFiles .FirstOrDefault(x => x.Name.Equals(pluginDescriptor.PluginFileName, StringComparison.InvariantCultureIgnoreCase)); pluginDescriptor.OriginalAssemblyFile = mainPluginFile; //shadow copy main plugin file pluginDescriptor.ReferencedAssembly = PerformFileDeploy(mainPluginFile); //load all other referenced assemblies now foreach (var plugin in pluginFiles .Where(x => !x.Name.Equals(mainPluginFile.Name, StringComparison.InvariantCultureIgnoreCase)) .Where(x => !IsAlreadyLoaded(x))) PerformFileDeploy(plugin); //init plugin type (only one plugin per assembly is allowed) foreach (var t in pluginDescriptor.ReferencedAssembly.GetTypes()) if (typeof(IPlugin).IsAssignableFrom(t)) if (!t.IsInterface) if (t.IsClass && !t.IsAbstract) { pluginDescriptor.PluginType = t; break; } referencedPlugins.Add(pluginDescriptor); } catch (ReflectionTypeLoadException ex) { var msg = string.Empty; foreach (var e in ex.LoaderExceptions) msg += e.Message + Environment.NewLine; var fail = new Exception(msg, ex); Debug.WriteLine(fail.Message, fail); throw fail; } } } catch (Exception ex) { var msg = string.Empty; for (var e = ex; e != null; e = e.InnerException) msg += e.Message + Environment.NewLine; var fail = new Exception(msg, ex); Debug.WriteLine(fail.Message, fail); throw fail; } ReferencedPlugins = referencedPlugins; IncompatiblePlugins = incompatiblePlugins; } } /// /// Mark plugin as installed /// /// Plugin system name public static void MarkPluginAsInstalled(string systemName) { if (String.IsNullOrEmpty(systemName)) throw new ArgumentNullException("systemName"); var filePath = HostingEnvironment.MapPath(InstalledPluginsFilePath); if (!File.Exists(filePath)) using (File.Create(filePath)) { //we use 'using' to close the file after it's created } var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath()); bool alreadyMarkedAsInstalled = installedPluginSystemNames .FirstOrDefault(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) != null; if (!alreadyMarkedAsInstalled) installedPluginSystemNames.Add(systemName); PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames, filePath); } /// /// Mark plugin as uninstalled /// /// Plugin system name public static void MarkPluginAsUninstalled(string systemName) { if (String.IsNullOrEmpty(systemName)) throw new ArgumentNullException("systemName"); var filePath = HostingEnvironment.MapPath(InstalledPluginsFilePath); if (!File.Exists(filePath)) using (File.Create(filePath)) { //we use 'using' to close the file after it's created } var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath()); bool alreadyMarkedAsInstalled = installedPluginSystemNames .FirstOrDefault(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) != null; if (alreadyMarkedAsInstalled) installedPluginSystemNames.Remove(systemName); PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames, filePath); } /// /// Mark plugin as uninstalled /// public static void MarkAllPluginsAsUninstalled() { var filePath = HostingEnvironment.MapPath(InstalledPluginsFilePath); if (File.Exists(filePath)) File.Delete(filePath); } #endregion #region Utilities /// /// Get description files /// /// Plugin direcotry info /// Original and parsed description files private static IEnumerable> GetDescriptionFilesAndDescriptors(DirectoryInfo pluginFolder) { if (pluginFolder == null) throw new ArgumentNullException("pluginFolder"); //create list () var result = new List>(); //add display order and path to list foreach (var descriptionFile in pluginFolder.GetFiles("Description.txt", SearchOption.AllDirectories)) { if (!IsPackagePluginFolder(descriptionFile.Directory)) continue; //parse file var pluginDescriptor = PluginFileParser.ParsePluginDescriptionFile(descriptionFile.FullName); //populate list result.Add(new KeyValuePair(descriptionFile, pluginDescriptor)); } result.Sort((firstPair, nextPair) => firstPair.Value.DisplayOrder.CompareTo(nextPair.Value.DisplayOrder)); return result; } /// /// Indicates whether assembly file is already loaded /// /// File info /// Result private static bool IsAlreadyLoaded(FileInfo fileInfo) { //compare full assembly name //var fileAssemblyName = AssemblyName.GetAssemblyName(fileInfo.FullName); //foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) //{ // if (a.FullName.Equals(fileAssemblyName.FullName, StringComparison.InvariantCultureIgnoreCase)) // return true; //} //return false; //do not compare the full assembly name, just filename try { string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileInfo.FullName); if (fileNameWithoutExt == null) throw new Exception(string.Format("Cannot get file extnension for {0}", fileInfo.Name)); foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) { string assemblyName = a.FullName.Split(new[] { ',' }).FirstOrDefault(); if (fileNameWithoutExt.Equals(assemblyName, StringComparison.InvariantCultureIgnoreCase)) return true; } } catch (Exception exc) { Debug.WriteLine("Cannot validate whether an assembly is already loaded. " + exc); } return false; } /// /// Perform file deply /// /// Plugin file info /// Assembly private static Assembly PerformFileDeploy(FileInfo plug) { if (plug.Directory.Parent == null) throw new InvalidOperationException("The plugin directory for the " + plug.Name + " file exists in a folder outside of the allowed nopCommerce folder heirarchy"); FileInfo shadowCopiedPlug; if (CommonHelper.GetTrustLevel() != AspNetHostingPermissionLevel.Unrestricted) { //all plugins will need to be copied to ~/Plugins/bin/ //this is aboslutely required because all of this relies on probingPaths being set statically in the web.config //were running in med trust, so copy to custom bin folder var shadowCopyPlugFolder = Directory.CreateDirectory(_shadowCopyFolder.FullName); shadowCopiedPlug = InitializeMediumTrust(plug, shadowCopyPlugFolder); } else { var directory = AppDomain.CurrentDomain.DynamicDirectory; Debug.WriteLine(plug.FullName + " to " + directory); //were running in full trust so copy to standard dynamic folder shadowCopiedPlug = InitializeFullTrust(plug, new DirectoryInfo(directory)); } //we can now register the plugin definition var shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName)); //add the reference to the build manager Debug.WriteLine("Adding to BuildManager: '{0}'", shadowCopiedAssembly.FullName); BuildManager.AddReferencedAssembly(shadowCopiedAssembly); return shadowCopiedAssembly; } /// /// Used to initialize plugins when running in Full Trust /// /// /// /// private static FileInfo InitializeFullTrust(FileInfo plug, DirectoryInfo shadowCopyPlugFolder) { var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name)); try { File.Copy(plug.FullName, shadowCopiedPlug.FullName, true); } catch (IOException) { Debug.WriteLine(shadowCopiedPlug.FullName + " is locked, attempting to rename"); //this occurs when the files are locked, //for some reason devenv locks plugin files some times and for another crazy reason you are allowed to rename them //which releases the lock, so that it what we are doing here, once it's renamed, we can re-shadow copy try { var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old"; File.Move(shadowCopiedPlug.FullName, oldFile); } catch (IOException exc) { throw new IOException(shadowCopiedPlug.FullName + " rename failed, cannot initialize plugin", exc); } //ok, we've made it this far, now retry the shadow copy File.Copy(plug.FullName, shadowCopiedPlug.FullName, true); } return shadowCopiedPlug; } /// /// Used to initialize plugins when running in Medium Trust /// /// /// /// private static FileInfo InitializeMediumTrust(FileInfo plug, DirectoryInfo shadowCopyPlugFolder) { var shouldCopy = true; var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name)); //check if a shadow copied file already exists and if it does, check if it's updated, if not don't copy if (shadowCopiedPlug.Exists) { //it's better to use LastWriteTimeUTC, but not all file systems have this property //maybe it is better to compare file hash? var areFilesIdentical = shadowCopiedPlug.CreationTimeUtc.Ticks >= plug.CreationTimeUtc.Ticks; if (areFilesIdentical) { Debug.WriteLine("Not copying; files appear identical: '{0}'", shadowCopiedPlug.Name); shouldCopy = false; } else { //delete an existing file //More info: http://www.nopcommerce.com/boards/t/11511/access-error-nopplugindiscountrulesbillingcountrydll.aspx?p=4#60838 Debug.WriteLine("New plugin found; Deleting the old file: '{0}'", shadowCopiedPlug.Name); File.Delete(shadowCopiedPlug.FullName); } } if (shouldCopy) { try { File.Copy(plug.FullName, shadowCopiedPlug.FullName, true); } catch (IOException) { Debug.WriteLine(shadowCopiedPlug.FullName + " is locked, attempting to rename"); //this occurs when the files are locked, //for some reason devenv locks plugin files some times and for another crazy reason you are allowed to rename them //which releases the lock, so that it what we are doing here, once it's renamed, we can re-shadow copy try { var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old"; File.Move(shadowCopiedPlug.FullName, oldFile); } catch (IOException exc) { throw new IOException(shadowCopiedPlug.FullName + " rename failed, cannot initialize plugin", exc); } //ok, we've made it this far, now retry the shadow copy File.Copy(plug.FullName, shadowCopiedPlug.FullName, true); } } return shadowCopiedPlug; } /// /// Determines if the folder is a bin plugin folder for a package /// /// /// private static bool IsPackagePluginFolder(DirectoryInfo folder) { if (folder == null) return false; if (folder.Parent == null) return false; if (!folder.Parent.Name.Equals("Plugins", StringComparison.InvariantCultureIgnoreCase)) return false; return true; } /// /// Gets the full path of InstalledPlugins.txt file /// /// private static string GetInstalledPluginsFilePath() { var filePath = HostingEnvironment.MapPath(InstalledPluginsFilePath); return filePath; } #endregion } }