PluginManager.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Threading.Tasks;
  8. using System.Web;
  9. using GreenTree.Nachtragsmanagement.Core.Plugins;
  10. using System.Threading;
  11. using System.Web.Hosting;
  12. using GreenTree.Nachtragsmanagement.Core.ComponentModel;
  13. using System.Configuration;
  14. using System.Reflection;
  15. using System.Web.Compilation;
  16. [assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")]
  17. namespace GreenTree.Nachtragsmanagement.Core.Plugins
  18. {
  19. public class PluginManager
  20. {
  21. #region Const
  22. private const string InstalledPluginsFilePath = "~/App_Data/InstalledPlugins.txt";
  23. private const string PluginsPath = "~/Plugins";
  24. private const string ShadowCopyPath = "~/Plugins/bin";
  25. #endregion
  26. #region Fields
  27. private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim();
  28. private static DirectoryInfo _shadowCopyFolder;
  29. private static bool _clearShadowDirectoryOnStartup;
  30. #endregion
  31. #region Methods
  32. /// <summary>
  33. /// Returns a collection of all referenced plugin assemblies that have been shadow copied
  34. /// </summary>
  35. public static IEnumerable<PluginDescriptor> ReferencedPlugins { get; set; }
  36. /// <summary>
  37. /// Returns a collection of all plugin which are not compatible with the current version
  38. /// </summary>
  39. public static IEnumerable<string> IncompatiblePlugins { get; set; }
  40. /// <summary>
  41. /// Initialize
  42. /// </summary>
  43. public static void Initialize()
  44. {
  45. using (new WriteLockDisposable(Locker))
  46. {
  47. // TODO: Add verbose exception handling / raising here since this is happening on app startup and could
  48. // prevent app from starting altogether
  49. var pluginFolder = new DirectoryInfo(HostingEnvironment.MapPath(PluginsPath));
  50. _shadowCopyFolder = new DirectoryInfo(HostingEnvironment.MapPath(ShadowCopyPath));
  51. var referencedPlugins = new List<PluginDescriptor>();
  52. var incompatiblePlugins = new List<string>();
  53. _clearShadowDirectoryOnStartup = !String.IsNullOrEmpty(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]) &&
  54. Convert.ToBoolean(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]);
  55. try
  56. {
  57. var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath());
  58. Debug.WriteLine("Creating shadow copy folder and querying for dlls");
  59. //ensure folders are created
  60. Directory.CreateDirectory(pluginFolder.FullName);
  61. Directory.CreateDirectory(_shadowCopyFolder.FullName);
  62. //get list of all files in bin
  63. var binFiles = _shadowCopyFolder.GetFiles("*", SearchOption.AllDirectories);
  64. if (_clearShadowDirectoryOnStartup)
  65. {
  66. //clear out shadow copied plugins
  67. foreach (var f in binFiles)
  68. {
  69. Debug.WriteLine("Deleting " + f.Name);
  70. try
  71. {
  72. File.Delete(f.FullName);
  73. }
  74. catch (Exception exc)
  75. {
  76. Debug.WriteLine("Error deleting file " + f.Name + ". Exception: " + exc);
  77. }
  78. }
  79. }
  80. //load description files
  81. foreach (var dfd in GetDescriptionFilesAndDescriptors(pluginFolder))
  82. {
  83. var descriptionFile = dfd.Key;
  84. var pluginDescriptor = dfd.Value;
  85. //ensure that version of plugin is valid
  86. if (!pluginDescriptor.SupportedVersions.Contains(AppendixVersion.CurrentVersion, StringComparer.InvariantCultureIgnoreCase))
  87. {
  88. incompatiblePlugins.Add(pluginDescriptor.SystemName);
  89. continue;
  90. }
  91. //some validation
  92. if (String.IsNullOrWhiteSpace(pluginDescriptor.SystemName))
  93. throw new Exception(string.Format("A plugin '{0}' has no system name. Try assigning the plugin a unique name and recompiling.", descriptionFile.FullName));
  94. if (referencedPlugins.Contains(pluginDescriptor))
  95. throw new Exception(string.Format("A plugin with '{0}' system name is already defined", pluginDescriptor.SystemName));
  96. //set 'Installed' property
  97. pluginDescriptor.Installed = installedPluginSystemNames
  98. .FirstOrDefault(x => x.Equals(pluginDescriptor.SystemName, StringComparison.InvariantCultureIgnoreCase)) != null;
  99. try
  100. {
  101. if (descriptionFile.Directory == null)
  102. throw new Exception(string.Format("Directory cannot be resolved for '{0}' description file", descriptionFile.Name));
  103. //get list of all DLLs in plugins (not in bin!)
  104. var pluginFiles = descriptionFile.Directory.GetFiles("*.dll", SearchOption.AllDirectories)
  105. //just make sure we're not registering shadow copied plugins
  106. .Where(x => !binFiles.Select(q => q.FullName).Contains(x.FullName))
  107. .Where(x => IsPackagePluginFolder(x.Directory))
  108. .ToList();
  109. //other plugin description info
  110. var mainPluginFile = pluginFiles
  111. .FirstOrDefault(x => x.Name.Equals(pluginDescriptor.PluginFileName, StringComparison.InvariantCultureIgnoreCase));
  112. pluginDescriptor.OriginalAssemblyFile = mainPluginFile;
  113. //shadow copy main plugin file
  114. pluginDescriptor.ReferencedAssembly = PerformFileDeploy(mainPluginFile);
  115. //load all other referenced assemblies now
  116. foreach (var plugin in pluginFiles
  117. .Where(x => !x.Name.Equals(mainPluginFile.Name, StringComparison.InvariantCultureIgnoreCase))
  118. .Where(x => !IsAlreadyLoaded(x)))
  119. PerformFileDeploy(plugin);
  120. //init plugin type (only one plugin per assembly is allowed)
  121. foreach (var t in pluginDescriptor.ReferencedAssembly.GetTypes())
  122. if (typeof(IPlugin).IsAssignableFrom(t))
  123. if (!t.IsInterface)
  124. if (t.IsClass && !t.IsAbstract)
  125. {
  126. pluginDescriptor.PluginType = t;
  127. break;
  128. }
  129. referencedPlugins.Add(pluginDescriptor);
  130. }
  131. catch (ReflectionTypeLoadException ex)
  132. {
  133. var msg = string.Empty;
  134. foreach (var e in ex.LoaderExceptions)
  135. msg += e.Message + Environment.NewLine;
  136. var fail = new Exception(msg, ex);
  137. Debug.WriteLine(fail.Message, fail);
  138. throw fail;
  139. }
  140. }
  141. }
  142. catch (Exception ex)
  143. {
  144. var msg = string.Empty;
  145. for (var e = ex; e != null; e = e.InnerException)
  146. msg += e.Message + Environment.NewLine;
  147. var fail = new Exception(msg, ex);
  148. Debug.WriteLine(fail.Message, fail);
  149. throw fail;
  150. }
  151. ReferencedPlugins = referencedPlugins;
  152. IncompatiblePlugins = incompatiblePlugins;
  153. }
  154. }
  155. /// <summary>
  156. /// Mark plugin as installed
  157. /// </summary>
  158. /// <param name="systemName">Plugin system name</param>
  159. public static void MarkPluginAsInstalled(string systemName)
  160. {
  161. if (String.IsNullOrEmpty(systemName))
  162. throw new ArgumentNullException("systemName");
  163. var filePath = HostingEnvironment.MapPath(InstalledPluginsFilePath);
  164. if (!File.Exists(filePath))
  165. using (File.Create(filePath))
  166. {
  167. //we use 'using' to close the file after it's created
  168. }
  169. var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath());
  170. bool alreadyMarkedAsInstalled = installedPluginSystemNames
  171. .FirstOrDefault(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) != null;
  172. if (!alreadyMarkedAsInstalled)
  173. installedPluginSystemNames.Add(systemName);
  174. PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames, filePath);
  175. }
  176. /// <summary>
  177. /// Mark plugin as uninstalled
  178. /// </summary>
  179. /// <param name="systemName">Plugin system name</param>
  180. public static void MarkPluginAsUninstalled(string systemName)
  181. {
  182. if (String.IsNullOrEmpty(systemName))
  183. throw new ArgumentNullException("systemName");
  184. var filePath = HostingEnvironment.MapPath(InstalledPluginsFilePath);
  185. if (!File.Exists(filePath))
  186. using (File.Create(filePath))
  187. {
  188. //we use 'using' to close the file after it's created
  189. }
  190. var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath());
  191. bool alreadyMarkedAsInstalled = installedPluginSystemNames
  192. .FirstOrDefault(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) != null;
  193. if (alreadyMarkedAsInstalled)
  194. installedPluginSystemNames.Remove(systemName);
  195. PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames, filePath);
  196. }
  197. /// <summary>
  198. /// Mark plugin as uninstalled
  199. /// </summary>
  200. public static void MarkAllPluginsAsUninstalled()
  201. {
  202. var filePath = HostingEnvironment.MapPath(InstalledPluginsFilePath);
  203. if (File.Exists(filePath))
  204. File.Delete(filePath);
  205. }
  206. #endregion
  207. #region Utilities
  208. /// <summary>
  209. /// Get description files
  210. /// </summary>
  211. /// <param name="pluginFolder">Plugin direcotry info</param>
  212. /// <returns>Original and parsed description files</returns>
  213. private static IEnumerable<KeyValuePair<FileInfo, PluginDescriptor>> GetDescriptionFilesAndDescriptors(DirectoryInfo pluginFolder)
  214. {
  215. if (pluginFolder == null)
  216. throw new ArgumentNullException("pluginFolder");
  217. //create list (<file info, parsed plugin descritor>)
  218. var result = new List<KeyValuePair<FileInfo, PluginDescriptor>>();
  219. //add display order and path to list
  220. foreach (var descriptionFile in pluginFolder.GetFiles("Description.txt", SearchOption.AllDirectories))
  221. {
  222. if (!IsPackagePluginFolder(descriptionFile.Directory))
  223. continue;
  224. //parse file
  225. var pluginDescriptor = PluginFileParser.ParsePluginDescriptionFile(descriptionFile.FullName);
  226. //populate list
  227. result.Add(new KeyValuePair<FileInfo, PluginDescriptor>(descriptionFile, pluginDescriptor));
  228. }
  229. result.Sort((firstPair, nextPair) => firstPair.Value.DisplayOrder.CompareTo(nextPair.Value.DisplayOrder));
  230. return result;
  231. }
  232. /// <summary>
  233. /// Indicates whether assembly file is already loaded
  234. /// </summary>
  235. /// <param name="fileInfo">File info</param>
  236. /// <returns>Result</returns>
  237. private static bool IsAlreadyLoaded(FileInfo fileInfo)
  238. {
  239. //compare full assembly name
  240. //var fileAssemblyName = AssemblyName.GetAssemblyName(fileInfo.FullName);
  241. //foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
  242. //{
  243. // if (a.FullName.Equals(fileAssemblyName.FullName, StringComparison.InvariantCultureIgnoreCase))
  244. // return true;
  245. //}
  246. //return false;
  247. //do not compare the full assembly name, just filename
  248. try
  249. {
  250. string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileInfo.FullName);
  251. if (fileNameWithoutExt == null)
  252. throw new Exception(string.Format("Cannot get file extnension for {0}", fileInfo.Name));
  253. foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
  254. {
  255. string assemblyName = a.FullName.Split(new[] { ',' }).FirstOrDefault();
  256. if (fileNameWithoutExt.Equals(assemblyName, StringComparison.InvariantCultureIgnoreCase))
  257. return true;
  258. }
  259. }
  260. catch (Exception exc)
  261. {
  262. Debug.WriteLine("Cannot validate whether an assembly is already loaded. " + exc);
  263. }
  264. return false;
  265. }
  266. /// <summary>
  267. /// Perform file deply
  268. /// </summary>
  269. /// <param name="plug">Plugin file info</param>
  270. /// <returns>Assembly</returns>
  271. private static Assembly PerformFileDeploy(FileInfo plug)
  272. {
  273. if (plug.Directory.Parent == null)
  274. throw new InvalidOperationException("The plugin directory for the " + plug.Name +
  275. " file exists in a folder outside of the allowed nopCommerce folder heirarchy");
  276. FileInfo shadowCopiedPlug;
  277. if (CommonHelper.GetTrustLevel() != AspNetHostingPermissionLevel.Unrestricted)
  278. {
  279. //all plugins will need to be copied to ~/Plugins/bin/
  280. //this is aboslutely required because all of this relies on probingPaths being set statically in the web.config
  281. //were running in med trust, so copy to custom bin folder
  282. var shadowCopyPlugFolder = Directory.CreateDirectory(_shadowCopyFolder.FullName);
  283. shadowCopiedPlug = InitializeMediumTrust(plug, shadowCopyPlugFolder);
  284. }
  285. else
  286. {
  287. var directory = AppDomain.CurrentDomain.DynamicDirectory;
  288. Debug.WriteLine(plug.FullName + " to " + directory);
  289. //were running in full trust so copy to standard dynamic folder
  290. shadowCopiedPlug = InitializeFullTrust(plug, new DirectoryInfo(directory));
  291. }
  292. //we can now register the plugin definition
  293. var shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName));
  294. //add the reference to the build manager
  295. Debug.WriteLine("Adding to BuildManager: '{0}'", shadowCopiedAssembly.FullName);
  296. BuildManager.AddReferencedAssembly(shadowCopiedAssembly);
  297. return shadowCopiedAssembly;
  298. }
  299. /// <summary>
  300. /// Used to initialize plugins when running in Full Trust
  301. /// </summary>
  302. /// <param name="plug"></param>
  303. /// <param name="shadowCopyPlugFolder"></param>
  304. /// <returns></returns>
  305. private static FileInfo InitializeFullTrust(FileInfo plug, DirectoryInfo shadowCopyPlugFolder)
  306. {
  307. var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name));
  308. try
  309. {
  310. File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
  311. }
  312. catch (IOException)
  313. {
  314. Debug.WriteLine(shadowCopiedPlug.FullName + " is locked, attempting to rename");
  315. //this occurs when the files are locked,
  316. //for some reason devenv locks plugin files some times and for another crazy reason you are allowed to rename them
  317. //which releases the lock, so that it what we are doing here, once it's renamed, we can re-shadow copy
  318. try
  319. {
  320. var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old";
  321. File.Move(shadowCopiedPlug.FullName, oldFile);
  322. }
  323. catch (IOException exc)
  324. {
  325. throw new IOException(shadowCopiedPlug.FullName + " rename failed, cannot initialize plugin", exc);
  326. }
  327. //ok, we've made it this far, now retry the shadow copy
  328. File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
  329. }
  330. return shadowCopiedPlug;
  331. }
  332. /// <summary>
  333. /// Used to initialize plugins when running in Medium Trust
  334. /// </summary>
  335. /// <param name="plug"></param>
  336. /// <param name="shadowCopyPlugFolder"></param>
  337. /// <returns></returns>
  338. private static FileInfo InitializeMediumTrust(FileInfo plug, DirectoryInfo shadowCopyPlugFolder)
  339. {
  340. var shouldCopy = true;
  341. var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name));
  342. //check if a shadow copied file already exists and if it does, check if it's updated, if not don't copy
  343. if (shadowCopiedPlug.Exists)
  344. {
  345. //it's better to use LastWriteTimeUTC, but not all file systems have this property
  346. //maybe it is better to compare file hash?
  347. var areFilesIdentical = shadowCopiedPlug.CreationTimeUtc.Ticks >= plug.CreationTimeUtc.Ticks;
  348. if (areFilesIdentical)
  349. {
  350. Debug.WriteLine("Not copying; files appear identical: '{0}'", shadowCopiedPlug.Name);
  351. shouldCopy = false;
  352. }
  353. else
  354. {
  355. //delete an existing file
  356. //More info: http://www.nopcommerce.com/boards/t/11511/access-error-nopplugindiscountrulesbillingcountrydll.aspx?p=4#60838
  357. Debug.WriteLine("New plugin found; Deleting the old file: '{0}'", shadowCopiedPlug.Name);
  358. File.Delete(shadowCopiedPlug.FullName);
  359. }
  360. }
  361. if (shouldCopy)
  362. {
  363. try
  364. {
  365. File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
  366. }
  367. catch (IOException)
  368. {
  369. Debug.WriteLine(shadowCopiedPlug.FullName + " is locked, attempting to rename");
  370. //this occurs when the files are locked,
  371. //for some reason devenv locks plugin files some times and for another crazy reason you are allowed to rename them
  372. //which releases the lock, so that it what we are doing here, once it's renamed, we can re-shadow copy
  373. try
  374. {
  375. var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old";
  376. File.Move(shadowCopiedPlug.FullName, oldFile);
  377. }
  378. catch (IOException exc)
  379. {
  380. throw new IOException(shadowCopiedPlug.FullName + " rename failed, cannot initialize plugin", exc);
  381. }
  382. //ok, we've made it this far, now retry the shadow copy
  383. File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
  384. }
  385. }
  386. return shadowCopiedPlug;
  387. }
  388. /// <summary>
  389. /// Determines if the folder is a bin plugin folder for a package
  390. /// </summary>
  391. /// <param name="folder"></param>
  392. /// <returns></returns>
  393. private static bool IsPackagePluginFolder(DirectoryInfo folder)
  394. {
  395. if (folder == null) return false;
  396. if (folder.Parent == null) return false;
  397. if (!folder.Parent.Name.Equals("Plugins", StringComparison.InvariantCultureIgnoreCase)) return false;
  398. return true;
  399. }
  400. /// <summary>
  401. /// Gets the full path of InstalledPlugins.txt file
  402. /// </summary>
  403. /// <returns></returns>
  404. private static string GetInstalledPluginsFilePath()
  405. {
  406. var filePath = HostingEnvironment.MapPath(InstalledPluginsFilePath);
  407. return filePath;
  408. }
  409. #endregion
  410. }
  411. }