Saturday, October 30, 2010

Windows Forms .NET handling unhandled exceptions

This posting is a lead in to another post I am going to write about the ConditionalAttribute in the .NET framework. Since this posting has its own subject as well as setting the scene for the next posting I decided to keep them as 2 separate posts.

This code was written for a project some 3 years ago but is still relevant. The code was built based on a lot of investigations as well as online resources. Since it is some time ago the code was written I will not be able to mention all the sources, however the code was heavily inspired by 


Goals: Handle any unhandled exceptions in a windows forms application, log them to an arbitrary log location (Webservice, Sharepoint list or similar) as well as gracefully exit the windows application after encountering an unhandled exception.

First lets take a look at the Program.cs file that is containing the static Main() method.

using System;
using System.Windows.Forms;
using System.Threading;

namespace WinFormsUnhandledException
{
 static class Program
 {
  /// <summary>
  /// The main entry point for the application.
  /// </summary>
  [STAThread]
  static void Main()
  {
#if DEBUG
   // To be able to debug properly we do not want to steal the exceptions from VS IDE
   safeMain();
#else
   // In release mode we want to be exception safe
   try
   {
     safeMain();
   }
   catch (Exception exception)
   {
     handleUnhandledExceptions(exception);
   }
#endif
  }

  /// <summary>
  /// safeMain is a try catch all for any possible unhandled exceptions
  /// </summary>
  static void safeMain()
  {
#if !DEBUG 
   // In release mode we want to be exception safe
   // CLR unhandled exceptions handler
   AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
   // Windows forms exceptions handler
   Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);
#endif
   Application.EnableVisualStyles();
   Application.SetCompatibleTextRenderingDefault(false);
   Application.Run(new MainForm());
  }

  /// <summary>
  /// Handles any unhandled windows exceptions. The applicatin can continue running
  /// after this.
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
  {
   handleUnhandledExceptions(e.Exception);
  }

  /// <summary>
  /// Handles any unhandles CLR exceptions. Note that in .NET 2.0 unless you set
  /// <legacyUnhandledExceptionPolicy enabled="1"/> in the app.config file your
  /// application will exit no matter what you do here anyway. The purpose of this
  /// eventhandler is not forcing the application to continue but rather logging 
  /// the exception and enforcing a clean exit where all the finalizers are run 
  /// (releasing the resources they hold)
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
  {
   handleUnhandledExceptions(e.ExceptionObject);

   // Prepare cooperative async shutdown from another thread
   Thread t = new Thread
   (
    delegate()
    {
     Environment.Exit(1);
    }
   );
   t.Start();
   t.Join();
  }


  /// <summary>
  /// Reports the unhandled exceptions
  /// </summary>
  /// <param name="exception"></param>
  static void handleUnhandledExceptions(object exception)
  {
   Exception e = exception as Exception;
   if (e != null)
   {
    ExceptionHelper.ReportException(e, new SpsSoftwareBugReporter(), new ReporterConfiguration("http://MYSHAREPOINTSERVER/SITE1/SITE2/_vti_bin/Lists.asmx"), string.Empty, true);
   }
  }
 }
}

There are several relevant points in this way to start a windows forms application and the comments in the code explains pretty well what is going on. Please note how cluttered the compiler directives for DEBUG ... is. These are the reason for my upcoming post on the ConditionalAttribute.

Basically this code in the Program.cs allows us to do the following in the main form.




MainForm.cs

using System;
using System.Windows.Forms;
using System.Threading;

namespace WinFormsUnhandledException
{
 public partial class MainForm : Form
 {
  public MainForm()
  {
   InitializeComponent();
  }

  private void button1_Click(object sender, EventArgs e)
  {
   throw new ArgumentNullException("something");
  }

  private void button2_Click(object sender, EventArgs e)
  {
   Thread.CurrentThread.Abort();
  }
 }
}

The Thread.CurrentThread.Abort(); line was specifically put there to show that this specific exception is a little bit different than the rest. This will in the current version of the code posted here show the exception dialog more than 1 time and potentially quite a few.  I will not go deep into Thread.Abort() now, but in short - don't use it. There are many articles about this on the net but a good reference I like is

Aborting Threads


Clicking on the 2 buttons above will show the following forms.




How do we get this form to appear and where do we get the information from? This is all happening in the ExceptionHelper class.


using System;
using System.Security;
using System.Reflection;
using System.Text;
using System.Diagnostics;
using System.Windows.Forms;

