Detect client disconnects using named pipes in C#

Detect client disconnects using named pipes in C#

TL;DR: Solved – after several failed attempts I discovered how to detect client disconnects when using named pipes in C# — the article below includes relevant code snippets from my CSNamedPipes GitHub project.

In my spare time — of which there’s not much since I have four kids — I’ve been helping someone solve a programming challenge related to inter-process communication between a desktop application and a Windows service application. The desktop app makes requests to the service app to perform tasks which require administrative permissions, like performing content updates for files that are stored in privileged folders.

Communicating between two applications seems like it ought to be straightforward, but like any programming task there is always undiscovered territory. Since the two applications are running in different window stations, on different desktops, and in different security contexts, the best way for the apps to talk is to use named pipes. Simple, right?

First challenge: the code needed to be written in C#. While I’ve read C# code, and even debugged C# code written by others, I’ve never actually written anything in C# myself. I figured I’d just Google some code and bang it into shape, because this seems like a problem that many folks must have solved before me. I wanted something that used asynchronous APIs (more optimal than synchronous communication — something I know from writing lots of server code) and handled all the common error cases; that’s not too much to ask, right?!? Sadly, after reading through a great deal of code found online I didn’t discover anything that fit the bill. The best was from Jeffrey Richter, who writes great code and books. I started with his code sample (see Ch28-1-IOOps.cs in the ZIP file) as a base.

But I quickly ran into a problem that seems to be common to all the implementations I found: the named pipe server has trouble detecting when a client disconnects. This is a big problem because it can be used by a malicious client to cause a denial-of-service attack.

My thought was that the C# library should detect that the pipe closed and automatically perform a callback with a zero-byte read, but this turns out not to be the case.

The basic pattern for an asynchronous read is this:

{
    ...
    m_pipe.BeginRead(m_data, 0, data.Length, OnAsyncMessage, null);
}

private void OnAsyncMessage(IAsyncResult result) {
    Int32 bytesRead = m_pipe.EndRead(result);
    if (bytesRead != 0) {
        // good times -- process the message
    }
    else
        // pipe disconnected, right?!? NOPE!
    }
}

Google for “How to detect a client disconnect using a named pipe” and you’ll get 430000 hits, and even more if you play with variations of the search! So clearly I’m not the only one having trouble…

It occurred to me to set a read timeout on the PipeStream object, but that doesn’t work: async pipes don’t support that feature.

I could set a timer to periodically cancel the read and start a new one, but what happens if I cancel the read on one thread while it being started in another thread? That seemed like a disaster waiting to happen.

Another idea was to have the server “ping” the client periodically to ensure the connection is still alive, but that’s lame because I’m writing a low-level network library. If I have to send pings I either have to define the application-level network protocol so that I can send pings, or I have to put the burden of pinging on the application layer code, which just places a burden on folks using my API.

So I ended up adding a background thread that polls to discover whether each connection is still alive. This is hacky: if C# already knows the connections have been closed, why isn’t it bothering to tell me?!? Update: it’s possible to detect disconnects without polling; I’ve updated the code in the github archive based on a suggestion by Chae Seong Lim, someone I worked with for many years at ArenaNet. Thanks CS!

An important point to note when writing your own IPC code using named pipes: when setting the pipe security permissions, you must include FullControl permission for the current user, otherwise creating the second instance of the pipe will fail (thanks Chris Dickson).

m_ps = new PipeSecurity();
m_ps.AddAccessRule(new PipeAccessRule(
    WindowsIdentity.GetCurrent().User,
    PipeAccessRights.FullControl,
    AccessControlType.Allow));
m_ps.AddAccessRule(new PipeAccessRule(
    new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null),
    PipeAccessRights.ReadWrite, AccessControlType.Allow));

As a bonus, here's the complete code for the project that implements the solution on my GitHub page CSNamedPipes.