A Singleton Application with Interprocess Communication in C#

Updated on 2020-05-01

Creating an application that can run once, but then accept command line args from subsequent runs

Introduction

Sometimes, you might have an application where it doesn't make sense to launch the main application twice, but you may want to run the app again passing additional command line parameters. This way, you can run the app, it will open your program, and then you can run the app again, and pass additional command line information to the already running app. This is useful, for example, when you want to get the existing app to open a new file from the command line. So it bakes out like this:

C:\Foo> MyApp myfile1.txt
C:\Foo> MyApp myfile2.txt myfile3.txt

In this case, MyApp only launches the actual application the first time. The second time, it detects that it's already running, and simply sends the passed in command line to the already running app for processing.

Conceptualizing this Mess

We've got two major aspects to this application in order to provide this functionality.

First, the app needs to detect if it's already running, and it will do different things depending on whether it is already running.

Second, we need a way for two processes to communicate. In this case, the primary app will wait on command line data coming from subsequence launches.

The first aspect is pretty easy. We use a named mutex in order to prevent the main app code from launching twice. Named mutexes can be seen by all running processes for this user.

The second aspect is a little more difficult, but not by much, thankfully. We will be using .NET's named pipes feature to facilitate communication between the main app (the server) and subsequent launches (the clients). A named pipe can also be seen by all running processes for this user.

Coding this Mess

The code we're using is presented here, almost in its entirety. I've only eliminated the using directives here to save some space.

class Program
{
    static string _AppName=
           Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly().GetName().Name);
    static void Main(string[] args)
    {
        // we need to get our app name so that
        // we can create unique names for our mutex and our pipe

        var notAlreadyRunning = true;
        // wrap the meat of the application code with a named mutex so it runs only once
        using (var mutex = new Mutex(true, _AppName + "Singleton", out notAlreadyRunning))
        {
            if (notAlreadyRunning)
            {
                // do additional work here, startup stuff
                Console.WriteLine("Running. Press any key to exit...");
                // ...
                // now process our initial main command line
                _ProcessCommandLine(args);
                // start the IPC sink.
                var srv = new NamedPipeServerStream(_AppName+"IPC",
                PipeDirection.InOut,1,PipeTransmissionMode.Message,PipeOptions.Asynchronous);
                // it's easier to use the AsyncCallback than it is to use Tasks here:
                // this can't block, so some form of async is a must

                srv.BeginWaitForConnection(new AsyncCallback(_ConnectionHandler), srv);

                // block here until exit
                Console.ReadKey();
                // if this was a windows forms app you would put your
                // "Applicantion.Run(new MyForm());" here
                // finally, run any teardown code and exit

                srv.Close();
            }
            else // another instance is running
            {
                // connect to the main app
                var cli = new NamedPipeClientStream(".",_AppName + "IPC",PipeDirection.InOut);
                cli.Connect();
                var bf = new BinaryFormatter();
                // serialize and send the command line
                bf.Serialize(cli, args);
                cli.Close();
                // and exit
            }
        }
    }
    static void _ConnectionHandler(IAsyncResult result)
    {
        var srv = result.AsyncState as NamedPipeServerStream;
        srv.EndWaitForConnection(result);
        // we're connected, now deserialize the incoming command line
        var bf = new BinaryFormatter();
        var inargs = bf.Deserialize(srv) as string[];

        // process incoming command line
        _ProcessCommandLine(inargs);
        srv.Close();
        srv = new NamedPipeServerStream(_AppName + "IPC", PipeDirection.InOut,
              1, PipeTransmissionMode.Message, PipeOptions.Asynchronous);

        srv.BeginWaitForConnection(new AsyncCallback(_ConnectionHandler), srv);
    }
    static void _ProcessCommandLine(string[] args)
    {
        // we received some command line
        // arguments.
        // do actual work here
        Console.Write("Command line received: ");
        for(var i = 0;i<args.Length;++i)
        {
            Console.Write(args[i]);
            Console.Write(" ");
        }
        Console.WriteLine();
    }
}

We've got a lot going on here.

Let's start at the top, getting the application name we'll be using to identify our app's mutex and named pipe.

Next. we create a named mutex, and wrap our main application code inside this block. This prevents the app from launching its main portion twice.

If it's not already running, the first thing we do is run any app initialization code and then process any command line arguments. We then start the named-pipe based IPC server which then registers a callback for whenever we get a connection from a subsequent instance of the app. The callback we've registered reads incoming command line data and processes the sent arguments as they come in. Finally, we block until exit, in this case using Console.ReadKey(), but realistically, you'll want to create your own exit condition. For a windows form app, you'd block by calling Application.Run() with your main application form, instead of Console.ReadKey().

Now, if it is already running, all we do is open the named pipe the main application instance created before, and serialize our command line data, sending it down the pipe, and then we shut down our pipe connection and exit.

In _ConnectionHandler() we handle each incoming connection, reading any command line args from the pipe, and then we actually restart the named pipe server because for some reason, it likes to close itself once you've ended a connection. I haven't determined if this is by design, or due to some sort of problem, but this workaround seems to be fine.

In the _ProcessCommandLine() args, we do our argument processing. All we do in the demo is spit it to the console, but realistically you're going to want to do something more substantial, like open a file based on the arguments.

History

  • 1st May, 2020 - Initial submission