#include "process.hpp" #include #include #include #include #include #include #include #include #include #include #include #include namespace TinyProcessLib { static int portable_execvpe(const char *file, char *const argv[], char *const envp[]) { #ifdef __GLIBC__ // Prefer native implementation. return execvpe(file, argv, envp); #else if(!file || !*file) { errno = ENOENT; return -1; } if(strchr(file, '/') != nullptr) { // If file contains a slash, no search is needed. return execve(file, argv, envp); } const char *path = getenv("PATH"); char cspath[PATH_MAX + 1] = {}; if(!path) { // If env variable is not set, use static path string. confstr(_CS_PATH, cspath, sizeof(cspath)); path = cspath; } const size_t path_len = strlen(path); const size_t file_len = strlen(file); if(file_len > NAME_MAX) { errno = ENAMETOOLONG; return -1; } // Indicates whether we encountered EACCESS at least once. bool eacces = false; const char *curr = nullptr; const char *next = nullptr; for(curr = path; *curr; curr = *next ? next + 1 : next) { next = strchr(curr, ':'); if(!next) { next = path + path_len; } const size_t sz = (next - curr); if(sz > PATH_MAX) { // Path is too long. Proceed to next path in list. continue; } char exe_path[PATH_MAX + 1 + NAME_MAX + 1]; // 1 byte for slash + 1 byte for \0 memcpy(exe_path, curr, sz); exe_path[sz] = '/'; memcpy(exe_path + sz + 1, file, file_len); exe_path[sz + 1 + file_len] = '\0'; execve(exe_path, argv, envp); switch(errno) { case EACCES: eacces = true; case ENOENT: case ESTALE: case ENOTDIR: case ENODEV: case ETIMEDOUT: // The errors above indicate that the executable was not found. // The list of errors replicates one from glibc. // In this case we proceed to next path in list. break; default: // Other errors indicate that executable was found but failed // to execute. In this case we return error. return -1; } } if(eacces) { // If search failed, and at least one iteration reported EACCESS, it means // that the needed executable exists but does not have suitable permissions. // In this case we report EACEESS for user. errno = EACCES; } // Otherwise we just keep last encountered errno. return -1; #endif } 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) : closed(true), read_stdout(std::move(read_stdout)), read_stderr(std::move(read_stderr)), open_stdin(open_stdin), config(config) { if(config.flatpak_spawn_host) throw std::invalid_argument("Cannot break out of a flatpak sandbox with this overload."); 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([this, &arguments, &path, &environment] { if(arguments.empty()) exit(127); std::vector argv_ptrs; if(config.flatpak_spawn_host) { // break out of sandbox, execute on host argv_ptrs.reserve(arguments.size() + 3); argv_ptrs.emplace_back("/usr/bin/flatpak-spawn"); argv_ptrs.emplace_back("--host"); } else 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) execvp(argv_ptrs[0], 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); portable_execvpe(argv_ptrs[0], 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([this, &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) { if(config.flatpak_spawn_host) // break out of sandbox, execute on host execl("/usr/bin/flatpak-spawn", "/usr/bin/flatpak-spawn", "--host", "/bin/sh", "-c", command_c_str, nullptr); else 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); if(config.flatpak_spawn_host) // break out of sandbox, execute on host execle("/usr/bin/flatpak-spawn", "/usr/bin/flatpak-spawn", "--host", "/bin/sh", "-c", command_c_str, nullptr, env_ptrs.data()); else 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 || (n < 0 && errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK)) { if(fd_is_stdout[i]) { if(config.on_stdout_close) config.on_stdout_close(); } else { if(config.on_stderr_close) config.on_stderr_close(); } pollfds[i].fd = -1; continue; } } else if(pollfds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) { if(fd_is_stdout[i]) { if(config.on_stdout_close) config.on_stdout_close(); } else { if(config.on_stderr_close) config.on_stderr_close(); } 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) { exit_status = -1; return true; } 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) { while(n != 0) { const ssize_t ret = ::write(*stdin_fd, bytes, n); if(ret < 0) { if(errno == EINTR) continue; else return false; } bytes += static_cast(ret); n -= static_cast(ret); } return true; } 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); ::kill(data.id, SIGTERM); // Based on comment in https://gitlab.com/eidheim/tiny-process-library/-/merge_requests/29#note_1146144166 } else { ::kill(-data.id, SIGINT); ::kill(data.id, SIGINT); } } } void Process::kill(id_type id, bool force) noexcept { if(id <= 0) return; if(force) { ::kill(-id, SIGTERM); ::kill(id, SIGTERM); } else { ::kill(-id, SIGINT); ::kill(id, SIGINT); } } void Process::signal(int signum) noexcept { std::lock_guard lock(close_mutex); if(data.id > 0 && !closed) { ::kill(-data.id, signum); ::kill(data.id, signum); } } } // namespace TinyProcessLib