Interprocess Communication With Memory-Mapped Files, Part 2

In the previous post, I provided a glimpse into how to use a memory-mapped file to establish a primitive interprocess communication system, responsible for passing along the process ID from one process, and a "shutdown gracefully" message from another.

To recap, we have two separate applications:

  • ConsoleAppListener
    Responsible for performing work and shutting down gracefully when it receives a “shutdown” message, after it has finished processing a full unit of work.  Multiple instances can exist, but each has a different “unique name” and a separate ConsoleAppController responsible for starting and monitoring each instance.
  • ConsoleAppController
    Responsible for starting ConsoleAppListener, ensuring it stays running, and sending a “shutdown” message instructing the ConsoleAppListener to shutdown gracefully when it’s finished processing.  Multiple instances can exist, but each is responsible for monitoring a ConsoleAppListener with a different “unique name”.
It can be difficult to explain in words what code can sometimes help clear up, so below I am posting the Program.cs files for each of the two console applications.  Note that the path to the .exe of the Listener app is hard-coded as a constant in the Controller app.
ConsoleAppListener's Program.cs:

 

// Code from www.jaypm.com
// Feel free to use, but please give credit where it's due.

using System;
using System.Diagnostics;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Threading;

namespace ConsoleAppListener
{
    /// <summary>
    /// Responsible for performing a unit of work ("processing") at a time,
    /// checking for a message to shutdown between processing.
    /// </summary>
    class Program
    {
        private const int FileSize = 4;  // Maximum number of bytes needed to hold an int32
        private const int CloseMessage = -1;

        private static string _instanceName;
        private static MemoryMappedFile _memoryMappedFile;

        static void Main(string[] args)
        {
            // Get the unique name of this application from the first commandline argument:
            _instanceName = args.Length > 0 ? args[0] : string.Empty;

            if (string.IsNullOrEmpty(_instanceName))
            {
                Console.WriteLine("Error: Expected a unique name for the application from the command line.");
                Console.Read();
                return;
            }

            _memoryMappedFile = MemoryMappedFile.CreateOrOpen(_instanceName, FileSize);

            using (var accessor = _memoryMappedFile.CreateViewAccessor(0, FileSize))
            {
                var processId = Process.GetCurrentProcess().Id;
                accessor.Write(0, processId);
            }

            while (!ShouldClose())
            {
                Console.WriteLine("Close message not received, beginning to process");

                // Perform a unit of work:
                PerformProcessing();
            }

            Console.WriteLine("Close message received. Hit Enter to close");
            Console.Read();
        }

        private static bool ShouldClose()
        {
            using (var stream = _memoryMappedFile.CreateViewStream())
            {
                var reader = new BinaryReader(stream);
                var message = reader.ReadInt32();
                if (message == CloseMessage)
                {
                    Console.WriteLine("Received shutdown message");
                    return true;
                }
            }

            return false;
        }

        static void PerformProcessing()
        {
            Console.WriteLine("Performing processing");

            // Begin processing a unit of work

            // In this case, just sleep. In real world, processing code goes here:
            Thread.Sleep(6000);  // 6 sec
        }
    }
}
ConsoleAppController's Program.cs:
// Code from www.jaypm.com
// Feel free to use, but please give credit where it's due.

using System;
using System.Diagnostics;
using System.IO;
using System.IO.MemoryMappedFiles;

namespace ConsoleAppController
{
    /// <summary>
    /// Responsible for launching a ConsoleAppListener process with a specific instance name and monitor it,
    /// if one isn't already running. If a ConsoleAppListener is already running, it will begin monitoring.
    /// If the ConsoleAppListener shuts down, this will restart it.
    /// If the user presses any key, the ConsoleAppListener is instructed to shut down gracefully.
    /// </summary>
    class Program
    {
        private const int MessageByteSize = 4;  // Number of bytes needed to store an int32
        private const int CloseMessage = -1;
        private const string ClientAppInstanceName = "SampleApp";
        private const string ClientProcessFileName = @"C:\Code\Personal\InterprocessCommunication\ConsoleAppListener\bin\Debug\ConsoleAppListener.exe";

