From eda153e72827d47397dcd00cd77d3de827722369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20M=C3=BCller?= Date: Wed, 17 Feb 2021 14:23:19 +0100 Subject: [PATCH] Vendor tiny-process-library Fixes #113. --- .gitmodules | 3 - external/external.cmake | 24 +-- external/tiny-process-library | 1 - external/tpl/CMakeLists.txt | 66 ++++++ external/tpl/process.cpp | 55 +++++ external/tpl/process.hpp | 169 +++++++++++++++ external/tpl/process_unix.cpp | 376 ++++++++++++++++++++++++++++++++++ external/tpl/process_win.cpp | 350 +++++++++++++++++++++++++++++++ 8 files changed, 1021 insertions(+), 23 deletions(-) delete mode 160000 external/tiny-process-library create mode 100644 external/tpl/CMakeLists.txt create mode 100644 external/tpl/process.cpp create mode 100644 external/tpl/process.hpp create mode 100644 external/tpl/process_unix.cpp create mode 100644 external/tpl/process_win.cpp diff --git a/.gitmodules b/.gitmodules index 28186a8..3b26f01 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "external/type_safe"] path = external/type_safe url = https://github.com/foonathan/type_safe -[submodule "external/tiny-process-library"] - path = external/tiny-process-library - url = https://gitlab.com/eidheim/tiny-process-library.git [submodule "external/cxxopts"] path = external/cxxopts url = https://github.com/jarro2783/cxxopts diff --git a/external/external.cmake b/external/external.cmake index 2da7599..ef3582b 100644 --- a/external/external.cmake +++ b/external/external.cmake @@ -16,27 +16,13 @@ endif() # # install the tiny-process-library # -message(STATUS "Installing tiny-process-library via submodule") -execute_process(COMMAND git submodule update --init -- external/tiny-process-library - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) find_package(Threads REQUIRED QUIET) # create a target here instead of using the one provided -set(tiny_process_dir ${CMAKE_CURRENT_SOURCE_DIR}/external/tiny-process-library) -if(WIN32) - add_library(_cppast_tiny_process EXCLUDE_FROM_ALL - ${tiny_process_dir}/process.hpp - ${tiny_process_dir}/process.cpp - ${tiny_process_dir}/process_win.cpp) -else() - add_library(_cppast_tiny_process EXCLUDE_FROM_ALL - ${tiny_process_dir}/process.hpp - ${tiny_process_dir}/process.cpp - ${tiny_process_dir}/process_unix.cpp) -endif() -target_include_directories(_cppast_tiny_process PUBLIC ${tiny_process_dir}) -target_link_libraries(_cppast_tiny_process PUBLIC Threads::Threads) -set_target_properties(_cppast_tiny_process PROPERTIES CXX_STANDARD 11) +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/external/tpl) +add_library(_cppast_tiny_process INTERFACE) +target_include_directories(_cppast_tiny_process INTERFACE ${tiny_process_dir}) +target_link_libraries(_cppast_tiny_process INTERFACE tiny-process-library::tiny-process-library Threads::Threads) # # install cxxopts, if needed @@ -114,7 +100,7 @@ function(_cppast_find_llvm_config) find_program(LLVM_CONFIG_BINARY "llvm-config" "${LLVM_DOWNLOAD_DIR}/bin" NO_DEFAULT_PATH) else() find_program(llvm_config_binary_no_suffix llvm-config) - find_program(llvm_config_binary_suffix NAMES llvm-config-7 llvm-config-6.0 llvm-config-5.0 llvm-config-4.0) + find_program(llvm_config_binary_suffix NAMES llvm-config-10 llvm-config-9 llvm-config-8 llvm-config-7 llvm-config-6.0 llvm-config-5.0 llvm-config-4.0) if(NOT llvm_config_binary_no_suffix) set(LLVM_CONFIG_BINARY ${llvm_config_binary_suffix} CACHE INTERNAL "") diff --git a/external/tiny-process-library b/external/tiny-process-library deleted file mode 160000 index a677327..0000000 --- a/external/tiny-process-library +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a6773276efdbc322f1abb0159301f5502647eb52 diff --git a/external/tpl/CMakeLists.txt b/external/tpl/CMakeLists.txt new file mode 100644 index 0000000..8662ba3 --- /dev/null +++ b/external/tpl/CMakeLists.txt @@ -0,0 +1,66 @@ +cmake_minimum_required(VERSION 3.1) + +project(tiny-process-library) + +if(CMAKE_SOURCE_DIR STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}") + option(BUILD_TESTING "set ON to build library tests" ON) +else() + option(BUILD_TESTING "set ON to build library tests" OFF) +endif() + +add_library(tiny-process-library process.cpp) +add_library(tiny-process-library::tiny-process-library ALIAS tiny-process-library) + +if(MSVC) + target_compile_definitions(tiny-process-library PRIVATE /D_CRT_SECURE_NO_WARNINGS) +else() + target_compile_options(tiny-process-library PRIVATE -std=c++11 -Wall -Wextra) +endif() + +if(WIN32) + set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) + target_sources(tiny-process-library PRIVATE process_win.cpp) + #If compiled using MSYS2, use sh to run commands + if(MSYS) + target_compile_definitions(tiny-process-library PUBLIC MSYS_PROCESS_USE_SH) + endif() +else() + target_sources(tiny-process-library PRIVATE process_unix.cpp) +endif() + +find_package(Threads REQUIRED) + +target_link_libraries(tiny-process-library ${CMAKE_THREAD_LIBS_INIT}) +target_include_directories(tiny-process-library PUBLIC $ + $) + +# if tiny-process-library is not a sub-project: +if(CMAKE_SOURCE_DIR STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}") + if(MSVC) + add_definitions(/D_CRT_SECURE_NO_WARNINGS) + else() + add_compile_options(-std=c++11 -Wall -Wextra) + endif() + + add_executable(examples examples.cpp) + target_link_libraries(examples tiny-process-library) + + install(TARGETS tiny-process-library + EXPORT ${PROJECT_NAME}-config + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin) + + install(EXPORT ${PROJECT_NAME}-config + FILE ${PROJECT_NAME}-config.cmake + NAMESPACE ${PROJECT_NAME}:: + DESTINATION lib/cmake/${PROJECT_NAME} + ) + + install(FILES process.hpp DESTINATION include) +endif() + +if(BUILD_TESTING) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/external/tpl/process.cpp b/external/tpl/process.cpp new file mode 100644 index 0000000..a68e518 --- /dev/null +++ b/external/tpl/process.cpp @@ -0,0 +1,55 @@ +#include "process.hpp" + +namespace TinyProcessLib { + +Process::Process(const std::vector &arguments, const string_type &path, + std::function read_stdout, + std::function read_stderr, + bool open_stdin, const Config &config) noexcept + : closed(true), read_stdout(std::move(read_stdout)), read_stderr(std::move(read_stderr)), open_stdin(open_stdin), config(config) { + open(arguments, path); + async_read(); +} + +Process::Process(const string_type &command, const string_type &path, + std::function read_stdout, + std::function read_stderr, + bool open_stdin, const Config &config) noexcept + : closed(true), read_stdout(std::move(read_stdout)), read_stderr(std::move(read_stderr)), open_stdin(open_stdin), config(config) { + open(command, path); + async_read(); +} + +Process::Process(const std::vector &arguments, const string_type &path, + const environment_type &environment, + std::function read_stdout, + std::function read_stderr, + bool open_stdin, const Config &config) noexcept + : closed(true), read_stdout(std::move(read_stdout)), read_stderr(std::move(read_stderr)), open_stdin(open_stdin), config(config) { + open(arguments, path, &environment); + async_read(); +} + +Process::Process(const string_type &command, const string_type &path, + const environment_type &environment, + std::function read_stdout, + std::function read_stderr, + bool open_stdin, const Config &config) noexcept + : closed(true), read_stdout(std::move(read_stdout)), read_stderr(std::move(read_stderr)), open_stdin(open_stdin), config(config) { + open(command, path, &environment); + async_read(); +} + +Process::~Process() noexcept { + close_fds(); +} + +Process::id_type Process::get_id() const noexcept { + return data.id; +} + +bool Process::write(const std::string &str) { + return write(str.c_str(), str.size()); +} + +} // namespace TinyProcessLib diff --git a/external/tpl/process.hpp b/external/tpl/process.hpp new file mode 100644 index 0000000..ad1ab3a --- /dev/null +++ b/external/tpl/process.hpp @@ -0,0 +1,169 @@ +#ifndef TINY_PROCESS_LIBRARY_HPP_ +#define TINY_PROCESS_LIBRARY_HPP_ +#include +#include +#include +#include +#include +#include +#include +#ifndef _WIN32 +#include +#endif + +namespace TinyProcessLib { +/// Additional parameters to Process constructors. +struct Config { + /// Buffer size for reading stdout and stderr. Default is 131072 (128 kB). + std::size_t buffer_size = 131072; + /// Set to true to inherit file descriptors from parent process. Default is false. + /// On Windows: has no effect unless read_stdout==nullptr, read_stderr==nullptr and open_stdin==false. + bool inherit_file_descriptors = false; + + /// On Windows only: controls how the process is started, mimics STARTUPINFO's wShowWindow. + /// See: https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/ns-processthreadsapi-startupinfoa + /// and https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-showwindow + enum class ShowWindow { + hide = 0, + show_normal = 1, + show_minimized = 2, + maximize = 3, + show_maximized = 3, + show_no_activate = 4, + show = 5, + minimize = 6, + show_min_no_active = 7, + show_na = 8, + restore = 9, + show_default = 10, + force_minimize = 11 + }; + /// On Windows only: controls how the window is shown. + ShowWindow show_window{ShowWindow::show_default}; +}; + +/// Platform independent class for creating processes. +/// Note on Windows: it seems not possible to specify which pipes to redirect. +/// Thus, at the moment, if read_stdout==nullptr, read_stderr==nullptr and open_stdin==false, +/// the stdout, stderr and stdin are sent to the parent process instead. +class Process { +public: +#ifdef _WIN32 + typedef unsigned long id_type; // Process id type + typedef void *fd_type; // File descriptor type +#ifdef UNICODE + typedef std::wstring string_type; +#else + typedef std::string string_type; +#endif +#else + typedef pid_t id_type; + typedef int fd_type; + typedef std::string string_type; +#endif + typedef std::unordered_map environment_type; + +private: + class Data { + public: + Data() noexcept; + id_type id; +#ifdef _WIN32 + void *handle{nullptr}; +#endif + int exit_status{-1}; + }; + +public: + /// Starts a process with the environment of the calling process. + Process(const std::vector &arguments, const string_type &path = string_type(), + std::function read_stdout = nullptr, + std::function read_stderr = nullptr, + bool open_stdin = false, + const Config &config = {}) noexcept; + /// Starts a process with the environment of the calling process. + Process(const string_type &command, const string_type &path = string_type(), + std::function read_stdout = nullptr, + std::function read_stderr = nullptr, + bool open_stdin = false, + const Config &config = {}) noexcept; + + /// Starts a process with specified environment. + Process(const std::vector &arguments, + const string_type &path, + const environment_type &environment, + std::function read_stdout = nullptr, + std::function read_stderr = nullptr, + bool open_stdin = false, + const Config &config = {}) noexcept; + /// Starts a process with specified environment. + Process(const string_type &command, + const string_type &path, + const environment_type &environment, + std::function read_stdout = nullptr, + std::function read_stderr = nullptr, + bool open_stdin = false, + const Config &config = {}) noexcept; /// Starts a process with specified environment. +#ifndef _WIN32 + /// Starts a process with the environment of the calling process. + /// Supported on Unix-like systems only. + Process(const std::function &function, + std::function read_stdout = nullptr, + std::function read_stderr = nullptr, + bool open_stdin = false, + const Config &config = {}) noexcept; +#endif + ~Process() noexcept; + + /// Get the process id of the started process. + id_type get_id() const noexcept; + /// Wait until process is finished, and return exit status. + int get_exit_status() noexcept; + /// If process is finished, returns true and sets the exit status. Returns false otherwise. + bool try_get_exit_status(int &exit_status) noexcept; + /// Write to stdin. + bool write(const char *bytes, size_t n); + /// Write to stdin. Convenience function using write(const char *, size_t). + bool write(const std::string &str); + /// Close stdin. If the process takes parameters from stdin, use this to notify that all parameters have been sent. + void close_stdin() noexcept; + + /// Kill the process. force=true is only supported on Unix-like systems. + void kill(bool force = false) noexcept; + /// Kill a given process id. Use kill(bool force) instead if possible. force=true is only supported on Unix-like systems. + static void kill(id_type id, bool force = false) noexcept; +#ifndef _WIN32 + /// Send the signal signum to the process. + void signal(int signum) noexcept; +#endif + +private: + Data data; + bool closed; + std::mutex close_mutex; + std::function read_stdout; + std::function read_stderr; +#ifndef _WIN32 + std::thread stdout_stderr_thread; +#else + std::thread stdout_thread, stderr_thread; +#endif + bool open_stdin; + std::mutex stdin_mutex; + + Config config; + + std::unique_ptr stdout_fd, stderr_fd, stdin_fd; + + id_type open(const std::vector &arguments, const string_type &path, const environment_type *environment = nullptr) noexcept; + id_type open(const string_type &command, const string_type &path, const environment_type *environment = nullptr) noexcept; +#ifndef _WIN32 + id_type open(const std::function &function) noexcept; +#endif + void async_read() noexcept; + void close_fds() noexcept; +}; + +} // namespace TinyProcessLib + +#endif // TINY_PROCESS_LIBRARY_HPP_ diff --git a/external/tpl/process_unix.cpp b/external/tpl/process_unix.cpp new file mode 100644 index 0000000..01d6ca1 --- /dev/null +++ b/external/tpl/process_unix.cpp @@ -0,0 +1,376 @@ +#include "process.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace TinyProcessLib { + +Process::Data::Data() noexcept : id(-1) {} + +Process::Process(const std::function &function, + std::function read_stdout, + std::function read_stderr, + bool open_stdin, const Config &config) noexcept + : closed(true), read_stdout(std::move(read_stdout)), read_stderr(std::move(read_stderr)), open_stdin(open_stdin), config(config) { + open(function); + async_read(); +} + +Process::id_type Process::open(const std::function &function) noexcept { + if(open_stdin) + stdin_fd = std::unique_ptr(new fd_type); + if(read_stdout) + stdout_fd = std::unique_ptr(new fd_type); + if(read_stderr) + stderr_fd = std::unique_ptr(new fd_type); + + int stdin_p[2], stdout_p[2], stderr_p[2]; + + if(stdin_fd && pipe(stdin_p) != 0) + return -1; + if(stdout_fd && pipe(stdout_p) != 0) { + if(stdin_fd) { + close(stdin_p[0]); + close(stdin_p[1]); + } + return -1; + } + if(stderr_fd && pipe(stderr_p) != 0) { + if(stdin_fd) { + close(stdin_p[0]); + close(stdin_p[1]); + } + if(stdout_fd) { + close(stdout_p[0]); + close(stdout_p[1]); + } + return -1; + } + + id_type pid = fork(); + + if(pid < 0) { + if(stdin_fd) { + close(stdin_p[0]); + close(stdin_p[1]); + } + if(stdout_fd) { + close(stdout_p[0]); + close(stdout_p[1]); + } + if(stderr_fd) { + close(stderr_p[0]); + close(stderr_p[1]); + } + return pid; + } + else if(pid == 0) { + if(stdin_fd) + dup2(stdin_p[0], 0); + if(stdout_fd) + dup2(stdout_p[1], 1); + if(stderr_fd) + dup2(stderr_p[1], 2); + if(stdin_fd) { + close(stdin_p[0]); + close(stdin_p[1]); + } + if(stdout_fd) { + close(stdout_p[0]); + close(stdout_p[1]); + } + if(stderr_fd) { + close(stderr_p[0]); + close(stderr_p[1]); + } + + if(!config.inherit_file_descriptors) { + // Optimization on some systems: using 8 * 1024 (Debian's default _SC_OPEN_MAX) as fd_max limit + int fd_max = std::min(8192, static_cast(sysconf(_SC_OPEN_MAX))); // Truncation is safe + if(fd_max < 0) + fd_max = 8192; + for(int fd = 3; fd < fd_max; fd++) + close(fd); + } + + setpgid(0, 0); + //TODO: See here on how to emulate tty for colors: http://stackoverflow.com/questions/1401002/trick-an-application-into-thinking-its-stdin-is-interactive-not-a-pipe + //TODO: One solution is: echo "command;exit"|script -q /dev/null + + if(function) + function(); + + _exit(EXIT_FAILURE); + } + + if(stdin_fd) + close(stdin_p[0]); + if(stdout_fd) + close(stdout_p[1]); + if(stderr_fd) + close(stderr_p[1]); + + if(stdin_fd) + *stdin_fd = stdin_p[1]; + if(stdout_fd) + *stdout_fd = stdout_p[0]; + if(stderr_fd) + *stderr_fd = stderr_p[0]; + + closed = false; + data.id = pid; + return pid; +} + +Process::id_type Process::open(const std::vector &arguments, const string_type &path, const environment_type *environment) noexcept { + return open([&arguments, &path, &environment] { + if(arguments.empty()) + exit(127); + + std::vector argv_ptrs; + argv_ptrs.reserve(arguments.size() + 1); + for(auto &argument : arguments) + argv_ptrs.emplace_back(argument.c_str()); + argv_ptrs.emplace_back(nullptr); + + if(!path.empty()) { + if(chdir(path.c_str()) != 0) + exit(1); + } + + if(!environment) + execv(arguments[0].c_str(), const_cast(argv_ptrs.data())); + else { + std::vector env_strs; + std::vector env_ptrs; + env_strs.reserve(environment->size()); + env_ptrs.reserve(environment->size() + 1); + for(const auto &e : *environment) { + env_strs.emplace_back(e.first + '=' + e.second); + env_ptrs.emplace_back(env_strs.back().c_str()); + } + env_ptrs.emplace_back(nullptr); + + execve(arguments[0].c_str(), const_cast(argv_ptrs.data()), const_cast(env_ptrs.data())); + } + }); +} + +Process::id_type Process::open(const std::string &command, const std::string &path, const environment_type *environment) noexcept { + return open([&command, &path, &environment] { + auto command_c_str = command.c_str(); + std::string cd_path_and_command; + if(!path.empty()) { + auto path_escaped = path; + size_t pos = 0; + // Based on https://www.reddit.com/r/cpp/comments/3vpjqg/a_new_platform_independent_process_library_for_c11/cxsxyb7 + while((pos = path_escaped.find('\'', pos)) != std::string::npos) { + path_escaped.replace(pos, 1, "'\\''"); + pos += 4; + } + cd_path_and_command = "cd '" + path_escaped + "' && " + command; // To avoid resolving symbolic links + command_c_str = cd_path_and_command.c_str(); + } + + if(!environment) + execl("/bin/sh", "/bin/sh", "-c", command_c_str, nullptr); + else { + std::vector env_strs; + std::vector env_ptrs; + env_strs.reserve(environment->size()); + env_ptrs.reserve(environment->size() + 1); + for(const auto &e : *environment) { + env_strs.emplace_back(e.first + '=' + e.second); + env_ptrs.emplace_back(env_strs.back().c_str()); + } + env_ptrs.emplace_back(nullptr); + execle("/bin/sh", "/bin/sh", "-c", command_c_str, nullptr, env_ptrs.data()); + } + }); +} + +void Process::async_read() noexcept { + if(data.id <= 0 || (!stdout_fd && !stderr_fd)) + return; + + stdout_stderr_thread = std::thread([this] { + std::vector pollfds; + std::bitset<2> fd_is_stdout; + if(stdout_fd) { + fd_is_stdout.set(pollfds.size()); + pollfds.emplace_back(); + pollfds.back().fd = fcntl(*stdout_fd, F_SETFL, fcntl(*stdout_fd, F_GETFL) | O_NONBLOCK) == 0 ? *stdout_fd : -1; + pollfds.back().events = POLLIN; + } + if(stderr_fd) { + pollfds.emplace_back(); + pollfds.back().fd = fcntl(*stderr_fd, F_SETFL, fcntl(*stderr_fd, F_GETFL) | O_NONBLOCK) == 0 ? *stderr_fd : -1; + pollfds.back().events = POLLIN; + } + auto buffer = std::unique_ptr(new char[config.buffer_size]); + bool any_open = !pollfds.empty(); + while(any_open && (poll(pollfds.data(), static_cast(pollfds.size()), -1) > 0 || errno == EINTR)) { + any_open = false; + for(size_t i = 0; i < pollfds.size(); ++i) { + if(pollfds[i].fd >= 0) { + if(pollfds[i].revents & POLLIN) { + const ssize_t n = read(pollfds[i].fd, buffer.get(), config.buffer_size); + if(n > 0) { + if(fd_is_stdout[i]) + read_stdout(buffer.get(), static_cast(n)); + else + read_stderr(buffer.get(), static_cast(n)); + } + else if(n < 0 && errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) { + pollfds[i].fd = -1; + continue; + } + } + if(pollfds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) { + pollfds[i].fd = -1; + continue; + } + any_open = true; + } + } + } + }); +} + +int Process::get_exit_status() noexcept { + if(data.id <= 0) + return -1; + + int exit_status; + id_type pid; + do { + pid = waitpid(data.id, &exit_status, 0); + } while(pid < 0 && errno == EINTR); + + if(pid < 0 && errno == ECHILD) { + // PID doesn't exist anymore, return previously sampled exit status (or -1) + return data.exit_status; + } + else { + // Store exit status for future calls + if(exit_status >= 256) + exit_status = exit_status >> 8; + data.exit_status = exit_status; + } + + { + std::lock_guard lock(close_mutex); + closed = true; + } + close_fds(); + + return exit_status; +} + +bool Process::try_get_exit_status(int &exit_status) noexcept { + if(data.id <= 0) + return false; + + const id_type pid = waitpid(data.id, &exit_status, WNOHANG); + if(pid < 0 && errno == ECHILD) { + // PID doesn't exist anymore, set previously sampled exit status (or -1) + exit_status = data.exit_status; + return true; + } + else if(pid <= 0) { + // Process still running (p==0) or error + return false; + } + else { + // store exit status for future calls + if(exit_status >= 256) + exit_status = exit_status >> 8; + data.exit_status = exit_status; + } + + { + std::lock_guard lock(close_mutex); + closed = true; + } + close_fds(); + + return true; +} + +void Process::close_fds() noexcept { + if(stdout_stderr_thread.joinable()) + stdout_stderr_thread.join(); + + if(stdin_fd) + close_stdin(); + if(stdout_fd) { + if(data.id > 0) + close(*stdout_fd); + stdout_fd.reset(); + } + if(stderr_fd) { + if(data.id > 0) + close(*stderr_fd); + stderr_fd.reset(); + } +} + +bool Process::write(const char *bytes, size_t n) { + if(!open_stdin) + throw std::invalid_argument("Can't write to an unopened stdin pipe. Please set open_stdin=true when constructing the process."); + + std::lock_guard lock(stdin_mutex); + if(stdin_fd) { + if(::write(*stdin_fd, bytes, n) >= 0) { + return true; + } + else { + return false; + } + } + return false; +} + +void Process::close_stdin() noexcept { + std::lock_guard lock(stdin_mutex); + if(stdin_fd) { + if(data.id > 0) + close(*stdin_fd); + stdin_fd.reset(); + } +} + +void Process::kill(bool force) noexcept { + std::lock_guard lock(close_mutex); + if(data.id > 0 && !closed) { + if(force) + ::kill(-data.id, SIGTERM); + else + ::kill(-data.id, SIGINT); + } +} + +void Process::kill(id_type id, bool force) noexcept { + if(id <= 0) + return; + + if(force) + ::kill(-id, SIGTERM); + else + ::kill(-id, SIGINT); +} + +void Process::signal(int signum) noexcept { + std::lock_guard lock(close_mutex); + if(data.id > 0 && !closed) { + ::kill(-data.id, signum); + } +} + +} // namespace TinyProcessLib diff --git a/external/tpl/process_win.cpp b/external/tpl/process_win.cpp new file mode 100644 index 0000000..305289a --- /dev/null +++ b/external/tpl/process_win.cpp @@ -0,0 +1,350 @@ +#include "process.hpp" +// clang-format off +#include +// clang-format on +#include +#include +#include + +namespace TinyProcessLib { + +Process::Data::Data() noexcept : id(0) {} + +// Simple HANDLE wrapper to close it automatically from the destructor. +class Handle { +public: + Handle() noexcept : handle(INVALID_HANDLE_VALUE) {} + ~Handle() noexcept { + close(); + } + void close() noexcept { + if(handle != INVALID_HANDLE_VALUE) + CloseHandle(handle); + } + HANDLE detach() noexcept { + HANDLE old_handle = handle; + handle = INVALID_HANDLE_VALUE; + return old_handle; + } + operator HANDLE() const noexcept { return handle; } + HANDLE *operator&() noexcept { return &handle; } + +private: + HANDLE handle; +}; + +//Based on the discussion thread: https://www.reddit.com/r/cpp/comments/3vpjqg/a_new_platform_independent_process_library_for_c11/cxq1wsj +std::mutex create_process_mutex; + +Process::id_type Process::open(const std::vector &arguments, const string_type &path, const environment_type *environment) noexcept { + string_type command; + for(auto &argument : arguments) +#ifdef UNICODE + command += (command.empty() ? L"" : L" ") + argument; +#else + command += (command.empty() ? "" : " ") + argument; +#endif + return open(command, path, environment); +} + +//Based on the example at https://msdn.microsoft.com/en-us/library/windows/desktop/ms682499(v=vs.85).aspx. +Process::id_type Process::open(const string_type &command, const string_type &path, const environment_type *environment) noexcept { + if(open_stdin) + stdin_fd = std::unique_ptr(new fd_type(nullptr)); + if(read_stdout) + stdout_fd = std::unique_ptr(new fd_type(nullptr)); + if(read_stderr) + stderr_fd = std::unique_ptr(new fd_type(nullptr)); + + Handle stdin_rd_p; + Handle stdin_wr_p; + Handle stdout_rd_p; + Handle stdout_wr_p; + Handle stderr_rd_p; + Handle stderr_wr_p; + + SECURITY_ATTRIBUTES security_attributes; + + security_attributes.nLength = sizeof(SECURITY_ATTRIBUTES); + security_attributes.bInheritHandle = TRUE; + security_attributes.lpSecurityDescriptor = nullptr; + + std::lock_guard lock(create_process_mutex); + if(stdin_fd) { + if(!CreatePipe(&stdin_rd_p, &stdin_wr_p, &security_attributes, 0) || + !SetHandleInformation(stdin_wr_p, HANDLE_FLAG_INHERIT, 0)) + return 0; + } + if(stdout_fd) { + if(!CreatePipe(&stdout_rd_p, &stdout_wr_p, &security_attributes, 0) || + !SetHandleInformation(stdout_rd_p, HANDLE_FLAG_INHERIT, 0)) { + return 0; + } + } + if(stderr_fd) { + if(!CreatePipe(&stderr_rd_p, &stderr_wr_p, &security_attributes, 0) || + !SetHandleInformation(stderr_rd_p, HANDLE_FLAG_INHERIT, 0)) { + return 0; + } + } + + PROCESS_INFORMATION process_info; + STARTUPINFO startup_info; + + ZeroMemory(&process_info, sizeof(PROCESS_INFORMATION)); + + ZeroMemory(&startup_info, sizeof(STARTUPINFO)); + startup_info.cb = sizeof(STARTUPINFO); + startup_info.hStdInput = stdin_rd_p; + startup_info.hStdOutput = stdout_wr_p; + startup_info.hStdError = stderr_wr_p; + if(stdin_fd || stdout_fd || stderr_fd) + startup_info.dwFlags |= STARTF_USESTDHANDLES; + + if(config.show_window != Config::ShowWindow::show_default) { + startup_info.dwFlags |= STARTF_USESHOWWINDOW; + startup_info.wShowWindow = static_cast(config.show_window); + } + + auto process_command = command; +#ifdef MSYS_PROCESS_USE_SH + size_t pos = 0; + while((pos = process_command.find('\\', pos)) != string_type::npos) { + process_command.replace(pos, 1, "\\\\\\\\"); + pos += 4; + } + pos = 0; + while((pos = process_command.find('\"', pos)) != string_type::npos) { + process_command.replace(pos, 1, "\\\""); + pos += 2; + } + process_command.insert(0, "sh -c \""); + process_command += "\""; +#endif + + string_type environment_str; + if(environment) { +#ifdef UNICODE + for(const auto &e : *environment) + environment_str += e.first + L'=' + e.second + L'\0'; + environment_str += L'\0'; +#else + for(const auto &e : *environment) + environment_str += e.first + '=' + e.second + '\0'; + environment_str += '\0'; +#endif + } + BOOL bSuccess = CreateProcess(nullptr, process_command.empty() ? nullptr : &process_command[0], nullptr, nullptr, + stdin_fd || stdout_fd || stderr_fd || config.inherit_file_descriptors, // Cannot be false when stdout, stderr or stdin is used + stdin_fd || stdout_fd || stderr_fd ? CREATE_NO_WINDOW : 0, // CREATE_NO_WINDOW cannot be used when stdout or stderr is redirected to parent process + environment_str.empty() ? nullptr : &environment_str[0], + path.empty() ? nullptr : path.c_str(), + &startup_info, &process_info); + + if(!bSuccess) + return 0; + else + CloseHandle(process_info.hThread); + + if(stdin_fd) + *stdin_fd = stdin_wr_p.detach(); + if(stdout_fd) + *stdout_fd = stdout_rd_p.detach(); + if(stderr_fd) + *stderr_fd = stderr_rd_p.detach(); + + closed = false; + data.id = process_info.dwProcessId; + data.handle = process_info.hProcess; + return process_info.dwProcessId; +} + +void Process::async_read() noexcept { + if(data.id == 0) + return; + + if(stdout_fd) { + stdout_thread = std::thread([this]() { + DWORD n; + std::unique_ptr buffer(new char[config.buffer_size]); + for(;;) { + BOOL bSuccess = ReadFile(*stdout_fd, static_cast(buffer.get()), static_cast(config.buffer_size), &n, nullptr); + if(!bSuccess || n == 0) + break; + read_stdout(buffer.get(), static_cast(n)); + } + }); + } + if(stderr_fd) { + stderr_thread = std::thread([this]() { + DWORD n; + std::unique_ptr buffer(new char[config.buffer_size]); + for(;;) { + BOOL bSuccess = ReadFile(*stderr_fd, static_cast(buffer.get()), static_cast(config.buffer_size), &n, nullptr); + if(!bSuccess || n == 0) + break; + read_stderr(buffer.get(), static_cast(n)); + } + }); + } +} + +int Process::get_exit_status() noexcept { + if(data.id == 0) + return -1; + + if(!data.handle) + return data.exit_status; + + WaitForSingleObject(data.handle, INFINITE); + + DWORD exit_status; + if(!GetExitCodeProcess(data.handle, &exit_status)) + data.exit_status = -1; // Store exit status for future calls + else + data.exit_status = static_cast(exit_status); // Store exit status for future calls + + { + std::lock_guard lock(close_mutex); + CloseHandle(data.handle); + data.handle = nullptr; + closed = true; + } + close_fds(); + + return data.exit_status; +} + +bool Process::try_get_exit_status(int &exit_status) noexcept { + if(data.id == 0) + return false; + + if(!data.handle) { + exit_status = data.exit_status; + return true; + } + + DWORD wait_status = WaitForSingleObject(data.handle, 0); + if(wait_status == WAIT_TIMEOUT) + return false; + + DWORD exit_status_tmp; + if(!GetExitCodeProcess(data.handle, &exit_status_tmp)) + exit_status = -1; + else + exit_status = static_cast(exit_status_tmp); + data.exit_status = exit_status; // Store exit status for future calls + + { + std::lock_guard lock(close_mutex); + CloseHandle(data.handle); + data.handle = nullptr; + closed = true; + } + close_fds(); + + return true; +} + +void Process::close_fds() noexcept { + if(stdout_thread.joinable()) + stdout_thread.join(); + if(stderr_thread.joinable()) + stderr_thread.join(); + + if(stdin_fd) + close_stdin(); + if(stdout_fd) { + if(*stdout_fd != nullptr) + CloseHandle(*stdout_fd); + stdout_fd.reset(); + } + if(stderr_fd) { + if(*stderr_fd != nullptr) + CloseHandle(*stderr_fd); + stderr_fd.reset(); + } +} + +bool Process::write(const char *bytes, size_t n) { + if(!open_stdin) + throw std::invalid_argument("Can't write to an unopened stdin pipe. Please set open_stdin=true when constructing the process."); + + std::lock_guard lock(stdin_mutex); + if(stdin_fd) { + DWORD written; + BOOL bSuccess = WriteFile(*stdin_fd, bytes, static_cast(n), &written, nullptr); + if(!bSuccess || written == 0) { + return false; + } + else { + return true; + } + } + return false; +} + +void Process::close_stdin() noexcept { + std::lock_guard lock(stdin_mutex); + if(stdin_fd) { + if(*stdin_fd != nullptr) + CloseHandle(*stdin_fd); + stdin_fd.reset(); + } +} + +//Based on http://stackoverflow.com/a/1173396 +void Process::kill(bool /*force*/) noexcept { + std::lock_guard lock(close_mutex); + if(data.id > 0 && !closed) { + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if(snapshot) { + PROCESSENTRY32 process; + ZeroMemory(&process, sizeof(process)); + process.dwSize = sizeof(process); + if(Process32First(snapshot, &process)) { + do { + if(process.th32ParentProcessID == data.id) { + HANDLE process_handle = OpenProcess(PROCESS_TERMINATE, FALSE, process.th32ProcessID); + if(process_handle) { + TerminateProcess(process_handle, 2); + CloseHandle(process_handle); + } + } + } while(Process32Next(snapshot, &process)); + } + CloseHandle(snapshot); + } + TerminateProcess(data.handle, 2); + } +} + +//Based on http://stackoverflow.com/a/1173396 +void Process::kill(id_type id, bool /*force*/) noexcept { + if(id == 0) + return; + + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if(snapshot) { + PROCESSENTRY32 process; + ZeroMemory(&process, sizeof(process)); + process.dwSize = sizeof(process); + if(Process32First(snapshot, &process)) { + do { + if(process.th32ParentProcessID == id) { + HANDLE process_handle = OpenProcess(PROCESS_TERMINATE, FALSE, process.th32ProcessID); + if(process_handle) { + TerminateProcess(process_handle, 2); + CloseHandle(process_handle); + } + } + } while(Process32Next(snapshot, &process)); + } + CloseHandle(snapshot); + } + HANDLE process_handle = OpenProcess(PROCESS_TERMINATE, FALSE, id); + if(process_handle) + TerminateProcess(process_handle, 2); +} + +} // namespace TinyProcessLib