Dealing With Exceptions in Plugins |
As a general rule if an exception is left unhandled by a plugin then it will eventually be displayed to the user and logged by the unhandled exception handler. In some cases where the plugin is guaranteed to be the only consumer of an event this is acceptable, but in most cases letting an exception go unhandled could cause problems for the other objects that are listening to the event. They may not see the event.
There are a few ways of dealing with this problem, which are outlined here.
Many event handlers in Virtual Radar Server are called on background threads. Those threads could be aborted, particular during the disposal of the objects that are raising the events. When a thread aborts it always raises a ThreadAbortException. Generally it is alright to catch this exception - the framework will throw another when your catch block quits so that the shutdown of the thread continues - but they are a routine occurrence so they are not worth reporting or logging.
The code examples on the page will just show a catch of every exception but if you find that you are reporting or logging ThreadAbort exceptions then you may want to add an empty catch block for them so that they're excluded.
// This is the normal catch-all pattern: try { // Do some work here... } catch(Exception ex) { // Deal with the exception here rather than let it bubble up to the unhandled exception handler } // And this is the same pattern but with ThreadAbort exceptions ignored. You cannot swallow ThreadAborts, // another will be thrown automatically once you are out of the catch block: try { // Do some work here... } catch(ThreadAbort) { } catch(Exception ex) { // Deal with the exception here rather than let it bubble up to the unhandled exception handler }
If you just want to catch the exception and then pass it on to the default exception handler, which will display it to the user and log it, then you can implement IBackgroundThreadExceptionCatcher.
public class MyPlugin : IPlugin, IBackgroundThreadExceptionCatcher { public event EventHandler<EventArgs<Exception>> ExceptionCaught; protected virtual void OnExceptionCaught(EventArgs<Exception> args) { if(ExceptionCaught != null) ExceptionCaught(this, args); } private void BaseStationMessageRelay_MessageReceived(object sender, BaseStationMessageEventArgs args) { try { // Do your processing here... } catch(Exception ex) { OnExceptionCaught(new EventArgs<Exception>(ex)); } } }
Your plugin doesn't have to do anything else. Before the splash screen calls your Startup method it will hook the ExceptionCaught event and the handler will take your exception and move it up to the GUI thread to be displayed to the user and logged.
Alternatively you might want to just log the exception and not display it to the user. You can do this by obtaining a reference to the ILog singleton.
private void BaseStationMessageRelay_MessageReceived(object sender, BaseStationMessageEventArgs args) { try { // Do your processing here... } catch(Exception ex) { var log = Factory.Singleton.Resolve<ILog>().Singleton; log.WriteLine("Exception caught by my plugin: {0}", ex.ToString()); } }
Regardless of whether you show the exception to the user or just log it it can also be useful to display information about the exception on your plugin's status so that if the user is looking at the plugin screen they'll be informed that something is going wrong. All plugins must implement two properties, Status and StatusDescription, and they must implement an event called StatusChanged that gets raised when either of those two properties is changed. The plugins screen displays these properties and hooks the event so that it knows when to update the display.
public class MyPlugin : IPlugin { public string Status { get; private set; } public string StatusDescription { get; private set; } public event EventHandler StatusChanged; protected virtual void OnStatusChanged(EventArgs args) { if(StatusChanged != null) StatusChanged(this, args); } private void BaseStationMessageRelay_MessageReceived(object sender, BaseStationMessageEventArgs args) { try { // Do your processing here... } catch(Exception ex) { // ... StatusDescription = String.Format("Exception caught: {0}", ex.Message); OnStatusChanged(EventArgs.Empty); } } }
Some events, such as the MessageReceived event from an IBaseStationListener, can be raised hundreds of times a second. If exceptions could potentially be thrown on each of those events then you can end up spamming the user interface or filling the log with exception messages.
There are a couple of approaches to solving this problem. The first is that when an exception takes place you stop processing future messages, usually by having the plugin clearing a configurable 'Enabled' option that the user can go in and switch back on.
Another approach would be to limit the rate at which you display exceptions. You always display the first one and then if any further exceptions are caught within the next minute or so you throw them away. Once the timeout has elapsed you display or log the next exception raised. This has the advantage of not swamping the user with exceptions but does mean that information carried by the exceptions that get swallowed is lost, which may make it harder to diagnose the cause of the exceptions on a user's site.
public class MyPlugin : IPlugin { private DateTime _LastExceptionShown; private void BaseStationMessageRelay_MessageReceived(object sender, BaseStationMessageEventArgs args) { try { // Do your processing here... } catch(Exception ex) { // ... var now = DateTime.UtcNow; if(_LastExceptionShown.AddMinutes(1) <= now) { _LastExceptionShown = now; // Do something with the exception here } } } }
In the example above we don't have to worry about multi-threading issues because that event handler, while it is called on a background thread, is called serially. You get called with the first message, you return, you get the next message, you return and so on. However if you have another event handler attached to some other object that raises events on another thread (e.g. an IWebServer) and both event handlers are using the same variable to control when they swallow exceptions then you will need a little bit of extra code so that you don't end up with two exceptions being shown.
public class MyPlugin : IPlugin { private DateTime _LastExceptionShown; private object _LastExceptionShownLock = new object(); private void BaseStationMessageRelay_MessageReceived(object sender, BaseStationMessageEventArgs args) { try { // Do your processing here... } catch(Exception ex) { // ... var now = DateTime.UtcNow; bool processException = false; lock(_LastExceptionShownLock) { processException = _LastExceptionShown.AddMinutes(1) <= now; if(processException) _LastExceptionShown = now; } if(processException) { // Do something with the exception here } } } }