Skip to main content
4 of 4
fixed small bug, `process::terminate` used hardcoded default value in calls instead of the provided parameter
hoffmale
  • 6.5k
  • 17
  • 41

Using multiple console windows for output

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
}
hoffmale
  • 6.5k
  • 17
  • 41