aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--.appveyor.yml28
-rw-r--r--include/winapi/process.hpp31
-rw-r--r--include/winapi/window_style.hpp27
-rw-r--r--src/process.cpp156
-rw-r--r--test/unit_tests/CMakeLists.txt18
-rw-r--r--test/unit_tests/console.cpp40
-rw-r--r--test/unit_tests/fixtures.hpp31
-rw-r--r--test/unit_tests/process.cpp3
-rw-r--r--test/unit_tests/process_worker.cpp278
-rw-r--r--test/unit_tests/shared/command.hpp142
-rw-r--r--test/unit_tests/shared/console.hpp138
-rw-r--r--test/unit_tests/shared/fixed_size.hpp79
-rw-r--r--test/unit_tests/shared/test_data.hpp24
-rw-r--r--test/unit_tests/shared/worker.hpp86
-rw-r--r--test/unit_tests/worker/worker.cpp118
15 files changed, 1145 insertions, 54 deletions
diff --git a/.appveyor.yml b/.appveyor.yml
index 2624062..2978e33 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -9,6 +9,7 @@ image:
environment:
python_exe: C:\Python36-x64\python.exe
install_dir: C:\Projects\install\winapi-common
+ test_logs_dir: C:\Projects\test_logs
platform:
- Win32
@@ -45,7 +46,32 @@ after_build:
- appveyor.exe PushArtifact "%APPVEYOR_PROJECT_NAME%-%PLATFORM%-%CONFIGURATION%.zip"
test_script:
- - '"%install_dir%\bin\winapi-common-unit-tests.exe" -- "--echo_exe=%install_dir%\bin\winapi-common-test-echo.exe"'
+ - mkdir "%test_logs_dir%"
+ - >-
+ "%install_dir%\bin\winapi-common-unit-tests.exe"
+ --log_level=all
+ "--log_sink=%test_logs_dir%\tests.log"
+ "--report_sink=%test_logs_dir%\report.txt"
+ --run_test=!console_tests,process_console_tests
+ --
+ "--echo_exe=%install_dir%\bin\winapi-common-test-echo.exe"
+ "--worker_exe=%install_dir%\bin\winapi-common-test-worker.exe"
+ - type "%test_logs_dir%\report.txt"
+ - >-
+ start "Console tests" /wait
+ "%install_dir%\bin\winapi-common-unit-tests.exe"
+ --log_level=all
+ "--log_sink=%test_logs_dir%\tests_console.log"
+ "--report_sink=%test_logs_dir%\report_console.txt"
+ --run_test=console_tests,process_console_tests
+ --
+ "--echo_exe=%install_dir%\bin\winapi-common-test-echo.exe"
+ "--worker_exe=%install_dir%\bin\winapi-common-test-worker.exe"
+ - type "%test_logs_dir%\report_console.txt"
+
+on_finish:
+ - 7z.exe a test_logs.zip "%test_logs_dir%"
+ - appveyor.exe PushArtifact test_logs.zip
for:
# Build Release on master only to speed things up:
diff --git a/include/winapi/process.hpp b/include/winapi/process.hpp
index 5c9e3c9..095b50c 100644
--- a/include/winapi/process.hpp
+++ b/include/winapi/process.hpp
@@ -11,6 +11,7 @@
#include "resource.hpp"
#include <boost/config.hpp>
+#include <boost/optional.hpp>
#include <windows.h>
@@ -19,12 +20,40 @@
namespace winapi {
+struct ProcessParameters {
+ enum ConsoleCreationMode {
+ ConsoleNone,
+ ConsoleInherit,
+ ConsoleNew,
+ };
+
+ explicit ProcessParameters(const CommandLine& cmd_line) : cmd_line{cmd_line} {}
+
+ CommandLine cmd_line;
+ boost::optional<process::IO> io;
+ ConsoleCreationMode console_mode = ConsoleNew;
+};
+
+struct ShellParameters : ProcessParameters {
+ explicit ShellParameters(const CommandLine& cmd_line) : ProcessParameters{cmd_line} {}
+
+ static ShellParameters runas(const CommandLine& cmd_line) {
+ ShellParameters params{cmd_line};
+ params.verb = "runas";
+ return params;
+ }
+
+ boost::optional<std::string> verb;
+};
+
class Process {
public:
+ static Process create(ProcessParameters);
static Process create(const CommandLine&);
static Process create(const CommandLine&, process::IO);
- static Process runas(const CommandLine&);
+ static Process shell(const ShellParameters&);
+ static Process shell(const CommandLine&);
bool is_running() const;
void wait() const;
diff --git a/include/winapi/window_style.hpp b/include/winapi/window_style.hpp
new file mode 100644
index 0000000..dd12a56
--- /dev/null
+++ b/include/winapi/window_style.hpp
@@ -0,0 +1,27 @@
+// Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com>
+// 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
+
+namespace winapi {
+
+enum class WindowStyle {
+ // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
+ ForceMinimize = 11,
+ Hide = 0,
+ Maximize = 3,
+ Minimize = 6,
+ Restore = 9,
+ Show = 5,
+ ShowDefault = 10,
+ ShowMaximized = 3,
+ ShowMinimized = 2,
+ ShowMinNoActive = 7,
+ ShowNA = 8,
+ ShowNoActivate = 4,
+ ShowNormal = 1,
+};
+
+} // namespace winapi
diff --git a/src/process.cpp b/src/process.cpp
index c5e68d7..1eca85a 100644
--- a/src/process.cpp
+++ b/src/process.cpp
@@ -30,28 +30,87 @@ namespace {
typedef std::vector<wchar_t> EscapedCommandLine;
-Handle create_process(EscapedCommandLine cmd_line, process::IO& io) {
- BOOST_STATIC_CONSTEXPR DWORD flags = /*CREATE_NO_WINDOW | */ CREATE_UNICODE_ENVIRONMENT;
+EscapedCommandLine escape_command_line(const CommandLine& cmd_line) {
+ const auto unicode_cmd_line = widen(cmd_line.to_string());
+ 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(ProcessParameters& params) {
+ /*
+ * When creating a new console process, the options are:
+ * 1) inherit the parent console (the default),
+ * 2) CREATE_NO_WINDOW,
+ * 3) CREATE_NEW_CONSOLE,
+ * 4) DETACHED_PROCESS.
+ *
+ * Child processes can inherit the console.
+ * By that I mean they will display their output in the same window.
+ * If both the child process and the parent process read from stdin, there
+ * is no way to say which process will read any given input byte.
+ *
+ * There's an excellent guide into all the intricacies of the CreateProcess
+ * system call at
+ *
+ * https://github.com/rprichard/win32-console-docs/blob/master/README.md
+ *
+ * Another useful link is https://ikriv.com/dev/cpp/ConsoleProxy/flags.
+ */
+ BOOST_STATIC_CONSTEXPR DWORD default_dwCreationFlags = 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<HANDLE>(io.std_in.handle);
- startup_info.hStdOutput = static_cast<HANDLE>(io.std_out.handle);
- startup_info.hStdError = static_cast<HANDLE>(io.std_err.handle);
+
+ if (params.io) {
+ startup_info.dwFlags |= STARTF_USESTDHANDLES;
+ startup_info.hStdInput = static_cast<HANDLE>(params.io->std_in.handle);
+ startup_info.hStdOutput = static_cast<HANDLE>(params.io->std_out.handle);
+ startup_info.hStdError = static_cast<HANDLE>(params.io->std_err.handle);
+ }
+
+ auto dwCreationFlags = default_dwCreationFlags;
+
+ switch (params.console_mode) {
+ case ProcessParameters::ConsoleNone:
+ dwCreationFlags |= CREATE_NO_WINDOW;
+ break;
+ case ProcessParameters::ConsoleInherit:
+ // This is the default.
+ break;
+ case ProcessParameters::ConsoleNew:
+ dwCreationFlags |= CREATE_NEW_CONSOLE;
+ break;
+ }
PROCESS_INFORMATION child_info;
std::memset(&child_info, 0, sizeof(child_info));
- const auto ret = ::CreateProcessW(
- NULL, cmd_line.data(), NULL, NULL, TRUE, flags, NULL, NULL, &startup_info, &child_info);
-
- if (!ret) {
- throw error::windows(GetLastError(), "CreateProcessW");
+ {
+ auto cmd_line = escape_command_line(params.cmd_line);
+
+ const auto ret = ::CreateProcessW(NULL,
+ cmd_line.data(),
+ NULL,
+ NULL,
+ TRUE,
+ dwCreationFlags,
+ NULL,
+ NULL,
+ &startup_info,
+ &child_info);
+
+ if (!ret) {
+ throw error::windows(GetLastError(), "CreateProcessW");
+ }
}
- io.close();
+ if (params.io) {
+ params.io->close();
+ }
Handle process{child_info.hProcess};
Handle thread{child_info.hThread};
@@ -59,35 +118,38 @@ Handle create_process(EscapedCommandLine cmd_line, process::IO& io) {
return process;
}
-EscapedCommandLine escape_command_line(const CommandLine& cmd_line) {
- const auto unicode_cmd_line = widen(cmd_line.to_string());
- 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, process::IO& io) {
- return create_process(escape_command_line(cmd_line), io);
-}
-
-Handle shell_execute(const CommandLine& cmd_line) {
- BOOST_STATIC_CONSTEXPR unsigned long flags =
- SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI | SEE_MASK_NO_CONSOLE;
-
- const auto exe_path = widen(cmd_line.get_argv0());
- const auto args = widen(cmd_line.args_to_string());
+Handle shell_execute(const ShellParameters& params) {
+ const auto lpVerb = params.verb ? widen(*params.verb) : L"open";
+ const auto lpFile = widen(params.cmd_line.get_argv0());
+ const auto lpParameters = widen(params.cmd_line.args_to_string());
+
+ BOOST_STATIC_CONSTEXPR unsigned long default_fMask =
+ SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
+
+ auto fMask = default_fMask;
+ auto nShow = SW_SHOWDEFAULT;
+
+ switch (params.console_mode) {
+ case ProcessParameters::ConsoleNone:
+ nShow = SW_HIDE;
+ break;
+ case ProcessParameters::ConsoleInherit:
+ fMask |= SEE_MASK_NO_CONSOLE;
+ break;
+ case ProcessParameters::ConsoleNew:
+ // This is the default.
+ break;
+ }
SHELLEXECUTEINFOW info;
std::memset(&info, 0, sizeof(info));
info.cbSize = sizeof(info);
- info.fMask = flags;
- info.lpVerb = L"runas";
- info.lpFile = exe_path.c_str();
- if (!args.empty())
- info.lpParameters = args.c_str();
- info.nShow = SW_SHOWDEFAULT;
+ info.fMask = fMask;
+ info.lpVerb = lpVerb.c_str();
+ info.lpFile = lpFile.c_str();
+ if (!lpParameters.empty())
+ info.lpParameters = lpParameters.c_str();
+ info.nShow = nShow;
if (!::ShellExecuteExW(&info)) {
throw error::windows(GetLastError(), "ShellExecuteExW");
@@ -98,16 +160,28 @@ Handle shell_execute(const CommandLine& cmd_line) {
} // namespace
+Process Process::create(ProcessParameters params) {
+ return Process{create_process(params)};
+}
+
Process Process::create(const CommandLine& cmd_line) {
- return create(cmd_line, {});
+ ProcessParameters params{cmd_line};
+ return create(std::move(params));
}
Process Process::create(const CommandLine& cmd_line, process::IO io) {
- return Process{create_process(cmd_line, io)};
+ ProcessParameters params{cmd_line};
+ params.io = std::move(io);
+ return create(std::move(params));
+}
+
+Process Process::shell(const ShellParameters& params) {
+ return Process{shell_execute(params)};
}
-Process Process::runas(const CommandLine& cmd_line) {
- return Process{shell_execute(cmd_line)};
+Process Process::shell(const CommandLine& cmd_line) {
+ ShellParameters params{cmd_line};
+ return shell(params);
}
bool Process::is_running() const {
diff --git a/test/unit_tests/CMakeLists.txt b/test/unit_tests/CMakeLists.txt
index fb87230..7311d75 100644
--- a/test/unit_tests/CMakeLists.txt
+++ b/test/unit_tests/CMakeLists.txt
@@ -1,5 +1,7 @@
+file(GLOB shared_src "shared/*.hpp")
+
file(GLOB unit_tests_src "*.cpp" "*.h" "*.hpp" "*.rc")
-add_executable(unit_tests ${unit_tests_src})
+add_executable(unit_tests ${unit_tests_src} ${shared_src})
set_target_properties(unit_tests PROPERTIES OUTPUT_NAME winapi-common-unit-tests)
target_link_libraries(unit_tests PRIVATE winapi_common winapi_utf8)
@@ -11,3 +13,17 @@ install(TARGETS unit_tests RUNTIME DESTINATION bin)
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
install(FILES "$<TARGET_PDB_FILE:unit_tests>" DESTINATION bin OPTIONAL)
endif()
+
+file(GLOB worker_src "worker/*.cpp")
+add_executable(worker ${worker_src} ${shared_src})
+set_target_properties(worker PROPERTIES OUTPUT_NAME winapi-common-test-worker)
+
+target_link_libraries(worker PRIVATE winapi_common winapi_utf8)
+
+find_package(Boost REQUIRED)
+target_link_libraries(worker PRIVATE Boost::disable_autolinking Boost::boost)
+
+install(TARGETS worker RUNTIME DESTINATION bin)
+if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
+ install(FILES "$<TARGET_PDB_FILE:worker>" DESTINATION bin OPTIONAL)
+endif()
diff --git a/test/unit_tests/console.cpp b/test/unit_tests/console.cpp
new file mode 100644
index 0000000..9eb787c
--- /dev/null
+++ b/test/unit_tests/console.cpp
@@ -0,0 +1,40 @@
+// Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com>
+// 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 "shared/console.hpp"
+
+#include <boost/test/unit_test.hpp>
+
+#include <iostream>
+#include <string>
+#include <vector>
+
+BOOST_AUTO_TEST_SUITE(console_tests)
+
+BOOST_AUTO_TEST_CASE(read_last_lines) {
+ std::cout << "abc\ndef" << std::endl;
+ console::Buffer buffer;
+ const auto last = buffer.read_last_line();
+ const auto last2 = buffer.read_last_lines(2);
+ const std::string expected{"def"};
+ const std::vector<std::string> expected2{"abc", "def"};
+ BOOST_TEST(last == expected);
+ BOOST_TEST(last2 == expected2);
+}
+
+BOOST_AUTO_TEST_CASE(read_last_lines_overflow) {
+ console::Buffer buffer;
+ const std::string output(buffer.get_columns() + 5, 'X');
+ std::cout << output << std::endl;
+ buffer.update();
+ const auto last = buffer.read_last_lines(1);
+ const auto last2 = buffer.read_last_lines(2);
+ const std::vector<std::string> expected{"XXXXX"};
+ const std::vector<std::string> expected2{std::string(buffer.get_columns(), 'X'), "XXXXX"};
+ BOOST_TEST(last == expected);
+ BOOST_TEST(last2 == expected2);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/unit_tests/fixtures.hpp b/test/unit_tests/fixtures.hpp
index 293ad35..aaf6571 100644
--- a/test/unit_tests/fixtures.hpp
+++ b/test/unit_tests/fixtures.hpp
@@ -5,6 +5,8 @@
#pragma once
+#include "shared/command.hpp"
+
#include <winapi/cmd_line.hpp>
#include <winapi/file.hpp>
#include <winapi/path.hpp>
@@ -34,18 +36,13 @@ private:
RemoveFileGuard& operator=(const RemoveFileGuard&) = delete;
};
-class WithEchoExe {
+class WithParam {
public:
- WithEchoExe() : m_echo_exe(find_echo_exe()) {}
+ WithParam(const std::string& param_prefix) : m_value(find_param_value(param_prefix)) {}
- const std::string& get_echo_exe() { return m_echo_exe; }
+ const std::string& get_value() { return m_value; }
private:
- 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 = winapi::CommandLine::query();
const auto& args = cmd_line.get_args();
@@ -57,5 +54,21 @@ private:
throw std::runtime_error{"couldn't find parameter " + param_prefix};
}
- std::string m_echo_exe;
+ std::string m_value;
+};
+
+class WithEchoExe : public WithParam {
+public:
+ WithEchoExe() : WithParam{"--echo_exe="} {}
+
+ const std::string& get_echo_exe() { return get_value(); }
+};
+
+class WithWorkerExe : public WithParam {
+public:
+ WithWorkerExe() : WithParam{"--worker_exe="}, m_cmd{worker::Command::create()} {}
+
+ const std::string& get_worker_exe() { return get_value(); }
+
+ worker::Command::Shared m_cmd;
};
diff --git a/test/unit_tests/process.cpp b/test/unit_tests/process.cpp
index cfc50f4..a048c84 100644
--- a/test/unit_tests/process.cpp
+++ b/test/unit_tests/process.cpp
@@ -86,7 +86,8 @@ BOOST_FIXTURE_TEST_CASE(echo_stdin_from_file, WithEchoExe) {
BOOST_FIXTURE_TEST_CASE(echo_runas, WithEchoExe) {
const CommandLine cmd_line{get_echo_exe(), {"foo", "bar"}};
- const auto process = Process::runas(cmd_line);
+ const auto params = ShellParameters::runas(cmd_line);
+ const auto process = Process::shell(params);
process.wait();
BOOST_TEST(process.get_exit_code() == 0);
}
diff --git a/test/unit_tests/process_worker.cpp b/test/unit_tests/process_worker.cpp
new file mode 100644
index 0000000..492ffff
--- /dev/null
+++ b/test/unit_tests/process_worker.cpp
@@ -0,0 +1,278 @@
+// Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com>
+// 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 "fixtures.hpp"
+#include "shared/command.hpp"
+#include "shared/console.hpp"
+#include "shared/test_data.hpp"
+#include "shared/worker.hpp"
+
+#include <winapi/buffer.hpp>
+#include <winapi/cmd_line.hpp>
+#include <winapi/handle.hpp>
+#include <winapi/pipe.hpp>
+#include <winapi/process.hpp>
+#include <winapi/process_io.hpp>
+
+#include <boost/test/unit_test.hpp>
+
+#include <windows.h>
+
+#include <cstddef>
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+using winapi::Buffer;
+using winapi::CommandLine;
+using winapi::Handle;
+using winapi::Pipe;
+using winapi::Process;
+using winapi::ProcessParameters;
+namespace process = winapi::process;
+
+using worker::StdHandles;
+using worker::Worker;
+
+namespace {
+
+void check_window_same(Worker& worker) {
+ BOOST_TEST(::GetConsoleWindow());
+ BOOST_TEST(worker.get_console_window() == ::GetConsoleWindow());
+ BOOST_TEST(worker.is_window_visible());
+}
+
+void check_window_none(Worker& worker) {
+ BOOST_TEST(::GetConsoleWindow());
+ BOOST_TEST(!worker.get_console_window());
+ BOOST_TEST(!worker.is_window_visible());
+}
+
+void check_window_new(Worker& worker) {
+ BOOST_TEST(::GetConsoleWindow());
+ BOOST_TEST(worker.get_console_window());
+ BOOST_TEST(worker.get_console_window() != ::GetConsoleWindow());
+ BOOST_TEST(worker.is_window_visible());
+}
+
+void check_std_handles(Worker& worker, const StdHandles& expected) {
+ const auto actual = worker.get_std_handles();
+ BOOST_TEST(actual.in == expected.in);
+ BOOST_TEST(actual.out == expected.out);
+ BOOST_TEST(actual.err == expected.err);
+}
+
+void check_std_handles_same(Worker& worker) {
+ check_std_handles(worker,
+ {::GetStdHandle(STD_INPUT_HANDLE),
+ ::GetStdHandle(STD_OUTPUT_HANDLE),
+ ::GetStdHandle(STD_ERROR_HANDLE)});
+}
+
+void check_std_handles_different(Worker& worker) {
+ const auto actual = worker.get_std_handles();
+ BOOST_TEST(actual.in);
+ BOOST_TEST(actual.out);
+ BOOST_TEST(actual.err);
+ BOOST_TEST(actual.in != ::GetStdHandle(STD_INPUT_HANDLE));
+ BOOST_TEST(actual.out != ::GetStdHandle(STD_OUTPUT_HANDLE));
+ BOOST_TEST(actual.err != ::GetStdHandle(STD_ERROR_HANDLE));
+}
+
+void check_write(Worker& worker) {
+ const auto handles = worker.test_write();
+ BOOST_TEST(!Handle::is_invalid(handles.in));
+ BOOST_TEST(!Handle::is_invalid(handles.out));
+ BOOST_TEST(!Handle::is_invalid(handles.err));
+}
+
+void check_redirected_output(const Buffer& buffer, const std::vector<std::string>& expected_lines) {
+ const auto actual = buffer.as_utf8();
+ std::ostringstream oss;
+ for (const auto& line : expected_lines) {
+ oss << line << "\r\n";
+ }
+ const std::string expected{oss.str()};
+ BOOST_TEST(actual == expected);
+}
+
+const std::string PLACEHOLDER{"This is a placeholder line."};
+
+void check_console_buffer_inherit(Worker& worker, const std::vector<std::string>& expected) {
+ // Check that
+ // 1) the worker process writes the expected lines to its console window,
+ // 2) the lines appear in this process's window, since they're the same.
+
+ const auto numof_lines = expected.size();
+ // Write some lines in this process's window, they should be overwritten
+ // by worker's writes.
+ for (std::size_t i = 0; i < numof_lines; ++i) {
+ std::cout << PLACEHOLDER << std::endl;
+ }
+ // Order worker to write the lines:
+ const auto handles = worker.test_write();
+ // Query worker process's window:
+ const auto worker_actual = worker.read_last_lines(numof_lines);
+ // Query this process's window:
+ const auto this_actual = console::Buffer{}.read_last_lines(numof_lines);
+
+ // They should look the same:
+ const auto& worker_expected = expected;
+ const auto& this_expected = worker_expected;
+
+ BOOST_TEST(worker_actual == worker_expected);
+ BOOST_TEST(this_actual == this_expected);
+}
+
+void check_console_buffer_new(Worker& worker, const std::vector<std::string>& expected) {
+ // Check that
+ // 1) the worker process writes the expected lines in its console window,
+ // 2) the lines _do not_ appear in this process's window, since they're
+ // different windows.
+
+ const auto numof_lines = expected.size();
+ // Write some lines in this process's window, they _should not_ be
+ // overwritten by worker's writes.
+ for (std::size_t i = 0; i < numof_lines; ++i) {
+ std::cout << PLACEHOLDER << std::endl;
+ }
+ // Order worker to write the lines:
+ const auto handles = worker.test_write();
+ // Query worker process's window:
+ const auto worker_actual = worker.read_last_lines(numof_lines);
+ // Query this process's window:
+ const auto this_actual = console::Buffer{}.read_last_lines(numof_lines);
+
+ // Placeholder lines are still last in this process's window:
+ const auto& worker_expected = expected;
+ const std::vector<std::string> this_expected(numof_lines, PLACEHOLDER);
+
+ BOOST_TEST(worker_actual == worker_expected);
+ BOOST_TEST(this_actual == this_expected);
+}
+
+// CREATE_NO_WINDOW actually does create a new buffer, it's just invisible.
+void check_console_buffer_none(Worker& worker, const std::vector<std::string>& expected) {
+ check_console_buffer_new(worker, expected);
+}
+
+} // namespace
+
+BOOST_AUTO_TEST_SUITE(process_console_tests)
+
+BOOST_FIXTURE_TEST_CASE(create_inherit, WithWorkerExe) {
+ const CommandLine cmd_line{get_worker_exe()};
+ ProcessParameters params{cmd_line};
+ params.console_mode = ProcessParameters::ConsoleInherit;
+
+ Worker worker{Process::create(std::move(params))};
+ check_window_same(worker);
+ check_std_handles_same(worker);
+ check_write(worker);
+ check_console_buffer_inherit(worker, {worker::test_data::out(), worker::test_data::err()});
+ BOOST_TEST(worker.exit() == 0);
+}
+
+BOOST_FIXTURE_TEST_CASE(create_inherit_override, WithWorkerExe) {
+ const CommandLine cmd_line{get_worker_exe()};
+ ProcessParameters params{cmd_line};
+ params.console_mode = ProcessParameters::ConsoleInherit;
+
+ Pipe stdin_pipe, stderr_pipe;
+ const StdHandles expected_handles{
+ stdin_pipe.read_end().get(), Handle::std_out().get(), stderr_pipe.write_end().get()};
+
+ process::IO io;
+ io.std_in = process::Stdin{stdin_pipe};
+ io.std_err = process::Stderr{stderr_pipe};
+ params.io = std::move(io);
+
+ Worker worker{Process::create(std::move(params))};
+ check_window_same(worker);
+ check_std_handles(worker, expected_handles);
+ check_write(worker);
+ check_console_buffer_inherit(worker, {worker::test_data::out()});
+ BOOST_TEST(worker.exit() == 0);
+ check_redirected_output(stderr_pipe.read_end().read(),
+ {worker::test_data::err(), worker::test_data::err()});
+}
+
+BOOST_FIXTURE_TEST_CASE(create_none, WithWorkerExe) {
+ const CommandLine cmd_line{get_worker_exe()};
+ ProcessParameters params{cmd_line};
+ params.console_mode = ProcessParameters::ConsoleNone;
+
+ Worker worker{Process::create(std::move(params))};
+ check_window_none(worker);
+ check_std_handles_different(worker);
+ check_write(worker);
+ check_console_buffer_none(worker, {worker::test_data::out(), worker::test_data::err()});
+ BOOST_TEST(worker.exit() == 0);
+}
+
+BOOST_FIXTURE_TEST_CASE(create_none_override, WithWorkerExe) {
+ const CommandLine cmd_line{get_worker_exe()};
+ ProcessParameters params{cmd_line};
+ params.console_mode = ProcessParameters::ConsoleNone;
+
+ Pipe stdin_pipe, stderr_pipe;
+ const StdHandles expected_handles{
+ stdin_pipe.read_end().get(), Handle::std_out().get(), stderr_pipe.write_end().get()};
+
+ process::IO io;
+ io.std_in = process::Stdin{stdin_pipe};
+ io.std_err = process::Stderr{stderr_pipe};
+ params.io = std::move(io);
+
+ Worker worker{Process::create(std::move(params))};
+ check_window_none(worker);
+ check_std_handles(worker, expected_handles);
+ check_write(worker);
+ check_console_buffer_none(worker, {worker::test_data::out()});
+ BOOST_TEST(worker.exit() == 0);
+ check_redirected_output(stderr_pipe.read_end().read(),
+ {worker::test_data::err(), worker::test_data::err()});
+}
+
+BOOST_FIXTURE_TEST_CASE(create_new, WithWorkerExe) {
+ const CommandLine cmd_line{get_worker_exe()};
+ ProcessParameters params{cmd_line};
+ params.console_mode = ProcessParameters::ConsoleNew;
+
+ Worker worker{Process::create(std::move(params))};
+ check_window_new(worker);
+ check_std_handles_different(worker);
+ check_write(worker);
+ check_console_buffer_new(worker, {worker::test_data::out(), worker::test_data::err()});
+ BOOST_TEST(worker.exit() == 0);
+}
+
+BOOST_FIXTURE_TEST_CASE(create_new_override, WithWorkerExe) {
+ const CommandLine cmd_line{get_worker_exe()};
+ ProcessParameters params{cmd_line};
+ params.console_mode = ProcessParameters::ConsoleNew;
+
+ Pipe stdin_pipe, stderr_pipe;
+ const StdHandles expected_handles{
+ stdin_pipe.read_end().get(), Handle::std_out().get(), stderr_pipe.write_end().get()};
+
+ process::IO io;
+ io.std_in = process::Stdin{stdin_pipe};
+ io.std_err = process::Stderr{stderr_pipe};
+ params.io = std::move(io);
+
+ Worker worker{Process::create(std::move(params))};
+ check_window_new(worker);
+ check_std_handles(worker, expected_handles);
+ check_write(worker);
+ check_console_buffer_new(worker, {worker::test_data::out()});
+ BOOST_TEST(worker.exit() == 0);
+ check_redirected_output(stderr_pipe.read_end().read(),
+ {worker::test_data::err(), worker::test_data::err()});
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/unit_tests/shared/command.hpp b/test/unit_tests/shared/command.hpp
new file mode 100644
index 0000000..de7c704
--- /dev/null
+++ b/test/unit_tests/shared/command.hpp
@@ -0,0 +1,142 @@
+// Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com>
+// 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 "fixed_size.hpp"
+
+#include <winapi/shmem.hpp>
+
+#include <boost/config.hpp>
+#include <boost/interprocess/sync/interprocess_condition.hpp>
+#include <boost/interprocess/sync/interprocess_mutex.hpp>
+#include <boost/interprocess/sync/scoped_lock.hpp>
+
+#include <windows.h>
+
+#include <cstddef>
+#include <exception>
+#include <functional>
+#include <stdexcept>
+
+namespace worker {
+
+struct StdHandles {
+ HANDLE in;
+ HANDLE out;
+ HANDLE err;
+};
+
+class Command {
+public:
+ BOOST_STATIC_CONSTEXPR auto SHMEM_NAME = "shmem-test-cmd";
+
+ typedef winapi::SharedObject<Command> Shared;
+
+ static Shared create() { return Shared::create(SHMEM_NAME); }
+ static Shared open() { return Shared::open(SHMEM_NAME); }
+
+ typedef boost::interprocess::interprocess_mutex mutex;
+ typedef boost::interprocess::interprocess_condition condition_variable;
+ typedef boost::interprocess::scoped_lock<mutex> lock;
+
+ enum Action {
+ EXIT = 1,
+ GET_CONSOLE_WINDOW,
+ IS_WINDOW_VISIBLE,
+ GET_STD_HANDLES,
+ TEST_WRITE,
+ GET_CONSOLE_BUFFER,
+ };
+
+ union Args {
+ std::size_t numof_lines;
+ };
+
+ union Result {
+ HWND console_window;
+ bool is_window_visible;
+ StdHandles std_handles;
+ fixed_size::StringList<> console_buffer;
+ };
+
+ typedef std::function<void(Args&)> SetArgs;
+ typedef std::function<void(const Result&)> ReadResult;
+
+ void get_result(Action action, const SetArgs& set_args, const ReadResult& read_result) {
+ {
+ lock lck{m_mtx};
+ m_action = action;
+ set_args(m_args);
+
+ m_action_requested = true;
+ m_result_ready = false;
+ m_error_occured = false;
+ }
+ m_cv.notify_all();
+
+ lock lck{m_mtx};
+ m_cv.wait(lck, [this]() { return m_error_occured || m_result_ready; });
+
+ const auto error = m_error_occured;
+
+ m_action_requested = false;
+ m_result_ready = false;
+ m_error_occured = false;
+
+ if (error) {
+ throw std::runtime_error{"Worker error: " + m_error.extract()};
+ }
+
+ read_result(m_result);
+ }
+
+ void get_result(Action action, const ReadResult& read_result) {
+ return get_result(
+ action, [](Args&) {}, read_result);
+ }
+
+ void get_result(Action action) {
+ return get_result(action, [](const Result&) {});
+ }
+
+ typedef std::function<void(Action, const Args&, Result&)> ProcessAction;
+
+ void process_action(const ProcessAction& callback) {
+ {
+ lock lck{m_mtx};
+ m_cv.wait(lck, [this]() { return m_action_requested; });
+
+ m_action_requested = false;
+ m_result_ready = false;
+ m_error_occured = false;
+
+ try {
+ callback(m_action, m_args, m_result);
+ m_result_ready = true;
+ } catch (const std::exception& e) {
+ m_error_occured = true;
+ m_error = m_error.convert(e.what());
+ }
+ }
+ m_cv.notify_all();
+ }
+
+private:
+ Action m_action;
+ Args m_args;
+ bool m_action_requested = false;
+
+ Result m_result;
+ bool m_result_ready = false;
+
+ bool m_error_occured = false;
+ fixed_size::String<> m_error;
+
+ mutex m_mtx;
+ condition_variable m_cv;
+};
+
+} // namespace worker
diff --git a/test/unit_tests/shared/console.hpp b/test/unit_tests/shared/console.hpp
new file mode 100644
index 0000000..0a415e5
--- /dev/null
+++ b/test/unit_tests/shared/console.hpp
@@ -0,0 +1,138 @@
+// Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com>
+// 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 <winapi/error.hpp>
+#include <winapi/handle.hpp>
+#include <winapi/utf8.hpp>
+
+#include <boost/algorithm/string.hpp>
+
+#include <windows.h>
+
+#include <cstddef>
+#include <cstring>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace console {
+
+class Buffer {
+public:
+ typedef CONSOLE_SCREEN_BUFFER_INFO Info;
+
+ Buffer() : m_handle{winapi::Handle::std_out()}, m_info{get_info(m_handle)} {}
+
+ Buffer(winapi::Handle&& handle) : m_handle{std::move(handle)}, m_info{get_info(m_handle)} {}
+
+ std::size_t get_columns() const { return m_info.dwSize.X; }
+
+ std::size_t get_lines() const { return m_info.dwSize.Y; }
+
+ std::size_t get_cursor_column() const { return m_info.dwCursorPosition.X; }
+
+ std::size_t get_cursor_line() const { return m_info.dwCursorPosition.Y; }
+
+ void update() { m_info = get_info(m_handle); }
+
+ /*
+ * This is a stupid little function to read the console screen buffer.
+ * It's fragile and will break whenever anything happens (like, if the
+ * console screen is resized).
+ *
+ * The screen buffer:
+ * 1) doesn't preserve line breaks,
+ * 2) pads text lines with spaces (ASCII 0x20) for storage.
+ *
+ * Hence, the "lines" read are one-dimensional arrays, right-trimmed, and
+ * there's no way to learn whether the whole line printed by the user was
+ * read back in its entirety.
+ *
+ * For example, let the console window be 80 columns wide, and the user
+ * prints 85 consecutive characters 'a' using
+ *
+ * std::cout << std::string(85, 'a') << '\n';
+ *
+ * The following holds:
+ * 1) read_lines(-1, -1) == {"aaaaa"},
+ * 2) read_lines(-2, -2) == {std::string(80, 'a'), "aaaaa"}.
+ *
+ * I also don't know how it interacts with tab characters '\t', encodings,
+ * etc. It sucks, don't use it.
+ */
+ std::vector<std::string> read_lines(int top, int bottom) const {
+ if (top < 0) {
+ top = get_cursor_line() + top;
+ }
+ if (bottom < 0) {
+ bottom = get_cursor_line() + bottom;
+ }
+ if (top > bottom) {
+ std::swap(top, bottom);
+ }
+ int numof_lines = bottom - top + 1;
+
+ COORD buffer_size;
+ buffer_size.X = get_columns();
+ buffer_size.Y = numof_lines;
+
+ COORD buffer_coord;
+ buffer_coord.X = 0;
+ buffer_coord.Y = 0;
+
+ std::vector<CHAR_INFO> buffer;
+ buffer.resize(buffer_size.X * buffer_size.Y);
+
+ SMALL_RECT read_region;
+ read_region.Top = top;
+ read_region.Left = 0;
+ read_region.Bottom = bottom;
+ read_region.Right = buffer_size.X - 1;
+
+ if (!::ReadConsoleOutputW(
+ m_handle.get(), buffer.data(), buffer_size, buffer_coord, &read_region)) {
+ throw winapi::error::windows(GetLastError(), "ReadConsoleOutputW");
+ }
+
+ std::vector<std::string> result;
+ for (std::size_t i = 0; i < numof_lines; ++i) {
+ std::wostringstream oss;
+ for (std::size_t c = 0; c < buffer_size.X; ++c) {
+ oss << buffer[i * buffer_size.X + c].Char.UnicodeChar;
+ }
+ result.emplace_back(boost::trim_right_copy(winapi::narrow(oss.str())));
+ }
+
+ return result;
+ }
+
+ std::vector<std::string> read_last_lines(int numof_lines = 1) const {
+ return read_lines(-numof_lines, -1);
+ }
+
+ std::string read_last_line() const { return read_lines(-1, -1)[0]; }
+
+ std::string read_line(int n) const { return read_lines(n, n)[0]; }
+
+private:
+ static Info get_info(const winapi::Handle& handle) {
+ Info dest;
+ std::memset(&dest, 0, sizeof(dest));
+
+ if (!::GetConsoleScreenBufferInfo(static_cast<HANDLE>(handle), &dest)) {
+ throw winapi::error::windows(GetLastError(), "GetConsoleScreenBufferInfo");
+ }
+
+ return dest;
+ }
+
+ winapi::Handle m_handle;
+ Info m_info;
+};
+
+} // namespace console
diff --git a/test/unit_tests/shared/fixed_size.hpp b/test/unit_tests/shared/fixed_size.hpp
new file mode 100644
index 0000000..e4b11c7
--- /dev/null
+++ b/test/unit_tests/shared/fixed_size.hpp
@@ -0,0 +1,79 @@
+// Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com>
+// 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 <array>
+#include <cstddef>
+#include <cstring>
+#include <string>
+#include <vector>
+
+namespace fixed_size {
+
+/*
+ * These are fixed-size classes, to be used as part of an interprocess shared
+ * memory region.
+ */
+
+// 120 characters is an arbitrary limit, strings are cut to this many
+// characters for storage.
+template <std::size_t Length = 120>
+class String : public std::array<char, Length> {
+public:
+ static String convert(const std::string& src) {
+ String dest;
+ std::size_t nch = dest.size() - 1;
+ if (src.size() < nch) {
+ nch = src.size();
+ }
+ std::memcpy(dest.data(), src.c_str(), nch);
+ dest[nch] = '\0';
+ return dest;
+ }
+
+ std::string extract() const {
+ // Lines are null-terminated, and don't store their lenghts, so...
+ return data();
+ }
+
+private:
+};
+
+// 5 lines to store is also arbitrary, set it higher if needed.
+template <std::size_t Length = 5, std::size_t StringLength = 120>
+struct StringList : public std::array<String<StringLength>, Length> {
+ static StringList convert(const std::vector<std::string>& src) {
+ // If src.size() > Length, only the last Length lines from the source
+ // list are stored.
+
+ StringList dest;
+ std::memset(&dest, 0, sizeof(dest));
+
+ std::size_t src_offset = 0;
+ if (src.size() > dest.size()) {
+ src_offset = src.size() - dest.size();
+ }
+
+ for (std::size_t i = 0, j = src_offset; i < dest.size() && j < src.size();
+ ++i, ++j, ++dest.numof_lines) {
+ dest[i] = dest[i].convert(src[j]);
+ }
+
+ return dest;
+ }
+
+ std::vector<std::string> extract() const {
+ std::vector<std::string> dest;
+ for (std::size_t i = 0; i < numof_lines; ++i) {
+ dest.emplace_back(at(i).extract());
+ }
+ return dest;
+ }
+
+ std::size_t numof_lines;
+};
+
+} // namespace fixed_size
diff --git a/test/unit_tests/shared/test_data.hpp b/test/unit_tests/shared/test_data.hpp
new file mode 100644
index 0000000..0094f0b
--- /dev/null
+++ b/test/unit_tests/shared/test_data.hpp
@@ -0,0 +1,24 @@
+// Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com>
+// 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 <string>
+
+namespace worker {
+namespace test_data {
+
+BOOST_STATIC_CONSTEXPR auto str = "Test output.";
+
+inline std::string out() {
+ return "stdout: " + std::string{str};
+}
+
+inline std::string err() {
+ return "stderr: " + std::string{str};
+}
+
+} // namespace test_data
+} // namespace worker
diff --git a/test/unit_tests/shared/worker.hpp b/test/unit_tests/shared/worker.hpp
new file mode 100644
index 0000000..320a924
--- /dev/null
+++ b/test/unit_tests/shared/worker.hpp
@@ -0,0 +1,86 @@
+// Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com>
+// 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 "command.hpp"
+
+#include <winapi/process.hpp>
+
+#include <windows.h>
+
+#include <exception>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace worker {
+
+class Worker {
+public:
+ Worker(winapi::Process&& process) : m_cmd{Command::create()}, m_process{std::move(process)} {}
+
+ ~Worker() {
+ try {
+ if (m_process.is_running()) {
+ exit();
+ }
+ } catch (const std::exception&) {
+ }
+ }
+
+ HWND get_console_window() {
+ HWND ret = NULL;
+ m_cmd->get_result(Command::GET_CONSOLE_WINDOW,
+ [&ret](const Command::Result& result) { ret = result.console_window; });
+ return ret;
+ }
+
+ bool is_window_visible() {
+ bool ret = false;
+ m_cmd->get_result(Command::IS_WINDOW_VISIBLE, [&ret](const Command::Result& result) {
+ ret = result.is_window_visible;
+ });
+ return ret;
+ }
+
+ StdHandles get_std_handles() {
+ StdHandles ret;
+ m_cmd->get_result(Command::GET_STD_HANDLES,
+ [&ret](const Command::Result& result) { ret = result.std_handles; });
+ return ret;
+ }
+
+ StdHandles test_write() {
+ StdHandles ret;
+ m_cmd->get_result(Command::TEST_WRITE,
+ [&ret](const Command::Result& result) { ret = result.std_handles; });
+ return ret;
+ }
+
+ std::vector<std::string> read_last_lines(std::size_t numof_lines) {
+ std::vector<std::string> ret;
+ const auto set_args = [numof_lines](Command::Args& args) {
+ args.numof_lines = numof_lines;
+ };
+ const auto read_result = [&ret](const Command::Result& result) {
+ ret = result.console_buffer.extract();
+ };
+ m_cmd->get_result(Command::GET_CONSOLE_BUFFER, set_args, read_result);
+ return ret;
+ }
+
+ int exit() {
+ m_cmd->get_result(Command::EXIT);
+ m_process.wait();
+ return m_process.get_exit_code();
+ }
+
+private:
+ Command::Shared m_cmd;
+ winapi::Process m_process;
+};
+
+} // namespace worker
diff --git a/test/unit_tests/worker/worker.cpp b/test/unit_tests/worker/worker.cpp
new file mode 100644
index 0000000..9e48deb
--- /dev/null
+++ b/test/unit_tests/worker/worker.cpp
@@ -0,0 +1,118 @@
+// Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com>
+// 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 "../shared/command.hpp"
+#include "../shared/console.hpp"
+#include "../shared/test_data.hpp"
+
+#include <winapi/error.hpp>
+#include <winapi/handle.hpp>
+
+#include <windows.h>
+
+#include <chrono>
+#include <cstddef>
+#include <exception>
+#include <stdexcept>
+#include <string>
+#include <thread>
+#include <vector>
+
+using worker::Command;
+
+namespace {
+
+bool is_window_visible() {
+ HWND window = ::GetConsoleWindow();
+
+ if (window == NULL) {
+ return false;
+ }
+
+ const auto mask = ::GetWindowLongW(window, GWL_STYLE);
+
+ if (!mask) {
+ throw winapi::error::windows(GetLastError(), "GetWindowLongW");
+ }
+
+ return mask & WS_VISIBLE;
+}
+
+winapi::Handle write_to(winapi::Handle dest, const std::string& msg) {
+ try {
+ dest.write(msg + "\r\n");
+ } catch (const std::exception& e) {
+ return winapi::Handle{};
+ }
+ return dest;
+}
+
+std::vector<std::string> get_console_buffer(std::size_t numof_lines) {
+ return console::Buffer{}.read_last_lines(numof_lines);
+}
+
+void process_action(Command::Action action, const Command::Args& args, Command::Result& result) {
+ switch (action) {
+ case Command::EXIT:
+ break;
+
+ case Command::GET_CONSOLE_WINDOW:
+ result.console_window = ::GetConsoleWindow();
+ break;
+
+ case Command::IS_WINDOW_VISIBLE:
+ result.is_window_visible = is_window_visible();
+ break;
+
+ case Command::GET_STD_HANDLES:
+ result.std_handles.in = ::GetStdHandle(STD_INPUT_HANDLE);
+ result.std_handles.out = ::GetStdHandle(STD_OUTPUT_HANDLE);
+ result.std_handles.err = ::GetStdHandle(STD_ERROR_HANDLE);
+ break;
+
+ case Command::TEST_WRITE:
+ result.std_handles.in = winapi::Handle::std_in().get();
+ result.std_handles.out =
+ write_to(winapi::Handle::std_out(), worker::test_data::out()).get();
+ result.std_handles.err =
+ write_to(winapi::Handle::std_err(), worker::test_data::err()).get();
+ break;
+
+ case Command::GET_CONSOLE_BUFFER:
+ result.console_buffer =
+ result.console_buffer.convert(get_console_buffer(args.numof_lines));
+ break;
+
+ default:
+ throw std::runtime_error{"invalid worker command"};
+ }
+}
+
+int loop() {
+ auto exit_loop = false;
+
+ const auto cmd = Command::open();
+
+ while (!exit_loop) {
+ cmd->process_action([&exit_loop](Command::Action action,
+ const Command::Args& args,
+ Command::Result& result) {
+ if (action == Command::EXIT) {
+ exit_loop = true;
+ } else {
+ process_action(action, args, result);
+ }
+ });
+ }
+ return 0;
+}
+
+} // namespace
+
+int main(int argc, char* argv[]) {
+ int ec = loop();
+ std::this_thread::sleep_for(std::chrono::milliseconds{1000});
+ return ec;
+}