diff --git a/test/tap/tap/utils.cpp b/test/tap/tap/utils.cpp index e31c47e5f..33e9ce546 100644 --- a/test/tap/tap/utils.cpp +++ b/test/tap/tap/utils.cpp @@ -1,8 +1,14 @@ +#include #include #include "tap.h" #include "utils.h" +#include +#include +#include +#include + int show_variable(MYSQL *mysql, const std::string& var_name, std::string& var_value) { char query[128]; @@ -102,3 +108,200 @@ int get_server_version(MYSQL *mysql, std::string& version) { return 0; } + +// Pipes definition +constexpr uint8_t NUM_PIPES = 3; +constexpr uint8_t PARENT_WRITE_PIPE = 0; +constexpr uint8_t PARENT_READ_PIPE = 1; +constexpr uint8_t PARENT_ERR_PIPE = 2; +int pipes[NUM_PIPES][2]; +// Pipe selection +constexpr uint8_t READ_FD = 0; +constexpr uint8_t WRITE_FD = 1; +// Parent pipes +const auto& PARENT_READ_FD = pipes[PARENT_READ_PIPE][READ_FD]; +const auto& PARENT_READ_ERR = pipes[PARENT_ERR_PIPE][READ_FD]; +const auto& PARENT_WRITE_FD = pipes[PARENT_WRITE_PIPE][WRITE_FD]; +// Child pipes +const auto& CHILD_READ_FD = pipes[PARENT_WRITE_PIPE][READ_FD]; +const auto& CHILD_WRITE_FD = pipes[PARENT_READ_PIPE][WRITE_FD]; +const auto& CHILD_WRITE_ERR = pipes[PARENT_ERR_PIPE][WRITE_FD]; + +int kill_child_proc(pid_t child_pid, const uint timeout_us, const uint it_sleep_us) { + uint err = 0; + uint waited = 0; + int child_status = 0; + + err = kill(child_pid, SIGTERM); + + while (waitpid(child_pid, &child_status, WNOHANG) > 0) { + if (waited >= timeout_us) { + kill(child_pid, SIGKILL); + waited = 0; + } else { + waited += it_sleep_us; + } + + usleep(it_sleep_us); + } + + return err; +} + +int read_pipe(int pipe_fd, std::string& sbuffer) { + char buffer[128]; + char stderr_buffer[128]; + ssize_t count = 0; + int res = 1; + + for (;;) { + count = read(pipe_fd, buffer, sizeof(buffer)); + if (count > 0) { + buffer[count] = 0; + sbuffer += buffer; + } else if (count == 0){ + res = 0; + break; + } else { + if (errno != EWOULDBLOCK && errno != EINTR) { + res = -1; + } + break; + } + } + + return res; +} + +int wexecvp(const std::string& file, const std::vector& argv, const to_opts* opts, std::string& s_stdout, std::string& s_stderr) { + int err = 0; + int pipe_err = 0; + std::string stdout_ = ""; + std::string stderr_ = ""; + std::vector _argv = argv; + to_opts to_opts { 1000*1000, 1000*100 }; + + // Append null to end of _argv for extra safety + _argv.push_back(nullptr); + // Duplicate file argument to avoid manual duplication + _argv.insert(_argv.begin(), file.c_str()); + + if (opts) { + to_opts.timeout_us = opts->timeout_us; + to_opts.it_delay_us = opts->it_delay_us; + } + + int outfd[2]; + int infd[2]; + + // Pipes for parent to write and read + pipe(pipes[PARENT_READ_PIPE]); + pipe(pipes[PARENT_WRITE_PIPE]); + pipe(pipes[PARENT_ERR_PIPE]); + + pid_t child_pid = fork(); + if(child_pid == 0) { + // Copy the pipe descriptors + dup2(CHILD_READ_FD, STDIN_FILENO); + dup2(CHILD_WRITE_FD, STDOUT_FILENO); + dup2(CHILD_WRITE_ERR, STDERR_FILENO); + + // Close no longer needed pipes + close(CHILD_READ_FD); + close(CHILD_WRITE_FD); + close(CHILD_WRITE_ERR); + + close(PARENT_READ_FD); + close(PARENT_READ_ERR); + close(PARENT_WRITE_FD); + + char** args = const_cast(_argv.data()); + err = execvp(file.c_str(), args); + + if (err) { + exit(errno); + } else { + exit(0); + } + } else { + int errno_cpy = 0; + char stdout_buffer[128]; + char stderr_buffer[128]; + ssize_t stdout_count = 0; + ssize_t stderr_count = 0; + + // Close no longer needed pipes + close(CHILD_READ_FD); + close(CHILD_WRITE_FD); + close(CHILD_WRITE_ERR); + + // Set the pipes in non-blocking mode + fcntl(PARENT_READ_FD, F_SETFL, fcntl(PARENT_READ_FD, F_GETFL) | O_NONBLOCK); + fcntl(PARENT_READ_ERR, F_SETFL, fcntl(PARENT_READ_ERR, F_GETFL) | O_NONBLOCK); + + fd_set read_fds; + uint read_fds_sz = 2; + int maxfd = PARENT_READ_FD > PARENT_READ_ERR ? PARENT_READ_FD : PARENT_READ_ERR; + + bool stdout_eof = false; + bool stderr_eof = false; + + while (!stdout_eof && !stderr_eof) { + FD_ZERO(&read_fds); + FD_SET(PARENT_READ_FD, &read_fds); + FD_SET(PARENT_READ_ERR, &read_fds); + + // Wait for the pipes to be ready + select(maxfd + 1, &read_fds, NULL, NULL, NULL); + + if (FD_ISSET(PARENT_READ_FD, &read_fds)) { + int read_res = read_pipe(PARENT_READ_FD, stdout_); + + if (read_res == 0) { + stdout_eof = true; + } + // Unexpected error while reading pipe + if (read_res < 0) { + pipe_err = -1; + // Backup read errno + errno_cpy = errno; + // Kill child and return error + kill_child_proc(child_pid, to_opts.timeout_us, to_opts.it_delay_us); + // Recover errno before return + errno = errno_cpy; + } + } + + if(FD_ISSET(PARENT_READ_ERR, &read_fds)) { + int read_res = read_pipe(PARENT_READ_ERR, stderr_); + + if (read_res == 0) { + stderr_eof = true; + } + // Unexpected error while reading pipe + if (read_res < 0) { + pipe_err = -1; + // Backup read errno + errno_cpy = errno; + // Kill child and return error + kill_child_proc(child_pid, to_opts.timeout_us, to_opts.it_delay_us); + // Recover errno before return + errno = errno_cpy; + } + } + } + + if (pipe_err == 0) { + waitpid(child_pid, &err, 0); + } + } + + if (pipe_err == 0) { + s_stdout = stdout_; + s_stderr = stderr_; + } else { + err = pipe_err; + } + + return err; +} diff --git a/test/tap/tap/utils.h b/test/tap/tap/utils.h index ac740ec45..b87cf9891 100644 --- a/test/tap/tap/utils.h +++ b/test/tap/tap/utils.h @@ -3,6 +3,7 @@ #include #include +#include #define MYSQL_QUERY(mysql, query) \ do { \ @@ -27,5 +28,27 @@ int select_config_file(MYSQL* mysql, std::string& resultset); } #endif +/** + * @brief Simple struct that holds the 'timeout options' for 'wexecvp'. + */ +struct to_opts { + uint timeout_us; + uint it_delay_us; +}; + +/** + * @brief Execute the given comand, and stores it's output. + * + * @param file File to be executed. + * @param argv Arguments to be given to the executable. + * @param result The output of the file execution. If the execution succeed it contains `stdout` output, + * in case of failure `stderr` contents are returned. + * @param opts In case of pipe readin error, this timeout options are used for trying to terminate + * the child process nicely, before seding a SIGKILL to it: + * - timeout_us: Member specifies the total timeout to wait for the child to exit. + * - it_delay_us: Member specifies the waiting delay between checks. + * @return int Zero in case of success, or the errno returned by `execvp` in case of failure. + */ +int wexecvp(const std::string& file, const std::vector& argv, const to_opts* opts, std::string& s_stdout, std::string& s_stderr); #endif // #define UTILS_H