Created
December 26, 2020 14:46
-
-
Save Epicguru/14a9b989344498b7d3f21c38eb210150 to your computer and use it in GitHub Desktop.
Loads and executes assemblies, also loading dependencies.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Reflection; | |
/* | |
* Loads and runs an assembly file. | |
* Assembly file is expected to have a static function called | |
* Main() with either no args or a string[] array. | |
* For example: public static void Main(string[] args) is valid. | |
* Also: private static void Main() is also valid. | |
* | |
* Loads all required dependencies as well. | |
* | |
* Author Epicguru. | |
*/ | |
namespace LoadAndRun | |
{ | |
public static class RunUtil | |
{ | |
private static readonly Stack<string> depDirs = new Stack<string>(); | |
private static bool hasHooked = false; | |
public static Exception LoadAndRun(string dllPath, params string[] args) | |
{ | |
if (!hasHooked) | |
{ | |
AppDomain.CurrentDomain.AssemblyResolve += DomainAssemblyResolve; | |
hasHooked = true; | |
} | |
Exception error = LoadAssAndEntryPoint(dllPath, out var entry, out bool hasStringArray); | |
if (error != null) | |
return error; | |
try | |
{ | |
entry.Invoke(null, hasStringArray ? new object[] {args} : new object[0]); | |
} | |
catch (Exception e) | |
{ | |
return e; | |
} | |
return null; | |
} | |
internal static Exception LoadAssAndEntryPoint(string dllPath, out MethodInfo entryPoint, out bool hasStringArray) | |
{ | |
entryPoint = null; | |
hasStringArray = false; | |
Assembly ass; | |
try | |
{ | |
ass = LoadAssembly(dllPath); | |
} | |
catch (Exception e) | |
{ | |
return e; | |
} | |
var entry = FindMainFunction(ass, out hasStringArray); | |
if (entry != null) | |
{ | |
LoadDeps(ass, new FileInfo(dllPath).DirectoryName); | |
entryPoint = entry; | |
return null; | |
} | |
return new Exception($"Failed to find entry point in {ass.FullName}"); | |
} | |
internal static Assembly LoadAssembly(string dllPath) | |
{ | |
byte[] bytes; | |
try | |
{ | |
bytes = File.ReadAllBytes(dllPath); | |
} | |
catch (Exception e) | |
{ | |
throw new Exception($"Failed to read assembly bytes from '{dllPath}'", e); | |
} | |
Assembly ass; | |
try | |
{ | |
ass = Assembly.Load(bytes); | |
} | |
catch (Exception e) | |
{ | |
throw new Exception("Failed created assembly from file bytes. Duplicate assembly?", e); | |
} | |
return ass; | |
} | |
internal static Exception LoadDeps(Assembly a, string sourceFolder) | |
{ | |
var domain = AppDomain.CurrentDomain; | |
bool IsLoaded(AssemblyName name) | |
{ | |
foreach (var item in domain.GetAssemblies()) | |
{ | |
// TODO make this comparison better. | |
if (item.ToString() == name.ToString()) | |
return true; | |
} | |
return false; | |
} | |
depDirs.Push(sourceFolder); | |
var refs = a.GetReferencedAssemblies(); | |
foreach (var item in refs) | |
{ | |
bool loaded = IsLoaded(item); | |
if (!loaded) | |
{ | |
Assembly created; | |
try | |
{ | |
created = domain.Load(item); | |
} | |
catch (Exception e) | |
{ | |
depDirs.Pop(); | |
return e; | |
} | |
// Note: source folder never changes, so all deps are expected be be in the same folder as main | |
// dll. | |
// For example, if A depends on B and B depends on C then | |
// when loading A, B.dll and C.dll should be in the same folder as A.dll | |
Exception newError = LoadDeps(created, sourceFolder); | |
if (newError != null) | |
{ | |
depDirs.Pop(); | |
return newError; | |
} | |
} | |
} | |
depDirs.Pop(); | |
return null; | |
} | |
private static Assembly DomainAssemblyResolve(object sender, ResolveEventArgs args) | |
{ | |
string root = depDirs.Peek(); | |
string dllName = args.Name.Split(',')[0].Trim() + ".dll"; | |
string path = Path.Combine(root, dllName); | |
Console.WriteLine($"Loading dependency '{dllName}' from '{root}'... "); | |
return LoadAssembly(path); | |
} | |
internal static MethodInfo FindMainFunction(Assembly a, out bool hasStringArray) | |
{ | |
foreach (var type in a.GetTypes()) | |
{ | |
if (!type.IsClass) | |
continue; | |
if (type.IsGenericType) | |
continue; | |
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | | |
BindingFlags.Static)) | |
{ | |
if (method.IsGenericMethod) | |
continue; | |
// Must be called Main just like regular program. | |
if (method.Name == "Main") | |
{ | |
// Allowed parameters: none, or an array of strings (such as string[] args) | |
var args = method.GetParameters(); | |
if (args.Length == 0) | |
{ | |
hasStringArray = false; | |
return method; | |
} | |
if (args.Length == 1 && args[0].ParameterType == typeof(string[])) | |
{ | |
hasStringArray = true; | |
return method; | |
} | |
} | |
} | |
} | |
hasStringArray = false; | |
return null; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment