从应用程序的资源加载.NET程序集并从内存运行它,但不终止主/主机应用程序

介绍


我正在使用David Heffernan共享的下一个C#代码示例, 从应用程序的资源加载.NET程序集并从内存运行它 :

Assembly a = Assembly.Load(bytes); MethodInfo method = a.EntryPoint; if (method != null) method.Invoke(a.CreateInstance(method.Name), null); 

在这里,我只是在VB.NET中分享我正在使用的改编:

 Public Shared Sub Execute(ByVal resource As Byte(), ByVal parameters As Object()) Dim ass As Assembly = Assembly.Load(resource) Dim method As MethodInfo = ass.EntryPoint If (method IsNot Nothing) Then Dim instance As Object = ass.CreateInstance(method.Name) method.Invoke(instance, parameters) If (instance IsNot Nothing) AndAlso (instance.GetType().GetInterfaces.Contains(GetType(IDisposable))) Then DirectCast(instance, IDisposable).Dispose() End If instance = Nothing method = Nothing ass = Nothing Else Throw New EntryPointNotFoundException("Entrypoint not found in the specified resource. Are you sure it is a .NET assembly?") End If End Sub 

问题


问题是如果执行的程序集有一个应用程序退出指令,那么它也会终止我的主/主机应用程序。 例如:

从这个源代码编译的ConsoleApplication1.exe

 Module Module1 Sub Main() Environment.Exit(0) End Sub End Module 

当我将ConsoleApplication1.exe添加到应用程序资源,然后我加载它并使用Assembly.Load方法运行它时,它也会因为对Environment.Exit的调用而终止我的应用程序。


如何在不修改已执行程序集的源代码的情况下防止这种情况?

也许我可以做一些事情,比如将一种退出事件处理程序关联到已执行的程序集以正确处理/忽略它? 在这一点上我有什么选择?

PS:对我来说无论给定的解决方案是用C#还是VB.NET编写的。

请注意两件事,首先是我的意图是以自动/抽象的方式解决这个问题,我的意思是最后的结果应该只需要调用“Execute”方法传递资源和参数而不用担心其余的部分; 其次,我希望执行的程序集同步运行,而不是异步运行……如果这可能对可能的解决方案很重要。

更新 :我的第一个解决方案不适用于OP等程序资源中包含的程序集; 而是从磁盘加载它。 从字节数组加载的解决方案将遵循(进行中)。 请注意,以下几点适用于这两种解决方案:

  • 由于Environment.Exit()方法由于缺少权限而引发exception,因此在遇到方法后,方法的执行将不会继续。

  • 您将需要Main方法所需的所有权限,但您可以通过在intellisense中键入“Permission”或通过检查SecurityExceptionTargetSite属性(这是MethodBase一个实例并将告诉它)来快速找到这些权限。你哪个方法失败了)。

  • 如果您的Main中的另一个方法需要UnmanagedCode权限,那么您运气不好,至少使用此解决方案。

  • 请注意,我发现UnmanagedCode权限是Environment.Exit() 纯粹通过反复试验所需的权限。

解决方案1:当程序集在磁盘上时

好的,这是我到目前为止所发现的,请耐心等待。 我们将创建一个沙盒AppDomain:

 AppDomainSetup adSetup = new AppDomainSetup(); adSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; // This is where the main executable resides. For more info on this, see "Remarks" in // https://msdn.microsoft.com/en-us/library/system.appdomainsetup.applicationbase(v=vs.110).aspx#Anchor_1 PermissionSet permission = new PermissionSet(PermissionState.None); // Permissions of the AppDomain/what it can do permission.AddPermission(new SecurityPermission(SecurityPermissionFlag.AllFlags & ~SecurityPermissionFlag.UnmanagedCode)); // All SecurityPermission flags EXCEPT UnmanagedCode, which is required by Environment.Exit() // BUT the assembly needs SecurityPermissionFlag.Execution to be run; // otherwise you'll get an exception. permission.AddPermission(new FileIOPermission(PermissionState.Unrestricted)); permission.AddPermission(new UIPermission(PermissionState.Unrestricted)); // the above two are for Console.WriteLine() to run, which is what I had in the Main method var assembly = Assembly.LoadFile(exePath); // path to ConsoleApplication1.exe var domain = AppDomain.CreateDomain("SomeGenericName", null, adSetup, permission, null); // sandboxed AppDomain try { domain.ExecuteAssemblyByName(assembly.GetName(), new string[] { }); } // The SecurityException is thrown by Environment.Exit() not being able to run catch (SecurityException e) when (e.TargetSite == typeof(Environment).GetMethod("Exit")) { Console.WriteLine("Tried to run Exit"); } catch (SecurityException e) { // Some other action in your method needs SecurityPermissionFlag.UnmanagedCode to run, // or the PermissionSet is missing some other permission } catch { Console.WriteLine("Something else failed in ConsoleApplication1.exe's main..."); } 

解决方案2:当程序集是字节数组时

警告:癌症解决方案如下。

在更改我的解决方案以加载字节数组时,OP和我发现了一个奇怪的exception文件未找到exception: 即使您将一个字节数组传递给Assembly.Load()domain.ExecuteAssemblyByName()仍会在磁盘中搜索程序集,出于一些奇怪的原因。 显然我们不是唯一有问题的人: 加载字节数组assembly 。

