Search code examples
c#c++windowsservicenamed-pipes

Windows Named pipe communication between C# .NET Windows Service and C++ client freezes


I need some help with Windows named pipes please.

  • Aim: run silent .msi installers from C++ app running WITHOUT admin permissions

  • Idea: bundle the .msi with another product as an installer daemon running in the background with nesessary permissions

  • Wrote a C# .NET Windows Service, which opens a NamedPipeServerStream to wait for a request

  • The Service will return a String response message to the client

  • The C++ client uses CreateFileA() to bind to the pipe and sends a String request

  1. How to ensure, that the pipe does not freeze the execution?
  2. Is my approach generally okay?
  3. Any tips?

C# Service:

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

    public interface ICommandService
    {
        string ExecuteCommand(string json);

        string CommandAddress { get; }
    }

    public class ServiceCommunicationPipe
    {
        private static ICommandService _service;
        private static readonly Logger _log = Logger.Instance;
        private NamedPipeServerStream _pipe;

        public ServiceCommunicationPipe(ICommandService service)
        {
            _service = service;
        }

        public void Start()
        {
            string pipeAddress = $"InstallerService\\{_service.CommandAddress}";
            _pipe = new NamedPipeServerStream(pipeAddress, PipeDirection.InOut, 1, PipeTransmissionMode.Message);

            Task.Run(() =>
            {
                while (true)
                {
                    _log.Log($"Waiting for client connection on {pipeAddress}...");
                    _pipe.WaitForConnection();
                    _log.Log("Client connected.");

                    try
                    {
                        StreamReader reader = new StreamReader(_pipe);
                        string request = reader.ReadToEnd();

                        string response = _service.ExecuteCommand(request);

                        StreamWriter writer = new StreamWriter(_pipe);
                        writer.Write(response);
                        writer.Flush();
                        _pipe.WaitForPipeDrain();
                    }
                    catch (Exception e)
                    {
                        _log.Log($"Error during pipe stream: {e.Message}");
                    }
                    finally
                    {
                        _pipe.Disconnect();
                    }
                }
            });
        }

        public void Stop()
        {
            _pipe?.Close();
            _log.Log("Pipe server stopped.");
        }
    }

C++ client:

#include "windows_service_calling.h"
#include <stdio.h>

#define BUFSIZE 512

std::string GetLastErrorAsString()
{
    DWORD errorMessageID = ::GetLastError();
    return std::to_string(errorMessageID);
}

std::string AsJsonString(const std::string& action, const std::string& path) {
    std::string jsonString = "{ \"action\": \"" + action + "\", \"path\": \"" + path + "\" }";
    return jsonString;
}

// The C++ application opens the named pipe using CreateFileA().
// The C++ application writes the request to the named pipe using WriteFile(). The request includes the method name (ExecuteCommand) and the method parameters (action and path).
// The C# service reads the request from the named pipe, executes the ExecuteCommand method with the provided parameters, and writes the response to the named pipe.
// The C++ application reads the response from the named pipe using ReadFile().
std::string ExecuteActionAtWindowsService(std::string action, std::string path)
{
    HANDLE hPipe;
    char  chBuf[BUFSIZE];
    BOOL   fSuccess = FALSE;
    DWORD  cbRead, cbToWrite, cbWritten, dwMode;
    std::string pipeName = "\\\\.\\pipe\\InstallerService\\executeInstaller";
    std::string response = "";

    hPipe = CreateFileA(
        pipeName.c_str(),   // pipe name
        GENERIC_READ |  // read and write access
        GENERIC_WRITE,
        0,              // no sharing
        NULL,           // default security attributes
        OPEN_EXISTING,  // opens existing pipe
        0,              // default attributes
        NULL);          // no template file

    if (hPipe == INVALID_HANDLE_VALUE)
    {
        return "Failure: Error occurred while connecting to the service via CreateFileA: " + GetLastErrorAsString();
    }

    dwMode = PIPE_READMODE_MESSAGE;
    fSuccess = SetNamedPipeHandleState(
        hPipe,    // pipe handle
        &dwMode,  // new pipe mode
        NULL,     // don't set maximum bytes
        NULL);    // don't set maximum time

    if (!fSuccess)
    {
        return "Failure: Error occurred while SetNamedPipeHandleState: " + GetLastErrorAsString();
    }

    std::string json = AsJsonString(action, path);

    size_t length = (json.length() + 1) * sizeof(char);
    if (length > MAXDWORD) {
        return "Failure: json too long for WriteFile function";
    }
    cbToWrite = static_cast<DWORD>(length);

    fSuccess = WriteFile(
        hPipe,                  // pipe handle
        json.c_str(),        // message
        cbToWrite,              // message length
        &cbWritten,             // bytes written
        NULL);

    if (!fSuccess)
    {
        return "Error occurred while writing to the server: " + GetLastErrorAsString();
    }

    FlushFileBuffers(hPipe); // Ensure all data is written to the pipe

    do
    {
        fSuccess = ReadFile(
            hPipe,    // pipe handle
            chBuf,    // buffer to receive reply
            BUFSIZE*sizeof(char),  // size of buffer
            &cbRead,  // number of bytes read
            NULL);    // not overlapped

        if (!fSuccess && GetLastError() != ERROR_MORE_DATA)
            break;

        response = std::string(chBuf, cbRead);  // Convert char array to string

    } while (!fSuccess);

    if (!fSuccess)
    {
        return "Error occurred while reading from the server: " + GetLastErrorAsString();
    }

    CloseHandle(hPipe);

    return response;
}

