Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/windows-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Windows Named Pipe Tests

on:
push:
pull_request:

jobs:
client-server:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
static: [ON, OFF]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Configure
run: >
cmake -S . -B build-${{ matrix.static }}
-DSIMPLE_NAMED_PIPE_BUILD_TESTS=ON
-DSIMPLE_NAMED_PIPE_BUILD_STATIC=${{ matrix.static }}
-DSIMPLE_NAMED_PIPE_BUILD_EXAMPLES=OFF

- name: Build
run: cmake --build build-${{ matrix.static }} --config Release --target client_server_test

- name: Run client-server test
shell: pwsh
run: |
$exe = "build-${{ matrix.static }}\Release\client_server_test.exe"
if (!(Test-Path $exe)) {
$exe = "build-${{ matrix.static }}\client_server_test.exe"
}
& $exe
24 changes: 22 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)

option(SIMPLE_NAMED_PIPE_BUILD_EXAMPLES "Build example programs" ON)
option(SIMPLE_NAMED_PIPE_BUILD_STATIC "Build static library from .ipp implementation" ON)
option(SIMPLE_NAMED_PIPE_BUILD_TESTS "Build test programs" OFF)

# Header-only library
add_library(SimpleNamedPipe INTERFACE)
Expand All @@ -17,10 +18,17 @@ if(SIMPLE_NAMED_PIPE_BUILD_STATIC)
src/NamedPipeServer.cpp
)

add_library(SimpleNamedPipeClient STATIC
src/NamedPipeClient.cpp
)

