示例#1
0
        public static void Start()
        {
            var preloaderListener = new PreloaderConsoleListener();

            Logger.Listeners.Add(preloaderListener);

            var entrypointAssemblyPath = Path.ChangeExtension(Process.GetCurrentProcess().MainModule.FileName, "dll");

            Paths.SetExecutablePath(entrypointAssemblyPath);

            TypeLoader.SearchDirectories.Add(Path.GetDirectoryName(entrypointAssemblyPath));

            Logger.Sources.Add(TraceLogSource.CreateSource());

            ChainloaderLogHelper.PrintLogInfo(Log);

            Log.Log(LogLevel.Info, $"CLR runtime version: {Environment.Version}");


            AssemblyBuildInfo executableInfo;

            using (var entrypointAssembly = AssemblyDefinition.ReadAssembly(entrypointAssemblyPath))
                executableInfo = AssemblyBuildInfo.DetermineInfo(entrypointAssembly);

            Log.LogInfo($"Game executable build architecture: {executableInfo}");

            Log.Log(LogLevel.Message, "Preloader started");

            using (var assemblyPatcher = new AssemblyPatcher())
            {
                assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);

                Log.Log(LogLevel.Info,
                        $"{assemblyPatcher.PatcherContext.PatchDefinitions.Count} patcher definition(s) loaded");

                assemblyPatcher.LoadAssemblyDirectories(new[] { Paths.GameRootPath }, new[] { "dll", "exe" });

                Log.Log(LogLevel.Info, $"{assemblyPatcher.PatcherContext.AvailableAssemblies.Count} assemblies discovered");

                assemblyPatcher.PatchAndLoad();
            }

            Log.LogMessage("Preloader finished");

            Logger.Listeners.Remove(preloaderListener);

            var chainloader = new NetChainloader();

            chainloader.Initialize();
            chainloader.Execute();
        }
示例#2
0
        public static void Run()
        {
            try
            {
                ConsoleManager.Initialize(false);

                PreloaderLog = new PreloaderConsoleListener();
                Logger.Listeners.Add(PreloaderLog);


                if (ConsoleManager.ConfigConsoleEnabled.Value)
                {
                    ConsoleManager.CreateConsole();
                    Logger.Listeners.Add(new ConsoleLogListener());
                }

                ChainloaderLogHelper.PrintLogInfo(Log);

                Log.LogDebug($"Game executable path: {Paths.ExecutablePath}");
                Log.LogDebug($"Unhollowed assembly directory: {IL2CPPUnhollowedPath}");
                Log.LogDebug($"BepInEx root path: {Paths.BepInExRootPath}");

                UnhollowerLog              = Logger.CreateLogSource("Unhollower");
                LogSupport.InfoHandler    += UnhollowerLog.LogInfo;
                LogSupport.WarningHandler += UnhollowerLog.LogWarning;
                LogSupport.TraceHandler   += UnhollowerLog.LogDebug;
                LogSupport.ErrorHandler   += UnhollowerLog.LogError;


                if (ProxyAssemblyGenerator.CheckIfGenerationRequired())
                {
                    ProxyAssemblyGenerator.GenerateAssemblies();
                }


                InitializeUnityVersion();


                using (var assemblyPatcher = new AssemblyPatcher())
                {
                    assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);

                    Log.LogInfo($"{assemblyPatcher.PatcherPlugins.Count} patcher plugin{(assemblyPatcher.PatcherPlugins.Count == 1 ? "" : "s")} loaded");

                    assemblyPatcher.LoadAssemblyDirectories(IL2CPPUnhollowedPath);

                    Log.LogInfo($"{assemblyPatcher.PatcherPlugins.Count} assemblies discovered");

                    assemblyPatcher.PatchAndLoad();
                }


                Logger.Listeners.Remove(PreloaderLog);


                Chainloader = new IL2CPPChainloader();

                Chainloader.Initialize();
            }
            catch (Exception ex)
            {
                Log.LogFatal(ex);

                throw;
            }
        }