首先,我们有一个Helper类:

 public class Helper : MarshalByRefObject { public void LoadAssembly(Byte[] data) { var a = Assembly.Load(data); a.EntryPoint.Invoke(null, null); } } 

如您所见,使用Assembly.Load()加载程序集并调用它的入口点。 这是我们将加载到AppDomain的代码:

 AppDomainSetup adSetup = new AppDomainSetup(); adSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; // This is where the main executable resides. For more info on this, see "Remarks" in // https://msdn.microsoft.com/en-us/library/system.appdomainsetup.applicationbase(v=vs.110).aspx#Anchor_1 PermissionSet permission = new PermissionSet(PermissionState.None); // Permissions of the AppDomain/what it can do permission.AddPermission(new SecurityPermission(SecurityPermissionFlag.AllFlags & ~SecurityPermissionFlag.UnmanagedCode)); // All SecurityPermission flags EXCEPT UnmanagedCode, which is required by Environment.Exit() // BUT the assembly needs SecurityPermissionFlag.Execution to be run; // otherwise you'll get an exception. permission.AddPermission(new FileIOPermission(PermissionState.Unrestricted)); permission.AddPermission(new UIPermission(PermissionState.Unrestricted)); // the above two are for Console.WriteLine() to run, which is what I had in the Main method var domain = AppDomain.CreateDomain("SomeGenericName", null, adSetup, permission, null); // sandboxed AppDomain try { Helper helper = (Helper)domain.CreateInstanceAndUnwrap(typeof(Helper).Assembly.FullName, typeof(Helper).FullName); // create an instance of Helper in the new AppDomain helper.LoadAssembly(bytes); // bytes is the in-memory assembly } catch (TargetInvocationException e) when (e.InnerException.GetType() == typeof(SecurityException)) { Console.WriteLine("some kind of permissions issue here"); } catch (Exception e) { Console.WriteLine("Something else failed in ConsoleApplication1.exe's main... " + e.Message); } 

请注意,在第二个解决方案中, SecurityException变为TargetInvocationException ,其InnerException属性为SecurityException 。 不幸的是,这意味着您无法使用e.TargetSite查看引发exception的方法。

结论/要记住的事情

这个解决方案并不完美。 以某种方式通过方法的IL并人为地删除对Environment.Exit()的调用会好得多。

所有学分都归Kirill Osenkov – MSFT所有

我可以成功将程序集加载到另一个AppDomain并调用其入口点。 Environment.Exit始终关闭托管进程 。
解决方法是,从已加载的控制台应用程序的Main中返回一个int。 成功为零,其他错误数字为零。

而不是这个:

 Module Module1 Sub Main() // your code Environment.Exit(0) End Sub End Module 

写道:(我希望这是有效的VB.NET :-))

 Module Module1 Function Main() As Integer // your code Return 0 // 0 == no error End Function End Module 

演示 – C#

 class Program { static void Main(string[] args) { Launcher.Start(@"C:\Users\path\to\your\console\app.exe"); } } public class Launcher : MarshalByRefObject { public static void Start(string pathToAssembly) { TextWriter originalConsoleOutput = Console.Out; StringWriter writer = new StringWriter(); Console.SetOut(writer); AppDomain appDomain = AppDomain.CreateDomain("Loading Domain"); Launcher program = (Launcher)appDomain.CreateInstanceAndUnwrap( typeof(Launcher).Assembly.FullName, typeof(Launcher).FullName); program.Execute(pathToAssembly); AppDomain.Unload(appDomain); Console.SetOut(originalConsoleOutput); string result = writer.ToString(); Console.WriteLine(result); } ///  /// This gets executed in the temporary appdomain. /// No error handling to simplify demo. ///  public void Execute(string pathToAssembly) { // load the bytes and run Main() using reflection // working with bytes is useful if the assembly doesn't come from disk byte[] bytes = File.ReadAllBytes(pathToAssembly); //"Program.exe" Assembly assembly = Assembly.Load(bytes); MethodInfo main = assembly.EntryPoint; main.Invoke(null, new object[] { null }); } } 

另请注意:

另请注意,如果使用LoadFrom,您可能会收到FileNotFoundexception,因为程序集解析程序将尝试查找您在GAC或当前应用程序的bin文件夹中加载的程序集。 使用LoadFile来加载任意组件文件 – 但请注意,如果这样做,您将需要自己加载任何依赖项。

只有一种方法可以做到这一点。 您必须动态检测程序集将要执行的所有代码。 这归结为拦截系统调用。 没有简单的方法可以做到这一点。 请注意,这不需要修改源代码。

为什么.NET安全系统不能这样做? 虽然系统可以为您提供安全权限,您可以使用它来控制对Environment.Exit调用,但这并不能解决问题。 程序集仍然可以调用非托管代码。 其他答案指出,这可以通过创建AppDomain并撤消SecurityPermissionFlag.UnmanagedCode来完成。 实际上,这是有效的,但您在评论中指出您要使程序集调用非托管代码。

如果你想在同一个进程中运行代码就是这样。你也可以在另一个进程中运行代码,但是你必须进行进程间通信。

更多AppDomain代码可以帮助您找到解决方案。 代码可以在LoadUnload中找到

项目LoadUnload中包含的小应用程序包含您可能在解决方案中适应的AppDomain代码。