8
\$\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\$
9
  • \$\begingroup\$ skimming through, seems reasonable. I would wrap the HANDLE tightly in its own RTTI construct. \$\endgroup\$
    – JDługosz
    Commented May 25, 2018 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
    Commented May 25, 2018 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
    Commented May 25, 2018 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
    Commented May 25, 2018 at 22:14
  • 1
    \$\begingroup\$ @JonoJames You need to compile console_helper.cpp into its own executable (the "console_helper.exe" mentioned in console.cpp). \$\endgroup\$
    – hoffmale
    Commented Aug 2, 2022 at 12:25

1 Answer 1

1
\$\begingroup\$

ITNOA

Process class:

I suggest using boost::process::child for removing extra process class, and use well defined boost version. (for creating new windows you can see Boost process open process in new window (Windows))

Pipe class:

I suggest using boost::process::pipe instead of creating pipe class and reinventing wheel.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for your answer. Sadly, boost::process doesn't have the flag for a new independent console of the child process, and I haven't been able to figure out if they have wrappers for named pipes. Unless I have overlooked something, it seems boost::process doesn't offer what I need in at least one of those two cases. \$\endgroup\$
    – hoffmale
    Commented Oct 30, 2021 at 17:21
  • \$\begingroup\$ I check your code, and your flags using in above code that supported in boost::process::detail::winapi \$\endgroup\$ Commented Oct 30, 2021 at 18:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.