示例#3
0
        public static void Start(string[] args)
        {
            if (ConfigEntrypointExecutable.Value == null)
            {
                Log.LogFatal($"Entry executable was not set. Please set this in your config before launching the application");
                Program.ReadExit();
                return;
            }

            string executablePath = Path.GetFullPath(ConfigEntrypointExecutable.Value);

            if (!File.Exists(executablePath))
            {
                Log.LogFatal($"Unable to locate executable: {ConfigEntrypointExecutable.Value}");
                Program.ReadExit();
                return;
            }

            Paths.SetExecutablePath(executablePath);
            Program.ResolveDirectories.Add(Paths.GameRootPath);
            TypeLoader.SearchDirectories.Add(Paths.GameRootPath);

            Logger.Sources.Add(TraceLogSource.CreateSource());

            ChainloaderLogHelper.PrintLogInfo(Log);

            Log.LogInfo($"CLR runtime version: {Environment.Version}");

            Log.LogMessage("Preloader started");

            Assembly entrypointAssembly;

            using (var assemblyPatcher = new AssemblyPatcher())
            {
                assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);

                Log.LogInfo($"{assemblyPatcher.PatcherPlugins.Count} patcher plugin(s) loaded");

                assemblyPatcher.LoadAssemblyDirectory(Paths.GameRootPath, "dll", "exe");

                Log.LogInfo($"{assemblyPatcher.AssembliesToPatch.Count} assemblies discovered");

                assemblyPatcher.PatchAndLoad();


                var assemblyName = AssemblyName.GetAssemblyName(executablePath);

                entrypointAssembly = assemblyPatcher.LoadedAssemblies.Values.FirstOrDefault(x => x.FullName == assemblyName.FullName);

                if (entrypointAssembly != null)
                {
                    Log.LogDebug("Found patched entrypoint assembly! Using it");
                }
                else
                {
                    Log.LogDebug("Using entrypoint assembly from disk");
                    entrypointAssembly = Assembly.LoadFrom(executablePath);
                }
            }

            Log.LogMessage("Preloader finished");

            var chainloader = new NetChainloader();

            chainloader.Initialize();
            chainloader.Execute();


            AssemblyFix.Execute(entrypointAssembly);

            entrypointAssembly.EntryPoint.Invoke(null, new [] { args });
        }
示例#4
0
        public static void Run()
        {
            try
            {
                AllocateConsole();

                bool bridgeInitialized = Utility.TryDo(() =>
                {
                    if (ConfigShimHarmony.Value)
                    {
                        HarmonyDetourBridge.Init();
                    }
                }, out var harmonyBridgeException);

                Exception runtimePatchException = null;
                if (bridgeInitialized)
                {
                    Utility.TryDo(() =>
                    {
                        if (ConfigApplyRuntimePatches.Value)
                        {
                            UnityPatches.Apply();
                        }
                    }, out runtimePatchException);
                }

                Logger.Sources.Add(TraceLogSource.CreateSource());

                HarmonyFixes.Apply();

                PreloaderLog = new PreloaderConsoleListener(ConfigPreloaderCOutLogging.Value);
                Logger.Listeners.Add(PreloaderLog);

                string consoleTile = $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Process.GetCurrentProcess().ProcessName}";
                ConsoleWindow.Title = consoleTile;
                Logger.LogMessage(consoleTile);

                //See BuildInfoAttribute for more information about this section.
                object[] attributes = typeof(BuildInfoAttribute).Assembly.GetCustomAttributes(typeof(BuildInfoAttribute), false);

                if (attributes.Length > 0)
                {
                    var attribute = (BuildInfoAttribute)attributes[0];
                    Logger.LogMessage(attribute.Info);
                }

                Logger.LogInfo($"Running under Unity v{FileVersionInfo.GetVersionInfo(Paths.ExecutablePath).FileVersion}");
                Logger.LogInfo($"CLR runtime version: {Environment.Version}");
                Logger.LogInfo($"Supports SRE: {Utility.CLRSupportsDynamicAssemblies}");

                if (harmonyBridgeException != null)
                {
                    Logger.LogWarning($"Failed to enable fix for Harmony for .NET Standard API. Error message: {harmonyBridgeException.Message}");
                }

                if (runtimePatchException != null)
                {
                    Logger.LogWarning($"Failed to apply runtime patches for Mono. See more info in the output log. Error message: {runtimePatchException.Message}");
                }

                Logger.LogMessage("Preloader started");

                AssemblyPatcher.AddPatcher(new PatcherPlugin
                {
                    TargetDLLs = () => new[] { ConfigEntrypointAssembly.Value },
                    Patcher    = PatchEntrypoint,
                    TypeName   = "BepInEx.Chainloader"
                });

                AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);

                Logger.LogInfo($"{AssemblyPatcher.PatcherPlugins.Count} patcher plugin(s) loaded");

                AssemblyPatcher.PatchAndLoad(Paths.ManagedPath);
                AssemblyPatcher.DisposePatchers();


                Logger.LogMessage("Preloader finished");

                Logger.Listeners.Remove(PreloaderLog);
                Logger.Listeners.Add(new ConsoleLogListener());

                PreloaderLog.Dispose();
            }
            catch (Exception ex)
            {
                try
                {
                    Logger.LogFatal("Could not run preloader!");
                    Logger.LogFatal(ex);

                    PreloaderLog?.Dispose();

                    if (!ConsoleWindow.IsAttached)
                    {
                        //if we've already attached the console, then the log will already be written to the console
                        AllocateConsole();
                        Console.Write(PreloaderLog);
                    }

                    PreloaderLog = null;
                }
                finally
                {
                    File.WriteAllText(
                        Path.Combine(Paths.GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"),
                        PreloaderLog + "\r\n" + ex);

                    PreloaderLog?.Dispose();
                    PreloaderLog = null;
                }
            }
        }
