From 2d11434c5c7d7821d842e0525c1ecc8d12525b7f Mon Sep 17 00:00:00 2001 From: zhaohe Date: Wed, 11 Jan 2023 20:31:54 +0800 Subject: [PATCH] add process --- core/components/process/.clang-format | 9 + core/components/process/README.md | 8 + core/components/process/process.cpp | 40 ++ core/components/process/process.hpp | 133 +++++++ core/components/process/process_unix.cpp | 614 +++++++++++++++++++++++++++++++ module.cmake | 5 + 6 files changed, 809 insertions(+) create mode 100644 core/components/process/.clang-format create mode 100644 core/components/process/README.md create mode 100644 core/components/process/process.cpp create mode 100644 core/components/process/process.hpp create mode 100644 core/components/process/process_unix.cpp diff --git a/core/components/process/.clang-format b/core/components/process/.clang-format new file mode 100644 index 0000000..956b0fb --- /dev/null +++ b/core/components/process/.clang-format @@ -0,0 +1,9 @@ +IndentWidth: 2 +AccessModifierOffset: -2 +UseTab: Never +ColumnLimit: 0 +MaxEmptyLinesToKeep: 2 +SpaceBeforeParens: Never +BreakBeforeBraces: Custom +BraceWrapping: {BeforeElse: true, BeforeCatch: true} +NamespaceIndentation: None diff --git a/core/components/process/README.md b/core/components/process/README.md new file mode 100644 index 0000000..4baa7aa --- /dev/null +++ b/core/components/process/README.md @@ -0,0 +1,8 @@ +# tiny-process-library + +使用说明: +``` + + + +``` \ No newline at end of file diff --git a/core/components/process/process.cpp b/core/components/process/process.cpp new file mode 100644 index 0000000..36ba73e --- /dev/null +++ b/core/components/process/process.cpp @@ -0,0 +1,40 @@ +#include "process.hpp" + +namespace iflytop { +namespace process { + + +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 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 process +} // namespace iflytop diff --git a/core/components/process/process.hpp b/core/components/process/process.hpp new file mode 100644 index 0000000..d8b8100 --- /dev/null +++ b/core/components/process/process.hpp @@ -0,0 +1,133 @@ +#ifndef TINY_PROCESS_LIBRARY_HPP_ +#define TINY_PROCESS_LIBRARY_HPP_ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace iflytop { +namespace process { +/// 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: + typedef pid_t id_type; + typedef int fd_type; + typedef std::string string_type; + typedef std::unordered_map environment_type; + +private: + class Data { + public: + Data() noexcept; + id_type id; + int exit_status{-1}; + }; + +public: + /// 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 = true, /*默认打开标准输入,使得其行为更接近于system()*/ + 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 = true, + const Config &config = {}) noexcept; /// Starts a process with specified environment. + /// 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 = true, + const Config &config = {}) noexcept; + ~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; + void signal(int signum) noexcept; + + // void show_all_child_process(); +private: + Data data; + bool closed; + std::mutex close_mutex; + std::function read_stdout; + std::function read_stderr; + std::thread stdout_stderr_thread; + + bool open_stdin; + std::mutex stdin_mutex; + + Config config; + + std::unique_ptr stdout_fd, stderr_fd, stdin_fd; + + id_type open(const string_type &command, const string_type &path, const environment_type *environment = nullptr) noexcept; + id_type open(const std::function &function) noexcept; + void async_read() noexcept; + void close_fds() noexcept; +}; +// 为了确保所有子进程被安全回收,在主进程退出时,调用下面的方法,确保子进程的退出。 +void killallsubprocess(int, void *); + +} // namespace process +} // namespace iflytop +#endif // TINY_PROCESS_LIBRARY_HPP_ diff --git a/core/components/process/process_unix.cpp b/core/components/process/process_unix.cpp new file mode 100644 index 0000000..d32c632 --- /dev/null +++ b/core/components/process/process_unix.cpp @@ -0,0 +1,614 @@ +#include "process.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace iflytop { +namespace process { +using namespace std; + + +typedef long long int num; + +typedef struct pid_stat_fields { + + num pid; + char tcomm[PATH_MAX]; + char state; + + num ppid; + num pgid; + num sid; + num tty_nr; + num tty_pgrp; + + num flags; + num min_flt; + num cmin_flt; + num maj_flt; + num cmaj_flt; + num utime; + num stime; + + num cutime; + num cstime; + num priority; + num nicev; + num num_threads; + num it_real_value; + + unsigned long long start_time; + + num vsize; + num rss; + num rsslim; + num start_code; + num end_code; + num start_stack; + num esp; + num eip; + + num pending; + num blocked; + num sigign; + num sigcatch; + num wchan; + num zero1; + num zero2; + num exit_signal; + num cpu; + num rt_priority; + num policy; + +} pid_stat_fields; + +void readone(num *x, FILE *input) { fscanf(input, "%lld ", x); } +void readunsigned(unsigned long long *x, FILE *input) { fscanf(input, "%llu ", x); } +void readstr(char *x, FILE *input) { fscanf(input, "%s ", x); } +void readchar(char *x, FILE *input) { fscanf(input, "%c ", x); } +int filter(const struct dirent *dir) { // select number folder + int i; + int n = strlen(dir->d_name); + for(i = 0; i < n; i++) { + if(!isdigit(dir->d_name[i])) // 返回所有数字目录 + return 0; + else + return 1; + } + return 0; +} +pid_stat_fields populate_pid_stats(FILE *stat_file) { + + pid_stat_fields stats; + // reading order is important!!! + readone(&(stats.pid), stat_file); + readstr(&(stats.tcomm[0]), stat_file); + readchar(&(stats.state), stat_file); + readone(&(stats.ppid), stat_file); + readone(&(stats.pgid), stat_file); + readone(&(stats.sid), stat_file); + readone(&(stats.tty_nr), stat_file); + readone(&(stats.tty_pgrp), stat_file); + readone(&(stats.flags), stat_file); + readone(&(stats.min_flt), stat_file); + readone(&(stats.cmin_flt), stat_file); + readone(&(stats.maj_flt), stat_file); + readone(&(stats.cmaj_flt), stat_file); + readone(&(stats.utime), stat_file); + readone(&(stats.stime), stat_file); + readone(&(stats.cutime), stat_file); + readone(&(stats.cstime), stat_file); + readone(&(stats.priority), stat_file); + readone(&(stats.nicev), stat_file); + readone(&(stats.num_threads), stat_file); + readone(&(stats.it_real_value), stat_file); + readunsigned(&(stats.start_time), stat_file); + readone(&(stats.vsize), stat_file); + readone(&(stats.rss), stat_file); + readone(&(stats.rsslim), stat_file); + readone(&(stats.start_code), stat_file); + readone(&(stats.end_code), stat_file); + readone(&(stats.start_stack), stat_file); + readone(&(stats.esp), stat_file); + readone(&(stats.eip), stat_file); + readone(&(stats.pending), stat_file); + readone(&(stats.blocked), stat_file); + readone(&(stats.sigign), stat_file); + readone(&(stats.sigcatch), stat_file); + readone(&(stats.wchan), stat_file); + readone(&(stats.zero1), stat_file); + readone(&(stats.zero2), stat_file); + readone(&(stats.exit_signal), stat_file); + readone(&(stats.cpu), stat_file); + readone(&(stats.rt_priority), stat_file); + readone(&(stats.policy), stat_file); + + return stats; +} + +pid_stat_fields get_pid_stats(num pid) { + + char stat_file_name[PATH_MAX]; + sprintf(stat_file_name, "/proc/%lld/stat", pid); + FILE *stat_file = fopen(stat_file_name, "r"); + if(stat_file == NULL) { + throw runtime_error("Could not open /proc/pid/stat"); + } + pid_stat_fields stat = populate_pid_stats(stat_file); + fclose(stat_file); + return stat; +} + +list get_pid_stats_list() { + struct dirent **namelist; + int total = scandir("/proc", &namelist, filter, alphasort); + list pid_stats_list; + for(int i = 0; i < total; i++) { + pid_stat_fields stat = get_pid_stats(atoi(namelist[i]->d_name)); + pid_stats_list.push_back(stat); + free(namelist[i]); + } + free(namelist); + return pid_stats_list; +} + +/** + * @brief 找到某个进程的所子进程,同时也包含其子进程的子进程 + */ +void get_child_processes(list allpids, list &childprocess, num pid) { + list::iterator it; + for(it = allpids.begin(); it != allpids.end(); it++) { + if((*it).ppid == pid && (*it).pid != pid) { + childprocess.push_back(*it); + get_child_processes(allpids, childprocess, (*it).pid); + } + } +} +/** + * @brief Get the child processes object + * + * @param pid + * @return list + * + * 进程按照下面规则排序 + * + * + */ +list get_child_processes(num pid) { + list allpids = get_pid_stats_list(); + list childprocess; + get_child_processes(allpids, childprocess, pid); + return childprocess; +} + +bool isprocessrunning(num pid) { + char stat_file_name[PATH_MAX]; + sprintf(stat_file_name, "/proc/%lld/stat", pid); + FILE *stat_file = fopen(stat_file_name, "r"); + if(stat_file == NULL) { + return false; + } + num _pid; + char tcomm[PATH_MAX]; + char state; + + readone(&(_pid), stat_file); + readstr(&(tcomm[0]), stat_file); + readchar(&(state), stat_file); + + fclose(stat_file); + return state != 'Z'; +} + + +void show_all_child_process(num pid) { + list childprocess_list = get_child_processes(pid); + list::iterator it; + for(it = childprocess_list.begin(); it != childprocess_list.end(); it++) { + printf("%lld %s\n", (*it).pid, (*it).tcomm); + } +} +void force_kill(num pid) { + bool firstkill = false; + int maxkilltimes = 3; + int killtimes = 0; + while(isprocessrunning(pid)) { + if(killtimes >= maxkilltimes) { + fprintf(stderr, "kill process %lld, fail %s\n", pid, strerror(errno)); + break; + } + + if(firstkill) { + fprintf(stderr, "rekill process %lld\n", pid); + } + ::kill(pid, SIGTERM); + usleep(500 * 1000); + firstkill = true; + killtimes++; + } +}; + + +void killallsubprocess(int, void *) { + + list childprocess_list = get_child_processes(getpid()); + + // trykill all child process + for(auto &childprocess : childprocess_list) { + if(isprocessrunning(childprocess.pid)) { + ::kill(childprocess.pid, SIGTERM); + } + } + + // forse kill al child process + for(auto &childprocess : childprocess_list) { + force_kill(childprocess.pid); + usleep(10 * 1000); + } +} + + +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) { + prctl(PR_SET_PDEATHSIG, SIGTERM); + 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::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); + // printf("waitpid(%d, %d, 0) = %d\n", data.id, exit_status, pid); + } 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); + int ret = 0; + if(data.id > 0 && !closed) { + // show_all_child_process(); + bool firstkill = false; + if(force) { + // 按照从最底层的进程开始杀死 + list childprocess_list = get_child_processes(data.id); + force_kill(data.id); + usleep(10 * 1000); + for(auto &childprocess : childprocess_list) { + force_kill(childprocess.pid); + usleep(10 * 1000); + } + } + else { + kill(-data.id, SIGTERM); + } + } +} + +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 process +} // namespace iflytop diff --git a/module.cmake b/module.cmake index 296e12a..43fccbd 100644 --- a/module.cmake +++ b/module.cmake @@ -24,6 +24,11 @@ set(DEP_SRC dep/iflytopcpp/core/components/modbus/zmodbus_common.c dep/iflytopcpp/core/components/modbus/zmodbus_master.c dep/iflytopcpp/core/components/modbus/zmodbus_slave.c + + + dep/iflytopcpp/core/components/process/process_unix.cpp + dep/iflytopcpp/core/components/process/process.cpp + # ) set(DEP_DEFINE ${DEP_DEFINE})