Saltar al contenido

Manejo elegante de excepciones de estado corruptas

Solución:

En lugar de usar <legacyCorruptedStateExceptionsPolicy> sería mejor usar [HandleProcessCorruptedStateExceptions] (y [SecurityCritical]) como se indica aquí:

https://msdn.microsoft.com/en-us/magazine/dd419661.aspx

Siguiendo eso, tu Main El método debería verse así:

[HandleProcessCorruptedStateExceptions, SecurityCritical]
static void Main(string[] args)
{
    try
    {
        ...
    }
    catch (Exception ex)
    {
        // Log the CSE.
    }
}

Pero tenga en cuenta que esto no detecta las excepciones más serias como StackOverflowException y ExecutionEngineException.

También finally de involucrados try los bloques no se ejecutarán:

#920 – A finally Block Is Not Executed When a Corrupted State Exception Occurs

Para otras excepciones de dominio de aplicación no controladas, puede usar:

  • AppDomain.CurrentDomain.UnhandledException
  • Application.Current.DispatcherUnhandledException
  • TaskScheduler.UnobservedTaskException

(Realice una búsqueda de los detalles cuando un controlador específico sea apropiado para su situación. TaskScheduler.UnobservedTaskException por ejemplo, es un poco complicado).

Si no tiene acceso al Main , también puede marcar el controlador de excepciones de AppDomain para detectar el CSE:

AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;

...

[HandleProcessCorruptedStateExceptions, SecurityCritical]
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    // AccessViolationExceptions will get caught here but you cannot stop
    // the termination of the process if e.IsTerminating is true.
}

La última línea de defensa podría ser un UnhandledExceptionFilter no administrado como este:

[DllImport("kernel32"), SuppressUnmanagedCodeSecurity]
private static extern int SetUnhandledExceptionFilter(Callback cb);
// This has to be an own non generic delegate because generic delegates cannot be marshalled to unmanaged code.
private delegate uint Callback(IntPtr ptrToExceptionInfo);

Y luego en algún lugar al comienzo de su proceso:

SetUnhandledExceptionFilter(ptrToExceptionInfo =>
{
    var errorCode = "0x" + Marshal.GetExceptionCode().ToString("x2");
    ...
    return 1;
});

Puede encontrar más información sobre los posibles códigos de retorno aquí:

https://msdn.microsoft.com/en-us/library/ms680634(VS.85).aspx

Una “especialidad” del UnhandledExceptionFilter es que no se llama si se adjunta un depurador. (Al menos no en mi caso de tener una aplicación WPF). Así que tenlo en cuenta.

Si configura todos los ExceptionHandlers apropiados desde arriba, debe registrar todas las excepciones que se pueden registrar. Para las excepciones más graves (como StackOverflowException y ExecutionEngineException) tienes que encontrar otra manera porque todo el proceso es inutilizable después de que sucedieron. Una forma posible podría ser quizás otro proceso que observe el proceso principal y registre cualquier error fatal.

Sugerencias adicionales:

  • En el AppDomain.CurrentDomain.UnhandledException puedes lanzar con seguridad el e.ExceptionObject para Exception sin tener que preocuparse, al menos si no tiene ningún código IL que arroje otros objetos que no sean Exception: ¿Por qué UnhandledExceptionEventArgs.ExceptionObject es un objeto y no una excepción?
  • Si desea suprimir el cuadro de diálogo Informe de errores de Windows, puede echar un vistazo aquí: ¿Cómo terminar un programa cuando falla? (que debería fallar una prueba unitaria en lugar de quedarse atascado para siempre)
  • Si tiene una aplicación WPF con varios despachadores, también puede usar un Dispatcher.UnhandledException para los otros despachadores.

Gracias a @haindl por señalar que también puedes decorar métodos de controlador con el [HandleProcessCorruptedStateExceptions]1 atributo, así que hice una pequeña aplicación de prueba solo para confirmar si las cosas realmente funcionan como se supone que deben hacerlo.

1 Nota: La mayoría de las respuestas afirman que también debería incluir el [SecurityCritical] atributo, aunque en las pruebas siguientes omitirlo no cambió el comportamiento (el [HandleProcessCorruptedStateExceptions] solo parecía funcionar bien). Sin embargo, dejaré ambos atributos a continuación, ya que supongo que todas estas personas sabían lo que estaban diciendo. Ese es un ejemplo escolar del patrón “Copiado de StackOverflow” en acción.

