2

I have a simple server-client application, using a named pipe. I use a StreamWriter within the server, and a StreamReader within the client. The StreamWriter doesn't get disposed as long as the client process doesn't read from the pipe (that is, doesn't read from the StreamReader, wrapping the pipe). I'd like to understand why.

Here are the details:

This is the server:

using System;
using System.IO;
using System.IO.Pipes;

class PipeServer
{
    static void Main()
    {
        using (NamedPipeServerStream pipeServer =
            new NamedPipeServerStream("testpipe"))
        {
            Console.Write("Waiting for client connection...");
            pipeServer.WaitForConnection();
            Console.WriteLine("Client connected.");

            try
            {
                StreamWriter sw = new StreamWriter(pipeServer);
                try
                {
                    sw.WriteLine("hello client!");
                }
                finally
                {
                    sw.Dispose();
                }
                // Would print only after the client finished sleeping
                // and reading from its StreamReader
                Console.WriteLine("StreamWriter is now closed"); 
            }
            catch (IOException e)
            {
                Console.WriteLine("ERROR: {0}", e.Message);
            }
        }
    }
}

and here's the client:

using System;
using System.IO;
using System.IO.Pipes;
using System.Threading;

class PipeClient
{
    static void Main(string[] args)
    {
        using (NamedPipeClientStream pipeClient =
            new NamedPipeClientStream(".", "testpipe"))
        {
            Console.Write("Attempting to connect to pipe...");
            pipeClient.Connect();
            Console.WriteLine("Connected to pipe.");

            using (StreamReader sr = new StreamReader(pipeClient))
            {
                Thread.Sleep(100000);

                string temp = sr.ReadLine();
                if (temp != null)
                {
                    Console.WriteLine("Received from server: {0}", temp);
                }
            }
        }
    }
}

Notice the Thread.Sleep(100000); in the Client: I added it to make sure that the StreamWriter is not being disposed in the Server as long as the client process is sleeping, and the server won't execute Console.WriteLine("StreamWriter is now closed");. Why?

EDIT:

I cut off the previous information which in second-thought I guess is probably irrelevant. I'd also like to add that - thanks to Scott in the comments - I observed this behavior happening the other way around: If the server writes, then sleep, and the client (tries to) read with its StreamReader - the reading isn't happening untill the server awakes.

SECOND EDIT:

The other-way-around-behavior I talked about in the first edit is irrelevant, it is a flush issue. I tried giving it some more trials, and came to a conclusion the Scott's right - the pipe can't be disposed if it isn't drained. Why, then? This also seems to be in contradiction to the fact that StreamWriter assumes it owns the stream, unless otherwise specified (see here).

Here are the added details to the code above:

In the server program, the try-finally now looks like this:

try
{
    sw.AutoFlush = true;
    sw.WriteLine("hello client!");
    Thread.Sleep(10000);
    sw.WriteLine("hello again, client!");
}
finally
{
    sw.Dispose(); // awaits while client is sleeping
}
Console.WriteLine("StreamWriter is now closed");

In the client program, the using block now looks like this:

using (StreamReader sr = new StreamReader(pipeClient))
{
    string temp = sr.ReadLine();
    Console.WriteLine("blah"); // prints while server sleeps

    Console.WriteLine("Received from server: {0}", temp); // prints while server is sleeping
    Thread.Sleep(10000);

    temp = sr.ReadLine();
    Console.WriteLine("Received from server: {0}", temp);
}
Community
  • 1
  • 1
OfirD
  • 9,442
  • 5
  • 47
  • 90
  • Does the behavior change at all if you change the server to `new NamedPipeServerStream("testpipe", PipeDirection.Out)` and the client to `new NamedPipeClientStream(".", "testpipe", PipeDirection.In)`? Also, what happens if you put a `Thread.Sleep(100000);` after the read but still inside the `using` on the client, does the stream still stay open on the server till it exits the using? – Scott Chamberlain Sep 25 '16 at 15:12
  • @ScottChamberlain, no change at all, and changing the location of `Sleep` is what I'd expect: The server process continues, prints `"StreamWriter is now closed"` and returns, while the client sleeps. – OfirD Sep 25 '16 at 15:39
  • Maybe it does not let you close till the buffer is drained. What happens if you do `Server write -> Client read -> Server write -> Server dispose -> Client read`? – Scott Chamberlain Sep 25 '16 at 15:42
  • What is your expectation? It worked fine, shouldn't it? I did found out another thing: I tried `Server write -> Server long sleeping -> Client read` - but the client didn't read until the server woke up. – OfirD Sep 25 '16 at 15:57
  • I was thinking it would hang till the 2nd read of the client, showing that it prevents dispose until the buffer is empty. Honestly, you are out of my expertise, I don't know what causes it or how to prevent it. – Scott Chamberlain Sep 25 '16 at 15:59
  • @ScottChamberlain, I think you were right, see edits. Question still remains, though - why does it happen? – OfirD Sep 25 '16 at 17:31

1 Answers1

3

So the problem is down to the way Windows Named Pipes work. When CreateNamedPipe is called you can specify the output buffer size. Now the documentation says something like this:

The input and output buffer sizes are advisory. The actual buffer size reserved for each end of the named pipe is either the system default, the system minimum or maximum, or the specified size rounded up to the next allocation boundary.

The trouble here is the NamedPipeServerStream constructor passes 0 as the default output size (we can verify this using the source, or in my case just firing up ILSpy). You might assume this would create a "default" buffer size, as per the comment, but it doesn't, it literally creates a 0 byte output buffer. This means that unless someone is reading the buffer then any write to the pipe blocks. You can also verify the size using the OutBufferSize property.

The behaviour you're seeing is because when you dispose the StreamWriter it calls Write on the pipe stream (as it has buffered the contents). The write call blocks, so Dispose never returns.

To verify this we'll use WinDBG (as VS just shows the blocking thread on the sw.Dispose call).

Run the server application under WinDBG. When it's blocking pause the debugger (hit CTRL+Break for example) and issue the following commands:

Load the SOS debugger module: .loadby sos clr

Dump all running stacks with interleaved managed and un-managed stack frames (might need to run this twice because of a dumb bug in sos.dll): !eestack

You should see the first stack in the process looks something this (heavily edited for brevity):

Thread   0
Current frame: ntdll!NtWriteFile+0x14
KERNELBASE!WriteFile+0x76, calling ntdll!NtWriteFile
System.IO.Pipes.PipeStream.WriteCore
System.IO.StreamWriter.Flush(Boolean, Boolean))
System.IO.StreamWriter.Dispose(Boolean))

