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 requestThe 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
- How to ensure, that the pipe does not freeze the execution?
- Is my approach generally okay?
- 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();
orFlushFileBuffers(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
Task.Run
you probably want to handle the task somehow, to ensure a clean shutdown, and that any errors are handled correctly.Task.Run
, because the Windows Service starting froze, maybe there is a better way. I will research your suggestions, any maybe reach you again.