c# appdomain .net-3.5 process.start

c# - Reemplazar proceso. Comience con AppDomains



.net-3.5 process.start (3)

Fondo

Tengo un servicio de Windows que usa varias DLL de terceros para realizar trabajos en archivos PDF. Estas operaciones pueden usar bastantes recursos del sistema y ocasionalmente parecen sufrir pérdidas de memoria cuando ocurren errores. Los archivos DLL son contenedores administrados alrededor de otros archivos DLL no administrados.

Solución actual

Ya estoy mitigando este problema en un caso al envolver una llamada a uno de los archivos DLL en una aplicación de consola dedicada y llamar a esa aplicación a través de Process.Start (). Si la operación falla y hay pérdidas de memoria o identificadores de archivos no publicados, realmente no importa. El proceso finalizará y el sistema operativo recuperará los identificadores.

Me gustaría aplicar esta misma lógica a los otros lugares de mi aplicación que usan estos archivos DLL. Sin embargo, no estoy muy entusiasmado por agregar más proyectos de consola a mi solución, y escribir aún más código de placa de caldera que llame a Process.Start () y analizar la salida de las aplicaciones de la consola.

Nueva solución

Una alternativa elegante a las aplicaciones de consola dedicadas y Process.Start () parece ser el uso de AppDomains, como este: http://blogs.geekdojo.net/richard/archive/2003/12/10/428.aspx .

Implementé un código similar en mi aplicación, pero las pruebas unitarias no han sido prometedoras. Creo un FileStream en un archivo de prueba en un Dominio de aplicación diferente, pero no lo elimino. Luego intento crear otro FileStream en el dominio principal y falla debido al bloqueo de archivos no publicados.

Curiosamente, agregar un evento DomainUnload vacío al dominio worker hace que pase la prueba de la unidad. De todos modos, me preocupa que tal vez la creación de AppDomains "trabajador" no resuelva mi problema.

¿Pensamientos?

El código

/// <summary> /// Executes a method in a separate AppDomain. This should serve as a simple replacement /// of running code in a separate process via a console app. /// </summary> public T RunInAppDomain<T>( Func<T> func ) { AppDomain domain = AppDomain.CreateDomain ( "Delegate Executor " + func.GetHashCode (), null, new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory } ); domain.DomainUnload += ( sender, e ) => { // this empty event handler fixes the unit test, but I don''t know why }; try { domain.DoCallBack ( new AppDomainDelegateWrapper ( domain, func ).Invoke ); return (T)domain.GetData ( "result" ); } finally { AppDomain.Unload ( domain ); } } public void RunInAppDomain( Action func ) { RunInAppDomain ( () => { func (); return 0; } ); } /// <summary> /// Provides a serializable wrapper around a delegate. /// </summary> [Serializable] private class AppDomainDelegateWrapper : MarshalByRefObject { private readonly AppDomain _domain; private readonly Delegate _delegate; public AppDomainDelegateWrapper( AppDomain domain, Delegate func ) { _domain = domain; _delegate = func; } public void Invoke() { _domain.SetData ( "result", _delegate.DynamicInvoke () ); } }

La prueba unitaria

[Test] public void RunInAppDomainCleanupCheck() { const string path = @"../../Output/appdomain-hanging-file.txt"; using( var file = File.CreateText ( path ) ) { file.WriteLine( "test" ); } // verify that file handles that aren''t closed in an AppDomain-wrapped call are cleaned up after the call returns Portal.ProcessService.RunInAppDomain ( () => { // open a test file, but don''t release it. The handle should be released when the AppDomain is unloaded new FileStream ( path, FileMode.Open, FileAccess.ReadWrite, FileShare.None ); } ); // sleeping for a while doesn''t make a difference //Thread.Sleep ( 10000 ); // creating a new FileStream will fail if the DomainUnload event is not bound using( var file = new FileStream ( path, FileMode.Open, FileAccess.ReadWrite, FileShare.None ) ) { } }


¿Ha considerado abrir una tubería entre la aplicación principal y las aplicaciones secundarias? De esta forma, podría pasar información más estructurada entre las dos aplicaciones sin analizar el resultado estándar.


Los dominios de aplicación y la interacción entre dominios es una cuestión muy delgada, por lo que uno debe asegurarse de que realmente entienda cómo funciona la cosa antes de hacer cualquier cosa ... Mmm ... Digamos, "no estándar" :-)

En primer lugar, su método de creación de flujo se ejecuta en su dominio "predeterminado" (sorpresa sorpresa). ¿Por qué? Simple: el método que pasa a AppDomain.DoCallBack se define en un objeto AppDomainDelegateWrapper , y ese objeto existe en su dominio predeterminado, por lo que es donde se ejecuta su método. MSDN no dice acerca de esta pequeña "característica", pero es fácil de verificar: simplemente configure un punto de interrupción en AppDomainDelegateWrapper.Invoke .