namespace WinFormsUnhandledException
{
 public class ExceptionHelper
 {
  /// <summary>
  /// Reports an exception a central place. This would most likely
  /// find a use in Windows smart clients in the 
  /// AppDomain.CurrentDomain.UnhandledException handler and the
  /// Application.ThreadException handler
  /// </summary>
  /// <param name="exception">The exception</param>
  /// <param name="reporter">The logic that takes care of actually reporting the error to the place you want it</param>
  /// <param name="reporterConfiguration">Storage configuration for the reporter</param>
  /// <param name="userComments">User comments about the error</param>
  /// <param name="showForm">True - show error form and ask user to accept report.
  /// False - dont show form just report</param>
  public static void ReportException(Exception exception, IReporter<UnhandledExceptionReport> reporter, ReporterConfiguration reporterConfiguration, string userComments, bool showForm)
  {
   StringBuilder sb = new StringBuilder();
   Assembly a = Assembly.GetEntryAssembly();
   UnhandledExceptionReport unhandledExceptionReport = new UnhandledExceptionReport();
   unhandledExceptionReport.Application = a.FullName;

   AssemblyName[] references = a.GetReferencedAssemblies();
   foreach (AssemblyName reference in references)
   {
    sb.Append(reference.FullName);
    sb.Append(Environment.NewLine);
   }

   unhandledExceptionReport.Assemblies = sb.ToString();
   unhandledExceptionReport.Date = DateTime.Now.ToString();
   unhandledExceptionReport.Exceptions = exception.ToString();
   unhandledExceptionReport.FileAttachments = new string[] { GetSystemInfo() };
   unhandledExceptionReport.UserComments = userComments;

   if (showForm)
   {
    UnhandledExceptionReportForm form = new UnhandledExceptionReportForm(unhandledExceptionReport);
    if (form.ShowDialog() == DialogResult.OK)
    {
     reporter.Submit(unhandledExceptionReport);
    }
   }
   else
   {
    reporter.Submit(unhandledExceptionReport);
   }
  }

  
  /// <summary>
  /// Calls the MSINFO32.exe and generates a nfo file. The path of the 
  /// generated file is returned.
  /// </summary>
  /// <returns>Path of generated msinfo32 report file</returns>
  public static string GetSystemInfo()
  {
   try
   {
    // retrieve the path to MSINFO32.EXE
    Microsoft.Win32.RegistryKey key;
    key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Shared Tools\MSInfo");
    if (key != null)
    {
     string exeFile = key.GetValue("Path", "").ToString();
     key.Close();

     if (exeFile.Length != 0)
     {
      ProcessStartInfo processStartInfo = new ProcessStartInfo();
      processStartInfo.FileName = exeFile;
      processStartInfo.Arguments = "/nfo " + Environment.GetEnvironmentVariable("TEMP") + "\\sysinfo /categories +systemsummary-Resources-Components-SWEnv-InternetSettings-Apps11+SWEnvEnvVars+SWEnvRunningTasks+SWEnvServices";
      Process process = Process.Start(processStartInfo);
      process.WaitForExit();
     }
    }
    return Environment.GetEnvironmentVariable("TEMP") + "\\sysinfo.nfo";
   }
   catch (SecurityException)
   {
    // mostlikely due to not having the correct permissions
    // to access the registry, ignore this exception
    return null;
   }
  }
 }
}


This class gathers the information about the exception as well as a system information dump and shows the Unhandled Exception Form where the user can enter some information to explain what they did to provoke the exception.

I am using the following support classes
using System;

namespace WinFormsUnhandledException
{
 /// 
 /// 
 /// 
 public class UnhandledExceptionReport
 {
  public UnhandledExceptionReport()
  { 
   Title = GetType().Name;
  }
  public string Title {get; set;}
  public string Application {get; set;}
  public string Date {get; set;}
  public string UserComments {get; set;}
  public string Exceptions {get; set;}
  public string Assemblies {get; set;}
  public string[] FileAttachments { get; set; }
 }
}

Here is the code for the UnhandledExceptionReportForm

using System;
using System.Windows.Forms;
using System.Diagnostics;

namespace WinFormsUnhandledException
{
 public partial class UnhandledExceptionReportForm : Form
 {
  private UnhandledExceptionReport m_unhandledExceptionReport;