Expected:

  • The service executes the command and responds to the client via the named pipe
  • The client can be used during execution

Bug:

  • The client freezes
  • The service logs "Client connected."
  • When killing the client, the service logs the correct json
  • When killing the service (in a separate test), the client logs "Error occurred while reading from the server: 109" (broken pipe)

Analysis:

  • Probably the service gets stuck in string request = reader.ReadToEnd();
  • Probably the client gets stuck in ReadFile()
  • I'm new to both languages, and am unsure if all methods, e.g. _pipe.WaitForPipeDrain(); or FlushFileBuffers(hPipe); are needed

Other Tests: I tried using WCF on the C# side instead:

    [ServiceContract]
    public interface ICommandService
    {
        [OperationContract]
        string ExecuteCommand(string json);

        string CommandAddress { get; }
    }

    public class ServiceCommunicationPipe
    {
        private static readonly Uri ServiceUri = new Uri("net.pipe://localhost/InstallerService");
        private static ICommandService _service;
        private static ServiceHost _host = null;
        private static ServiceCommunicationPipe _instance;
        private static Timer _timer;
        private static readonly Logger _log = Logger.Instance;

        public static ServiceCommunicationPipe Instance
        {
            get
            {
                if (_instance == null)
                {
                    throw new Exception("Instance not initialized. Call Initialize() first.");
                }
                return _instance;
            }
        }

        private ServiceCommunicationPipe(ICommandService service)
        {
            _service = service;
        }

        public static void Initialize(ICommandService service)
        {
            if (_instance != null)
            {
                throw new Exception("Instance already initialized.");
            }
            _instance = new ServiceCommunicationPipe(service);
        }

        public void Start()
        {
            _host = new ServiceHost(_service, ServiceUri);
            _host.AddServiceEndpoint(typeof(ICommandService), new NetNamedPipeBinding(), _service.CommandAddress);
            _host.Open();
        }

        public void Stop()
        {
            if ((_host != null) && (_host.State != CommunicationState.Closed))
            {
                _host.Close();
                _host = null;
            }
        }
    }

  • With WCF, I could not get the pipes connected from C++ to C#
  • Tried different addesses on both sides

