1

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
2
  • With most forms of stream based IPC you need some form of message framing, like prefixing the length of the message. And when using Task.Run you probably want to handle the task somehow, to ensure a clean shutdown, and that any errors are handled correctly.
    – JonasH
    Commented May 28 at 12:30
  • Hey @JonasH, thanks for the tips. I went for Task.Run, because the Windows Service starting froze, maybe there is a better way. I will research your suggestions, any maybe reach you again.
    – Haj
    Commented May 29 at 9:30

1 Answer 1

0

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.