diff --git a/src/tiny-process-library/LICENSE b/src/tiny-process-library/LICENSE new file mode 100644 index 00000000..dc40ee76 --- /dev/null +++ b/src/tiny-process-library/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015-2016 Ole Christian Eidheim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/src/tiny-process-library/README.md b/src/tiny-process-library/README.md new file mode 100644 index 00000000..80f7e25c --- /dev/null +++ b/src/tiny-process-library/README.md @@ -0,0 +1,42 @@ +# tiny-process-library [![Build Status](https://travis-ci.org/eidheim/tiny-process-library.svg?branch=master)](https://travis-ci.org/eidheim/tiny-process-library) +A small platform independent library making it simple to create and stop new processes in C++, as well as writing to stdin and reading from stdout and stderr of a new process. + +This library was created for, and is used by the C++ IDE project [juCi++](https://github.com/cppit/jucipp). + +### Features +* No external dependencies +* Simple to use +* Platform independent + * Creating processes using executables is supported on all platforms + * Creating processes using functions is only possible on Unix-like systems +* Read separately from stdout and stderr using anonymous functions +* Write to stdin +* Kill a running process (SIGTERM is supported on Unix-like systems) +* Correctly closes file descriptors/handles + +### Usage +See [examples.cpp](https://github.com/eidheim/tiny-process-library/blob/master/examples.cpp). + +### Get, compile and run + +#### Unix-like systems +```sh +git clone http://github.com/eidheim/tiny-process-library +cd tiny-process-library +mkdir build +cd build +cmake .. +make +./examples +``` + +#### Windows with MSYS2 (https://msys2.github.io/) +```sh +git clone http://github.com/eidheim/tiny-process-library +cd tiny-process-library +mkdir build +cd build +cmake -G"MSYS Makefiles" .. +make +./examples +``` diff --git a/src/tiny-process-library/process.cpp b/src/tiny-process-library/process.cpp new file mode 100644 index 00000000..e3d39105 --- /dev/null +++ b/src/tiny-process-library/process.cpp @@ -0,0 +1,26 @@ +#include "process.hpp" + +namespace TinyProcessLib { + +Process::Process(const string_type &command, const string_type &path, + std::function read_stdout, + std::function read_stderr, + bool open_stdin, size_t buffer_size) noexcept: + closed(true), read_stdout(read_stdout), read_stderr(read_stderr), open_stdin(open_stdin), buffer_size(buffer_size) { + open(command, path); + async_read(); +} + +Process::~Process() noexcept { + close_fds(); +} + +Process::id_type Process::get_id() const noexcept { + return data.id; +} + +bool Process::write(const std::string &data) { + return write(data.c_str(), data.size()); +} + +} // TinyProsessLib diff --git a/src/tiny-process-library/process.hpp b/src/tiny-process-library/process.hpp new file mode 100644 index 00000000..83d42f4e --- /dev/null +++ b/src/tiny-process-library/process.hpp @@ -0,0 +1,99 @@ +#ifndef TINY_PROCESS_LIBRARY_HPP_ +#define TINY_PROCESS_LIBRARY_HPP_ + +#include +#include +#include +#include +#include +#include +#ifndef _WIN32 +#include +#endif + +namespace TinyProcessLib { + +///Platform independent class for creating processes +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 +private: + class Data { + public: + Data() noexcept ; + id_type id; +#ifdef _WIN32 + void *handle; +#endif + }; +public: + ///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. + 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, + size_t buffer_size=131072) noexcept; +#ifndef _WIN32 + /// Supported on Unix-like systems only. + Process(std::function function, + std::function read_stdout=nullptr, + std::function read_stderr=nullptr, + bool open_stdin=false, + size_t buffer_size=131072) 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; + ///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 &data); + ///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; + +private: + Data data; + bool closed; + std::mutex close_mutex; + std::function read_stdout; + std::function read_stderr; + std::thread stdout_thread, stderr_thread; + bool open_stdin; + std::mutex stdin_mutex; + size_t buffer_size; + + std::unique_ptr stdout_fd, stderr_fd, stdin_fd; + + id_type open(const string_type &command, const string_type &path) noexcept; +#ifndef _WIN32 + id_type open(std::function function) noexcept; +#endif + void async_read() noexcept; + void close_fds() noexcept; +}; + +} // TinyProsessLib + +#endif // TINY_PROCESS_LIBRARY_HPP_ diff --git a/src/tiny-process-library/process_unix.cpp b/src/tiny-process-library/process_unix.cpp new file mode 100644 index 00000000..7c44c531 --- /dev/null +++ b/src/tiny-process-library/process_unix.cpp @@ -0,0 +1,204 @@ +#include "process.hpp" +#include +#include +#include +#include + +namespace TinyProcessLib { + +Process::Data::Data() noexcept : id(-1) {} + +Process::Process(std::function function, + std::function read_stdout, + std::function read_stderr, + bool open_stdin, size_t buffer_size) noexcept : + closed(true), read_stdout(read_stdout), read_stderr(read_stderr), open_stdin(open_stdin), buffer_size(buffer_size) { + open(function); + async_read(); +} + +Process::id_type Process::open(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]);} + + //Based on http://stackoverflow.com/a/899533/3808293 + int fd_max=static_cast(sysconf(_SC_OPEN_MAX)); // truncation is safe + for(int fd=3;fd( new char[buffer_size] ); + ssize_t n; + while ((n=read(*stdout_fd, buffer.get(), buffer_size)) > 0) + read_stdout(buffer.get(), static_cast(n)); + }); + } + if(stderr_fd) { + stderr_thread=std::thread([this](){ + auto buffer = std::unique_ptr( new char[buffer_size] ); + ssize_t n; + while ((n=read(*stderr_fd, buffer.get(), buffer_size)) > 0) + read_stderr(buffer.get(), static_cast(n)); + }); + } +} + +int Process::get_exit_status() noexcept { + if(data.id<=0) + return -1; + int exit_status; + waitpid(data.id, &exit_status, 0); + { + std::lock_guard lock(close_mutex); + closed=true; + } + close_fds(); + + if(exit_status>=256) + exit_status=exit_status>>8; + return exit_status; +} + +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(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); +} + +} // TinyProsessLib diff --git a/src/tiny-process-library/process_win.cpp b/src/tiny-process-library/process_win.cpp new file mode 100644 index 00000000..0e59c5e4 --- /dev/null +++ b/src/tiny-process-library/process_win.cpp @@ -0,0 +1,270 @@ +#include "process.hpp" +#include +#include +#include +#include + +namespace TinyProcessLib { + +Process::Data::Data() noexcept : id(0), handle(NULL) {} + +// Simple HANDLE wrapper to close it automatically from the destructor. +class Handle { +public: + Handle() : handle(INVALID_HANDLE_VALUE) { } + ~Handle() { + close(); + } + void close() { + if (handle != INVALID_HANDLE_VALUE) + CloseHandle(handle); + } + HANDLE detach() { + HANDLE old_handle = handle; + handle = INVALID_HANDLE_VALUE; + return old_handle; + } + operator HANDLE() const { return handle; } + HANDLE* operator&() { 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; + +//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) noexcept { + if(open_stdin) + stdin_fd=std::unique_ptr(new fd_type(NULL)); + if(read_stdout) + stdout_fd=std::unique_ptr(new fd_type(NULL)); + if(read_stderr) + stderr_fd=std::unique_ptr(new fd_type(NULL)); + + 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; + + string_type 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 + + BOOL bSuccess = CreateProcess(nullptr, process_command.empty()?nullptr:&process_command[0], nullptr, nullptr, TRUE, 0, + nullptr, path.empty()?nullptr:path.c_str(), &startup_info, &process_info); + + if(!bSuccess) { + CloseHandle(process_info.hProcess); + CloseHandle(process_info.hThread); + 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[buffer_size]); + for (;;) { + BOOL bSuccess = ReadFile(*stdout_fd, static_cast(buffer.get()), static_cast(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[buffer_size]); + for (;;) { + BOOL bSuccess = ReadFile(*stderr_fd, static_cast(buffer.get()), static_cast(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; + DWORD exit_status; + WaitForSingleObject(data.handle, INFINITE); + if(!GetExitCodeProcess(data.handle, &exit_status)) + exit_status=-1; + { + std::lock_guard lock(close_mutex); + CloseHandle(data.handle); + closed=true; + } + close_fds(); + + return static_cast(exit_status); +} + +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!=NULL) CloseHandle(*stdout_fd); + stdout_fd.reset(); + } + if(stderr_fd) { + if(*stderr_fd!=NULL) 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!=NULL) 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); +} + +} // TinyProsessLib