Solution

  • Thanks to @JonasH, i looked into message framing, and came up with a solution. I Also had issues with encodings, which are solved below. It works now. Did i miss anything?

    Essentially, we need to send/read the length of the pipe message first, and read/send that many characters in a second operation.

    using System;
    using System.IO.Pipes;
    using System.Threading.Tasks;
    using System.Text;
    
        public interface ICommandService
        {
            string ExecuteCommand(string json);
    
            string CommandAddress { get; }
        }
    
        public class ServiceCommunicationPipe
        {
            private readonly ICommandService _service;
            private static readonly Logger _log = Logger.Instance;
            private NamedPipeServerStream _pipe;
            private Task _task;
    
            public ServiceCommunicationPipe(ICommandService service)
            {
                _service = service;
            }
    
            public void Start()
            {
                string pipeAddress = $"InstallerService\\{_service.CommandAddress}";
                _pipe = new NamedPipeServerStream(pipeAddress, PipeDirection.InOut, 1, PipeTransmissionMode.Message);
    
                _task = Task.Run(() =>
                {
                    while (true)
                    {
                        _log.Log($"Waiting for client connection on {pipeAddress}...");
                        _pipe.WaitForConnection();
                        _log.Log("Client connected.");
    
                        try
                        {
                            byte[] lengthBuffer = new byte[4];
                            _pipe.Read(lengthBuffer, 0, 4);
                            int messageLength = BitConverter.ToInt32(lengthBuffer, 0);
                            _log.Log($"Received request with message length: {messageLength}.");
    
                            byte[] messageBuffer = new byte[messageLength];
                            _pipe.Read(messageBuffer, 0, messageLength);
                            string request = Encoding.UTF8.GetString(messageBuffer);
                            _log.Log($"Received request: {request}");
    
                            string response = _service.ExecuteCommand(request);
                            _log.Log($"Will respond with: {response}");
    
                            byte[] responseBytes = Encoding.UTF8.GetBytes(response);
                            byte[] lengthPrefix = BitConverter.GetBytes(responseBytes.Length);
                            _log.Log($"Length of response: {lengthPrefix.Length}");
    
                            _pipe.Write(lengthPrefix, 0, lengthPrefix.Length);
                            _pipe.Write(responseBytes, 0, responseBytes.Length);
                            _pipe.Flush();
                        }
                        catch (Exception e)
                        {
                            _log.Log($"Error during pipe stream: {e.Message}, {e.StackTrace}");
                        }
                        finally
                        {
                            _pipe.Disconnect();
                        }
                    }
                });
            }
    
            public void Stop()
            {
                _task?.Dispose();
                _task = null;
                _pipe?.Close();
                _pipe = null;
                _log.Log("Pipe closed.");
            }
        }
    

    C++ client:

    #include "windows_service_calling.h"
    #include <vector>
    #include <cstring>
    #include <stdio.h>
    
    std::string GetErrorMessage() {
        DWORD errorMessageID = ::GetLastError();
        return std::to_string(errorMessageID);
    }
    
    std::string ConvertToJsonString(const std::string &action, const std::string &path) {
        std::string jsonString = "{ \"action\": \"" + action + "\", \"path\": \"" + path + "\" }";
        return jsonString;
    }
    
    std::string ExecuteActionAtWindowsService(std::string action, std::string path) {
        HANDLE pipeHandle;
        BOOL isSuccessful = FALSE;
        std::string pipeName = "\\\\.\\pipe\\InstallerService\\executeInstaller";
    
        pipeHandle = CreateFileA(
                pipeName.c_str(),
                GENERIC_READ |
                GENERIC_WRITE,
                0,              // no sharing
                NULL,           // default security attributes
                OPEN_EXISTING,  // opens existing pipe
                0,              // default attributes
                NULL);          // no template file
    
        if (pipeHandle == INVALID_HANDLE_VALUE) {
            return "Failure: Error occurred while connecting to the service via CreateFileA: " +
                   GetErrorMessage();
        }
    
        DWORD pipeMode = PIPE_READMODE_MESSAGE;
        isSuccessful = SetNamedPipeHandleState(
                pipeHandle,
                &pipeMode,
                NULL, // don't set maximum bytes
                NULL);// don't set maximum time
    
        if (!isSuccessful) {
            return "Failure: Error occurred while SetNamedPipeHandleState: " + GetErrorMessage();
        }
    
        std::string json = ConvertToJsonString(action, path);
        std::vector<char> jsonBytes(json.begin(), json.end());
        int jsonLength = static_cast<int>(jsonBytes.size());
    
        std::vector<char> lengthBytes(4);
        std::memcpy(lengthBytes.data(), &jsonLength, 4);
    
        DWORD bytesWritten;
        isSuccessful = WriteFile(
                pipeHandle,
                lengthBytes.data(),
                static_cast<DWORD>(lengthBytes.size()),
                &bytesWritten,
                NULL
        );
    
        if (!isSuccessful || bytesWritten != lengthBytes.size()) {
            return "Error occurred while writing length prefix to the server: " + GetErrorMessage();
        }
    
        isSuccessful = WriteFile(
                pipeHandle,
                jsonBytes.data(),
                static_cast<DWORD>(jsonBytes.size()),
                &bytesWritten,
                NULL
        );
    
        if (!isSuccessful || bytesWritten != jsonBytes.size()) {
            return "Error occurred while writing message to the server: " + GetErrorMessage();
        }
    
        FlushFileBuffers(pipeHandle); // Ensure all data is written to the pipe
    
        DWORD bytesRead;
        int responseLength;
        isSuccessful = ReadFile(
                pipeHandle,
                &responseLength,
                sizeof(int),
                &bytesRead,
                NULL
        );
    
        if (!isSuccessful || bytesRead != sizeof(int)) {
            return "Error occurred while reading length prefix from the server: " + GetErrorMessage();
        }
    
        std::vector<char> responseBytes(static_cast<size_t>(responseLength));
        isSuccessful = ReadFile(
                pipeHandle,
                responseBytes.data(),
                static_cast<DWORD>(responseBytes.size()), // Cast size_t to DWORD
                &bytesRead,
                NULL
        );
    
        if (!isSuccessful || static_cast<DWORD>(bytesRead) != responseBytes.size()) {
            return "Error occurred while reading message from the server: " + GetErrorMessage();
        }
    
        std::string response(responseBytes.begin(), responseBytes.end());
    
        CloseHandle(pipeHandle);
    
        return response;
    }