From a632bb8e796be52929f1541d305910d704e55076 Mon Sep 17 00:00:00 2001 From: Egor Tensin Date: Fri, 16 Oct 2020 10:09:55 +0300 Subject: Process: support pipe redirection --- .appveyor.yml | 2 +- include/winapi/file.hpp | 28 ++++++++ include/winapi/handle.hpp | 51 ++++++++------ include/winapi/pipe.hpp | 26 +++++++ include/winapi/process.hpp | 12 ++++ include/winapi/stream.hpp | 42 +++++++++++ src/file.cpp | 75 ++++++++++++++++++++ src/handle.cpp | 153 +++++++++++++++++++++++++++++++++++++++++ src/pipe.cpp | 46 +++++++++++++ src/process.cpp | 44 ++++++++---- src/stream.cpp | 41 +++++++++++ test/unit_tests/CMakeLists.txt | 2 +- test/unit_tests/process.cpp | 51 ++++++++++---- 13 files changed, 522 insertions(+), 51 deletions(-) create mode 100644 include/winapi/file.hpp create mode 100644 include/winapi/pipe.hpp create mode 100644 include/winapi/stream.hpp create mode 100644 src/file.cpp create mode 100644 src/handle.cpp create mode 100644 src/pipe.cpp create mode 100644 src/stream.cpp diff --git a/.appveyor.yml b/.appveyor.yml index da5c40a..2624062 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -45,7 +45,7 @@ after_build: - appveyor.exe PushArtifact "%APPVEYOR_PROJECT_NAME%-%PLATFORM%-%CONFIGURATION%.zip" test_script: - - '"%install_dir%\bin\winapi-common-unit-tests.exe" -- "--test_exe=%install_dir%\bin\winapi-common-test-echo.exe"' + - '"%install_dir%\bin\winapi-common-unit-tests.exe" -- "--echo_exe=%install_dir%\bin\winapi-common-test-echo.exe"' for: # Build Release on master only to speed things up: diff --git a/include/winapi/file.hpp b/include/winapi/file.hpp new file mode 100644 index 0000000..60a8f1d --- /dev/null +++ b/include/winapi/file.hpp @@ -0,0 +1,28 @@ +// Copyright (c) 2020 Egor Tensin +// This file is part of the "winapi-common" project. +// For details, see https://github.com/egor-tensin/winapi-common. +// Distributed under the MIT License. + +#pragma once + +#include "handle.hpp" + +#include +#include + +namespace winapi { + +class File : private Handle { +public: + explicit File(Handle&& handle) : Handle{std::move(handle)} {} + + static Handle open_for_reading(const std::string&); + static Handle open_for_writing(const std::string&); + + using Handle::close; + + using Handle::read; + using Handle::write; +}; + +} // namespace winapi diff --git a/include/winapi/handle.hpp b/include/winapi/handle.hpp index 9714db3..af37424 100644 --- a/include/winapi/handle.hpp +++ b/include/winapi/handle.hpp @@ -5,50 +5,57 @@ #pragma once -#include "workarounds.hpp" - #include #include -#include +#include #include #include +#include namespace winapi { class Handle { public: Handle() = default; + explicit Handle(HANDLE); + + Handle(Handle&& other) BOOST_NOEXCEPT_OR_NOTHROW; + Handle& operator=(Handle other) BOOST_NOEXCEPT_OR_NOTHROW; + + void swap(Handle& other) BOOST_NOEXCEPT_OR_NOTHROW; + + explicit operator HANDLE() const { return m_impl.get(); } + + bool is_invalid() const; + + void close(); + + bool is_std() const; + static Handle std_in(); + static Handle std_out(); + static Handle std_err(); - explicit Handle(HANDLE raw) : impl{raw} {} + typedef std::vector Buffer; - Handle(Handle&& other) BOOST_NOEXCEPT_OR_NOTHROW { swap(other); } + Buffer read() const; - Handle& operator=(Handle other) BOOST_NOEXCEPT_OR_NOTHROW { - swap(other); - return *this; - } + BOOST_STATIC_CONSTEXPR std::size_t max_chunk_size = 16 * 1024; + bool read_chunk(Buffer& read_chunk) const; - void swap(Handle& other) BOOST_NOEXCEPT_OR_NOTHROW { - using std::swap; - swap(impl, other.impl); - } + void write(const void*, std::size_t nb) const; + void write(const Buffer& buffer) const; - explicit operator HANDLE() const { return impl.get(); } + void inherit(bool yes = true) const; + void dont_inherit() const { inherit(false); } private: struct Close { - void operator()(HANDLE raw) const { - if (raw == NULL || raw == INVALID_HANDLE_VALUE) - return; - const auto ret = ::CloseHandle(raw); - assert(ret); - WINAPI_UNUSED_PARAMETER(ret); - } + void operator()(HANDLE) const; }; - std::unique_ptr impl; + std::unique_ptr m_impl; Handle(const Handle&) = delete; }; diff --git a/include/winapi/pipe.hpp b/include/winapi/pipe.hpp new file mode 100644 index 0000000..ebd99e8 --- /dev/null +++ b/include/winapi/pipe.hpp @@ -0,0 +1,26 @@ +// Copyright (c) 2020 Egor Tensin +// This file is part of the "winapi-common" project. +// For details, see https://github.com/egor-tensin/winapi-common. +// Distributed under the MIT License. + +#pragma once + +#include "handle.hpp" + +namespace winapi { + +class Pipe { +public: + Pipe(); + + Handle& read_end() { return m_read_end; } + const Handle& read_end() const { return m_read_end; } + Handle& write_end() { return m_write_end; } + const Handle& write_end() const { return m_write_end; } + +private: + Handle m_read_end; + Handle m_write_end; +}; + +} // namespace winapi diff --git a/include/winapi/process.hpp b/include/winapi/process.hpp index d20ac38..22317ca 100644 --- a/include/winapi/process.hpp +++ b/include/winapi/process.hpp @@ -3,8 +3,11 @@ // For details, see https://github.com/egor-tensin/winapi-common. // Distributed under the MIT License. +#pragma once + #include "cmd_line.hpp" #include "handle.hpp" +#include "stream.hpp" #include @@ -12,7 +15,16 @@ namespace winapi { class Process { public: + struct IO { + process::Stdin std_in; + process::Stdout std_out; + process::Stderr std_err; + + void close(); + }; + static Process create(const CommandLine&); + static Process create(const CommandLine&, IO); void wait(); diff --git a/include/winapi/stream.hpp b/include/winapi/stream.hpp new file mode 100644 index 0000000..cf85508 --- /dev/null +++ b/include/winapi/stream.hpp @@ -0,0 +1,42 @@ +// Copyright (c) 2020 Egor Tensin +// This file is part of the "winapi-common" project. +// For details, see https://github.com/egor-tensin/winapi-common. +// Distributed under the MIT License. + +#pragma once + +#include "handle.hpp" +#include "pipe.hpp" + +#include +#include + +namespace winapi { +namespace process { + +struct Stream { + Stream(Handle&& handle) : handle{std::move(handle)} {} + + Handle handle; +}; + +struct Stdin : Stream { + Stdin(); + explicit Stdin(const std::string& file); + explicit Stdin(Pipe&); +}; + +struct Stdout : Stream { + Stdout(); + explicit Stdout(const std::string& file); + explicit Stdout(Pipe&); +}; + +struct Stderr : Stream { + Stderr(); + explicit Stderr(const std::string& file); + explicit Stderr(Pipe&); +}; + +} // namespace process +} // namespace winapi diff --git a/src/file.cpp b/src/file.cpp new file mode 100644 index 0000000..cb9be81 --- /dev/null +++ b/src/file.cpp @@ -0,0 +1,75 @@ +// Copyright (c) 2020 Egor Tensin +// This file is part of the "winapi-common" project. +// For details, see https://github.com/egor-tensin/winapi-common. +// Distributed under the MIT License. + +#include +#include +#include +#include + +#include +#include + +namespace winapi { +namespace { + +struct CreateFileParams { + static CreateFileParams read() { + CreateFileParams params; + params.dwDesiredAccess = GENERIC_READ; + params.dwShareMode = FILE_SHARE_READ; + params.dwCreationDisposition = OPEN_EXISTING; + return params; + } + + static CreateFileParams write() { + CreateFileParams params; + params.dwDesiredAccess = GENERIC_WRITE; + params.dwShareMode = FILE_SHARE_READ; + params.dwCreationDisposition = OPEN_ALWAYS; + return params; + } + + DWORD dwDesiredAccess = 0; + DWORD dwShareMode = 0; + DWORD dwCreationDisposition = 0; + +private: + CreateFileParams() = default; +}; + +Handle open_file(const std::string& path, const CreateFileParams& params) { + const auto unicode_path = LR"(\\?\)" + widen(path); + + SECURITY_ATTRIBUTES attributes; + std::memset(&attributes, 0, sizeof(attributes)); + attributes.nLength = sizeof(attributes); + attributes.bInheritHandle = TRUE; + + const auto handle = ::CreateFileW(unicode_path.c_str(), + params.dwDesiredAccess, + params.dwShareMode, + &attributes, + params.dwCreationDisposition, + FILE_ATTRIBUTE_NORMAL, + NULL); + + if (handle == INVALID_HANDLE_VALUE) { + throw error::windows(GetLastError(), "CreateFileW"); + } + + return Handle{handle}; +} + +} // namespace + +Handle File::open_for_reading(const std::string& path) { + return open_file(path, CreateFileParams::read()); +} + +Handle File::open_for_writing(const std::string& path) { + return open_file(path, CreateFileParams::write()); +} + +} // namespace winapi diff --git a/src/handle.cpp b/src/handle.cpp new file mode 100644 index 0000000..a6ab3a5 --- /dev/null +++ b/src/handle.cpp @@ -0,0 +1,153 @@ +// Copyright (c) 2020 Egor Tensin +// This file is part of the "winapi-common" project. +// For details, see https://github.com/egor-tensin/winapi-common. +// Distributed under the MIT License. + +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include + +namespace winapi { +namespace { + +std::runtime_error write_file_incomplete(std::size_t expected, std::size_t actual) { + std::ostringstream oss; + oss << "WriteFile could only write " << actual << " bytes instead of " << expected; + return std::runtime_error{oss.str()}; +} + +bool is_invalid_handle(HANDLE handle) { + return handle == NULL || handle == INVALID_HANDLE_VALUE; +} + +bool is_std_handle(HANDLE handle) { + return handle == ::GetStdHandle(STD_INPUT_HANDLE) || + handle == ::GetStdHandle(STD_OUTPUT_HANDLE) || + handle == ::GetStdHandle(STD_ERROR_HANDLE); +} + +} // namespace + +Handle::Handle(HANDLE impl) : m_impl{impl} {} + +Handle::Handle(Handle&& other) BOOST_NOEXCEPT_OR_NOTHROW { + swap(other); +} + +Handle& Handle::operator=(Handle other) BOOST_NOEXCEPT_OR_NOTHROW { + swap(other); + return *this; +} + +void Handle::swap(Handle& other) BOOST_NOEXCEPT_OR_NOTHROW { + using std::swap; + swap(m_impl, other.m_impl); +} + +bool Handle::is_invalid() const { + return !m_impl || is_invalid_handle(m_impl.get()); +} + +void Handle::close() { + m_impl.reset(); +} + +bool Handle::is_std() const { + return is_std_handle(m_impl.get()); +} + +Handle Handle::std_in() { + return Handle{::GetStdHandle(STD_INPUT_HANDLE)}; +} + +Handle Handle::std_out() { + return Handle{::GetStdHandle(STD_OUTPUT_HANDLE)}; +} + +Handle Handle::std_err() { + return Handle{::GetStdHandle(STD_ERROR_HANDLE)}; +} + +bool Handle::read_chunk(Buffer& buffer) const { + buffer.resize(max_chunk_size); + DWORD nb_read = 0; + + const auto ret = ::ReadFile(m_impl.get(), buffer.data(), buffer.size(), &nb_read, NULL); + + buffer.resize(nb_read); + + if (ret) { + return nb_read != 0; + } + + const auto ec = GetLastError(); + + switch (ec) { + case ERROR_BROKEN_PIPE: + // We've been reading from an anonymous pipe, and it's been closed. + return false; + default: + throw error::windows(ec, "ReadFile"); + } +} + +Handle::Buffer Handle::read() const { + Buffer buffer; + Buffer chunk; + + while (true) { + const auto next = read_chunk(chunk); + + buffer.reserve(buffer.size() + chunk.size()); + buffer.insert(buffer.cend(), chunk.cbegin(), chunk.cend()); + + if (!next) { + break; + } + } + + return buffer; +} + +void Handle::write(const void* data, std::size_t nb) const { + DWORD nb_written = 0; + + const auto ret = ::WriteFile(m_impl.get(), data, nb, &nb_written, NULL); + + if (!ret) { + throw error::windows(GetLastError(), "WriteFile"); + } + + if (nb != nb_written) { + throw write_file_incomplete(nb, nb_written); + } +} + +void Handle::write(const Buffer& buffer) const { + write(buffer.data(), buffer.size()); +} + +void Handle::inherit(bool yes) const { + if (!::SetHandleInformation(m_impl.get(), HANDLE_FLAG_INHERIT, yes ? 1 : 0)) { + throw error::windows(GetLastError(), "SetHandleInformation"); + } +} + +void Handle::Close::operator()(HANDLE impl) const { + if (is_invalid_handle(impl) || is_std_handle(impl)) + return; + const auto ret = ::CloseHandle(impl); + assert(ret); + WINAPI_UNUSED_PARAMETER(ret); +} + +} // namespace winapi diff --git a/src/pipe.cpp b/src/pipe.cpp new file mode 100644 index 0000000..a00a8f6 --- /dev/null +++ b/src/pipe.cpp @@ -0,0 +1,46 @@ +// Copyright (c) 2020 Egor Tensin +// This file is part of the "winapi-common" project. +// For details, see https://github.com/egor-tensin/winapi-common. +// Distributed under the MIT License. + +#include +#include +#include + +#include + +#include + +#include + +namespace winapi { +namespace { + +void create_pipe(Handle& read_end, Handle& write_end) { + HANDLE h_read_end = INVALID_HANDLE_VALUE; + HANDLE h_write_end = INVALID_HANDLE_VALUE; + + SECURITY_ATTRIBUTES attributes; + std::memset(&attributes, 0, sizeof(attributes)); + attributes.nLength = sizeof(attributes); + attributes.bInheritHandle = TRUE; + + BOOST_STATIC_CONSTEXPR DWORD buffer_size = 16 * 1024; + + const auto ret = ::CreatePipe(&h_read_end, &h_write_end, &attributes, buffer_size); + + if (!ret) { + throw error::windows(GetLastError(), "CreatePipe"); + } + + read_end = Handle{h_read_end}; + write_end = Handle{h_write_end}; +} + +} // namespace + +Pipe::Pipe() { + create_pipe(m_read_end, m_write_end); +} + +} // namespace winapi diff --git a/src/process.cpp b/src/process.cpp index 1771328..5887b05 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -21,42 +21,62 @@ namespace { typedef std::vector EscapedCommandLine; -Handle create_process(EscapedCommandLine cmd_line) { - BOOST_STATIC_CONSTEXPR DWORD flags = CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT; +Handle create_process(EscapedCommandLine cmd_line, Process::IO& io) { + BOOST_STATIC_CONSTEXPR DWORD flags = /*CREATE_NO_WINDOW | */ CREATE_UNICODE_ENVIRONMENT; STARTUPINFOW startup_info; std::memset(&startup_info, 0, sizeof(startup_info)); startup_info.cb = sizeof(startup_info); + startup_info.dwFlags = STARTF_USESTDHANDLES; + startup_info.hStdInput = static_cast(io.std_in.handle); + startup_info.hStdOutput = static_cast(io.std_out.handle); + startup_info.hStdError = static_cast(io.std_err.handle); PROCESS_INFORMATION child_info; std::memset(&child_info, 0, sizeof(child_info)); const auto ret = ::CreateProcessW( - NULL, cmd_line.data(), NULL, NULL, FALSE, flags, NULL, NULL, &startup_info, &child_info); + NULL, cmd_line.data(), NULL, NULL, TRUE, flags, NULL, NULL, &startup_info, &child_info); if (!ret) { throw error::windows(GetLastError(), "CreateProcessW"); } - Handle h_process{child_info.hProcess}; - Handle h_thread{child_info.hThread}; + io.close(); - return std::move(h_process); + Handle process{child_info.hProcess}; + Handle thread{child_info.hThread}; + + return std::move(process); } EscapedCommandLine escape_command_line(const CommandLine& cmd_line) { - const auto whole = widen(cmd_line.join()); - return {whole.cbegin(), whole.cend()}; + const auto unicode_cmd_line = widen(cmd_line.join()); + EscapedCommandLine buffer; + buffer.reserve(unicode_cmd_line.size() + 1); + buffer.assign(unicode_cmd_line.cbegin(), unicode_cmd_line.cend()); + buffer.emplace_back(L'\0'); + return buffer; } -Handle create_process(const CommandLine& cmd_line) { - return create_process(escape_command_line(cmd_line)); +Handle create_process(const CommandLine& cmd_line, Process::IO& io) { + return create_process(escape_command_line(cmd_line), io); } } // namespace +void Process::IO::close() { + std_in.handle.close(); + std_out.handle.close(); + std_err.handle.close(); +} + Process Process::create(const CommandLine& cmd_line) { - return Process{create_process(cmd_line)}; + return create(cmd_line, {}); +} + +Process Process::create(const CommandLine& cmd_line, IO io) { + return Process{create_process(cmd_line, io)}; } void Process::wait() { @@ -64,7 +84,7 @@ void Process::wait() { switch (ret) { case WAIT_OBJECT_0: - m_handle = Handle{}; + m_handle.close(); return; case WAIT_FAILED: throw error::windows(GetLastError(), "WaitForSingleObject"); diff --git a/src/stream.cpp b/src/stream.cpp new file mode 100644 index 0000000..b31635e --- /dev/null +++ b/src/stream.cpp @@ -0,0 +1,41 @@ +// Copyright (c) 2020 Egor Tensin +// This file is part of the "winapi-common" project. +// For details, see https://github.com/egor-tensin/winapi-common. +// Distributed under the MIT License. + +#include +#include +#include + +#include +#include + +namespace winapi { +namespace process { + +Stdin::Stdin() : Stream{Handle::std_in()} {} + +Stdout::Stdout() : Stream{Handle::std_out()} {} + +Stderr::Stderr() : Stream{Handle::std_err()} {} + +Stdin::Stdin(const std::string& path) : Stream{File::open_for_reading(path)} {} + +Stdout::Stdout(const std::string& path) : Stream{File::open_for_writing(path)} {} + +Stderr::Stderr(const std::string& path) : Stream{File::open_for_writing(path)} {} + +Stdin::Stdin(Pipe& pipe) : Stream{std::move(pipe.read_end())} { + pipe.write_end().dont_inherit(); +} + +Stdout::Stdout(Pipe& pipe) : Stream{std::move(pipe.write_end())} { + pipe.read_end().dont_inherit(); +} + +Stderr::Stderr(Pipe& pipe) : Stream{std::move(pipe.write_end())} { + pipe.read_end().dont_inherit(); +} + +} // namespace process +} // namespace winapi diff --git a/test/unit_tests/CMakeLists.txt b/test/unit_tests/CMakeLists.txt index 33a6101..4580a57 100644 --- a/test/unit_tests/CMakeLists.txt +++ b/test/unit_tests/CMakeLists.txt @@ -1,6 +1,6 @@ file(GLOB unit_tests_src "*.cpp") add_executable(unit_tests ${unit_tests_src}) -target_link_libraries(unit_tests PRIVATE winapi_common) +target_link_libraries(unit_tests PRIVATE winapi_common winapi_utf8) set_target_properties(unit_tests PROPERTIES OUTPUT_NAME winapi-common-unit-tests) find_package(Boost REQUIRED COMPONENTS unit_test_framework) diff --git a/test/unit_tests/process.cpp b/test/unit_tests/process.cpp index 69327e1..57deef0 100644 --- a/test/unit_tests/process.cpp +++ b/test/unit_tests/process.cpp @@ -4,52 +4,73 @@ // Distributed under the MIT License. #include +#include #include +#include #include #include #include +#include + +using namespace winapi; namespace { -class WhereTestExe { +class WithEchoExe { public: - WhereTestExe() : m_test_exe(find_test_exe()) {} + WithEchoExe() : m_echo_exe(find_echo_exe()) {} - const std::string& get_test_exe() { return m_test_exe; } + const std::string& get_echo_exe() { return m_echo_exe; } private: - static std::string find_test_exe() { - static const std::string param_prefix{"--test_exe="}; - const auto cmd_line = winapi::CommandLine::query(); + static std::string find_echo_exe() { + static const std::string prefix{"--echo_exe="}; + return find_param_value(prefix); + } + + static std::string find_param_value(const std::string& param_prefix) { + const auto cmd_line = CommandLine::query(); const auto& args = cmd_line.get_args(); for (const auto& arg : args) { if (arg.rfind(param_prefix, 0) == 0) { return arg.substr(param_prefix.length()); } } - throw std::runtime_error{"couldn't find parameter --test_exe"}; + throw std::runtime_error{"couldn't find parameter " + param_prefix}; } - const std::string m_test_exe; + const std::string m_echo_exe; }; } // namespace BOOST_AUTO_TEST_SUITE(process_tests) -BOOST_AUTO_TEST_CASE(create_dir) { - const winapi::CommandLine cmd_line{"cmd.exe", {"/c", "dir"}}; - auto process = winapi::Process::create(cmd_line); +BOOST_FIXTURE_TEST_CASE(create_echo, WithEchoExe) { + const CommandLine cmd_line{get_echo_exe()}; + auto process = Process::create(cmd_line); + process.wait(); + BOOST_TEST(true, "Successfully created test process"); +} + +BOOST_FIXTURE_TEST_CASE(create_echo_with_args, WithEchoExe) { + const CommandLine cmd_line{get_echo_exe(), {"1", "2", "3"}}; + auto process = Process::create(cmd_line); process.wait(); } -BOOST_FIXTURE_TEST_CASE(create_test_exe, WhereTestExe) { - const winapi::CommandLine cmd_line{get_test_exe()}; - auto process = winapi::Process::create(cmd_line); +BOOST_FIXTURE_TEST_CASE(create_echo_pipe, WithEchoExe) { + const CommandLine cmd_line{get_echo_exe(), {"1", "2", "3"}}; + Process::IO io; + Pipe stdout_pipe; + io.std_out = process::Stdout{stdout_pipe}; + auto process = Process::create(cmd_line, std::move(io)); + const auto output = stdout_pipe.read_end().read(); process.wait(); - BOOST_TEST(true, "Successfully created test process"); + const auto utf8 = narrow(output); + BOOST_TEST(utf8 == "1\r\n2\r\n3\r\n"); } BOOST_AUTO_TEST_SUITE_END() -- cgit v1.2.3