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.

234 lines
12 KiB

  1. using IPA.Logging;
  2. using IPA.Utilities;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Reflection;
  7. using System.Linq.Expressions;
  8. #if NET4
  9. using Task = System.Threading.Tasks.Task;
  10. using TaskEx = System.Threading.Tasks.Task;
  11. using Expression = System.Linq.Expressions.Expression;
  12. using ExpressionEx = System.Linq.Expressions.Expression;
  13. #endif
  14. #if NET3
  15. using System.Threading.Tasks;
  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. // NOTE: TaskEx.WhenAll() (Task.WhenAll() in .NET 4) returns CompletedTask if it has no arguments, which we need for .NET 3
  24. internal class PluginExecutor
  25. {
  26. public enum Special
  27. {
  28. None, Self, Bare
  29. }
  30. public PluginMetadata Metadata { get; }
  31. public Special SpecialType { get; }
  32. public PluginExecutor(PluginMetadata meta, Special specialType = Special.None)
  33. {
  34. Metadata = meta;
  35. SpecialType = specialType;
  36. if (specialType != Special.None)
  37. {
  38. CreatePlugin = m => null;
  39. LifecycleEnable = o => TaskEx.WhenAll();
  40. LifecycleDisable = o => TaskEx.WhenAll();
  41. }
  42. else
  43. PrepareDelegates();
  44. }
  45. public object Instance { get; private set; } = null;
  46. private Func<PluginMetadata, object> CreatePlugin { get; set; }
  47. private Func<object, Task> LifecycleEnable { get; set; }
  48. private Func<object, Task> LifecycleDisable { get; set; }
  49. public void Create()
  50. {
  51. if (Instance != null) return;
  52. Instance = CreatePlugin(Metadata);
  53. }
  54. public Task Enable() => LifecycleEnable(Instance);
  55. public Task Disable() => LifecycleDisable(Instance);
  56. private void PrepareDelegates()
  57. { // TODO: use custom exception types or something
  58. PluginLoader.Load(Metadata);
  59. var type = Metadata.Assembly.GetType(Metadata.PluginType.FullName);
  60. CreatePlugin = MakeCreateFunc(type, Metadata.Name);
  61. LifecycleEnable = MakeLifecycleEnableFunc(type, Metadata.Name);
  62. LifecycleDisable = MakeLifecycleDisableFunc(type, Metadata.Name);
  63. }
  64. private static Func<PluginMetadata, object> MakeCreateFunc(Type type, string name)
  65. { // TODO: what do i want the visibiliy of Init methods to be?
  66. var ctors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
  67. .Select(c => (c, attr: c.GetCustomAttribute<InitAttribute>()))
  68. .NonNull(t => t.attr)
  69. .OrderByDescending(t => t.c.GetParameters().Length)
  70. .Select(t => t.c).ToArray();
  71. if (ctors.Length > 1)
  72. Logger.Loader.Warn($"Plugin {name} has multiple [Init] constructors. Picking the one with the most parameters.");
  73. bool usingDefaultCtor = false;
  74. var ctor = ctors.FirstOrDefault();
  75. if (ctor == null)
  76. { // this is a normal case
  77. usingDefaultCtor = true;
  78. ctor = type.GetConstructor(Type.EmptyTypes);
  79. if (ctor == null)
  80. throw new InvalidOperationException($"{type.FullName} does not expose a public default constructor and has no constructors marked [Init]");
  81. }
  82. var initMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
  83. .Select(m => (m, attr: m.GetCustomAttribute<InitAttribute>()))
  84. .NonNull(t => t.attr).Select(t => t.m).ToArray();
  85. // verify that they don't have lifecycle attributes on them
  86. foreach (var method in initMethods)
  87. {
  88. var attrs = method.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false);
  89. if (attrs.Length != 0)
  90. throw new InvalidOperationException($"Method {method} on {type.FullName} has both an [Init] attribute and a lifecycle attribute.");
  91. }
  92. var metaParam = Expression.Parameter(typeof(PluginMetadata), "meta");
  93. var objVar = ExpressionEx.Variable(type, "objVar");
  94. var persistVar = ExpressionEx.Variable(typeof(object), "persistVar");
  95. var createExpr = Expression.Lambda<Func<PluginMetadata, object>>(
  96. ExpressionEx.Block(new[] { objVar, persistVar },
  97. initMethods
  98. .Select(m => PluginInitInjector.InjectedCallExpr(m.GetParameters(), metaParam, persistVar, es => Expression.Call(objVar, m, es)))
  99. .Prepend(ExpressionEx.Assign(objVar,
  100. usingDefaultCtor
  101. ? Expression.New(ctor)
  102. : PluginInitInjector.InjectedCallExpr(ctor.GetParameters(), metaParam, persistVar, es => Expression.New(ctor, es))))
  103. .Append(Expression.Convert(objVar, typeof(object)))),
  104. metaParam);
  105. // TODO: since this new system will be doing a fuck load of compilation, maybe add FastExpressionCompiler
  106. return createExpr.Compile();
  107. }
  108. // TODO: make enable and disable able to take a bool indicating which it is
  109. private static Func<object, Task> MakeLifecycleEnableFunc(Type type, string name)
  110. {
  111. var noEnableDisable = type.GetCustomAttribute<NoEnableDisableAttribute>() is not null;
  112. var enableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
  113. .Select(m => (m, attrs: m.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false)))
  114. .Select(t => (t.m, attrs: t.attrs.Cast<IEdgeLifecycleAttribute>()))
  115. .Where(t => t.attrs.Any(a => a.Type == EdgeLifecycleType.Enable))
  116. .Select(t => t.m).ToArray();
  117. if (enableMethods.Length == 0)
  118. {
  119. if (!noEnableDisable)
  120. Logger.Loader.Notice($"Plugin {name} has no methods marked [OnStart] or [OnEnable]. Is this intentional?");
  121. return o => TaskEx.WhenAll();
  122. }
  123. var taskMethods = new List<MethodInfo>();
  124. var nonTaskMethods = new List<MethodInfo>();
  125. foreach (var m in enableMethods)
  126. {
  127. if (m.GetParameters().Length > 0)
  128. throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and has parameters.");
  129. if (m.ReturnType != typeof(void))
  130. {
  131. if (typeof(Task).IsAssignableFrom(m.ReturnType))
  132. {
  133. taskMethods.Add(m);
  134. continue;
  135. }
  136. Logger.Loader.Warn($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and returns a non-Task value. It will be ignored.");
  137. }
  138. nonTaskMethods.Add(m);
  139. }
  140. Expression<Func<Task>> completedTaskDel = () => TaskEx.WhenAll();
  141. var getCompletedTask = completedTaskDel.Body;
  142. var taskWhenAll = typeof(TaskEx).GetMethod(nameof(TaskEx.WhenAll), new[] { typeof(Task[]) });
  143. var objParam = Expression.Parameter(typeof(object), "obj");
  144. var instVar = ExpressionEx.Variable(type, "inst");
  145. var createExpr = Expression.Lambda<Func<object, Task>>(
  146. ExpressionEx.Block(new[] { instVar },
  147. nonTaskMethods
  148. .Select(m => (Expression)Expression.Call(instVar, m))
  149. .Prepend(ExpressionEx.Assign(instVar, Expression.Convert(objParam, type)))
  150. .Append(
  151. taskMethods.Count == 0
  152. ? getCompletedTask
  153. : Expression.Call(taskWhenAll,
  154. Expression.NewArrayInit(typeof(Task),
  155. taskMethods.Select(m =>
  156. (Expression)Expression.Convert(Expression.Call(instVar, m), typeof(Task))))))),
  157. objParam);
  158. return createExpr.Compile();
  159. }
  160. private static Func<object, Task> MakeLifecycleDisableFunc(Type type, string name)
  161. {
  162. var noEnableDisable = type.GetCustomAttribute<NoEnableDisableAttribute>() is not null;
  163. var disableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
  164. .Select(m => (m, attrs: m.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false)))
  165. .Select(t => (t.m, attrs: t.attrs.Cast<IEdgeLifecycleAttribute>()))
  166. .Where(t => t.attrs.Any(a => a.Type == EdgeLifecycleType.Disable))
  167. .Select(t => t.m).ToArray();
  168. if (disableMethods.Length == 0)
  169. {
  170. if (!noEnableDisable)
  171. Logger.Loader.Notice($"Plugin {name} has no methods marked [OnExit] or [OnDisable]. Is this intentional?");
  172. return o => TaskEx.WhenAll();
  173. }
  174. var taskMethods = new List<MethodInfo>();
  175. var nonTaskMethods = new List<MethodInfo>();
  176. foreach (var m in disableMethods)
  177. {
  178. if (m.GetParameters().Length > 0)
  179. throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and has parameters.");
  180. if (m.ReturnType != typeof(void))
  181. {
  182. if (typeof(Task).IsAssignableFrom(m.ReturnType))
  183. {
  184. taskMethods.Add(m);
  185. continue;
  186. }
  187. Logger.Loader.Warn($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and returns a non-Task value. It will be ignored.");
  188. }
  189. nonTaskMethods.Add(m);
  190. }
  191. Expression<Func<Task>> completedTaskDel = () => TaskEx.WhenAll();
  192. var getCompletedTask = completedTaskDel.Body;
  193. var taskWhenAll = typeof(TaskEx).GetMethod(nameof(TaskEx.WhenAll), new[] { typeof(Task[]) });
  194. var objParam = Expression.Parameter(typeof(object), "obj");
  195. var instVar = ExpressionEx.Variable(type, "inst");
  196. var createExpr = Expression.Lambda<Func<object, Task>>(
  197. ExpressionEx.Block(new[] { instVar },
  198. nonTaskMethods
  199. .Select(m => (Expression)Expression.Call(instVar, m))
  200. .Prepend(ExpressionEx.Assign(instVar, Expression.Convert(objParam, type)))
  201. .Append(
  202. taskMethods.Count == 0
  203. ? getCompletedTask
  204. : Expression.Call(taskWhenAll,
  205. Expression.NewArrayInit(typeof(Task),
  206. taskMethods.Select(m =>
  207. (Expression)Expression.Convert(Expression.Call(instVar, m), typeof(Task))))))),
  208. objParam);
  209. return createExpr.Compile();
  210. }
  211. }
  212. }