Feb 16, 2007 16:59
If like me, you sometimes have to develop and maintain programs that other people actually use, you'll eventually get tired of 'old fashioned' bug reporting (read: people coming by your desk and telling you about a crash). The obvious solution to this is:
1. Trap errors automatically
2. Send them somewhere automatically
Once you get this working, suddenly people don't stop by your desk as often, and when they do, they have interesting things to say. You also find out about bugs you didn't know existed, because people have a tendency not to report bugs unless they care about them.
So, I finally got around to implementing this in C# for my tools. We already have an internal solution here that does a great job of it for C++, but until recently we didn't really use .NET at all. So the task fell to me!
#1 is honestly the hardest part of it. It took me a little while to dig up the necessary documentation, as it involves mucking around in parts of the .NET framework that most people don't even know exist.
All the magic happens in two specific event handlers attached to static classes in the framework. Their names are:
System.Windows.Forms.Application.ThreadException
and
AppDomain.CurrentDomain.UnhandledException
They both behave fairly similarly. If you add a handler to these events, they both will fire events when an unhandled exception is caught at their level. The Windows.Forms one catches exceptions that make their way into the message pump, or that get thrown within an Invoke/BeginInvoke. The AppDomain one catches the rest of the exceptions that manage to make their way all the way out of your code and into the mysterious ether of the VM, as I understand it.
So, all you really need are some simple handlers for these two events. You do, however, need to make sure that you can REMOVE your handlers after you've added them, since these events are process-wide.
The result might look something like this.
private static ThreadExceptionEventHandler ThreadExceptionHandler = new ThreadExceptionEventHandler(UnhandledExceptionCatcher);
private static UnhandledExceptionEventHandler DomainExceptionHandler = new UnhandledExceptionEventHandler(UnhandledDomainExceptionCatcher);
private static void UnhandledDomainExceptionCatcher(object sender, UnhandledExceptionEventArgs e) {
HandleException(e.ExceptionObject as Exception);
}
private static void UnhandledExceptionCatcher(object sender, ThreadExceptionEventArgs e) {
HandleException(e.Exception);
}
Nothing particularly magical going on here. The only other thing of note is that e.ExceptionObject is an object instead of an Exception for some strange reason (I assume because it might be marshalled across an AppDomain boundary, or something.) To put them to use, just do something like:
System.Windows.Forms.Application.ThreadException += ThreadExceptionHandler;
AppDomain.CurrentDomain.UnhandledException += DomainExceptionHandler;
So, now unhandled exceptions in your application are being automatically dispatched to a method called HandleException. Even better, as far as I know they're always dispatched in the same thread they were thrown in. So, now all you have to do is get this exception out of your app and into your inbox (or perhaps /dev/null). The process for this is remarkably simple! The key is System.Net.Mail, which provides a fairly painless way of creating and sending messages to an SMTP server. The resulting code might look something like this:
public static void HandleException(Exception exception) {
System.Net.Mail.SmtpClient client = new System.Net.Mail.SmtpClient("smtp.mymailserver.net", 25);
// Construct to and from mail addresses
System.Net.Mail.MailAddress addressFrom = new System.Net.Mail.MailAddress("somevalidaddress@domain.com", "Error Report");
System.Net.Mail.MailAddress addressTo = new System.Net.Mail.MailAddress("me@mymailserver.net", "Error Reports");
System.Net.Mail.MailMessage message = new System.Net.Mail.MailMessage(addressFrom, addressTo);
message.Subject = String.Format("Unhandled exception in {0}: {1}", exception.Source, exception.GetType().Name);
message.Body = exception.ToString();
try {
// Send the message inside a try/catch, because unhandled exceptions in an unhandled exception handler make things die spectacularly.
client.Send(message);
} catch (Exception exception) {
// You might want to do something here to notify the user that the automated report failed.
} finally {
// Clean up our message object!
message.Dispose();
}
}
The key things to note here are:
1. Unhandled exceptions in an unhandled exception handler are bad.
2. System.Net.Mail makes you use these really annoying MailAddress types instead of strings.
3. Automated reporting can fail, so you should consider what your application needs to do if it does.
In my particular case, I show UI to the user that allows them to enter some notes on what caused the crash and optionally cancel the automatic reporting. This way I don't send myself reports for bugs that occur while I'm testing or developing, and users don't feel like I'm spying on them with x-ray glasses. For a server-side application, UI is obviously a bad idea, so you'd probably just want to log any reporting failures to a file that can be checked by hand.
Also, once you've handled the exception, it's still going to bubble its way up to the top of the stack and result in a crash dialog or a terminated thread/process. You may want to do an Application.Exit or kill your process to make your application terminate without user intervention at this point, so that you don't end up with zombie processes lying around.
Some other good ideas I leave up to the reader include collecting system and application information by using the System.Environment, System.Diagnostics and System.Reflection namespaces and including that info in the report. You can also manually pull info out of the Exception instead of using ToString, which allows you to format your reports more carefully and make it possible to do automatic filtering of them using mail rules.
code