Interprocess Communication With Memory-Mapped Files, Part 3

I wasn't intending to write a third piece on this, but after spending more time with the prototype from the previous post, I realized that we've got a little more redundant code than I typically like to see.  While it's still fresh, I'd like to take the time to clean up the code, making the whole thing more reusable and much easier to extend later if needed.

To recap one last time, we had written 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”.
In the previous post, both ConsoleAppListener and ConsoleAppController reused two crucial functions, which I've wrapped into reusable methods, BroadcastMessage and GetMessage:
private void BroadcastMessage(int message)
{
	_memoryMappedFile = MemoryMappedFile.CreateOrOpen(_memoryMappedFileName, MessageByteSize);

	using (var accessor = _memoryMappedFile.CreateViewAccessor(0, MessageByteSize))
	{
		accessor.Write(0, message);
	}
}

private int GetMessage()
{
	int messageReceived;

	using (var stream = _memoryMappedFile.CreateViewStream())
	{
		var reader = new BinaryReader(stream);
		messageReceived = reader.ReadInt32();
	}

	Console.WriteLine(string.Format("Broadcast '{0}' received", messageReceived));

	return messageReceived;
}

In addition, I found it was best to encapsulate this communication code into a single class that's common to both classes.  The recurring theme is to make each class do just one thing, and do it well.  For this reason I've created a new project, called "Shared", and wrapped all of the communication logic into one class called InterprocessCommunicator.  This class is referenced by both projects and contains both the BroadcastMessage() and GetMessage() methods.

I've also refactored the other two classes to offload their communication work to the new InterprocessCommunicator, resulting in less redundant and cleaner code.
Project ConsoleAppController\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 Shared;

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 string ClientAppInstanceName = "SampleApp";
        private const string ClientProcessFileName = @"C:\Code\Personal\InterprocessCommunication\ConsoleAppListener\bin\Debug\ConsoleAppListener.exe";

        private static bool _isClientProcessRunning;
        private static bool _isShuttingDown;

        private static readonly InterprocessCommunicator InterprocessCommunicator = new InterprocessCommunicator(ClientAppInstanceName);

        static void Main(string[] args)
        {
            // Determine whether the client process is already running:
            _isClientProcessRunning = InterprocessCommunicator.LocateClient();
            if (_isClientProcessRunning)
            {
                InterprocessCommunicator.ProcessExited += ProcessExited;
                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();
            Console.WriteLine("Sent message to shut down");

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

        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()
        {
            if (!_isClientProcessRunning)
            {
                return;
            }

            InterprocessCommunicator.BroadcastShutdownMessage();
        }
    }
}

Project ConsoleAppListener\Program.cs:

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

using System;
using System.Threading;
using Shared;

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 static string _instanceName;
        private static InterprocessCommunicator _interprocessCommunicator;

        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;
            }

            _interprocessCommunicator = new InterprocessCommunicator(_instanceName);
            _interprocessCommunicator.BroadcastProcessId();

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

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

                Console.WriteLine("Processing finished");
            }

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

        private static bool ShouldClose()
        {
            return _interprocessCommunicator.CheckForShutdownMessage();
        }

        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
        }
    }
}
Project Shared\InterprocessCommunicator.vb:
// 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 Shared
{
    public class InterprocessCommunicator
    {
        private const int MessageByteSize = 4;  // Number of bytes needed to store an int32
        private const int ShutdownMessage = -1;

        private MemoryMappedFile _memoryMappedFile;
        private readonly string _memoryMappedFileName;

        public EventHandler ProcessExited;

        public InterprocessCommunicator(string memoryMappedFileName)
        {
            _memoryMappedFileName = memoryMappedFileName;
        }

        #region Public Methods

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

            // Read the process ID from the memory-mapped file:
            var messageReceived = GetMessage();

            // 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 == ShutdownMessage)
            {
                return false;  // Client not found
            }

            var processId = messageReceived;

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

            return true;  // Client found
        }

        public bool CheckForShutdownMessage()
        {
            var message = GetMessage();
            if (message == ShutdownMessage)
            {
                Console.WriteLine("Received shutdown message");
                return true;
            }

            return false;
        }

        public void BroadcastProcessId()
        {
            var processId = Process.GetCurrentProcess().Id;
            BroadcastMessage(processId);
        }

        public void BroadcastShutdownMessage()
        {
            BroadcastMessage(ShutdownMessage);
        }

        #endregion

        #region Private Methods

        private void BroadcastMessage(int message)
        {
            _memoryMappedFile = MemoryMappedFile.CreateOrOpen(_memoryMappedFileName, MessageByteSize);

            using (var accessor = _memoryMappedFile.CreateViewAccessor(0, MessageByteSize))
            {
                accessor.Write(0, message);
            }
        }

        private int GetMessage()
        {
            int messageReceived;

            using (var stream = _memoryMappedFile.CreateViewStream())
            {
                var reader = new BinaryReader(stream);
                messageReceived = reader.ReadInt32();
            }

            Console.WriteLine(string.Format("Broadcast '{0}' received", messageReceived));

            return messageReceived;
        }

        #endregion
    }
}
Previous articles:
Reference:

Leave a Reply

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