target_include_directories(SimpleNamedPipeServer PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_include_directories(SimpleNamedPipeClient PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_compile_definitions(SimpleNamedPipeServer PUBLIC SIMPLE_NAMED_PIPE_STATIC_LIB)

set_target_properties(SimpleNamedPipeServer PROPERTIES OUTPUT_NAME snp_server)
target_compile_definitions(SimpleNamedPipeClient PUBLIC SIMPLE_NAMED_PIPE_STATIC_LIB)

set_target_properties(SimpleNamedPipeServer PROPERTIES OUTPUT_NAME snp_server)
set_target_properties(SimpleNamedPipeClient PROPERTIES OUTPUT_NAME snp_client)
endif()

# Examples
Expand All @@ -34,6 +42,18 @@ if(SIMPLE_NAMED_PIPE_BUILD_EXAMPLES)
target_link_libraries(${EXAMPLE_NAME} PRIVATE SimpleNamedPipe)
if(SIMPLE_NAMED_PIPE_BUILD_STATIC)
target_link_libraries(${EXAMPLE_NAME} PRIVATE SimpleNamedPipeServer)
target_link_libraries(${EXAMPLE_NAME} PRIVATE SimpleNamedPipeClient)
endif()
endforeach()
endif()

if(SIMPLE_NAMED_PIPE_BUILD_TESTS AND WIN32)
add_executable(client_server_test
tests/client_server_test.cpp
)
target_include_directories(client_server_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(client_server_test PRIVATE SimpleNamedPipe)
if(SIMPLE_NAMED_PIPE_BUILD_STATIC)
target_link_libraries(client_server_test PRIVATE SimpleNamedPipeServer SimpleNamedPipeClient)
endif()
endif()
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ For the documentation in Russian, see [README-RU.md](README-RU.md).
- supports up to 256 simultaneous clients;
- send queue with limits on message size and count;
- event notifications via callbacks or the `ServerEventHandler` class;
- synchronous C++ client for tests and lightweight local integrations;
- lightweight MQL5 client with optional global callbacks;
- the MQL5 client performs read/write synchronously; call `update()` for polling (e.g., in a timer).

Expand All @@ -38,6 +39,27 @@ int main() {
}
```

### Minimal C++ client

```cpp
#include "SimpleNamedPipe/NamedPipeClient.hpp"
using namespace SimpleNamedPipe;

int main() {
NamedPipeClient client({"ExamplePipe"});
std::error_code error;

if (!client.connect(&error))
return 1;

client.write("ping", &error);

std::string response;
client.read(response, 5000, &error);
client.close();
}
```

### Minimal MQL5 client

```mql5
Expand Down
135 changes: 135 additions & 0 deletions include/SimpleNamedPipe/NamedPipeClient.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#pragma once
#ifndef _SIMPLE_NAMED_PIPE_CLIENT_HPP_INCLUDED
#define _SIMPLE_NAMED_PIPE_CLIENT_HPP_INCLUDED

/// \file NamedPipeClient.hpp
/// \brief Synchronous named pipe client for tests and lightweight integrations.

#include "NamedPipeClient/ClientConfig.hpp"
#include "NamedPipeServer/errors.hpp"

#include <windows.h>
#include <atomic>
#include <chrono>
#include <functional>
#include <mutex>
#include <string>
#include <system_error>
#include <vector>

namespace SimpleNamedPipe {

/// \class NamedPipeClient
/// \brief Synchronous client for message-mode Windows named pipes.
class NamedPipeClient final {
public:
/// \brief Construct client with default configuration.
NamedPipeClient();

/// \brief Construct client with explicit configuration.
/// \param config Configuration to use.
explicit NamedPipeClient(const ClientConfig& config);

/// \brief Destructor closes the pipe handle if it is open.
~NamedPipeClient();

NamedPipeClient(const NamedPipeClient&) = delete;
NamedPipeClient& operator=(const NamedPipeClient&) = delete;

/// \brief Applies a new client configuration.
/// \param config Configuration to use for subsequent connect calls.
void set_config(const ClientConfig& config);

/// \brief Returns current client configuration.
/// \return Copy of the current configuration.
ClientConfig get_config() const;

/// \brief Opens the pipe configured in ClientConfig.
/// \param error Optional output error code.
/// \return true on success.
bool connect(std::error_code* error = nullptr);

/// \brief Opens the named pipe and stores that name in the configuration.
/// \param pipe_name Pipe name or full \\.\pipe\ path.
/// \param error Optional output error code.
/// \return true on success.
bool open(const std::string& pipe_name, std::error_code* error = nullptr);

/// \brief Closes the pipe handle.
void close();

/// \brief Returns true while a pipe handle is open.
bool is_connected() const;

/// \brief Alias for is_connected().
bool connected() const;

/// \brief Writes a complete message to the pipe.
/// \param message UTF-8 message.
/// \param error Optional output error code.
/// \return true when the message was fully written.
bool write(const std::string& message, std::error_code* error = nullptr);

/// \brief Reads one complete message, blocking until data is available.
/// \param message Output message.
/// \param error Optional output error code.
/// \return true when a message was read.
bool read(std::string& message, std::error_code* error = nullptr);

/// \brief Reads one message if bytes are already available.
/// \param message Output message.
/// \param error Optional output error code.
/// \return true when a message was read, false when no message is available or on error.
bool try_read(std::string& message, std::error_code* error = nullptr);

/// \brief Waits for one message up to timeout_ms and reads it.
/// \param message Output message.
/// \param timeout_ms Timeout in milliseconds.
/// \param error Optional output error code.
/// \return true when a message was read.
bool read(std::string& message, size_t timeout_ms, std::error_code* error = nullptr);

/// \brief Returns number of bytes currently available in the pipe.
/// \param error Optional output error code.
/// \return Available byte count, or 0 when disconnected or on error.
size_t available(std::error_code* error = nullptr);

/// \brief Flushes pipe write buffers.
/// \param error Optional output error code.
/// \return true on success.
bool flush(std::error_code* error = nullptr);

/// \brief Returns native Windows pipe handle.
HANDLE native_handle() const;

std::function<void()> on_connected; ///< Called after a successful connect.
std::function<void()> on_disconnected; ///< Called after close of an active connection.
std::function<void(const std::string&)> on_message; ///< Called by try_read/read timeout overloads.
std::function<void(const std::error_code&)> on_error; ///< Called when an operation fails.

private:
ClientConfig m_config;
mutable std::mutex m_mutex;
HANDLE m_pipe = INVALID_HANDLE_VALUE;
std::atomic<bool> m_is_connected{false};

static std::wstring make_pipe_path(const std::string& pipe_name);
static DWORD to_dword_timeout(size_t timeout_ms);

bool connect_no_lock(std::error_code* error);
bool read_no_lock(std::string& message, std::error_code* error, bool* disconnected);
bool close_no_lock();
bool validate_config_no_lock(std::error_code* error) const;
void set_error(std::error_code* out, const std::error_code& error) const;
void clear_error(std::error_code* out) const;
};

} // namespace SimpleNamedPipe

/// \note Implementation is included only in header-only mode.
/// When building as a static library, do NOT include the .ipp here.
#ifndef SIMPLE_NAMED_PIPE_STATIC_LIB
#include "NamedPipeClient/NamedPipeClient.ipp"
#endif

#endif // _SIMPLE_NAMED_PIPE_CLIENT_HPP_INCLUDED
36 changes: 36 additions & 0 deletions include/SimpleNamedPipe/NamedPipeClient/ClientConfig.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#pragma once
#ifndef _SIMPLE_NAMED_PIPE_CLIENT_CONFIG_HPP_INCLUDED
#define _SIMPLE_NAMED_PIPE_CLIENT_CONFIG_HPP_INCLUDED

/// \file ClientConfig.hpp
/// \brief Configuration for the named pipe client.

#include <cstddef>
#include <string>

namespace SimpleNamedPipe {

/// \class ClientConfig
/// \brief Named pipe client configuration.
class ClientConfig {
public:
std::string pipe_name; ///< Named pipe name or full \\.\pipe\ path.
size_t buffer_size; ///< Size of I/O buffers.
size_t timeout; ///< Connect/read polling timeout in milliseconds.

/// \brief Construct with optional parameters.
/// \param pipe_name Name of the pipe or full pipe path.
/// \param buffer_size Buffer size in bytes.
/// \param timeout Wait timeout in milliseconds.
ClientConfig(
const std::string& pipe_name = "server",
size_t buffer_size = 65536,
size_t timeout = 5000)
: pipe_name(pipe_name),
buffer_size(buffer_size),
timeout(timeout) {}
};

} // namespace SimpleNamedPipe

#endif // _SIMPLE_NAMED_PIPE_CLIENT_CONFIG_HPP_INCLUDED
Loading
Loading