  public UnhandledExceptionReportForm()
  {
   InitializeComponent();
  }
  public UnhandledExceptionReportForm(UnhandledExceptionReport unhandledExceptionReport) : this()
  {
   this.m_unhandledExceptionReport = unhandledExceptionReport;
   if (m_unhandledExceptionReport != null && m_unhandledExceptionReport.UserComments == string.Empty)
   {
    m_unhandledExceptionReport.UserComments = "Please write a description of what you were doing when the error occured";
   }
   
   titleTextBox.Text = m_unhandledExceptionReport.Title;
   applicationTextBox.Text = m_unhandledExceptionReport.Application;
   dateTextBox.Text = m_unhandledExceptionReport.Date;
   commentsTextBox.DataBindings.Add("Text", m_unhandledExceptionReport, "UserComments");
   exceptionsTextBox.Text = m_unhandledExceptionReport.Exceptions;
   assembliesTextBox.Text = m_unhandledExceptionReport.Assemblies;
   attachmentLinkLabel.Text = m_unhandledExceptionReport.FileAttachments[0];
  }

  private void reportButton_Click(object sender, EventArgs e)
  {
   Hide();
   DialogResult = DialogResult.OK;
  }

  private void cancelButton_Click(object sender, EventArgs e)
  {
   Hide();
   DialogResult = DialogResult.Cancel;
  }

  private void attachmentLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
  {
   Process.Start(m_unhandledExceptionReport.FileAttachments[0]);
  }

 }
}

Finally we have the interface for the class doing the actual error reporting

using System;

namespace WinFormsUnhandledException
{
 
 public interface IReporter<T>
 {
  /// 
  /// Reports some information to a specific location
  /// 
  /// The type to be reported on  
  /// The configuration the reporter need  
  void Submit(T report, ReporterConfiguration config);
 }
}

and here is a IReported implementation that is reporting unhandled exceptions to a Sharepoint list.

using System;
using System.Text;
using System.Reflection;
using System.Net;
using System.Xml;
using System.IO;
using System.Xml.XPath;

using System.Security;

namespace WinFormsUnhandledException
{
 public class SpsSoftwareBugReporter : IReporter<UnhandledExceptionReport>
 {
  #region IReporter<SoftwareBug> Members

  void IReporter<UnhandledExceptionReport>.Submit(UnhandledExceptionReport report, ReporterConfiguration config)
  {
   ListsService.Lists service = new ListsService.Lists();
   service.Url = config.ConnectionString;
   service.Credentials = CredentialCache.DefaultCredentials;

   XmlNode list = service.GetList("Bugs");
   string listName = list.Attributes["ID"].Value;

   StringBuilder sb = new StringBuilder();
   sb.Append("<Method ID=\"1\" Cmd=\"New\">");
   sb.Append("<Field Name=\"ID\">New</Field>");
   PropertyInfo[] properties = report.GetType().GetProperties();

   foreach (PropertyInfo propertyInfo in properties)
   {
    if (propertyInfo.Name != "FileAttachments")
    {
     sb.Append("<Field Name=\"");
     sb.Append(propertyInfo.Name);
     sb.Append("\">");
     sb.Append(SecurityElement.Escape(propertyInfo.GetValue(report, null).ToString()));
     sb.Append("</Field>");
    }
   }
   sb.Append("</Method>");

   XmlDocument doc = new XmlDocument();
   XmlElement newBugs = doc.CreateElement("Batch");
   newBugs.SetAttribute("OnError", "Continue");
   newBugs.SetAttribute("ListVersion", list.Attributes["Version"].Value);
   
   
   
   newBugs.InnerXml =  sb.ToString();
   // Add bug report
   XmlNode result = service.UpdateListItems(listName, newBugs);

   // Add item attachments
   XPathNavigator navigator = result.CreateNavigator();
   XmlNamespaceManager manager = new XmlNamespaceManager(navigator.NameTable);
   manager.AddNamespace("def", "http://schemas.microsoft.com/sharepoint/soap/");
   manager.AddNamespace("z", "#RowsetSchema");

   XPathNavigator node = navigator.SelectSingleNode("//def:ErrorCode", manager);
   if (node.InnerXml == "0x00000000")
   {
    XmlNodeList nodes = result.SelectNodes("//z:row", manager);
    
    foreach (string  attachment in report.FileAttachments)
    {
     if (attachment != null || attachment != string.Empty)
     {
      using (FileStream stream = File.OpenRead(attachment))
      {
       try
       {
        service.AddAttachment(listName, nodes[0].Attributes["ows_ID"].Value, Path.GetFileName(attachment), StreamHelper.ReadFully(stream));
       }
       catch (Exception exception)
       {
        Console.WriteLine(exception.ToString());
       }
      }
     }
    }
   }
  }
  #endregion
 }
}

1 comment:

Anonymous said...

Kenneth sweet job, could you attach the demo project