So we can see that we're stuck in NtWriteFile which is because it can't write out the string to a buffer of size 0.

To fix this issue is easy, specify an explicit output buffer size using the one of the NamedPipeServerStream constructors, such as this one. For example this will do everything the default constructor will do but with a sensible output buffer size:

new NamedPipeServerStream("testpipe", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.None, 0, 4096)
tyranid
  • 13,028
  • 1
  • 32
  • 34
  • I may be missing something in your explanation, but: Why does the write call blocks on Dispose only? You say it has a connection to the zero size buffer. So why, then, any other write call doesn't block? Another thing I didn't understand is why the server getting out of its blocking in Dispose is affected by the client getting out of its sleep? BTW, I'll have to get the hang of WinDBG, as I'm not quite familiar with it yet. In the meantime, I'd be glad for more clarification. – OfirD Sep 25 '16 at 22:23
  • So the blocking only occurs in Dispose because the StreamWriter buffers up the contents until it reaches a certain amount before writing it to the underlying file handle (which ultimately is all a Named Pipe is to the OS) and unless it hits that buffer limit it doesn't call Write. It you called Stream::Write directly it _should_ block but of course your don't, you wrap it in a StreamWriter. – tyranid Sep 25 '16 at 22:50
  • As for why it exits after the client wakes up and calls ReadLine, that's because it reads the data from the pipe, this unblocks the implicit Write from the Dispose call which it exits and everyone is happy. – tyranid Sep 25 '16 at 22:51
  • I now understand it better. One more, though: "the StreamWriter buffers up" - I think `AutoFlush = true` prevents that: I looked into `StreamWriter.Write(string value)`, and there's a call to `Flush` there, at the end - and within `Flush` there's indeed a call to `stream.Write`. So if I understand this correctly - if it blocks on `Dispose`, it should block on `sw.WriteLine`, but it doesn't. – OfirD Sep 25 '16 at 23:25
  • Well I was basing this on your original implementation, if I add `AutoFlush = true` then it instead blocks in the WriteLine call. Again you can verify this easily enough with WinDBG :-) – tyranid Sep 26 '16 at 09:03