示例#5
0
        public static void Run()
        {
            try
            {
                AllocateConsole();

                UnityPatches.Apply();

                Logger.Sources.Add(TraceLogSource.CreateSource());

                PreloaderLog = new PreloaderConsoleListener(Utility.SafeParseBool(Config.GetEntry("preloader-logconsole", "false", "BepInEx")));

                Logger.Listeners.Add(PreloaderLog);


                string consoleTile = $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Process.GetCurrentProcess().ProcessName}";

                ConsoleWindow.Title = consoleTile;
                Logger.LogMessage(consoleTile);

                //See BuildInfoAttribute for more information about this section.
                object[] attributes = typeof(BuildInfoAttribute).Assembly.GetCustomAttributes(typeof(BuildInfoAttribute), false);

                if (attributes.Length > 0)
                {
                    var attribute = (BuildInfoAttribute)attributes[0];

                    Logger.LogMessage(attribute.Info);
                }

#if UNITY_2018
                Logger.LogMessage("Compiled in Unity v2018 mode");
#else
                Logger.LogMessage("Compiled in Legacy Unity mode");
#endif

                Logger.LogInfo($"Running under Unity v{Process.GetCurrentProcess().MainModule.FileVersionInfo.FileVersion}");

                Logger.LogMessage("Preloader started");

                string entrypointAssembly = Config.GetEntry("entrypoint-assembly", "UnityEngine.dll", "Preloader");

                AssemblyPatcher.AddPatcher(new PatcherPlugin
                {
                    TargetDLLs = new[] { entrypointAssembly }, Patcher = PatchEntrypoint
                });
                AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath, GetPatcherMethods);

                Logger.LogInfo($"{AssemblyPatcher.PatcherPlugins.Count} patcher plugin(s) loaded");

                AssemblyPatcher.PatchAndLoad(Paths.ManagedPath);

                AssemblyPatcher.DisposePatchers();

                Logger.LogMessage("Preloader finished");

                Logger.Listeners.Remove(PreloaderLog);
                Logger.Listeners.Add(new ConsoleLogListener());

                PreloaderLog.Dispose();
            }
            catch (Exception ex)
            {
                try
                {
                    Logger.LogFatal("Could not run preloader!");
                    Logger.LogFatal(ex);

                    PreloaderLog?.Dispose();

                    if (!ConsoleWindow.IsAttached)
                    {
                        //if we've already attached the console, then the log will already be written to the console
                        AllocateConsole();
                        Console.Write(PreloaderLog);
                    }

                    PreloaderLog = null;
                }
                finally
                {
                    File.WriteAllText(
                        Path.Combine(Paths.GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"),
                        PreloaderLog + "\r\n" + ex);

                    PreloaderLog?.Dispose();
                    PreloaderLog = null;
                }
            }
        }
