You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

713 lines
27 KiB

  1. using IPA.Config;
  2. using IPA.Loader.Features;
  3. using IPA.Logging;
  4. using IPA.Utilities;
  5. using Mono.Cecil;
  6. using Newtonsoft.Json;
  7. using System;
  8. using System.Collections.Generic;
  9. using System.IO;
  10. using System.Linq;
  11. using System.Reflection;
  12. using System.Text.RegularExpressions;
  13. using System.Threading.Tasks;
  14. using Version = SemVer.Version;
  15. #if NET3
  16. using Net3_Proxy;
  17. using Path = Net3_Proxy.Path;
  18. using File = Net3_Proxy.File;
  19. using Directory = Net3_Proxy.Directory;
  20. #endif
  21. namespace IPA.Loader
  22. {
  23. /// <summary>
  24. /// A type to manage the loading of plugins.
  25. /// </summary>
  26. public class PluginLoader
  27. {
  28. internal static Task LoadTask() =>
  29. #if NET3
  30. TaskEx.Run(() =>
  31. #else
  32. Task.Run(() =>
  33. #endif
  34. {
  35. YeetIfNeeded();
  36. LoadMetadata();
  37. Resolve();
  38. ComputeLoadOrder();
  39. FilterDisabled();
  40. ResolveDependencies();
  41. });
  42. /// <summary>
  43. /// A class which describes a loaded plugin.
  44. /// </summary>
  45. public class PluginMetadata
  46. {
  47. /// <summary>
  48. /// The assembly the plugin was loaded from.
  49. /// </summary>
  50. /// <value>the loaded Assembly that contains the plugin main type</value>
  51. public Assembly Assembly { get; internal set; }
  52. /// <summary>
  53. /// The TypeDefinition for the main type of the plugin.
  54. /// </summary>
  55. /// <value>the Cecil definition for the plugin main type</value>
  56. public TypeDefinition PluginType { get; internal set; }
  57. /// <summary>
  58. /// The human readable name of the plugin.
  59. /// </summary>
  60. /// <value>the name of the plugin</value>
  61. public string Name { get; internal set; }
  62. /// <summary>
  63. /// The BeatMods ID of the plugin, or null if it doesn't have one.
  64. /// </summary>
  65. /// <value>the updater ID of the plugin</value>
  66. public string Id { get; internal set; }
  67. /// <summary>
  68. /// The version of the plugin.
  69. /// </summary>
  70. /// <value>the version of the plugin</value>
  71. public Version Version { get; internal set; }
  72. /// <summary>
  73. /// The file the plugin was loaded from.
  74. /// </summary>
  75. /// <value>the file the plugin was loaded from</value>
  76. public FileInfo File { get; internal set; }
  77. // ReSharper disable once UnusedAutoPropertyAccessor.Global
  78. /// <summary>
  79. /// The features this plugin requests.
  80. /// </summary>
  81. /// <value>the list of features requested by the plugin</value>
  82. public IReadOnlyList<Feature> Features => InternalFeatures;
  83. internal readonly List<Feature> InternalFeatures = new List<Feature>();
  84. internal bool IsSelf;
  85. /// <summary>
  86. /// Whether or not this metadata object represents a bare manifest.
  87. /// </summary>
  88. /// <value><see langword="true"/> if it is bare, <see langword="false"/> otherwise</value>
  89. public bool IsBare { get; internal set; }
  90. private PluginManifest manifest;
  91. internal HashSet<PluginMetadata> Dependencies { get; } = new HashSet<PluginMetadata>();
  92. internal PluginManifest Manifest
  93. {
  94. get => manifest;
  95. set
  96. {
  97. manifest = value;
  98. Name = value.Name;
  99. Version = value.Version;
  100. Id = value.Id;
  101. }
  102. }
  103. /// <summary>
  104. /// Gets all of the metadata as a readable string.
  105. /// </summary>
  106. /// <returns>the readable printable metadata string</returns>
  107. public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName, BeatSaber.InstallPath)}'";
  108. }
  109. /// <summary>
  110. /// A container object for all the data relating to a plugin.
  111. /// </summary>
  112. public class PluginInfo
  113. {
  114. internal IPlugin Plugin { get; set; }
  115. /// <summary>
  116. /// Metadata for the plugin.
  117. /// </summary>
  118. /// <value>the metadata for this plugin</value>
  119. public PluginMetadata Metadata { get; internal set; } = new PluginMetadata();
  120. }
  121. internal static void YeetIfNeeded()
  122. {
  123. string pluginDir = BeatSaber.PluginsPath;
  124. var gameVer = BeatSaber.GameVersion;
  125. var lastVerS = SelfConfig.LastGameVersion_;
  126. var lastVer = lastVerS != null ? new AlmostVersion(lastVerS, gameVer) : null;
  127. if (SelfConfig.YeetMods_ && lastVer != null && gameVer != lastVer)
  128. {
  129. var oldPluginsName = Path.Combine(BeatSaber.InstallPath, $"Old {lastVer} Plugins");
  130. var newPluginsName = Path.Combine(BeatSaber.InstallPath, $"Old {gameVer} Plugins");
  131. if (Directory.Exists(oldPluginsName))
  132. Directory.Delete(oldPluginsName, true);
  133. Directory.Move(pluginDir, oldPluginsName);
  134. if (Directory.Exists(newPluginsName))
  135. Directory.Move(newPluginsName, pluginDir);
  136. else
  137. Directory.CreateDirectory(pluginDir);
  138. }
  139. SelfConfig.Instance.LastGameVersion = gameVer.ToString();
  140. }
  141. internal static List<PluginMetadata> PluginsMetadata = new List<PluginMetadata>();
  142. internal static List<PluginMetadata> DisabledPlugins = new List<PluginMetadata>();
  143. private static readonly Regex embeddedTextDescriptionPattern = new Regex(@"#!\[(.+)\]", RegexOptions.Compiled | RegexOptions.Singleline);
  144. internal static void LoadMetadata()
  145. {
  146. string[] plugins = Directory.GetFiles(BeatSaber.PluginsPath, "*.dll");
  147. try
  148. {
  149. var selfMeta = new PluginMetadata
  150. {
  151. Assembly = Assembly.GetExecutingAssembly(),
  152. File = new FileInfo(Path.Combine(BeatSaber.InstallPath, "IPA.exe")),
  153. PluginType = null,
  154. IsSelf = true
  155. };
  156. string manifest;
  157. using (var manifestReader =
  158. new StreamReader(
  159. selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ??
  160. throw new InvalidOperationException()))
  161. manifest = manifestReader.ReadToEnd();
  162. selfMeta.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  163. PluginsMetadata.Add(selfMeta);
  164. }
  165. catch (Exception e)
  166. {
  167. Logger.loader.Critical("Error loading own manifest");
  168. Logger.loader.Critical(e);
  169. }
  170. foreach (var plugin in plugins)
  171. {
  172. var metadata = new PluginMetadata
  173. {
  174. File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, plugin)),
  175. IsSelf = false
  176. };
  177. try
  178. {
  179. var pluginModule = AssemblyDefinition.ReadAssembly(plugin, new ReaderParameters
  180. {
  181. ReadingMode = ReadingMode.Immediate,
  182. ReadWrite = false,
  183. AssemblyResolver = new CecilLibLoader()
  184. }).MainModule;
  185. string pluginNs = "";
  186. foreach (var resource in pluginModule.Resources)
  187. {
  188. const string manifestSuffix = ".manifest.json";
  189. if (!(resource is EmbeddedResource embedded) ||
  190. !embedded.Name.EndsWith(manifestSuffix)) continue;
  191. pluginNs = embedded.Name.Substring(0, embedded.Name.Length - manifestSuffix.Length);
  192. string manifest;
  193. using (var manifestReader = new StreamReader(embedded.GetResourceStream()))
  194. manifest = manifestReader.ReadToEnd();
  195. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  196. break;
  197. }
  198. if (metadata.Manifest == null)
  199. {
  200. #if DIRE_LOADER_WARNINGS
  201. Logger.loader.Error($"Could not find manifest.json for {Path.GetFileName(plugin)}");
  202. #else
  203. Logger.loader.Notice($"No manifest.json in {Path.GetFileName(plugin)}");
  204. #endif
  205. continue;
  206. }
  207. foreach (var type in pluginModule.Types)
  208. {
  209. if (type.Namespace != pluginNs) continue;
  210. if (type.HasInterface(typeof(IPlugin).FullName))
  211. {
  212. metadata.PluginType = type;
  213. break;
  214. }
  215. }
  216. if (metadata.PluginType == null)
  217. {
  218. Logger.loader.Error($"No plugin found in the manifest namespace ({pluginNs}) in {Path.GetFileName(plugin)}");
  219. continue;
  220. }
  221. Logger.loader.Debug($"Adding info for {Path.GetFileName(plugin)}");
  222. PluginsMetadata.Add(metadata);
  223. }
  224. catch (Exception e)
  225. {
  226. Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}");
  227. Logger.loader.Error(e);
  228. ignoredPlugins.Add(metadata);
  229. }
  230. }
  231. IEnumerable<string> bareManifests = Directory.GetFiles(BeatSaber.PluginsPath, "*.json");
  232. bareManifests = bareManifests.Concat(Directory.GetFiles(BeatSaber.PluginsPath, "*.manifest"));
  233. foreach (var manifest in bareManifests)
  234. {
  235. try
  236. {
  237. var metadata = new PluginMetadata
  238. {
  239. File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, manifest)),
  240. IsSelf = false,
  241. IsBare = true,
  242. };
  243. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(File.ReadAllText(manifest));
  244. Logger.loader.Debug($"Adding info for bare manifest {Path.GetFileName(manifest)}");
  245. PluginsMetadata.Add(metadata);
  246. }
  247. catch (Exception e)
  248. {
  249. Logger.loader.Error($"Could not load data for bare manifest {Path.GetFileName(manifest)}");
  250. Logger.loader.Error(e);
  251. }
  252. }
  253. foreach (var meta in PluginsMetadata)
  254. { // process description include
  255. var lines = meta.Manifest.Description.Split('\n');
  256. var m = embeddedTextDescriptionPattern.Match(lines[0]);
  257. if (m.Success)
  258. {
  259. if (meta.IsBare)
  260. {
  261. Logger.loader.Warn($"Bare manifest cannot specify description file");
  262. meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line
  263. continue;
  264. }
  265. var name = m.Groups[1].Value;
  266. string description;
  267. if (!meta.IsSelf)
  268. {
  269. var resc = meta.PluginType.Module.Resources.Select(r => r as EmbeddedResource)
  270. .NonNull()
  271. .FirstOrDefault(r => r.Name == name);
  272. if (resc == null)
  273. {
  274. Logger.loader.Warn($"Could not find description file for plugin {meta.Name} ({name}); ignoring include");
  275. meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line
  276. continue;
  277. }
  278. using (var reader = new StreamReader(resc.GetResourceStream()))
  279. description = reader.ReadToEnd();
  280. }
  281. else
  282. {
  283. using (var descriptionReader =
  284. new StreamReader(
  285. meta.Assembly.GetManifestResourceStream(name) ??
  286. throw new InvalidOperationException()))
  287. description = descriptionReader.ReadToEnd();
  288. }
  289. meta.Manifest.Description = description;
  290. }
  291. }
  292. }
  293. // keep track of these for the updater; it should still be able to update mods not loaded
  294. // TODO: add ignore reason
  295. internal static HashSet<PluginMetadata> ignoredPlugins = new HashSet<PluginMetadata>();
  296. internal static void Resolve()
  297. { // resolves duplicates and conflicts, etc
  298. PluginsMetadata.Sort((a, b) => b.Version.CompareTo(a.Version));
  299. var ids = new HashSet<string>();
  300. var ignore = new HashSet<PluginMetadata>();
  301. var resolved = new List<PluginMetadata>(PluginsMetadata.Count);
  302. foreach (var meta in PluginsMetadata)
  303. {
  304. if (meta.Id != null)
  305. {
  306. if (ids.Contains(meta.Id))
  307. {
  308. Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest");
  309. ignore.Add(meta);
  310. ignoredPlugins.Add(meta);
  311. continue; // because of sorted order, hightest order will always be the first one
  312. }
  313. bool processedLater = false;
  314. foreach (var meta2 in PluginsMetadata)
  315. {
  316. if (ignore.Contains(meta2)) continue;
  317. if (meta == meta2)
  318. {
  319. processedLater = true;
  320. continue;
  321. }
  322. if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue;
  323. var range = meta2.Manifest.Conflicts[meta.Id];
  324. if (!range.IsSatisfied(meta.Version)) continue;
  325. Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Name}");
  326. if (processedLater)
  327. {
  328. Logger.loader.Warn($"Ignoring {meta2.Name}");
  329. ignore.Add(meta2);
  330. }
  331. else
  332. {
  333. Logger.loader.Warn($"Ignoring {meta.Name}");
  334. ignore.Add(meta);
  335. break;
  336. }
  337. }
  338. }
  339. if (ignore.Contains(meta))
  340. {
  341. ignoredPlugins.Add(meta);
  342. continue;
  343. }
  344. if (meta.Id != null)
  345. ids.Add(meta.Id);
  346. resolved.Add(meta);
  347. }
  348. PluginsMetadata = resolved;
  349. }
  350. private static void FilterDisabled()
  351. {
  352. var enabled = new List<PluginMetadata>(PluginsMetadata.Count);
  353. var disabled = DisabledConfig.Instance.DisabledModIds;
  354. foreach (var meta in PluginsMetadata)
  355. {
  356. if (disabled.Contains(meta.Id ?? meta.Name))
  357. DisabledPlugins.Add(meta);
  358. else
  359. enabled.Add(meta);
  360. }
  361. PluginsMetadata = enabled;
  362. }
  363. internal static void ComputeLoadOrder()
  364. {
  365. #if DEBUG
  366. Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP()));
  367. #endif
  368. bool InsertInto(HashSet<PluginMetadata> root, PluginMetadata meta, bool isRoot = false)
  369. { // this is slow, and hella recursive
  370. bool inserted = false;
  371. foreach (var sr in root)
  372. {
  373. inserted = inserted || InsertInto(sr.Dependencies, meta);
  374. if (meta.Id != null)
  375. if (sr.Manifest.Dependencies.ContainsKey(meta.Id) || sr.Manifest.LoadAfter.Contains(meta.Id))
  376. inserted = inserted || sr.Dependencies.Add(meta);
  377. if (sr.Id != null)
  378. if (meta.Manifest.LoadBefore.Contains(sr.Id))
  379. inserted = inserted || sr.Dependencies.Add(meta);
  380. }
  381. if (isRoot)
  382. {
  383. foreach (var sr in root)
  384. {
  385. InsertInto(meta.Dependencies, sr);
  386. if (sr.Id != null)
  387. if (meta.Manifest.Dependencies.ContainsKey(sr.Id) || meta.Manifest.LoadAfter.Contains(sr.Id))
  388. meta.Dependencies.Add(sr);
  389. if (meta.Id != null)
  390. if (sr.Manifest.LoadBefore.Contains(meta.Id))
  391. meta.Dependencies.Add(sr);
  392. }
  393. root.Add(meta);
  394. }
  395. return inserted;
  396. }
  397. var pluginTree = new HashSet<PluginMetadata>();
  398. foreach (var meta in PluginsMetadata)
  399. InsertInto(pluginTree, meta, true);
  400. void DeTree(List<PluginMetadata> into, HashSet<PluginMetadata> tree)
  401. {
  402. foreach (var st in tree)
  403. if (!into.Contains(st))
  404. {
  405. DeTree(into, st.Dependencies);
  406. into.Add(st);
  407. }
  408. }
  409. PluginsMetadata = new List<PluginMetadata>();
  410. DeTree(PluginsMetadata, pluginTree);
  411. #if DEBUG
  412. Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP()));
  413. #endif
  414. }
  415. internal static void ResolveDependencies()
  416. {
  417. var metadata = new List<PluginMetadata>();
  418. var pluginsToLoad = new Dictionary<string, Version>();
  419. var disabledLookup = DisabledPlugins.NonNull(m => m.Id).ToDictionary(m => m.Id, m => m.Version);
  420. foreach (var meta in PluginsMetadata)
  421. {
  422. bool load = true;
  423. bool disable = false;
  424. foreach (var dep in meta.Manifest.Dependencies)
  425. {
  426. #if DEBUG
  427. Logger.loader.Debug($"Looking for dependency {dep.Key} with version range {dep.Value.Intersect(new SemVer.Range("*.*.*"))}");
  428. #endif
  429. if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key]))
  430. continue;
  431. load = false;
  432. if (disabledLookup.ContainsKey(dep.Key) && dep.Value.IsSatisfied(disabledLookup[dep.Key]))
  433. {
  434. disable = true;
  435. Logger.loader.Warn($"Dependency {dep.Key} was found, but disabled. Disabling {meta.Name} too.");
  436. }
  437. else
  438. Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}");
  439. break;
  440. }
  441. if (load)
  442. {
  443. metadata.Add(meta);
  444. if (meta.Id != null)
  445. pluginsToLoad.Add(meta.Id, meta.Version);
  446. }
  447. else if (disable)
  448. {
  449. DisabledPlugins.Add(meta);
  450. DisabledConfig.Instance.DisabledModIds.Add(meta.Id ?? meta.Name);
  451. }
  452. else
  453. ignoredPlugins.Add(meta);
  454. }
  455. PluginsMetadata = metadata;
  456. }
  457. internal static void InitFeatures()
  458. {
  459. var parsedFeatures = PluginsMetadata.Select(m =>
  460. (metadata: m,
  461. features: m.Manifest.Features.Select(feature =>
  462. (feature, parsed: Ref.Create<Feature.FeatureParse?>(null))
  463. ).ToList()
  464. )
  465. ).ToList();
  466. while (DefineFeature.NewFeature)
  467. {
  468. DefineFeature.NewFeature = false;
  469. foreach (var (metadata, features) in parsedFeatures)
  470. for (var i = 0; i < features.Count; i++)
  471. {
  472. var feature = features[i];
  473. var success = Feature.TryParseFeature(feature.feature, metadata, out var featureObj,
  474. out var exception, out var valid, out var parsed, feature.parsed.Value);
  475. if (!success && !valid && featureObj == null && exception == null) // no feature of type found
  476. feature.parsed.Value = parsed;
  477. else if (success)
  478. {
  479. if (valid && featureObj.StoreOnPlugin)
  480. metadata.InternalFeatures.Add(featureObj);
  481. else if (!valid)
  482. Logger.features.Warn(
  483. $"Feature not valid on {metadata.Name}: {featureObj.InvalidMessage}");
  484. features.RemoveAt(i--);
  485. }
  486. else
  487. {
  488. Logger.features.Error($"Error parsing feature definition on {metadata.Name}");
  489. Logger.features.Error(exception);
  490. features.RemoveAt(i--);
  491. }
  492. }
  493. foreach (var plugin in PluginsMetadata)
  494. foreach (var feature in plugin.Features)
  495. feature.Evaluate();
  496. }
  497. foreach (var plugin in parsedFeatures)
  498. {
  499. if (plugin.features.Count <= 0) continue;
  500. Logger.features.Warn($"On plugin {plugin.metadata.Name}:");
  501. foreach (var feature in plugin.features)
  502. Logger.features.Warn($" Feature not found with name {feature.feature}");
  503. }
  504. }
  505. internal static void ReleaseAll(bool full = false)
  506. {
  507. if (full)
  508. ignoredPlugins = new HashSet<PluginMetadata>();
  509. else
  510. {
  511. foreach (var m in PluginsMetadata)
  512. ignoredPlugins.Add(m);
  513. foreach (var m in ignoredPlugins)
  514. { // clean them up so we can still use the metadata for updates
  515. m.InternalFeatures.Clear();
  516. m.PluginType = null;
  517. m.Assembly = null;
  518. }
  519. }
  520. PluginsMetadata = new List<PluginMetadata>();
  521. DisabledPlugins = new List<PluginMetadata>();
  522. Feature.Reset();
  523. GC.Collect();
  524. }
  525. internal static void Load(PluginMetadata meta)
  526. {
  527. if (meta.Assembly == null && meta.PluginType != null)
  528. meta.Assembly = Assembly.LoadFrom(meta.File.FullName);
  529. }
  530. internal static PluginInfo InitPlugin(PluginMetadata meta)
  531. {
  532. if (meta.PluginType == null)
  533. return new PluginInfo()
  534. {
  535. Metadata = meta,
  536. Plugin = null
  537. };
  538. var info = new PluginInfo();
  539. if (meta.Manifest.GameVersion != BeatSaber.GameVersion)
  540. Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly.");
  541. try
  542. { // TODO: add dependency checking for when features prevent loading
  543. Load(meta);
  544. Feature denyingFeature = null;
  545. if (!meta.Features.All(f => (denyingFeature = f).BeforeLoad(meta)))
  546. {
  547. Logger.loader.Warn(
  548. $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from loading! {denyingFeature?.InvalidMessage}");
  549. ignoredPlugins.Add(meta);
  550. return null;
  551. }
  552. var type = meta.Assembly.GetType(meta.PluginType.FullName);
  553. var instance = Activator.CreateInstance(type) as IPlugin;
  554. info.Metadata = meta;
  555. info.Plugin = instance;
  556. var init = type.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public);
  557. if (init != null)
  558. {
  559. denyingFeature = null;
  560. if (!meta.Features.All(f => (denyingFeature = f).BeforeInit(info)))
  561. {
  562. Logger.loader.Warn(
  563. $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from initializing! {denyingFeature?.InvalidMessage}");
  564. ignoredPlugins.Add(meta);
  565. return null;
  566. }
  567. PluginInitInjector.Inject(init, info);
  568. }
  569. foreach (var feature in meta.Features)
  570. try
  571. {
  572. feature.AfterInit(info, info.Plugin);
  573. }
  574. catch (Exception e)
  575. {
  576. Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}: {e}");
  577. }
  578. try // TODO: move this out to after all plugins have been inited
  579. {
  580. instance.OnEnable();
  581. }
  582. catch (Exception e)
  583. {
  584. Logger.loader.Error($"Error occurred trying to enable {meta.Name}");
  585. Logger.loader.Error(e);
  586. return null; // is enable failure a full load failure?
  587. }
  588. }
  589. catch (AmbiguousMatchException)
  590. {
  591. Logger.loader.Error($"Only one Init allowed per plugin (ambiguous match in {meta.Name})");
  592. // not adding to ignoredPlugins here because this should only happen in a development context
  593. // if someone fucks this up on release thats on them
  594. return null;
  595. }
  596. catch (Exception e)
  597. {
  598. Logger.loader.Error($"Could not init plugin {meta.Name}: {e}");
  599. ignoredPlugins.Add(meta);
  600. return null;
  601. }
  602. return info;
  603. }
  604. internal static List<PluginInfo> LoadPlugins()
  605. {
  606. InitFeatures();
  607. DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily
  608. return PluginsMetadata.Select(InitPlugin).NonNull().ToList();
  609. }
  610. }
  611. }