Entonces, básicamente, debes prescindir de un objeto "envoltorio". Use el método estático para el argumento de DoCallBack.

Pero, ¿cómo pasa su argumento "func" al otro dominio para que su método estático pueda recogerlo y ejecutarlo?

La forma más evidente es usar AppDomain.SetData , o puede hacer la suya propia, pero independientemente de cómo lo haga exactamente, hay otro problema: si "func" es un método no estático, entonces el objeto que está definido en debe pasar de alguna manera al otro dominio de aplicación. Se puede pasar por valor (mientras se copia, campo por campo) o por referencia (creando una referencia de objetos entre dominios con toda la belleza de Remoting). Para hacer former, la clase debe estar marcada con un atributo [Serializable] . Para hacer esto último, debe heredar de MarshalByRefObject . Si la clase no es ninguno, se lanzará una excepción al intentar pasar el objeto al otro dominio. Tenga en cuenta, sin embargo, que pasar por referencia casi mata toda la idea, porque su método seguirá siendo invocado en el mismo dominio en el que existe el objeto, es decir, el predeterminado.

Al finalizar el párrafo anterior, tiene dos opciones: pasar un método definido en una clase marcada con un atributo [Serializable] (y tener en cuenta que el objeto se copiará), o pasar un método estático. Sospecho que, para sus propósitos, necesitará la primera.

Y en caso de que haya escapado a su atención, me gustaría señalar que su segunda sobrecarga de RunInAppDomain (la que toma la Action ) pasa un método definido en una clase que no está marcada [Serializable] . ¿No ves ninguna clase allí? No es necesario que: con delegados anónimos que contienen variables vinculadas, el compilador creará uno para usted. Y sucede que el compilador no se molesta en marcar esa clase autogenerada [Serializable] . Desafortunado, pero esta es la vida :-)

Habiendo dicho todo eso (muchas palabras, ¿no? :-), y asumiendo su promesa de no pasar ningún método no-estático y no [Serializable] , aquí están sus nuevos métodos RunInAppDomain :

/// <summary> /// Executes a method in a separate AppDomain. This should serve as a simple replacement /// of running code in a separate process via a console app. /// </summary> public static T RunInAppDomain<T>(Func<T> func) { AppDomain domain = AppDomain.CreateDomain("Delegate Executor " + func.GetHashCode(), null, new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory }); try { domain.SetData("toInvoke", func); domain.DoCallBack(() => { var f = AppDomain.CurrentDomain.GetData("toInvoke") as Func<T>; AppDomain.CurrentDomain.SetData("result", f()); }); return (T)domain.GetData("result"); } finally { AppDomain.Unload(domain); } } [Serializable] private class ActionDelegateWrapper { public Action Func; public int Invoke() { Func(); return 0; } } public static void RunInAppDomain(Action func) { RunInAppDomain<int>( new ActionDelegateWrapper { Func = func }.Invoke ); }

Si todavía estás conmigo, te agradezco :-)

Ahora, después de pasar tanto tiempo arreglando ese mecanismo, les voy a decir que de todos modos no tuvo ningún propósito.

El hecho es que AppDomains no lo ayudará para sus propósitos. Solo se ocupan de los objetos administrados, mientras que el código no administrado puede tener fugas y bloquear todo lo que quiera. El código no administrado ni siquiera sabe que hay tales cosas como appdomains. Solo sabe sobre procesos.

Entonces, al final, su mejor opción sigue siendo su solución actual: simplemente engendre otro proceso y esté contento con eso. Y, estaría de acuerdo con las respuestas anteriores, no es necesario que escriba otra aplicación de consola para cada caso. Simplemente pase un nombre completamente calificado de un método estático, y haga que la aplicación de la consola cargue su ensamblaje, cargue su tipo e invoque el método. De hecho, puedes empaquetarlo bastante bien de una manera muy similar a como lo hiciste con AppDomains. Puede crear un método llamado "RunInAnotherProcess", que examinará el argumento, obtendrá el nombre de tipo completo y el nombre del método (asegurándose de que el método sea estático) y generará la aplicación de consola, que hará el resto.


No tiene que crear muchas aplicaciones de consola, puede crear una sola aplicación que recibirá como parámetro el nombre completo del tipo calificado. La aplicación cargará ese tipo y lo ejecutará.
Separar todo en pequeños procesos es el mejor método para realmente disponer de todos los recursos. Un dominio de aplicación no puede eliminar recursos completos, pero un proceso puede hacerlo.