示例#6
0
        public static void Run()
        {
            try
            {
                ConsoleManager.Initialize(false);
                AllocateConsole();

                Utility.TryDo(() =>
                {
                    if (ConfigApplyRuntimePatches.Value)
                    {
                        UnityPatches.Apply();
                    }
                }, out var runtimePatchException);

                Logger.InitializeInternalLoggers();
                Logger.Sources.Add(TraceLogSource.CreateSource());

                PreloaderLog = new PreloaderConsoleListener(ConfigPreloaderCOutLogging.Value);
                Logger.Listeners.Add(PreloaderLog);

                string consoleTile = $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName)}";

                if (ConsoleManager.ConsoleActive)
                {
                    ConsoleManager.SetConsoleTitle(consoleTile);
                }

                Logger.LogMessage(consoleTile);

                //See BuildInfoAttribute for more information about this section.
                object[] attributes = typeof(BuildInfoAttribute).Assembly.GetCustomAttributes(typeof(BuildInfoAttribute), false);

                if (attributes.Length > 0)
                {
                    var attribute = (BuildInfoAttribute)attributes[0];
                    Logger.LogMessage(attribute.Info);
                }

                Logger.LogInfo($"Running under Unity v{GetUnityVersion()}");
                Logger.LogInfo($"CLR runtime version: {Environment.Version}");
                Logger.LogInfo($"Supports SRE: {Utility.CLRSupportsDynamicAssemblies}");

                if (runtimePatchException != null)
                {
                    Logger.LogWarning($"Failed to apply runtime patches for Mono. See more info in the output log. Error message: {runtimePatchException.Message}");
                }

                Logger.LogMessage("Preloader started");

                AssemblyPatcher.AddPatcher(new PatcherPlugin
                {
                    TargetDLLs = () => new[] { ConfigEntrypointAssembly.Value },
                    Patcher    = PatchEntrypoint,
                    TypeName   = "BepInEx.Chainloader"
                });

                AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);

                Logger.LogInfo($"{AssemblyPatcher.PatcherPlugins.Count} patcher plugin(s) loaded");

                AssemblyPatcher.PatchAndLoad(Paths.ManagedPath);
                AssemblyPatcher.DisposePatchers();


                Logger.LogMessage("Preloader finished");

                Logger.Listeners.Remove(PreloaderLog);
                Logger.Listeners.Add(new ConsoleLogListener());

                PreloaderLog.Dispose();
            }
            catch (Exception ex)
            {
                try
                {
                    Logger.LogFatal("Could not run preloader!");
                    Logger.LogFatal(ex);

                    if (!ConsoleManager.ConsoleActive)
                    {
                        //if we've already attached the console, then the log will already be written to the console
                        AllocateConsole();
                        Console.Write(PreloaderLog);
                    }
                }
                catch { }

                string log = string.Empty;

                try
                {
                    // We could use platform-dependent newlines, however the developers use Windows so this will be easier to read :)

                    log  = string.Join("\r\n", PreloaderConsoleListener.LogEvents.Select(x => x.ToString()).ToArray());
                    log += "\r\n";

                    PreloaderLog?.Dispose();
                    PreloaderLog = null;
                }
                catch { }

                File.WriteAllText(
                    Path.Combine(Paths.GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"),
                    log + ex);
            }
        }
