关于这个问题,我想强制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
}
}
但是,对于:
不起作用- 未处理的应用程序域异常(即在非前台线程上抛出)
- Windows Service应用程序(没有实际的
Main
入口点)
所以似乎<legacyCorruptedStateExceptionsPolicy>
是唯一的方法,使这项工作,在这种情况下,我不知道如何失败后记录CSE?
与其使用<legacyCorruptedStateExceptionsPolicy>
,不如使用[HandleProcessCorruptedStateExceptions]
(和[SecurityCritical]
):
Main
方法应该看起来像这样:
[HandleProcessCorruptedStateExceptions, SecurityCritical]
static void Main(string[] args)
{
try
{
...
}
catch (Exception ex)
{
// Log the CSE.
}
}
但要注意,这并不能捕获更严重的异常,如StackOverflowException
和ExecutionEngineException
。
同样try
块中的finally
也不会被执行:
对于其他未处理的appdomain异常,可以使用:
-
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;
});
你可以在这里找到更多关于可能的返回码的信息:
https://msdn.microsoft.com/en-us/library/ms680634 (VS.85) . aspx
UnhandledExceptionFilter
的一个"特点"是,如果附加了调试器,则不会调用它。(至少不是在我有一个WPF应用程序的情况下)所以要注意这一点。
如果您从上面设置了所有适当的ExceptionHandlers,您应该记录所有可以记录的异常。对于更严重的异常(如StackOverflowException
和ExecutionEngineException
),您必须找到另一种方法,因为整个过程在它们发生后不可用。一种可能的方法是另一个进程监视主进程并记录任何致命错误。
额外的提示:
- 在
AppDomain.CurrentDomain.UnhandledException
中,您可以安全地将e.ExceptionObject
转换为Exception
而不必担心-至少如果您没有任何IL代码抛出Exception
以外的其他对象:为什么是UnhandledExceptionEventArgs。ExceptionObject是对象而不是异常? - 如果你想抑制Windows错误报告对话框,你可以看看这里:如何终止一个程序,当它崩溃?(应该只是单元测试失败,而不是永远卡住)
- 如果你的WPF应用程序有多个调度程序,你也可以为其他调度程序使用
Dispatcher.UnhandledException
。
感谢@haindl指出,你也可以装饰处理程序方法与[HandleProcessCorruptedStateExceptions]
1属性,所以我做了一个小测试应用程序只是为了确认如果事情真的工作,因为他们应该。
1
注意:大多数答案表明我还应该包括[SecurityCritical]
属性,尽管在下面的测试中省略它并没有改变行为(单独的[HandleProcessCorruptedStateExceptions]
似乎工作得很好)。然而,我将把这两个属性都留在下面,因为我假设所有这些人都知道他们在说什么。这是一个学校的例子"复制自StackOverflow"模式在行动。
app.config
中删除的<legacyCorruptedStateExceptionsPolicy>
设置,即只允许最外层(入门级)处理程序捕获异常,记录它,然后失败。添加该设置将允许你的应用程序继续,如果你在一些内部处理程序中捕获异常,这是不是你想要的:这个想法只是为了获得准确的异常信息,然后悲惨地死去。
我使用以下方法抛出异常:
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。捕获所有异常,包括后台线程/任务:
// 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]
,以确保异常不会在错误的位置被捕获。也就是说,如果你在某个地方有一个try/catch
块,一个AccessViolationException
被抛出,你希望CLR跳过catch
块,并在结束应用程序之前传播到UnhandledException
。
派对结束了吗?别那么快
Microsoft: "使用应用程序域来隔离可能导致进程中断的任务"
下面的程序将保护您的主应用程序/线程免受不可恢复的故障,而没有与使用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
}
}