📜 ⬆️ ⬇️

[DotNetBook] Exception events and how to get StackOverflow and ExecutionEngineException from scratch


Exception Events


In general, we do not always know about the exceptions that will occur in our programs because we almost always use something that is written by other people and that is in other subsystems and libraries. Not only are various situations possible in your own code, in the code of other libraries, there are also many problems associated with the execution of code in isolated domains. And just in this case it would be extremely useful to be able to obtain data on the operation of isolated code. After all, the situation can be quite real when a third-party code intercepts all errors without exception, muffling their fault block:


 try { // ... } catch { // do nothing, just to make code call more safe } 

In such a situation, it may turn out that code execution is no longer as safe as it looks, but we have no reports that any problems have occurred. The second option is when the application suppresses some, even if legal, exception. And the result - the following exception in a random place will cause the application to fall in some future from a seemingly random error. Here I would like to have an idea what the background to this error was. What the course of events has led to this situation. And one of the ways to make this possible is to use additional events that relate to exceptional situations: AppDomain.FirstChanceException and AppDomain.UnhandledException .


This article is the second of four in a series of articles about exceptions. Full cycle:
- Type system architecture
- Events about exceptional situations (this article)
- Types of exceptional situations
- Serialization and processing units

Note


The chapter published on Habré is not updated and it is possible that it is already somewhat outdated. So, please ask for a more recent text to the original:



In fact, when you "throw an exception", the usual method of some internal subsystem, Throw , is called, which internally performs the following operations:



We should immediately make a reservation, answering the question that torments many minds: is it possible to somehow cancel an exception that has arisen in an uncontrolled code that is executed in an isolated domain, without thus unleashing the stream in which this exception was thrown? The answer is laconic and simple: no. If an exception is not caught on the whole range of methods called, it cannot be processed in principle. Otherwise, a strange situation arises: if we use an AppDomain.FirstChanceException handle (some kind of synthetic catch ) an exception, then to which frame should the thread stack roll back? How to set this in the framework of the .NET CLR rules? No It is simply not possible. The only thing we can do is to record the information received for future research.


The second thing that should be told "ashore" is why these events were not entered by Thread , but by AppDomain . After all, if you follow the logic, exceptions occur where? In the flow of command execution. Those. actually have a Thread . So why do problems arise at the domain? The answer is very simple: for what situations were AppDomain.FirstChanceException and AppDomain.UnhandledException ? Among other things - to create a sandbox for plug-ins. Those. for situations where there is a certain AppDomain that is configured for PartialTrust. Inside this AppDomain, anything can happen: there at any moment threads can be created, or existing ones from ThreadPool can be used. Then it turns out that we, being outside of this process (we did not write that code) cannot subscribe to events of internal streams in any way. Just because we have no idea what kind of threads were created there. But we are guaranteed to have AppDomain , which organizes the sandbox and the link to which we have.


So, in fact, we are provided with two boundary events: something happened, which was not supposed ( FirstChanceExecption ) and “all bad”, no one handled the exceptional situation: it turned out to be not provided. And because the flow of execution of commands does not make sense and he ( Thread ) will be shipped.


What can be obtained by having these events and why is it bad for developers to bypass these events?


AppDomain.FirstChanceException


This event is inherently purely informational in nature and cannot be "processed." Its task is to notify you that an exception has occurred within this domain and after processing the event it will begin to be processed by the application code. Its execution carries with it a couple of features that need to be remembered during the design of the handler.


But let's first take a look at a simple synthetic example of its processing:


 void Main() { var counter = 0; AppDomain.CurrentDomain.FirstChanceException += (_, args) => { Console.WriteLine(args.Exception.Message); if(++counter == 1) { throw new ArgumentOutOfRangeException(); } }; throw new Exception("Hello!"); } 

What is remarkable about this code? Wherever a certain code generates an exception, the first thing that happens is its logging to the console. Those. even if you forget or cannot foresee handling some type of exception, it will still appear in the event log that you organize. The second is a somewhat strange condition for the release of an internal exception. The thing is that inside the FirstChanceException handler FirstChanceException you can't just pick up and throw another exception. Rather, even this: inside the FirstChanceException handler, you are not able to throw at least some exception. If you do, there are two possible events. At first, if there were no if(++counter == 1) condition if(++counter == 1) , we would get an infinite FirstChanceException for all new and new ArgumentOutOfRangeException . What does it mean? This means that at a certain stage we would get a StackOverflowException : throw new Exception("Hello!") Calls the CLR method Throw, which throws a FirstChanceException , which calls Throw already for ArgumentOutOfRangeException and then on recursion. The second option - we defended by the depth of recursion using the counter condition. Those. in this case, we throw an exception only once. The result is more than unexpected: we get an exceptional situation, which actually runs inside the Throw instruction. And what is most suitable for this type of error? According to ECMA-335, if the instruction was entered in an exceptional position, ExecutionEngineException must be thrown! And we are unable to handle this exceptional situation. It leads to full departure from the application. What options of safe processing do we have?


The first thing that comes to mind is to try-catch block on the entire code of the FirstChanceException handler:


 void Main() { var fceStarted = false; var sync = new object(); EventHandler<FirstChanceExceptionEventArgs> handler; handler = new EventHandler<FirstChanceExceptionEventArgs>((_, args) => { lock (sync) { if (fceStarted) { //     - ,        -      , //   try  . Console.WriteLine($"FirstChanceException inside FirstChanceException ({args.Exception.GetType().FullName})"); return; } fceStarted = true; try { //     . ,   Console.WriteLine(args.Exception.Message); throw new ArgumentOutOfRangeException(); } catch (Exception exception) { //       Console.WriteLine("Success"); } finally { fceStarted = false; } } }); AppDomain.CurrentDomain.FirstChanceException += handler; try { throw new Exception("Hello!"); } finally { AppDomain.CurrentDomain.FirstChanceException -= handler; } } OUTPUT: Hello! Specified argument was out of the range of valid values. FirstChanceException inside FirstChanceException (System.ArgumentOutOfRangeException) Success !Exception: Hello! 

Those. on the one hand, we have the FirstChanceException event handling code, and on the other hand, additional exception handling code in FirstChanceException itself. However, the logging techniques for both situations should be different. If the logging of event handling can go arbitrarily, then error handling of the processing logic of the FirstChanceException should go without exception situations in principle. The second thing you probably noticed is the synchronization between the threads. There may be a question: why is it here if any exception is born in any thread and therefore the FirstChanceException is supposed to be thread-safe. However, everything is not so cheerful. FirstChanceException occurs at AppDomain. And this means that it occurs for any thread that is launched in a specific domain. Those. if we have a domain within which several threads have been started, then FirstChanceException can go in parallel. And this means that we need to somehow protect ourselves with synchronization: for example, using lock .


The second way is to try to divert processing to a neighboring stream belonging to another application domain. However, it is worth making a reservation here that with such an implementation we have to build a dedicated domain just for this task so that it does not work out so that other threads that are working may put this domain:


 static void Main() { using (ApplicationLogger.Go(AppDomain.CurrentDomain)) { throw new Exception("Hello!"); } } public class ApplicationLogger : MarshalByRefObject { ConcurrentQueue<Exception> queue = new ConcurrentQueue<Exception>(); CancellationTokenSource cancellation; ManualResetEvent @event; public void LogFCE(Exception message) { queue.Enqueue(message); } private void StartThread() { cancellation = new CancellationTokenSource(); @event = new ManualResetEvent(false); var thread = new Thread(() => { while (!cancellation.IsCancellationRequested) { if (queue.TryDequeue(out var exception)) { Console.WriteLine(exception.Message); } Thread.Yield(); } @event.Set(); }); thread.Start(); } private void StopAndWait() { cancellation.Cancel(); @event.WaitOne(); } public static IDisposable Go(AppDomain observable) { var dom = AppDomain.CreateDomain("ApplicationLogger", null, new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, }); var proxy = (ApplicationLogger)dom.CreateInstanceAndUnwrap(typeof(ApplicationLogger).Assembly.FullName, typeof(ApplicationLogger).FullName); proxy.StartThread(); var subscription = new EventHandler<FirstChanceExceptionEventArgs>((_, args) => { proxy.LogFCE(args.Exception); }); observable.FirstChanceException += subscription; return new Subscription(() => { observable.FirstChanceException -= subscription; proxy.StopAndWait(); }); } private class Subscription : IDisposable { Action act; public Subscription (Action act) { this.act = act; } public void Dispose() { act(); } } } 

In this case, the handling of FirstChanceException is as secure as possible: in a neighboring stream belonging to a neighboring domain. Message processing errors cannot crash application workflows. Plus, you can listen to the UnhandledException of the message logging domain separately: fatal errors during logging will not crash the entire application.


AppDomain.UnhandledException


The second message that we can intercept and which deals with exception handling is AppDomain.UnhandledException . This message is very bad news for us because it means that there was no one who could find a way to handle the error in some thread. Also, if such a situation has occurred, all we can do is to “unload” the consequences of such an error. Those. in any way, clean up the resources belonging only to this thread if any were created. However, an even better situation is to handle exceptions, being in the "root" of the threads without closing the flow. Those. essentially set try-catch . Let's try to consider the appropriateness of such behavior.


Let us have a library that needs to create threads and implement some kind of logic in these threads. We, as users of this library, are interested only in the guarantee of API calls and also in receiving error messages. If the library destroys the streams without notifying them, this is of little help to us. Moreover, a thread crash will result in an AppDomain.UnhandledException message, in which there is no information about which particular thread lay on its side. If we are talking about our code, the collapsing stream will hardly be useful to us either. In any case, I did not meet this need. Our task is to process errors correctly, send information about their occurrence to the error log and exit the stream correctly. Those. essentially wrap the method from which the thread starts in try-catch :


  ThreadPool.QueueUserWorkitem(_ => { using(Disposables aggregator = ...){ try { // do work here, plus: aggregator.Add(subscriptions); aggregator.Add(dependantResources); } catch (Exception ex) { logger.Error(ex, "Unhandled exception"); } } }); 

In this scheme, we get what we need: on the one hand, we will not bring down the flow. On the other hand, we will correctly clear local resources if they were created. Well, in the appendage - organize the logging of the error. But wait, you say. Somehow you famously jumped off the question of the event AppDomain.UnhandledException . Is it really not necessary? Need to. But just to let you know that we forgot to wrap some threads in a try-catch with all the necessary logic. With everything: with logging and cleaning of resources. Otherwise it will be completely wrong: to take and extinguish all exceptions, as if they were not there at all.


Link to the whole book




')

Source: https://habr.com/ru/post/419929/


All Articles