示例#7
0
        public static void Run()
        {
            try
            {
                InitializeHarmony();

                ConsoleManager.Initialize(false);
                AllocateConsole();

                Utility.TryDo(() =>
                {
                    if (ConfigApplyRuntimePatches.Value)
                    {
                        UnityPatches.Apply();
                    }
                }, out var runtimePatchException);

                Logger.Sources.Add(new HarmonyLogSource());
                Logger.Sources.Add(TraceLogSource.CreateSource());

                Logger.Listeners.Add(new ConsoleLogListener());
                PreloaderLog = new PreloaderConsoleListener();
                Logger.Listeners.Add(PreloaderLog);

                ChainloaderLogHelper.PrintLogInfo(Log);

                Log.LogInfo($"Running under Unity v{GetUnityVersion()}");
                Log.LogInfo($"CLR runtime version: {Environment.Version}");
                Log.LogInfo($"Supports SRE: {Utility.CLRSupportsDynamicAssemblies}");

                Log.LogDebug($"Game executable path: {Paths.ExecutablePath}");
                Log.LogDebug($"Unity Managed directory: {Paths.ManagedPath}");
                Log.LogDebug($"BepInEx root path: {Paths.BepInExRootPath}");

                if (runtimePatchException != null)
                {
                    Log.LogWarning($"Failed to apply runtime patches for Mono. See more info in the output log. Error message: {runtimePatchException.Message}");
                }

                Log.LogMessage("Preloader started");

                TypeLoader.SearchDirectories.UnionWith(Paths.DllSearchPaths);

                using (var assemblyPatcher = new AssemblyPatcher())
                {
                    assemblyPatcher.PatcherPlugins.Add(new PatcherPlugin
                    {
                        TargetDLLs = () => new[] { ConfigEntrypointAssembly.Value },
                        Patcher    = PatchEntrypoint,
                        TypeName   = "BepInEx.Chainloader"
                    });

                    assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);

                    Log.LogInfo($"{assemblyPatcher.PatcherPlugins.Count} patcher plugin{(assemblyPatcher.PatcherPlugins.Count == 1 ? "" : "s")} loaded");

                    assemblyPatcher.LoadAssemblyDirectories(Paths.DllSearchPaths);

                    Log.LogInfo($"{assemblyPatcher.PatcherPlugins.Count} assemblies discovered");

                    assemblyPatcher.PatchAndLoad();
                }


                Log.LogMessage("Preloader finished");

                Logger.Listeners.Remove(PreloaderLog);

                PreloaderLog.Dispose();
            }
            catch (Exception ex)
            {
                try
                {
                    Log.LogFatal("Could not run preloader!");
                    Log.LogFatal(ex);

                    if (!ConsoleManager.ConsoleActive)
                    {
                        //if we've already attached the console, then the log will already be written to the console
                        AllocateConsole();
                        Console.Write(PreloaderLog);
                    }
                }
                catch { }

                var log = string.Empty;

                try
                {
                    // We could use platform-dependent newlines, however the developers use Windows so this will be easier to read :)

                    log  = string.Join("\r\n", PreloaderConsoleListener.LogEvents.Select(x => x.ToString()).ToArray());
                    log += "\r\n";

                    PreloaderLog?.Dispose();
                    PreloaderLog = null;
                }
                catch { }

                File.WriteAllText(
                    Path.Combine(Paths.GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"),
                    log + ex);
            }
        }
