From our travels we have learned that in .NET 2.0, unlike in .NET 1.1, any running
application that throws an unhandled exception will immediately cause the app
to exit and the appDomain to unload. Even with the best - designed code and the
very best testing and exception handling, it's still possible for a running
app to hit some unknown condition that you haven't been able to anticipate
during development, and proceed to blow up on you.
If this is an application that you need to have running, then you need some reliable
way to get it back up again. For Windows Services, we have settings in the SCM
that can handle this- we get three chances to set the application to restart
itself if it fails. We can do this in the "Recovery" tab in the service's
property sheet. But what do you do if it's not a service?
The key here is to understand two events that you can catch when an application is
about to go into the "Black Hole" -- the Application.ThreadException event, and the AppDomain.CurrentDomain.UnhandledException event. Note that these are events, not "exception handlers". What that
means is that when one of these fires, your app is already headed into the black
hole and there isn't anything you can do about it. They are there in order
to provide the developer a last opportunity to do logging and any cleanup before
the AppDomain goes into Never-never Land.
Here's some sample code that illustrates how you can wire this up in your apps:
namespace UnhandledExceptionRestart
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// Adds the event handler to catch any exceptions that happen in
//the main UI thread.
Application.ThreadException += new ThreadExceptionEventHandler(OnThreadException);
// add the event handler for all threads in the appdomain
// except for the main UI thread
AppDomain.CurrentDomain.UnhandledException +=
new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
Application.EnableVisualStyles();
Application.Run(new Form1());
}
// Handles the exception event for all other threads
static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Logger.WriteToLog(e.ExceptionObject.ToString());
Assembly asm = Assembly.GetExecutingAssembly();
string asmName = asm.GetName().Name + ".exe";
Process.Start(System.Environment.CurrentDirectory + @"\" + asmName);
}
// Handles the exception event from a UI thread.
static void OnThreadException(object sender, ThreadExceptionEventArgs t)
{
Logger.WriteToLog(t.Exception.GetBaseException().ToString());
Assembly asm = Assembly.GetExecutingAssembly();
string asmName = asm.GetName().Name + ".exe";
Process.Start(System.Environment.CurrentDirectory + @"\" + asmName);
}
}
}
What happens here is that when there is either an AppDomain unhandled exception (any
thread except the main UI thread) or an Application ThreadException ( on the
main UI thread) our events get fired. We do some logging, and then we use the
Process class to restart a new instance of the application immediately. When
the closing brace of either method is reached, that's the end for this instance,
nothing else can be done.
My little sample Logger class just writes with a Filestream for demo purposes only:
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace UnhandledExceptionRestart
{
public class Logger
{
private Logger() { }
public static void WriteToLog( string message)
{
using (FileStream fs = new FileStream(System.Environment.CurrentDirectory + @"\log.txt",
FileMode.Append, FileAccess.Write))
{
message = DateTime.Now.ToString() + "\r\n" + message + "\r\n==========================================================\r\n";
byte[] b = System.Text.Encoding.UTF8.GetBytes(message);
fs.Write(b, 0, b.Length);
fs.Close();
}
}
}
}
In the sample Visual Studio 2005 solution that you can download below, I have a button
on the Form that triggers the KABOOM method which simply attempts a division
by zero:
private void button1_Click(object sender, EventArgs e)
{
// throw exception directly from event handler
//GoBoom();
// throw exception from secondary thread
Thread t = new Thread(GoBoom);
t.Start();
}
void GoBoom()
{
int a = 0;
int b = 1;
int c = b / a;
}
IMPORTANT:
For an application that includes managed code, the common language runtime will present
a dialog to JIT-attach a debugger. We don't want this in our unattended
scenario as it will prevent us from being able to spin up a new process. The
registry key that controls this option is called:
HKEY_LOCAL_MACHINE\Software\Microsoft\.NETFramework\DbgJITDebugLaunchSetting.
If value = 0, prompt the user by means of a message box. The choices are:
Continue. This results in a stack dump and process termination.
Attach a debugger. In this case, the runtime spawns the debugger listed in the DbgManagedDebugger
registry key. If none, control is returned, and the process is terminated.
If value = 1, simply return control. This results in a stack dump, after which the
process is terminated.
(No more dialog)
If value = 2, spawn the debugger listed in the DbgManagedDebugger registry key.
Option 2: If you want to disable the JIT debug dialog, but still want an error dialog:
Visual Studio.NET|Tools|Options|Debugging|Just-In-Time and deselect "Common
Language Runtime"
and now you’ll get an OK/Cancel dialog instead of the select a Debugger Dialog.
Note: The registry entry from Option 1 above will need to be 0 for the dialog to
show up. Here, we need to set this value to "1"- so no more dialog!
What will happen now is that we get our logging, the new app instance is spun
up, and the old quits with no dialog.
There is one other little item I've added to the sample app. If you look in the
assemblyinfo.cs file, you'll see this:
[assembly:
AssemblyTitle("UnhandledExceptionRestart")]
#if(DEBUG)
[assembly: AssemblyDescription("[DEBUG]")]
#else
[assembly: AssemblyDescription("[RELEASE]")]
#endif
I've started to do this with all my builds. Now, you can just right click on
the assembly in Windows Explorer and look at the summary, and you'll see
either "RELEASE" or "DEBUG". You can also retrieve these
properties programmatically through reflection, and have your app be able to
"self report" on itself and all the assemblies it uses. If you want
to get really creative, you could even add this information to the assemblyinfo
VisualStudio project template file, and have it be there automatically for all
your new assemblies.
As Top Ramen would say, "Now, that's using your noodle!". If your app blows up, wouldn't it be nice to at least get an idea of where and
what happened? That's what this will do - with a nice option to restart the
app.
Download the Solution zip File