CLI: Implement plugin interface
This commit is contained in:
@@ -5,7 +5,8 @@
|
|||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
<PublishTrimmed>true</PublishTrimmed>
|
<!-- Plugins may require bass class library assemblies we're not using so disable trimming -->
|
||||||
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<Version>2020.2.1</Version>
|
<Version>2020.2.1</Version>
|
||||||
|
|||||||
190
Il2CppInspector.CLI/PluginOptions.cs
Normal file
190
Il2CppInspector.CLI/PluginOptions.cs
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Reflection.Emit;
|
||||||
|
using CommandLine;
|
||||||
|
using Il2CppInspector.PluginAPI.V100;
|
||||||
|
using CommandLine.Text;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Il2CppInspector.CLI
|
||||||
|
{
|
||||||
|
// This ridiculous hack converts options from our plugin API to options classes that CommandLineParser can process and back again
|
||||||
|
internal static class PluginOptions
|
||||||
|
{
|
||||||
|
// Create an auto-property
|
||||||
|
private static PropertyBuilder CreateAutoProperty(TypeBuilder tb, string propertyName, Type propertyType) {
|
||||||
|
FieldBuilder fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);
|
||||||
|
|
||||||
|
PropertyBuilder propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
|
||||||
|
MethodBuilder getPropMthdBldr = tb.DefineMethod("get_" + propertyName,
|
||||||
|
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
|
||||||
|
ILGenerator getIl = getPropMthdBldr.GetILGenerator();
|
||||||
|
|
||||||
|
getIl.Emit(OpCodes.Ldarg_0);
|
||||||
|
getIl.Emit(OpCodes.Ldfld, fieldBuilder);
|
||||||
|
getIl.Emit(OpCodes.Ret);
|
||||||
|
|
||||||
|
MethodBuilder setPropMthdBldr =
|
||||||
|
tb.DefineMethod("set_" + propertyName,
|
||||||
|
MethodAttributes.Public |
|
||||||
|
MethodAttributes.SpecialName |
|
||||||
|
MethodAttributes.HideBySig,
|
||||||
|
null, new[] { propertyType });
|
||||||
|
|
||||||
|
ILGenerator setIl = setPropMthdBldr.GetILGenerator();
|
||||||
|
setIl.Emit(OpCodes.Ldarg_0);
|
||||||
|
setIl.Emit(OpCodes.Ldarg_1);
|
||||||
|
setIl.Emit(OpCodes.Stfld, fieldBuilder);
|
||||||
|
setIl.Emit(OpCodes.Ret);
|
||||||
|
|
||||||
|
propertyBuilder.SetGetMethod(getPropMthdBldr);
|
||||||
|
propertyBuilder.SetSetMethod(setPropMthdBldr);
|
||||||
|
|
||||||
|
return propertyBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a CommandLineParser-friendly attributed class of options from a loaded plugin
|
||||||
|
private static Type CreateOptionsFromPlugin(IPlugin plugin) {
|
||||||
|
// Name of class to create
|
||||||
|
var tn = plugin.Id + "Options";
|
||||||
|
|
||||||
|
// Create class and default constructor
|
||||||
|
var ab = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(Guid.NewGuid().ToString()), AssemblyBuilderAccess.Run);
|
||||||
|
var mb = ab.DefineDynamicModule("MainModule");
|
||||||
|
var pluginOptionClass = mb.DefineType(tn,
|
||||||
|
TypeAttributes.Public |
|
||||||
|
TypeAttributes.Class |
|
||||||
|
TypeAttributes.AutoClass |
|
||||||
|
TypeAttributes.AnsiClass |
|
||||||
|
TypeAttributes.BeforeFieldInit |
|
||||||
|
TypeAttributes.AutoLayout,
|
||||||
|
null);
|
||||||
|
pluginOptionClass.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);
|
||||||
|
|
||||||
|
// Create VerbAttribute with plugin ID
|
||||||
|
var verbCtorInfo = typeof(VerbAttribute).GetConstructor(new Type[] { typeof(string) });
|
||||||
|
var verbHelpPropInfo = typeof(VerbAttribute).GetProperty("HelpText");
|
||||||
|
var verbAttBuilder = new CustomAttributeBuilder(verbCtorInfo, new object[] { plugin.Id },
|
||||||
|
new PropertyInfo[] { verbHelpPropInfo }, new object[] { plugin.Description });
|
||||||
|
pluginOptionClass.SetCustomAttribute(verbAttBuilder);
|
||||||
|
|
||||||
|
// Create auto-property for each option
|
||||||
|
foreach (var option in plugin.Options) {
|
||||||
|
var optionType = option.GetType().GetProperty("Value").PropertyType;
|
||||||
|
var optionValue = option.Value;
|
||||||
|
|
||||||
|
// Hex numbers are strings that will be validated later
|
||||||
|
if (option is IPluginOptionNumber n && n.Style == PluginOptionNumberStyle.Hex) {
|
||||||
|
optionType = typeof(string);
|
||||||
|
optionValue = string.Format("0x{0:x}", optionValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginOptionProperty = CreateAutoProperty(pluginOptionClass, option.Name, optionType);
|
||||||
|
var optCtorInfo = typeof(OptionAttribute).GetConstructor(new Type[] { typeof(string) });
|
||||||
|
var optHelpPropInfo = typeof(OptionAttribute).GetProperty("HelpText");
|
||||||
|
var optDefaultInfo = typeof(OptionAttribute).GetProperty("Default");
|
||||||
|
var optRequiredInfo = typeof(OptionAttribute).GetProperty("Required");
|
||||||
|
var attBuilder = new CustomAttributeBuilder(optCtorInfo, new object[] { option.Name },
|
||||||
|
new PropertyInfo[] { optHelpPropInfo, optDefaultInfo, optRequiredInfo },
|
||||||
|
// Booleans are always optional
|
||||||
|
new object[] { option.Description, optionValue, option.Value is bool? false : option.Required });
|
||||||
|
pluginOptionProperty.SetCustomAttribute(attBuilder);
|
||||||
|
}
|
||||||
|
return pluginOptionClass.CreateTypeInfo().AsType();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get plugin option classes
|
||||||
|
public static Type[] GetPluginOptionTypes() {
|
||||||
|
// Don't do anything if there are no loaded plugins
|
||||||
|
var plugins = PluginManager.AvailablePlugins;
|
||||||
|
|
||||||
|
if (!plugins.Any())
|
||||||
|
return Array.Empty<Type>();
|
||||||
|
|
||||||
|
// Create CommandLine-friendly option classes for each plugin
|
||||||
|
return plugins.Select(p => CreateOptionsFromPlugin(p)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse all options for all plugins
|
||||||
|
public static bool ParsePluginOptions(IEnumerable<string> pluginOptions, Type[] optionsTypes) {
|
||||||
|
|
||||||
|
// Run CommandLine parser on each set of plugin options
|
||||||
|
foreach (var options in pluginOptions) {
|
||||||
|
|
||||||
|
var selectedPlugin = options.Split(' ')[0].ToLower();
|
||||||
|
|
||||||
|
// Cause an error on the first plugin arguments if no plugins are loaded
|
||||||
|
if (optionsTypes.Length == 0) {
|
||||||
|
Console.Error.WriteLine($"Plugin '{selectedPlugin}' does not exist or is not loaded");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse plugin arguments
|
||||||
|
var parser = new Parser(with => {
|
||||||
|
with.HelpWriter = null;
|
||||||
|
with.CaseSensitive = false;
|
||||||
|
with.AutoHelp = false;
|
||||||
|
with.AutoVersion = false;
|
||||||
|
});
|
||||||
|
var result = parser.ParseArguments(options.Split(' '), optionsTypes);
|
||||||
|
|
||||||
|
// Print plugin help if parsing failed
|
||||||
|
if (result is NotParsed<object> notParsed) {
|
||||||
|
if (!(notParsed.Errors.First() is BadVerbSelectedError)) {
|
||||||
|
var helpText = HelpText.AutoBuild(result, h => {
|
||||||
|
h.Heading = $"Usage for plugin '{selectedPlugin}':";
|
||||||
|
h.Copyright = string.Empty;
|
||||||
|
h.AutoHelp = false;
|
||||||
|
h.AutoVersion = false;
|
||||||
|
return h;
|
||||||
|
}, e => e);
|
||||||
|
Console.Error.WriteLine(helpText);
|
||||||
|
} else {
|
||||||
|
Console.Error.WriteLine($"Plugin '{selectedPlugin}' does not exist or is not loaded");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get plugin arguments and write them to plugin options class
|
||||||
|
var optionsObject = (result as Parsed<object>).Value;
|
||||||
|
var plugin = PluginManager.AvailablePlugins.First(p => optionsObject.GetType().FullName == p.Id + "Options");
|
||||||
|
|
||||||
|
foreach (var prop in optionsObject.GetType().GetProperties()) {
|
||||||
|
var targetProp = plugin.Options.First(x => x.Name == prop.Name);
|
||||||
|
|
||||||
|
// Validate hex strings
|
||||||
|
if (targetProp is IPluginOptionNumber n && n.Style == PluginOptionNumberStyle.Hex) {
|
||||||
|
try {
|
||||||
|
n.Value = Convert.ToUInt64((string) prop.GetValue(optionsObject), 16);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is ArgumentException || ex is FormatException || ex is OverflowException) {
|
||||||
|
Console.Error.WriteLine($"{prop.Name} must be a 32 or 64-bit hex value (optionally starting with '0x')");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Validate choices
|
||||||
|
// TODO: Validate path names - https://stackoverflow.com/questions/422090/in-c-sharp-check-that-filename-is-possibly-valid-not-that-it-exists
|
||||||
|
|
||||||
|
// All other input types
|
||||||
|
else {
|
||||||
|
targetProp.Value = prop.GetValue(optionsObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable plugin
|
||||||
|
PluginManager.AsInstance.ManagedPlugins.First(p => p.Plugin == plugin).Enabled = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
// Copyright (c) 2017-2020 Katy Coe - https://www.djkaty.com - https://github.com/djkaty
|
/*
|
||||||
// All rights reserved
|
Copyright 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Reflection.Emit;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Xml.Schema;
|
||||||
using CommandLine;
|
using CommandLine;
|
||||||
|
using CommandLine.Text;
|
||||||
using Il2CppInspector.Cpp;
|
using Il2CppInspector.Cpp;
|
||||||
using Il2CppInspector.Cpp.UnityHeaders;
|
using Il2CppInspector.Cpp.UnityHeaders;
|
||||||
using Il2CppInspector.Model;
|
using Il2CppInspector.Model;
|
||||||
@@ -98,6 +105,9 @@ namespace Il2CppInspector.CLI
|
|||||||
|
|
||||||
[Option("unity-version", Required = false, HelpText = "Version of Unity used to create the input files, if known. Used to enhance Python, C++ and JSON output. If not specified, a close match will be inferred automatically.", Default = null)]
|
[Option("unity-version", Required = false, HelpText = "Version of Unity used to create the input files, if known. Used to enhance Python, C++ and JSON output. If not specified, a close match will be inferred automatically.", Default = null)]
|
||||||
public UnityVersion UnityVersion { get; set; }
|
public UnityVersion UnityVersion { get; set; }
|
||||||
|
|
||||||
|
[Option("plugins", Required = false, HelpText = "Specify options for plugins. Enclose each plugin's configuration in quotes as follows: --plugins \"pluginone --option1 value1 --option2 value2\" \"plugintwo --option...\". Use --plugins <name> to get help on a specific plugin")]
|
||||||
|
public IEnumerable<string> PluginOptions { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adapted from: https://stackoverflow.com/questions/16376191/measuring-code-execution-time
|
// Adapted from: https://stackoverflow.com/questions/16376191/measuring-code-execution-time
|
||||||
@@ -119,19 +129,44 @@ namespace Il2CppInspector.CLI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int Main(string[] args) =>
|
public static void Main(string[] args) {
|
||||||
Parser.Default.ParseArguments<Options>(args).MapResult(
|
var parser = new Parser(config => config.HelpWriter = null);
|
||||||
options => Run(options),
|
var result = parser.ParseArguments<Options>(args);
|
||||||
_ => 1);
|
result.WithParsed(options => Run(options))
|
||||||
|
.WithNotParsed(errors => DisplayHelp(result, errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DisplayHelp(ParserResult<Options> result, IEnumerable<Error> errors) {
|
||||||
|
Console.Error.WriteLine(HelpText.AutoBuild(result));
|
||||||
|
|
||||||
|
var help = new HelpText();
|
||||||
|
help.Heading = "Available plugins:";
|
||||||
|
help.Copyright = string.Empty;
|
||||||
|
help.AddDashesToOption = false;
|
||||||
|
help.AdditionalNewLineAfterOption = true;
|
||||||
|
help.MaximumDisplayWidth = 80;
|
||||||
|
help.AutoHelp = false;
|
||||||
|
help.AutoVersion = false;
|
||||||
|
|
||||||
|
var pluginOptions = PluginOptions.GetPluginOptionTypes();
|
||||||
|
if (pluginOptions.Any())
|
||||||
|
Console.Error.WriteLine(help.AddVerbs(PluginOptions.GetPluginOptionTypes()));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
private static int Run(Options options) {
|
private static int Run(Options options) {
|
||||||
|
|
||||||
// Banner
|
// Banner
|
||||||
var asmInfo = FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetEntryAssembly().Location);
|
var asmInfo = FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetEntryAssembly().Location);
|
||||||
Console.WriteLine(asmInfo.ProductName);
|
Console.WriteLine(asmInfo.ProductName);
|
||||||
Console.WriteLine("Version " + asmInfo.ProductVersion);
|
Console.WriteLine("Version " + asmInfo.ProductVersion);
|
||||||
Console.WriteLine(asmInfo.LegalCopyright);
|
Console.WriteLine(asmInfo.LegalCopyright);
|
||||||
Console.WriteLine("");
|
Console.WriteLine("");
|
||||||
|
|
||||||
|
// Check plugin options are valid
|
||||||
|
if (!PluginOptions.ParsePluginOptions(options.PluginOptions, PluginOptions.GetPluginOptionTypes()))
|
||||||
|
return 1;
|
||||||
|
|
||||||
// Check script target is valid
|
// Check script target is valid
|
||||||
if (!PythonScript.GetAvailableTargets().Contains(options.ScriptTarget)) {
|
if (!PythonScript.GetAvailableTargets().Contains(options.ScriptTarget)) {
|
||||||
Console.Error.WriteLine($"Script target {options.ScriptTarget} is invalid.");
|
Console.Error.WriteLine($"Script target {options.ScriptTarget} is invalid.");
|
||||||
@@ -196,6 +231,16 @@ namespace Il2CppInspector.CLI
|
|||||||
Console.WriteLine("Using Unity assemblies at " + unityAssembliesPath);
|
Console.WriteLine("Using Unity assemblies at " + unityAssembliesPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set plugin handlers
|
||||||
|
PluginManager.ErrorHandler += (s, e) => {
|
||||||
|
Console.Error.WriteLine($"The plugin {e.Plugin.Name} encountered an error while executing {e.Operation}: {e.Exception.Message}."
|
||||||
|
+ " The application will continue but may not behave as expected.");
|
||||||
|
};
|
||||||
|
|
||||||
|
PluginManager.StatusHandler += (s, e) => {
|
||||||
|
Console.WriteLine("Plugin " + e.Plugin.Name + ": " + e.Text);
|
||||||
|
};
|
||||||
|
|
||||||
// Check that specified binary files exist
|
// Check that specified binary files exist
|
||||||
foreach (var file in options.BinaryFiles)
|
foreach (var file in options.BinaryFiles)
|
||||||
if (!File.Exists(file)) {
|
if (!File.Exists(file)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user