示例#8
0
    public static void Start(string[] args)
    {
        var preloaderListener = new PreloaderConsoleListener();

        Logger.Listeners.Add(preloaderListener);

        if (string.IsNullOrEmpty(ConfigEntrypointExecutable.Value))
        {
            Log.Log(LogLevel.Fatal,
                    $"Entry executable was not set. Please set this in your config before launching the application");
            Program.ReadExit();
            return;
        }

        var executablePath = Path.GetFullPath(ConfigEntrypointExecutable.Value);

        if (!File.Exists(executablePath))
        {
            Log.Log(LogLevel.Fatal, $"Unable to locate executable: {ConfigEntrypointExecutable.Value}");
            Program.ReadExit();
            return;
        }


        Paths.SetExecutablePath(executablePath);
        Program.ResolveDirectories.Add(Paths.GameRootPath);

        foreach (var searchDir in Program.ResolveDirectories)
        {
            TypeLoader.SearchDirectories.Add(searchDir);
        }

        if (PlatformHelper.Is(Platform.Windows))
        {
            AddDllDirectory(Paths.GameRootPath);
            SetDllDirectory(Paths.GameRootPath);
        }


        Logger.Sources.Add(TraceLogSource.CreateSource());

        ChainloaderLogHelper.PrintLogInfo(Log);

        Log.Log(LogLevel.Info, $"CLR runtime version: {Environment.Version}");


        AssemblyBuildInfo executableInfo, launcherInfo;

        using (var executableAssembly = AssemblyDefinition.ReadAssembly(executablePath))
            executableInfo = AssemblyBuildInfo.DetermineInfo(executableAssembly);

        using (var launcherAssembly = AssemblyDefinition.ReadAssembly(typeof(NetPreloader).Assembly.Location))
            launcherInfo = AssemblyBuildInfo.DetermineInfo(launcherAssembly);

        // we don't particularly care about AnyCPU here since the fallback bitness is almost never the case
        if (executableInfo.Is64Bit != launcherInfo.Is64Bit)
        {
            Log.LogError($"Game executable is {(executableInfo.Is64Bit ? "64" : "32")}-bit while BepInEx has been compiled as {(launcherInfo.Is64Bit ? "64" : "32")}-bit. Expect crashes");
        }

        if (executableInfo.NetFrameworkVersion != launcherInfo.NetFrameworkVersion || executableInfo.AssemblyFrameworkType != launcherInfo.AssemblyFrameworkType)
        {
            Log.LogWarning($"Game executable has been compiled as {executableInfo}, while BepInEx has been compiled as {launcherInfo}. There may be issues within the game caused by this");
        }

        Log.LogInfo($"Game executable build architecture: {executableInfo}");
        Log.LogInfo($"BepInEx launcher build architecture: {launcherInfo}");

        Log.Log(LogLevel.Message, "Preloader started");

        Assembly entrypointAssembly;

        using (var assemblyPatcher = new AssemblyPatcher())
        {
            assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);

            Log.Log(LogLevel.Info,
                    $"{assemblyPatcher.PatcherContext.PatchDefinitions.Count} patcher definition(s) loaded");

            assemblyPatcher.LoadAssemblyDirectories(new[] { Paths.GameRootPath }, new[] { "dll", "exe" });

            Log.Log(LogLevel.Info, $"{assemblyPatcher.PatcherContext.AvailableAssemblies.Count} assemblies discovered");

            assemblyPatcher.PatchAndLoad();


            var assemblyName = AssemblyName.GetAssemblyName(executablePath);

            entrypointAssembly =
                assemblyPatcher.PatcherContext.LoadedAssemblies.Values.FirstOrDefault(x => x.FullName ==
                                                                                      assemblyName.FullName);

            foreach (var loadedAssembly in assemblyPatcher.PatcherContext.LoadedAssemblies)
            {
                // TODO: Need full paths for loaded assemblies
                var assemblyPath = Path.Combine(Paths.GameRootPath, loadedAssembly.Key);

                Log.LogDebug($"Registering '{assemblyPath}' as a loaded assembly");
                AssemblyFixes.AssemblyLocations[loadedAssembly.Value.FullName] = assemblyPath;
            }

            if (entrypointAssembly != null)
            {
                Log.LogDebug("Found patched entrypoint assembly! Using it");
            }
            else
            {
                Log.LogDebug("Using entrypoint assembly from disk");
                entrypointAssembly = Assembly.LoadFrom(executablePath);
            }
        }

        Log.LogMessage("Preloader finished");

        Logger.Listeners.Remove(preloaderListener);

        var chainloader = new NetChainloader();

        chainloader.Initialize();
        chainloader.Execute();


        AssemblyFixes.Execute(entrypointAssembly);

        try
        {
            var argList = new List <object>();

            var paramTypes = entrypointAssembly.EntryPoint.GetParameters();

            if (paramTypes.Length == 1 && paramTypes[0].ParameterType == typeof(string[]))
            {
                argList.Add(args);
            }
            else if (paramTypes.Length == 1 && paramTypes[0].ParameterType == typeof(string))
            {
                argList.Add(string.Join(" ", args));
            }
            else if (paramTypes.Length != 0)
            {
                // Only other entrypoint signatures I can think of that .NET supports is Task / Task<int>
                //   async entrypoints. That's a can of worms for another time though

                Log.LogFatal($"Could not figure out how to handle entrypoint method with this signature: {entrypointAssembly.EntryPoint.FullDescription()}");
                return;
            }

            entrypointAssembly.EntryPoint.Invoke(null, argList.ToArray());
        }
        catch (Exception ex)
        {
            Log.LogFatal($"Unhandled exception: {ex}");
        }
    }