优雅地处理损坏的状态异常

Gracefully handling corrupted state exceptions

this question 相关,我想强制 CLR 让我的 .NET 4.5.2 应用程序捕获损坏的状态异常,其唯一目的是记录它们然后终止应用程序。如果我在应用周围的多个位置有 catch (Exception ex),正确的方法是什么?

所以,在我指定 <legacyCorruptedStateExceptionsPolicy> 属性后,如果我理解正确,所有 catch (Exception ex) 处理程序都会捕获异常,如 AccessViolationException 并愉快地继续。

是的,我知道 catch (Exception ex) 是个坏主意™,但如果 CLR 至少将正确的堆栈跟踪放入事件日志中,我会非常乐意向客户解释他的服务器应用程序在凌晨 1 点快速失败并在晚上离线是一件好事。但不幸的是,CLR 将 记录到事件日志中,然后关闭该进程,因此我无法找出实际发生的情况。

问题是,如何在整个过程中实现这一点:

if the exception thrown is a Corrupted State Exception:
    - write the message to the log file
    - end the process 

(更新)

换句话说,这可能适用于简单应用程序中的大多数异常:

[HandleProcessCorruptedStateExceptions] 
[SecurityCritical]
static void Main() // main entry point
{
    try 
    {

    }
    catch (Exception ex)
    {
        // this will catch CSEs
    }
}

但是,它不适用于:

所以似乎 <legacyCorruptedStateExceptionsPolicy> 是使这项工作有效的唯一方法,在这种情况下,我不知道如何在登录 CSE 后失败?

与其使用 <legacyCorruptedStateExceptionsPolicy>,不如使用 [HandleProcessCorruptedStateExceptions](和 [SecurityCritical]),如下所述:

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

之后,您的 Main 方法应如下所示:

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

但请注意,这不会捕获更严重的异常,例如 WhosebugExceptionExecutionEngineException

另外 finally 个涉及的 try 个块将不会被执行:

https://csharp.2000things.com/2013/08/30/920-a-finally-block-is-not-executed-when-a-corrupted-state-exception-occurs/

对于其他未处理的应用程序域异常,您可以使用:

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

(当特定处理程序适合您的情况时,请搜索详细信息。TaskScheduler.UnobservedTaskException 例如有点棘手。)

如果您无权访问 Main 方法,您还可以标记您的 AppDomain 异常处理程序以捕获 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.
}

最后一道防线可能是非托管的 UnhandledExceptionFilter,如下所示:

[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);

然后在流程开始的某个地方:

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

您可以在此处找到有关可能的 return 代码的更多信息:

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

UnhandledExceptionFilter 的 "specialty" 是如果附加了调试器则不会调用它。 (至少在我拥有 WPF 应用程序的情况下不是。)所以请注意这一点。

如果您从上面设置了所有适当的 ExceptionHandler,您应该记录所有可以记录的异常。对于更严重的异常(如WhosebugExceptionExecutionEngineException)你必须另辟蹊径,因为它们发生后整个过程无法使用。一种可能的方法可能是监视主进程并记录任何致命错误的另一个进程。

其他提示:

  • AppDomain.CurrentDomain.UnhandledException 中,您可以安全地将 e.ExceptionObject 转换为 Exception 而不必担心 - 至少如果您没有任何 IL 代码抛出 Exception: Why is UnhandledExceptionEventArgs.ExceptionObject an object and not an Exception?
  • 如果你想禁止 Windows 错误报告对话框,你可以看这里:How to terminate a program when it crashes? (which should just fail a unit test instead of getting stuck forever)
  • 如果您有一个包含多个调度程序的 WPF 应用程序,您还可以为其他调度程序使用 Dispatcher.UnhandledException

感谢@haindl 指出您还可以使用 [HandleProcessCorruptedStateExceptions]1 属性装饰处理程序方法,所以我做了一个小测试应用程序来确认是否事情确实按预期进行。

1 注意: 大多数答案都指出我还应该包括 [SecurityCritical] 属性,尽管在下面的测试中省略了它并没有改变行为(单独 [HandleProcessCorruptedStateExceptions] 似乎工作得很好)。但是,我将在下面保留这两个属性,因为我假设所有这些人都知道他们在说什么。这是 "Copied from Whosebug" 模式在学校的例子。

这个想法显然是 app.config 中删除 <legacyCorruptedStateExceptionsPolicy> 设置,即只允许我们的最外层(入门级)处理程序) 捕获异常,记录它,然后失败。添加设置将允许您的应用程序继续,如果您在某些内部处理程序中捕获异常,这不是您想要的:这个想法只是为了获取准确的异常信息,然后死得很惨

我用following method抛出异常:

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.从 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。捕获所有异常,包括背景 threads/tasks:

// 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);
}

我建议只使用后一种方法,并从 所有其他地方删除 [HandleProcessCorruptedStateExceptions] 以确保不会在错误的地方捕获异常. IE。如果你在某处有一个 try/catch 块并且抛出一个 AccessViolationException,你希望 CLR 在结束应用程序之前跳过 catch 块并传播到 UnhandledException

聚会结束了吗?没那么快

Microsoft: “使用应用程序域隔离可能会导致进程中断的任务。”

下面的程序将保护您的主要 application/thread 免受不可恢复的故障的影响,并且没有与使用 HandleProcessCorruptedStateExceptions<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
    }
}