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
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:
Bug:
Analysis:
string request = reader.ReadToEnd();
ReadFile()
_pipe.WaitForPipeDrain();
or FlushFileBuffers(hPipe);
are neededOther 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;
}
}
}
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;
}