In my current project I found the need to concurrently track the progress of multiple concurrent components, and having all of them output their (debug) information into a single console window made things very unorganised. So I wrote this little library so I can now display each component's output in a dedicated console window.
#Notes
- As Windows only allows 1 console per process, I need to start a little helper process per additional console. The path is currently hard-coded, but that can easily be changed.
- The
process
wrapper is very minimalistic - it only does what is necessary for this library to work and nothing more. (I might extend it in the future, though.)
#pipe.hpp
#pragma once
#include <string>
#include <Windows.h>
namespace win32
{
enum class pipe_state
{
client,
server_connected,
server_disconnected
};
enum class pipe_mode
{
read,
write,
both
};
struct buffer_size
{
const size_t value;
explicit buffer_size(const size_t value)
: value{ value } {}
};
struct read_buffer_size : public buffer_size
{
explicit read_buffer_size(const size_t value)
: buffer_size{ value } {}
};
struct write_buffer_size : buffer_size
{
explicit write_buffer_size(const size_t value)
: buffer_size{ value } {}
};
struct buffer
{
char *data;
buffer_size size;
buffer(char *data, size_t size)
: data{ data },
size{ size } {}
};
class pipe
{
HANDLE handle;
std::string pipe_name;
pipe_state state;
void connect();
void disconnect();
pipe(HANDLE handle, std::string name, pipe_state state);
public:
static pipe create(
const std::string &name,
pipe_mode mode,
read_buffer_size read_buffer,
write_buffer_size write_buffer);
static pipe open(const std::string &name, pipe_mode mode);
pipe(const pipe &other) = delete;
pipe(pipe &&other) noexcept;
pipe &operator=(const pipe &other) = delete;
pipe &operator=(pipe &&other) noexcept;
virtual ~pipe();
void close();
const std::string &name() const;
size_t read(buffer buf);
void write(const buffer buf);
};
}
#pipe.cpp
#include "pipe.hpp"
#include <thread>
using namespace std::string_literals;
namespace win32
{
constexpr static auto pipe_name_prefix = R"(\\.\pipe\)";
void pipe::connect()
{
if (state != pipe_state::server_disconnected) return;
if (!ConnectNamedPipe(handle, nullptr) && GetLastError() != ERROR_PIPE_CONNECTED)
{
CloseHandle(handle);
throw std::runtime_error("unable to connect to pipe");
}
state = pipe_state::server_connected;
}
void pipe::disconnect()
{
if (state == pipe_state::server_connected)
{
DisconnectNamedPipe(handle);
state = pipe_state::server_disconnected;
}
}
pipe::pipe(HANDLE handle, std::string name, pipe_state state)
: handle{ handle }, pipe_name{ std::move(name) }, state{ state } { }
pipe pipe::create(
const std::string &name,
pipe_mode mode,
read_buffer_size read_buffer,
write_buffer_size write_buffer)
{
DWORD open_mode = 0;
switch (mode)
{
case pipe_mode::both:
open_mode = PIPE_ACCESS_DUPLEX;
break;
case pipe_mode::read:
open_mode = PIPE_ACCESS_INBOUND;
break;
case pipe_mode::write:
open_mode = PIPE_ACCESS_OUTBOUND;
break;
}
auto full_name = pipe_name_prefix + name;
const auto handle = CreateNamedPipeA(
full_name.c_str(),
open_mode,
PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE | PIPE_WAIT,
1,
write_buffer.value,
read_buffer.value,
1,
nullptr);
if (handle == INVALID_HANDLE_VALUE) throw std::runtime_error("unable to create pipe");
return pipe{ handle, name, pipe_state::server_disconnected };
}
pipe pipe::open(const std::string &name, pipe_mode mode)
{
auto full_name = pipe_name_prefix + name;
DWORD open_mode = 0;
switch (mode)
{
case pipe_mode::read:
open_mode = GENERIC_READ;
break;
case pipe_mode::write:
open_mode = GENERIC_WRITE;
break;
case pipe_mode::both:
open_mode = GENERIC_READ | GENERIC_WRITE;
break;
}
for (auto tries = 3; tries > 0; --tries)
{
const auto handle = CreateFileA(full_name.c_str(), open_mode, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (handle != INVALID_HANDLE_VALUE)
{
return pipe{ handle, name, pipe_state::client };
}
if (GetLastError() != ERROR_PIPE_BUSY) throw std::runtime_error("unable to open pipe");
if (!WaitNamedPipeA(name.c_str(), 10000)) throw std::runtime_error("unable to wait on pipe");
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
throw std::runtime_error("unable to open pipe");
}
pipe::pipe(pipe &&other) noexcept
: handle{ other.handle }, pipe_name{ other.pipe_name }, state{ other.state }
{
other.handle = INVALID_HANDLE_VALUE;
}
pipe &pipe::operator=(pipe &&other) noexcept
{
close();
handle = other.handle;
state = other.state;
pipe_name = std::move(other.pipe_name);
other.handle = INVALID_HANDLE_VALUE;
return *this;
}
pipe::~pipe()
{
close();
}
void pipe::close()
{
if (handle != INVALID_HANDLE_VALUE)
{
disconnect();
CloseHandle(handle);
handle = INVALID_HANDLE_VALUE;
}
}
const std::string &pipe::name() const
{
return pipe_name;
}
size_t pipe::read(buffer buf)
{
connect();
DWORD bytes_read;
if (!ReadFile(handle, buf.data, buf.size.value, &bytes_read, nullptr))
{
throw std::runtime_error("pipe read failed");
}
return bytes_read;
}
void pipe::write(const buffer buf)
{
connect();
DWORD bytes_written;
if (!WriteFile(handle, buf.data, buf.size.value, &bytes_written, nullptr) || bytes_written != buf.size.value)
{
throw std::runtime_error("pipe write failed");
}
}
}
#process.hpp
#pragma once
#include <string>
#include <Windows.h>
#include <vector>
namespace win32
{
enum class process_creation_flags : DWORD
{
none = 0,
new_console = CREATE_NEW_CONSOLE,
};
class process
{
std::vector<char> cmdline;
PROCESS_INFORMATION process_info{};
public:
process(
const std::string &executable_path,
const std::string &command_line_args,
process_creation_flags flags = process_creation_flags::none);
process(const process &) = delete;
process(process &&other) noexcept;
process &operator=(const process &) = delete;
process &operator=(process &&other) noexcept;
void terminate(int exit_code = 0);
virtual ~process() noexcept;
};
}
#process.cpp
#include "process.hpp"
namespace win32
{
process::process(const std::string& executable_path, const std::string& command_line_args, process_creation_flags flags)
{
std::copy(std::begin(command_line_args), std::end(command_line_args), std::back_inserter(cmdline));
cmdline.push_back('\0');
STARTUPINFOA startup_info;
GetStartupInfoA(&startup_info);
if (!CreateProcessA(executable_path.c_str(), cmdline.data(), nullptr, nullptr, false, static_cast<DWORD>(flags), nullptr, nullptr, &startup_info, &process_info))
{
throw std::runtime_error("process creation failed!");
}
}
process::process(process &&other) noexcept : cmdline{ std::move(other.cmdline) }, process_info{ other.process_info }
{
other.process_info.hThread = INVALID_HANDLE_VALUE;
other.process_info.hProcess = INVALID_HANDLE_VALUE;
}
process & process::operator=(process &&other) noexcept
{
terminate();
cmdline = std::move(other.cmdline);
process_info = other.process_info;
other.process_info.hThread = INVALID_HANDLE_VALUE;
other.process_info.hProcess = INVALID_HANDLE_VALUE;
return *this;
}
void process::terminate(int exit_code)
{
if (process_info.hThread != INVALID_HANDLE_VALUE)
{
TerminateThread(process_info.hThread, exit_code);
CloseHandle(process_info.hThread);
process_info.hThread = INVALID_HANDLE_VALUE;
}
if (process_info.hProcess != INVALID_HANDLE_VALUE) {
TerminateProcess(process_info.hProcess, exit_code);
CloseHandle(process_info.hProcess);
process_info.hProcess = INVALID_HANDLE_VALUE;
}
}
process::~process() noexcept
{
terminate();
}
}
#console.hpp
#pragma once
#include <string>
#include <vector>
#include "pipe.hpp"
#include "process.hpp"
namespace win32
{
class console_streambuf : public std::basic_streambuf<char>
{
pipe &output;
std::vector<char> buffer;
protected:
int_type overflow(int_type) override;
int_type sync() override;
virtual void publish();
public:
explicit console_streambuf(pipe &output, buffer_size size);
};
class console : public std::ostream
{
pipe output;
process helper;
static std::string create_unique_pipe_name(std::string base);
static std::string build_command_line(std::string title, std::string pipe_name);
public:
explicit console(std::string name);
};
}
#console.cpp
#include "console.hpp"
#include <thread>
#include <utility>
using namespace std::string_literals;
namespace win32
{
std::basic_streambuf<char>::int_type console_streambuf::overflow(int_type character)
{
publish();
if (character != traits_type::eof())
{
*pptr() = character;
pbump(1);
return character;
}
return traits_type::eof();
}
std::basic_streambuf<char>::int_type console_streambuf::sync()
{
publish();
return 0;
}
void console_streambuf::publish()
{
const auto size = pptr() - pbase();
if (size > 0)
{
output.write(win32::buffer{ buffer.data(), static_cast<size_t>(size) });
setp(buffer.data(), buffer.data() + buffer.size());
}
}
console_streambuf::console_streambuf(pipe &output, buffer_size buffer_size)
: output{ output }, buffer(buffer_size.value)
{
const auto begin = buffer.data();
const auto end = begin + buffer.size();
setp(begin, end);
}
constexpr static auto default_buffer_size = 4096u;
const std::string executable_name = "console_helper.exe"s;
std::string console::create_unique_pipe_name(std::string base)
{
return base + std::to_string(::rand());
}
std::string console::build_command_line(std::string title, std::string pipe_name)
{
return executable_name + " \"" + title + "\" \"" + pipe_name + "\"";
}
console::console(std::string name)
: basic_ostream{ new console_streambuf{ output, buffer_size{ default_buffer_size } } },
output{
pipe::create(
create_unique_pipe_name(name),
pipe_mode::write,
read_buffer_size{ 0 },
write_buffer_size{ default_buffer_size })
},
helper{ executable_name, build_command_line(name, output.name()), process_creation_flags::new_console } { }
}
#console_helper.cpp
#include <windows.h>
#include <iostream>
#include "pipe.hpp"
#include <array>
int main(int argc, char *argv[])
{
if(argc != 3)
{
std::cerr << "not enough arguments\n" << "call syntax: console_helper [title] [pipe name]" << std::endl;
return -1;
}
try {
SetConsoleTitleA(argv[1]);
auto pipe = win32::pipe::open(argv[2], win32::pipe_mode::read);
auto buffer = std::array<char, 1024>{};
for (;;)
{
const auto bytes_read = pipe.read(win32::buffer{ buffer.data(), buffer.size() });
std::cout.write(buffer.data(), bytes_read);
}
}
catch(std::exception& ex)
{
std::cerr << "Caught exception: " << ex.what() << std::endl;
std::cin.get();
}
return 0;
}
#Example usage
#include "console.hpp"
#include <iomanip>
int main() {
// creation
win32::console debug{ "debug" };
// using new console for output
debug << "debug console\n";
// std stream manipulators function as usual
debug << std::hex << std::setfill('0') << std::setw(8) << 123456 << "\nflush!\n" << std::flush;
// wait for user input so the display stays up
std::cin.get();
// RAII destroys all used resources and terminates the helper process
}