7
\$\begingroup\$

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
}
\$\endgroup\$
  • \$\begingroup\$ skimming through, seems reasonable. I would wrap the HANDLE tightly in its own RTTI construct. \$\endgroup\$ – JDługosz May 25 '18 at 13:22
  • \$\begingroup\$ @JDługosz, please don't review code in comments. I know your comment isn't an elaborate answer, but for future reference: please add an actual answer. \$\endgroup\$ – Daniel May 25 '18 at 13:28
  • \$\begingroup\$ @JDługosz: I though about it, but it would get complicated. there are already 3 types of HANDLEs used in the code (pipe, process and thread), each requiring different acquire and release procedures and providing different capabilities, so there was little commonality (basically the only code duplication are the calls to CloseHandle between those). \$\endgroup\$ – hoffmale May 25 '18 at 13:32
  • \$\begingroup\$ Common destructor is enough, as that is the point! Copy constructor calling DuplicateHandle is common, too. I don’t have a kill as part of my HANDLE wrapper. Automatically doing the move semantics would be handy for you. \$\endgroup\$ – JDługosz May 25 '18 at 22:14
  • \$\begingroup\$ Have you considered using ANSI escape code to split up your debug output within the same console? You could print each component's output in its own place, splitting the entire console vertically. It wouldn't be possible to scroll back, but depending on your use case it might be a solution? \$\endgroup\$ – maxb Oct 15 '18 at 10:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.