3

I am attempting to set up IPC using named pipes. I need to transfer data from one server/manager-style Windows Service to a variety of clients. The request-response procedure is pretty simple, so I have a single model for each of those. I'm planning to have the server/manager host a number of named pipe servers. When a client needs to communicate, it will create a pipe client, connect, serialize a Request model as json and send it across the pipe. The server/manager will serialize a Response model and send it back, then disconnect and start waiting for a new connection.

The problem that I'm running into is that the System.Text.Json.JsonSerializer doesn't return when trying to deserialize data in PipeStream (NamedPipeServerStream or NamedPipeClientStream). I'm not sure what I'm doing wrong, as I can convert the model to json and send it across using a buffer just fine, but I'd rather serialize directly to the stream.

Here's a simplified version of the code.

The client:

using System;
using System.IO.Pipes;
using System.Text.Json;
using System.Threading.Tasks;

namespace NamedPipeDemo.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            while (true)
            {
                await using var client = new NamedPipeClientStream("PSSharp/NamedPipeDemo");
                await client.ConnectAsync();
                Console.Write("Enter message body for server: ");
                var message = Console.ReadLine();
                var request = new Request()
                {
                    Content = message
                };
                Console.WriteLine("{0}: Sending data.", DateTime.Now);
                await JsonSerializer.SerializeAsync(client, request);
                await client.FlushAsync();
                Console.WriteLine("{0}: Data written to pipe.", DateTime.Now);
                var response = await JsonSerializer.DeserializeAsync<Response>(client);
                Console.WriteLine("{0}: Response received from server.\n{1}", DateTime.Now, JsonSerializer.Serialize(response));
                if (response is null)
                {
                    break;
                }
            }
        }
    }
}

The server

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO.Pipes;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace NamedPipeDemo.Server
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;

        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            await using var server = new NamedPipeServerStream("PSSharp/NamedPipeDemo", PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.WriteThrough);

            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("{time} : Waiting for client to connect.", DateTime.Now);
                await server.WaitForConnectionAsync(stoppingToken);
                _logger.LogInformation("{time} : Client with identity (unknown) connected.", DateTime.Now);
                // this call never returns
                var request = await JsonSerializer.DeserializeAsync<Request>(server, null, stoppingToken);
                var response = await GetResponseAsync(request, stoppingToken);
                await JsonSerializer.SerializeAsync(server, response, null, stoppingToken);
                _logger.LogInformation("{time} : Communication with client terminated. Waiting for new client.", DateTime.Now);
                server.Disconnect();
            }
        }
        protected virtual async ValueTask<Response?> GetResponseAsync(Request? request, CancellationToken cancellation)
        {
            if (request?.Content?.Contains("wait") ?? false)
            {
                _logger.LogInformation("Stimulating work.");
                await Task.Delay(1000, cancellation);
            }
            if (request?.Content?.Contains("exit") ?? false)
            {
                return null;
            }
            var shouldEcho = request?.Content?.Contains("echo") ?? false;
            var echoContent = shouldEcho ? request?.Content : "The message was processed.";
            var error = default(string);
            if (echoContent is null)
            {
                echoContent = "No content to echo.";
                error = "No content to echo.";
            }
            var response = new Response(echoContent, error);
            return response;
        }
    }
}

Models

using System;
using System.Text.Json.Serialization;

namespace NamedPipeDemo
{
    public class Request
    {
        private static int s_id;
        public int Id { get; private set; } = ++s_id;
        public string? Content { get; set; }
    }
    public class Response
    {
        private static int s_id;
        [JsonConstructor]
        private Response(int id,string? error, string message)
        {
            Id = id;
            Error = error;
            Message = message;
        }
        public Response(string message, string? error = null)
        {
            Message = message;
            Error = error;
        }
        public int Id { get; } = ++s_id;
        public string? Error { get; }
        public string Message { get; }
    }
}
dbc
  • 104,963
  • 20
  • 228
  • 340
Stroniax
  • 718
  • 5
  • 15
  • 1
    Most stream-based deserializers want to read to EOF, which will never happen in this scenario. If this was a Socket stream, you could close the outbound socket which would work, but AFAIK no similar API exists for named pipes. As such, you might need to implement "framing", and have your own code carefully buffer a frame (without over-reading or trying to read to EOF), and have the JSON deserializer process the frame payload. In the case of a text-based protocol, you could perhaps use a NUL sentinel (a zero byte) as a post-fix way of denoting the end of the frame. – Marc Gravell Aug 15 '21 at 08:14
  • Looking at this from a streaming point of view, you are trying to parse a stream containing a *sequence* of JSON objects like so `{"Id":1,"Message":"text 1"}{"Id":1,"Message":"text 2"}` and so on. Json.NET supports this by setting setting [`JsonText.SupportMultipleContent = true`](https://www.newtonsoft.com/json/help/html/ReadMultipleContentWithJsonReader.htm). It also supports ignoring extra characters after the end of the JSON document by setting [`CheckAdditionalContent = false`](https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_JsonSerializer_CheckAdditionalContent.htm). – dbc Aug 15 '21 at 21:12
  • But System.Text.Json does not support anything like `SupportMultipleContent = true` or `CheckAdditionalContent = false`. It always verifies that the stream is a well-formed JSON document **by reading past the end of the document to see if there are any additional characters, and throwing an exception if so**. And this is what is causing your hang. Internally a `Utf8JsonReader` is ensuring the JSON document stream is well-formed by verifying that there is no additional content -- but the client hasn't written it to the stream yet, so it waits forever. – dbc Aug 15 '21 at 21:31
  • For confirmation see [`[System.Text.Json]` Utf8JsonReader should support multiple content #36750](https://github.com/dotnet/runtime/issues/36750) which is a duplicate of [Let Utf8JsonReader process input with one complete JSON document per line #33030](https://github.com/dotnet/runtime/issues/33030) which is open and unassigned. – dbc Aug 15 '21 at 21:31
  • 1
    Given that limitation, I don't see any alternative to some sort of message framing as suggested by @MarcGravell. He suggests using a NUL sentinal, but System.Text.Json works with utf8 byte sequences so you probably want to either preface the message with a byte length (in which case you could stream on the server side using a [substream](https://stackoverflow.com/q/30494541) or terminate the message with an 8-byte null sequence (which *I believe* never happens in a Utf8 stream lacking control characters) which would allow streaming on the sending side. – dbc Aug 15 '21 at 21:31
  • @dbc Thank you. I will mark that as the accepted solution if you post it as an answer. – Stroniax Aug 16 '21 at 22:55

0 Answers0