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.

306 lines
11 KiB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Reflection;
  6. using System.Threading.Tasks;
  7. using IPA.Logging;
  8. using IPA.Utilities;
  9. using Newtonsoft.Json;
  10. using Version = SemVer.Version;
  11. namespace IPA.Loader
  12. {
  13. /// <summary>
  14. /// A type to manage the loading of plugins.
  15. /// </summary>
  16. public class PluginLoader
  17. {
  18. /// <summary>
  19. /// The directory to load plugins from.
  20. /// </summary>
  21. public static string PluginsDirectory => Path.Combine(BeatSaber.InstallPath, "Plugins");
  22. internal static Task LoadTask() => Task.Run(() =>
  23. {
  24. LoadMetadata();
  25. Logger.log.Debug(string.Join(", ", PluginsMetadata));
  26. Resolve();
  27. Logger.log.Debug(string.Join(", ", PluginsMetadata));
  28. ComputeLoadOrder();
  29. });
  30. /// <summary>
  31. /// A class which describes
  32. /// </summary>
  33. public class PluginMetadata
  34. {
  35. // ReSharper disable once UnusedAutoPropertyAccessor.Global
  36. /// <summary>
  37. /// The assembly the plugin was loaded from.
  38. /// </summary>
  39. public Assembly Assembly { get; internal set; }
  40. /// <summary>
  41. /// The Type that is the main type for the plugin.
  42. /// </summary>
  43. public Type PluginType { get; internal set; }
  44. /// <summary>
  45. /// The human readable name of the plugin.
  46. /// </summary>
  47. public string Name { get; internal set; }
  48. /// <summary>
  49. /// The ModSaber ID of the plugin, or null if it doesn't have one.
  50. /// </summary>
  51. public string Id { get; internal set; }
  52. /// <summary>
  53. /// The version of the plugin.
  54. /// </summary>
  55. public Version Version { get; internal set; }
  56. /// <summary>
  57. /// The file the plugin was loaded from.
  58. /// </summary>
  59. public FileInfo File { get; internal set; }
  60. // ReSharper disable once UnusedAutoPropertyAccessor.Global
  61. /// <summary>
  62. /// The features this plugin requests.
  63. /// </summary>
  64. public string[] Features { get; internal set; }
  65. private PluginManifest manifest;
  66. internal PluginManifest Manifest
  67. {
  68. get => manifest;
  69. set
  70. {
  71. manifest = value;
  72. Name = value.Name;
  73. Version = value.Version;
  74. Id = value.Id;
  75. Features = value.Features;
  76. }
  77. }
  78. /// <inheritdoc />
  79. public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.AssemblyQualifiedName}) from '{File.Name}'";
  80. }
  81. /// <summary>
  82. /// A container object for all the data relating to a plugin.
  83. /// </summary>
  84. public class PluginInfo
  85. {
  86. internal IBeatSaberPlugin Plugin { get; set; }
  87. internal string Filename { get; set; }
  88. /// <summary>
  89. /// Metadata for the plugin.
  90. /// </summary>
  91. public PluginMetadata Metadata { get; internal set; } = new PluginMetadata();
  92. }
  93. internal static List<PluginMetadata> PluginsMetadata = new List<PluginMetadata>();
  94. internal static void LoadMetadata()
  95. {
  96. string[] plugins = Directory.GetFiles(PluginsDirectory, "*.dll");
  97. try
  98. {
  99. var selfmeta = new PluginMetadata
  100. {
  101. Assembly = Assembly.ReflectionOnlyLoadFrom(Assembly.GetExecutingAssembly()
  102. .Location), // load self as reflection only
  103. File = new FileInfo(Path.Combine(BeatSaber.InstallPath, "IPA.exe")),
  104. PluginType = null
  105. };
  106. string manifest;
  107. using (var manifestReader =
  108. new StreamReader(
  109. selfmeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ??
  110. throw new InvalidOperationException()))
  111. manifest = manifestReader.ReadToEnd();
  112. selfmeta.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  113. PluginsMetadata.Add(selfmeta);
  114. }
  115. catch (Exception e)
  116. {
  117. Logger.loader.Critical("Error loading own manifest");
  118. Logger.loader.Critical(e);
  119. }
  120. foreach (var plugin in plugins)
  121. { // should probably do patching first /shrug
  122. try
  123. {
  124. var metadata = new PluginMetadata();
  125. var assembly = Assembly.ReflectionOnlyLoadFrom(plugin);
  126. metadata.Assembly = assembly;
  127. metadata.File = new FileInfo(plugin);
  128. Type[] types;
  129. try
  130. {
  131. types = assembly.GetTypes();
  132. }
  133. catch (ReflectionTypeLoadException e)
  134. {
  135. types = e.Types;
  136. }
  137. foreach (var type in types)
  138. {
  139. if (type == null) continue;
  140. var iInterface = type.GetInterface(nameof(IBeatSaberPlugin));
  141. if (iInterface == null) continue;
  142. metadata.PluginType = type;
  143. break;
  144. }
  145. if (metadata.PluginType == null)
  146. {
  147. Logger.loader.Warn($"Could not find plugin type for {Path.GetFileName(plugin)}");
  148. continue;
  149. }
  150. Stream metadataStream;
  151. try
  152. {
  153. metadataStream = assembly.GetManifestResourceStream(metadata.PluginType, "manifest.json");
  154. if (metadataStream == null)
  155. {
  156. Logger.loader.Error($"manifest.json not found in plugin {Path.GetFileName(plugin)}");
  157. continue;
  158. }
  159. }
  160. catch (FileNotFoundException)
  161. {
  162. Logger.loader.Error($"manifest.json not found in plugin {Path.GetFileName(plugin)}");
  163. continue;
  164. }
  165. string manifest;
  166. using (var manifestReader = new StreamReader(metadataStream))
  167. manifest = manifestReader.ReadToEnd();
  168. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  169. PluginsMetadata.Add(metadata);
  170. }
  171. catch (Exception e)
  172. {
  173. Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}");
  174. Logger.loader.Error(e);
  175. }
  176. }
  177. }
  178. internal static void Resolve()
  179. { // resolves duplicates and conflicts, etc
  180. PluginsMetadata.Sort((a, b) => a.Version.CompareTo(b.Version));
  181. var ids = new HashSet<string>();
  182. var ignore = new HashSet<PluginMetadata>();
  183. var resolved = new List<PluginMetadata>(PluginsMetadata.Count);
  184. foreach (var meta in PluginsMetadata)
  185. {
  186. if (meta.Id != null)
  187. {
  188. if (ids.Contains(meta.Id))
  189. {
  190. Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest");
  191. ignore.Add(meta);
  192. continue; // because of sorted order, hightest order will always be the first one
  193. }
  194. bool processedLater = false;
  195. foreach (var meta2 in PluginsMetadata)
  196. {
  197. if (ignore.Contains(meta2)) continue;
  198. if (meta == meta2)
  199. {
  200. processedLater = true;
  201. continue;
  202. }
  203. if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue;
  204. var range = meta2.Manifest.Conflicts[meta.Id];
  205. if (!range.IsSatisfied(meta.Version)) continue;
  206. Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Name}");
  207. if (processedLater)
  208. {
  209. Logger.loader.Warn($"Ignoring {meta2.Name}");
  210. ignore.Add(meta2);
  211. }
  212. else
  213. {
  214. Logger.loader.Warn($"Ignoring {meta.Name}");
  215. ignore.Add(meta);
  216. break;
  217. }
  218. }
  219. }
  220. if (ignore.Contains(meta)) continue;
  221. if (meta.Id != null) ids.Add(meta.Id);
  222. resolved.Add(meta);
  223. }
  224. PluginsMetadata = resolved;
  225. }
  226. internal static void ComputeLoadOrder()
  227. {
  228. PluginsMetadata.Sort((a, b) =>
  229. {
  230. if (a.Id == b.Id) return 0;
  231. if (a.Id != null)
  232. {
  233. if (b.Manifest.Dependencies.ContainsKey(a.Id) || b.Manifest.LoadAfter.Contains(a.Id)) return -1;
  234. if (b.Manifest.LoadBefore.Contains(a.Id)) return 1;
  235. }
  236. if (b.Id != null)
  237. {
  238. if (a.Manifest.Dependencies.ContainsKey(b.Id) || a.Manifest.LoadAfter.Contains(b.Id)) return 1;
  239. if (a.Manifest.LoadBefore.Contains(b.Id)) return -1;
  240. }
  241. return 0;
  242. });
  243. Logger.log.Debug(string.Join(", ", PluginsMetadata));
  244. var metadata = new List<PluginMetadata>();
  245. var pluginsToLoad = new Dictionary<string, Version>();
  246. foreach (var meta in PluginsMetadata)
  247. {
  248. bool load = true;
  249. foreach (var dep in meta.Manifest.Dependencies)
  250. {
  251. if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key])) continue;
  252. load = false;
  253. Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}");
  254. }
  255. if (load)
  256. {
  257. metadata.Add(meta);
  258. if (meta.Id != null)
  259. pluginsToLoad.Add(meta.Id, meta.Version);
  260. }
  261. }
  262. PluginsMetadata = metadata;
  263. }
  264. internal static void LoadPlugins()
  265. {
  266. }
  267. }
  268. }