Saturday, December 20, 2008

Cleanup that Console

In the middle of investigating the new Rhino Service Bus (more on this on later posts), I was trying to create a console application that’d act like a Console Application Server. Using a console app as a development application service is very common, because running it is a breeze and you can get the log output directly on your screen. But what I didn’t know was that you can actually close the console application by pressing CTRL + C or CTRL + Break without properly closing the application and this sometime will results to very bad things happening like your port being left open and you won’t be able to re-run the application because the port is not properly closed. To my surprise you can not use any managed code to listen to those events!

My initial console startup code was like this :

public class Program
{
private static readonly ILog logger = LogManager.GetLogger(typeof(DefaultHost));

public static void Main(string[] args)
{
var host = new DefaultHost();
host.Start(Assembly.GetExecutingAssembly().FullName);

logger.Debug("Press CTRL+C to exit...");
bool shouldexit = false;
while(!shouldexit)
{
ConsoleKeyInfo key = Console.ReadKey(true);
if (key.Modifiers == ConsoleModifiers.Control && key.Key == ConsoleKey.C)
{
logger.Debug("Closing the Service Bus");
host.Close();

logger.Debug("Exiting the application");
shouldexit = true;
}
}
}
}
…and surprisingly application closed before I could run the code in the if block! Time to do a little unmanaged magic. There is existing functionality in Kernel32.dll to listen to console events but those are not exposed as .NET Console events. So first, let’s import that functionality into our managed world :
public enum ConsoleEventTypes
{
CtrlC = 0,
Break = 1,
Close = 2,
Logoff = 5,
Shutdown = 6
}

public delegate void ConsoleEventHandler(ConsoleEventTypes consoleEvent);

public class ConsoleController
{
public event ConsoleEventHandler ConsoleEvent;

public ConsoleController()
{
SetConsoleCtrlHandler(Handler, true);
}

private void Handler(ConsoleEventTypes consoleEvent)
{
if (ConsoleEvent != null)
ConsoleEvent(consoleEvent);
}

[DllImport("kernel32.dll")]
private static extern bool SetConsoleCtrlHandler(ConsoleEventHandler e, bool add);
}
With this, the startup code looks like this :
public class Program
{
private static readonly ILog logger = LogManager.GetLogger(typeof(DefaultHost));
private static DefaultHost host;
private static ConsoleController console;

public static void Main(string[] args)
{
console = new ConsoleController();
console.ConsoleEvent += ConsoleEventRaised;

host = new DefaultHost();
host.Start(Assembly.GetExecutingAssembly().FullName);

logger.Debug("Press CTRL+C to exit...");
while (true)
{
Console.ReadKey(true);
}
}

private static void ConsoleEventRaised(ConsoleEventTypes consoleEvent)
{
if(consoleEvent == ConsoleEventTypes.CtrlC || consoleEvent == ConsoleEventTypes.Break)
{
logger.Debug("Closing the service bus");
host.Close();

logger.Debug("Exiting the application now");
Environment.Exit(-1);
}
}
}
There actual attached code differs somehow. You need to keep the reference of ConsoleEventHandler in case of a garbage collection so the code has a couple of line of code to property handle disposing of the ConsoleController. You can download the source code here. Hope it helps.
Submit this story to DotNetKicks Shout it

1 comment:

Joshka said...

http://msdn.microsoft.com/en-us/library/system.console.cancelkeypress.aspx