        private static bool _isClientProcessRunning;
        private static bool _isShuttingDown;

        static void Main(string[] args)
        {
            // Determine whether the client process is already running:
            _isClientProcessRunning = LocateClient(ClientAppInstanceName);
            if (_isClientProcessRunning)
            {
                Console.WriteLine("Process already running");
            }
            else
            {
                // Launch the process:
                LaunchProcess();
                Console.WriteLine("Process launched");
            }

            Console.WriteLine("Hit any key to send shutdown message (at any time)");
            Console.ReadKey();

            WantProcessToShutDown(ClientAppInstanceName);

            _isShuttingDown = true;
            Console.WriteLine("Hit Enter to close");
            Console.ReadLine();
        }

        private static bool LocateClient(string clientAppUniqueName)
        {
            MemoryMappedFile memoryMappedFile;

            try
            {
                memoryMappedFile = MemoryMappedFile.OpenExisting(clientAppUniqueName);
            }
            catch (FileNotFoundException)  // Exception-driven logic is never ideal, but no method exists to check for the file first
            {
                return false;  // Client not found
            }

            int messageReceived;

            // Read the process ID from the memory-mapped file:
            using (memoryMappedFile)
            {
                using (var stream = memoryMappedFile.CreateViewStream())
                {
                    var reader = new BinaryReader(stream);
                    messageReceived = reader.ReadInt32();
                    Console.WriteLine(string.Format("Broadcast '{0}' received", messageReceived));
                }
            }

            // Check if the latest message in the file is a shutdown message
            // sent from a previous instance of this application
            // (which means that the client app is still running but no longer listening-- likely it's shutting down)
            if (messageReceived == CloseMessage)
            {
                return false;
            }

            var processId = messageReceived;

            // Get the client process by its ID:
            var process = Process.GetProcessById(processId);
            process.EnableRaisingEvents = true;
            process.Exited += ProcessExited;

            return true;
        }

        private static void LaunchProcess()
        {
            var process = new Process();

            // Create process to launch the desired application:
            var processProperties = new ProcessStartInfo
                                        {
                                            UseShellExecute = true,
                                            WorkingDirectory = Directory.GetParent(ClientProcessFileName).FullName,
                                            FileName = ClientProcessFileName,
                                            Arguments = ClientAppInstanceName,  // Pass in the application's unique name
                                            RedirectStandardOutput = false
                                        };

            Console.WriteLine(String.Format("Launching executable: {0} {1} ", processProperties.FileName,
                                      processProperties.Arguments));

            process.StartInfo = processProperties;
            process.EnableRaisingEvents = true;
            process.Exited += ProcessExited;
            process.Start();
            _isClientProcessRunning = true;
        }

        private static void ProcessExited(object sender, EventArgs e)
        {
            Console.WriteLine("Process exited");
            _isClientProcessRunning = false;

            // Start up the process again:
            if (!_isShuttingDown)
            {
                LaunchProcess();
            }
        }

        private static void WantProcessToShutDown(string destinationAppName)
        {
            if (!_isClientProcessRunning)
            {
                return;
            }

            var memoryMappedFile = MemoryMappedFile.OpenExisting(destinationAppName);

            using (var accessor = memoryMappedFile.CreateViewAccessor(0, MessageByteSize))
            {
                accessor.Write(0, CloseMessage);
            }

            Console.WriteLine("Sent message to shut down");
        }
    }
}

Ideally, this has provided you with the framework to create a basic method for allowing two processes to talk; the controller can notify the listener when it's time to shut down gracefully, and the listener can broadcast its process ID in case the controller shut down unexpectedly.

Note: Part 3 was written to refactor the above code in a more efficient and encapsulated format.  I'd highly recommend checking that out too.

Part 1
Part 3

Leave a Reply

Your email address will not be published. Required fields are marked *