The Last Console Driver I Will Ever Write

Nearly every application I write has a project called ConsoleDriver in it. This project is almost always the same as it is just a main routine that create the class that starts my program, wraps it in an exception block, usually configures logging, and waits until I hit enter or CTRL-C to signal the application to exit.

Well, after the writing this thousands of times, or copy and pasting it probably even more times, I have had enough. I declare an end to this driver insanity! I will actually take the time and make a single driver application that will take this task on for me whenever I need a console driver.

Given the absolute trivial nature of this functionality, I don’t want to do any real work in writing this. Lastly, I want to share this code with world, with the hopeful intent that others will help improve upon it (like add a WinForms, WPF, Silverlight, and maybe even Web based drivers). Over time I may just do those myself as a learning experience, but since I rarely need them, it may take a while.

So lets get to it. A very simple back of the napkin analysis revealed that I would need three assemblies.

  1. ConsoleDriver.exe – this is the actual executable that is started by the user.
  2. RunnableApplication.dll – this is the shared assembly defining the common interface loaded by the ConsoleDriver and exposed by the actual program.
  3. CustomApplicationAssembly – this is the assembly containing the actual program and exposing a class implementing the interface used by ConsoleDriver.

I decided to leverage MEF as the means of dynamically composing my application at run time. It seemed like a natural usage model, and it gave me an excuse to actually use for something other than another contrived example of MEF usage.

So, without further delay, here is some code.

using System;

namespace RunnableApplication
{
  public interface IRunnableApplication
  {
    /// <summary>
    /// Gets the number of ms to sleep between pulses.
    /// </summary>
    /// <value>The pulse time in ms.</value>
    int PulseMs { get; }

    /// <summary>
    /// Gets a value indicating whether [should quit].
    /// </summary>
    /// <value><c>true</c> if [should quit]; otherwise, <c>false</c>.</value>
    bool ShouldQuit { get; }

    /// <summary>
    /// Gets the delegate to execute after each pulse.
    /// </summary>
    /// <value>The on pulse delegate or null.</value>
    Func<bool> OnPulse { get; }

    /// <summary>
    /// Starts this instance.
    /// </summary>
    void Start();

    /// <summary>
    /// Stops this instance.
    /// </summary>
    void Stop();
  }
}

The IRunnableApplication interface is the glue used to connect the driver with your code. Right now it is very simple and represents the few properties and methods that seem to be very common in my test programs. Lets examine each of the elements.

PulseMs

This represents the number of milliseconds the launching thread will sleep before checking to see if it will quit. I usually use a number between 100 and 250 ms here, and it gives me the ability to throttle how often I call the OnPulse delegate.

ShouldQuit

This is checked after every pulse delay to see if the application’s main thread should exit.

OnPulse

This delegate can be null or can be a Func delegate that will execute after every pulse. The return value determines if the main thread should exit. If the value returned is true, it is the same as ShouldQuit being set to true.

Start

This is the method that is called in order to start the application run. This method must return, so this method must return after asynchronously starting the application.

Stop

This is the method that is called in order to stop the application run.

Since I use a set of defaults for the properties, I created the RunnableApplicationBase abstract base class derived from IRunnableApplication to set those defaults. Just derive from this class, change any properties in your constructor, and implement Start and Stop.

using System;

namespace RunnableApplication
{
  public abstract class RunnableApplicationBase : IRunnableApplication
  {
    #region Constants
    private const int DEFAULT_PULSE_TIME = 250;
    #endregion

    #region Constructors
    /// <summary>
    /// Initializes a new instance of the RunnableApplicationBase class.
    /// </summary>
    public RunnableApplicationBase()
    {
      PulseMs = DEFAULT_PULSE_TIME;
      ShouldQuit = false;
      OnPulse = () => { return false; };
    }
    #endregion

    #region IRunnableApplication Members
    public int PulseMs
    {
      get;
      protected set;
    }
    public bool ShouldQuit
    {
      get;
      protected set;
    }
    public Func<bool> OnPulse
    {
      get;
      protected set;
    }
    public abstract void Start();
    public abstract void Stop();
    #endregion
  }
}

Now lets take a look at the console driver.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ConsoleDriver
{
  class Program
  {
    private static bool _quit = false;

    static void Main(string[] args)
    {
      _quit = false;
      Console.TreatControlCAsInput = false;
      Console.CancelKeyPress += Console_CancelKeyPress;
      try
      {
        using (RunnableProgram run = new RunnableProgram())
        {
          if (run.TheApp == null)
            throw new InvalidOperationException("Cannot start without runnable application.");

          run.TheApp.Start();

          int pulse = run.TheApp.PulseMs;
          Func<bool> onPulse = run.TheApp.OnPulse ?? (() => { return false; });

          try
          {
            while (!_quit)
            {
              Thread.Sleep(pulse);
              if (onPulse() || run.TheApp.ShouldQuit)
                break;
            }
          }
          finally
          {
            run.TheApp.Stop();
          }
        }
      }
      catch (Exception ex)
      {
        Console.WriteLine("Global Exception: {0}", ex.Message);
      }
      finally
      {
        Console.CancelKeyPress -= Console_CancelKeyPress;
      }
    }

    #region CTRL-C Handler
    static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
    {
      _quit = true;
      e.Cancel = true;
    }
    #endregion
  }
}

Most of this is the boiler plate code I written and rewritten way too many times. In almost all cases, the real changes would be use the application class directly instead of the RunnableProgram class. You will also notice that I use the CTRL-C handler instead of waiting for key press. I have found that this works best for me, it is less error prone than key handling, and allows my applications to actually capture keys if needed.

By now, it should be obvious that the magic here is in the RunnableProgram class, so here it is.

using System;
using System.ComponentModel.Composition;
using RunnableApplication;
using System.ComponentModel.Composition.Hosting;

namespace ConsoleDriver
{
  class RunnableProgram : IDisposable
  {
    #region Fields
    private CompositionContainer _container;
    #endregion

    [Import]
    public IRunnableApplication TheApp { get; set; }

    #region Constructors
    public RunnableProgram()
    {
      Compose();
    }
    ~RunnableProgram()
    {
      Dispose(false);
    }
    #endregion

    #region IDisposable Members
    public void Dispose()
    {
      Dispose(true);
    }
    #endregion

    public void Dispose(bool disposing)
    {
      if (disposing && (_container != null))
        _container.Dispose();
      _container = null;
    }

    private void Compose()
    {
      var catalog = new AggregateCatalog();

      // TODO:  first we try and get a specific assembly
      //string assemblyName = ConfigurationManager.AppSettings.Get("sourceAssembly");
      //if (!string.IsNullOrEmpty(assemblyName))
      //  catalog.Catalogs.Add(new AssemblyCatalog(Assembly.LoadFrom(assemblyName)));

      // TODO:  then we try and get all assemblies specific path
      //string pathName = ConfigurationManager.AppSettings.Get("sourcePath");
      //if(!string.IsNullOrEmpty(pathName))
      //  catalog.Catalogs.Add(new DirectoryCatalog(pathName));
      
      //  then we check all DLL's in the running directory
      catalog.Catalogs.Add(new DirectoryCatalog("."));

      _container = new CompositionContainer(catalog);
      CompositionBatch batch = new CompositionBatch();
      batch.AddPart(this);

      _container.Compose(batch);
    }
  }
}

Now we can see the value of MEF. You will notice that on line 7 is an Import annotation for IRunnableApplication property. This is how MEF knows what to look for and where to put it. Starting on line 35 is the Compose method. This is the MEF composition magic that glues up the application. In the current version, I am only looking for assemblies in the current directory, and then telling MEF about the RunnableProgram class (line 54), and on line 56 I tell MEF to glue it all together.

There are some commented TODO blocks for areas I do plan on working on when I get a chance. These will allow me to specify a particular assembly for MEF to use for the application, and the other comment block will tell MEF to look into a subdirectory for any assemblies.

MEF is smart enough that if it encounters more than one class exporting IRunnableApplication, it will throw an exception, since my import only expects one. A possible future option might be to let the user select from one of a number of runnable applications, but I will leave that for another day.

After all this, what does the actual runnable application look like? Well, it is pretty simple. Here it is.

using System;
using System.ComponentModel.Composition;
using RunnableApplication;

namespace TestAppOne
{
  [Export(typeof(IRunnableApplication))]
  public class SampleApplication : RunnableApplicationBase
  {
    #region RunnableApplicationBase Members
    public override void Start()
    {
      Console.WriteLine("Starting Application");
    }
    public override void Stop()
    {
      Console.WriteLine("Stopping Application");
    }
    #endregion
  }
}

There we are. All of the source code for what will hopefully be the very last console driver application I ever write. I do plan to add a few more features, such as support for logging. I left it out of this version as I have not yet identified how I want to MEF-ify it. This little application is one of my first uses of MEF in something of real world use, and that means actually thinking about how and why to use MEF as opposed to just using it everywhere. Given that this took less than hour to write, was instantly useful, and will help me reduce redundant code – I have reached my happy place with it. Score one for MEF.

As with all of my sample and open source code, it is available on my GitHub site. This project is under the name. hazware.driver.

Related

comments powered by Disqus