La idea es, obviamente, retirar los <legacyCorruptedStateExceptionsPolicy> ajuste de app.config, es decir, solo permitir que nuestros manejadores más externos (de nivel de entrada) capturen la excepción, la registren y luego fallen. Agregar la configuración permitirá que su aplicación continúe, si detecta la excepción en algún controlador interno, y esto es no es lo que quieres: la idea es solo obtener la información precisa de la excepción y luego morir miserablemente.

Usé el siguiente método para lanzar la excepción:

static void DoSomeAccessViolation()
{
    // if you have any questions about why this throws,
    // the answer is "42", of course

    var ptr = new IntPtr(42);
    Marshal.StructureToPtr(42, ptr, true);
}

1. Captura de excepciones de Main:

[SecurityCritical]
[HandleProcessCorruptedStateExceptions]
static void Main(string[] args)
{
    try
    {
        DoSomeAccessViolation();
    }
    catch (Exception ex)
    {
        // this will catch all CSEs in the main thread
        Log(ex);
    }
}

2. Detectar todas las excepciones, incluidos los hilos / tareas en segundo plano:

// no need to add attributes here
static void Main(string[] args)
{
    AppDomain.CurrentDomain.UnhandledException += UnhandledException;

    // throw on a background thread
    var t = new Task(DoSomeAccessViolation);
    t.Start();
    t.Wait();
}

// but it's important that this method is marked
[SecurityCritical]
[HandleProcessCorruptedStateExceptions]
private static void UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    // this will catch all unhandled exceptions, including CSEs
    Log(e.ExceptionObject as Exception);
}

Recomendaría usar solo el último enfoque y eliminar el [HandleProcessCorruptedStateExceptions] de todos los demás lugares para asegurarse de que la excepción no quede atrapada en el lugar incorrecto. Es decir, si tienes un try/catch bloque en algún lugar y un AccessViolationException se lanza, desea que CLR omita el catch bloquear y propagar a la UnhandledException antes de finalizar la aplicación.

¿Se acabó la fiesta? no tan rapido

Microsoft: “Utilice dominios de aplicaciones para aislar las tareas que podrían provocar la caída de un proceso”.

El programa a continuación protegerá su aplicación / hilo principal de fallas irrecuperables sin riesgos asociados con el uso de HandleProcessCorruptedStateExceptions y <legacyCorruptedStateExceptionsPolicy>

public class BoundaryLessExecHelper : MarshalByRefObject
{
    public void DoSomething(MethodParams parms, Action action)
    {
        if (action != null)
            action();
        parms.BeenThere = true; // example of return value
    }
}

public struct MethodParams
{
    public bool BeenThere { get; set; }
}

class Program
{
    static void InvokeCse()
    {
        IntPtr ptr = new IntPtr(123);
        System.Runtime.InteropServices.Marshal.StructureToPtr(123, ptr, true);
    }
    // This is a plain code that will prove that CSE is thrown and not handled
    // this method is not a solution. Solution is below 
    private static void ExecInThisDomain()
    {
        try
        {
            var o = new BoundaryLessExecHelper();
            var p = new MethodParams() { BeenThere = false };
            Console.WriteLine("Before call");

            o.DoSomething(p, CausesAccessViolation);
            Console.WriteLine("After call. param been there? : " + p.BeenThere.ToString()); //never stops here
        }
        catch (Exception exc)
        {
            Console.WriteLine($"CSE: {exc.ToString()}");
        }
        Console.ReadLine();
    }

    // This is a solution for CSE not to break your app. 
    private static void ExecInAnotherDomain()
    {
        AppDomain dom = null;

        try
        {
            dom = AppDomain.CreateDomain("newDomain");
            var p = new MethodParams() { BeenThere = false };
            var o = (BoundaryLessExecHelper)dom.CreateInstanceAndUnwrap(typeof(BoundaryLessExecHelper).Assembly.FullName, typeof(BoundaryLessExecHelper).FullName);         
            Console.WriteLine("Before call");

            o.DoSomething(p, CausesAccessViolation);
            Console.WriteLine("After call. param been there? : " + p.BeenThere.ToString()); // never gets to here
        }
        catch (Exception exc)
        {
            Console.WriteLine($"CSE: {exc.ToString()}");
        }
        finally
        {
            AppDomain.Unload(dom);
        }

        Console.ReadLine();
    }


    static void Main(string[] args)
    {
        ExecInAnotherDomain(); // this will not break app
        ExecInThisDomain();  // this will
    }
}
¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *