Carica un assembly .NET dalle risorse dell’applicazione ed eseguilo dalla memoria, ma senza terminare l’applicazione principale / host

INTRODUZIONE


Sto usando il prossimo esempio di codice C # condiviso da David Heffernan ‘per caricare un assembly .NET dalle risorse dell’applicazione ed eseguirlo dalla memoria :

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

Qui ho appena condiviso un adattamento in VB.NET che sto usando anche:

 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 

PROBLEMA


Il problema è che se l’assembly eseguito ha un’istruzione di uscita dell’applicazione, termina anche la mia applicazione principale / host. Per esempio:

ConsoleApplication1.exe compilato da questo codice sorgente:

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

Quando aggiungo ConsoleApplication1.exe alle risorse dell’applicazione, quindi lo carico ed eseguo con la metodologia Assembly.Load , termina anche la mia applicazione perché la chiamata a Environment.Exit .

DOMANDA


Come posso evitare ciò, senza modificare il codice sorgente dell’assembly eseguito ?.

Forse potrei fare qualcosa come associare un tipo di gestore di eventi di uscita all’assembly eseguito per gestirlo / ignorarlo correttamente ?. Quali sono le mie opzioni a questo punto ?.

PS: Per me non importa se la soluzione data è scritta in C # o VB.NET.

Nota due cose, la prima è che la mia intenzione è di risolvere questo problema in modo automatico / astratto, voglio dire che il risultato finale dovrebbe semplicemente chiamare il metodo “Execute” passando la risorsa e gli argomenti e non preoccuparti il rest; e in secondo luogo, voglio che l’assembly eseguito sia eseguito sincronisticamente, non in modo asincrono … nel caso che ciò possa avere importanza per una ansible soluzione.

Aggiornamento : la mia prima soluzione non funziona per gli assembly contenuti nelle risorse di un programma come richiesto OP; invece lo carica dal disco. Seguirà (in corso) la soluzione per il caricamento da un array di byte. Si noti che i seguenti punti si applicano a entrambe le soluzioni:

  • Poiché il metodo Environment.Exit() genera un’eccezione a causa della mancanza di autorizzazioni, l’esecuzione del metodo non continuerà dopo che è stata rilevata.

  • Avrai bisogno di tutte le autorizzazioni necessarie per il tuo metodo Main, ma puoi trovarle rapidamente semplicemente digitando “Permission” in intellisense, o controllando la proprietà TargetSite SecurityException (che è un’istanza di MethodBase e dirà tu quale metodo non è riuscito).

  • Se un altro metodo nel Main richiede il permesso UnmanagedCode , sei sfortunato, almeno usando questa soluzione.

  • Si noti che ho trovato che l’authorization UnmanagedCode era quella che Environment.Exit() bisogno esclusivamente di tentativi ed errori .

Soluzione 1: quando l’assieme è sul disco

Ok, ecco quello che ho trovato finora, sopportami. Creeremo un AppDomain in modalità sandbox:

 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..."); } 

Soluzione 2: quando l’assieme è un array di byte

Attenzione: segue una soluzione cancerogena.

Quando ho cambiato la mia soluzione per caricare un array di byte, OP e ho scoperto un’eccezione di file strano non trovata: anche se si passa un array di byte a Assembly.Load() , domain.ExecuteAssemblyByName() cerca ancora il disco per l’assembly, per qualche strana ragione. Apparentemente non eravamo gli unici con il problema: Caricamento del gruppo di array di byte .

Innanzitutto, abbiamo una class Helper :

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

che come puoi vedere, carica l’assembly usando Assembly.Load() e chiama il suo punto di ingresso. Questo è il codice che verrà caricato 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); } 

Si noti che nella seconda soluzione, SecurityException diventa TargetInvocationException con la sua proprietà InnerException come SecurityException . Sfortunatamente, questo significa che non puoi usare e.TargetSite per vedere quale metodo ha lanciato l’eccezione.

Conclusione / Cose da tenere a mente

Questa soluzione non è perfetta. Sarebbe molto meglio in qualche modo passare attraverso l’IL del metodo e rimuovere artificialmente la chiamata a Environment.Exit() .

Tutti i crediti vanno a Kirill Osenkov – MSFT

Posso caricare correttamente l’assembly in un altro AppDomain e chiamare il suo punto di ingresso. Environment.Exit chiude sempre il processo di hosting .
Per ovviare al problema, restituire un int da Main dell’applicazione di console caricata. Zero per il successo e altri numeri per errori.

Invece di questo:

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

scrivi: (spero che questo sia valido VB.NET :-))

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

Demo – 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 }); } } 

Anche da notare:

Inoltre, si noti che se si utilizza LoadFrom è probabile che si verifichi un’eccezione FileNotFound poiché il resolver Assembly tenterà di trovare l’assembly che si sta caricando nel GAC o nella cartella bin dell’applicazione corrente. Usa LoadFile per caricare invece un file di assembly arbitrario, ma tieni presente che se lo fai dovrai caricare autonomamente qualsiasi dipendenza.

C’è solo un modo per farlo. Devi strumentare dynamicmente tutto il codice che l’assembly sta per eseguire. Questo si riduce all’intercettare le chiamate di sistema. Non c’è un modo semplice per farlo. Si noti che questo non richiede la modifica del codice sorgente.

Perché il sistema di sicurezza .NET non può farlo? Mentre il sistema potrebbe aver fornito un permesso di sicurezza che è ansible utilizzare per controllare le chiamate a Environment.Exit , questo non risolverà il problema. L’assembly potrebbe ancora chiamare in codice non gestito. Altre risposte hanno evidenziato che ciò può essere fatto creando un AppDomain e revocando SecurityPermissionFlag.UnmanagedCode . Effettivamente, questo funziona, ma nei commenti hai indicato di abilitare l’assembly a chiamare il codice non gestito.

È così se vuoi eseguire il codice nello stesso processo. Puoi anche eseguire il codice in un altro processo, ma devi fare una comunicazione tra processi.

Più codice AppDomain potrebbe aiutare a trovare una soluzione. Il codice può essere trovato su LoadUnload

Le piccole applicazioni incluse nel progetto LoadUnload contengono codice AppDomain che è ansible adattare alla soluzione.