| /*****************************************************************************\ |
| * run_command.c - run a command asynchronously and return output |
| ***************************************************************************** |
| * Copyright (C) 2014-2017 SchedMD LLC. |
| * Written by Morris Jette <jette@schedmd.com> |
| * |
| * This file is part of Slurm, a resource management program. |
| * For details, see <https://slurm.schedmd.com/>. |
| * Please also read the included file: DISCLAIMER. |
| * |
| * Slurm is free software; you can redistribute it and/or modify it under |
| * the terms of the GNU General Public License as published by the Free |
| * Software Foundation; either version 2 of the License, or (at your option) |
| * any later version. |
| * |
| * In addition, as a special exception, the copyright holders give permission |
| * to link the code of portions of this program with the OpenSSL library under |
| * certain conditions as described in each individual source file, and |
| * distribute linked combinations including the two. You must obey the GNU |
| * General Public License in all respects for all of the code used other than |
| * OpenSSL. If you modify file(s) with this exception, you may extend this |
| * exception to your version of the file(s), but you are not obligated to do |
| * so. If you do not wish to do so, delete this exception statement from your |
| * version. If you delete this exception statement from all source files in |
| * the program, then also delete it here. |
| * |
| * Slurm is distributed in the hope that it will be useful, but WITHOUT ANY |
| * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
| * details. |
| * |
| * You should have received a copy of the GNU General Public License along |
| * with Slurm; if not, write to the Free Software Foundation, Inc., |
| * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| \*****************************************************************************/ |
| |
| #include "config.h" |
| |
| #define _GNU_SOURCE /* For POLLRDHUP */ |
| #include <fcntl.h> |
| #include <poll.h> |
| #include <signal.h> |
| #include <stdlib.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| #include <sys/wait.h> |
| #include <unistd.h> |
| #include <inttypes.h> /* for uint16_t, uint32_t definitions */ |
| |
| #ifndef POLLRDHUP |
| #define POLLRDHUP POLLHUP |
| #endif |
| |
| #include "src/common/fd.h" |
| #include "src/common/macros.h" |
| #include "src/common/timers.h" |
| #include "src/common/xmalloc.h" |
| #include "src/common/xstring.h" |
| #include "src/common/list.h" |
| #include "src/common/run_command.h" |
| |
| static int command_shutdown = 0; |
| static int child_proc_count = 0; |
| static pthread_mutex_t proc_count_mutex = PTHREAD_MUTEX_INITIALIZER; |
| |
| #define MAX_POLL_WAIT 500 |
| |
| extern void run_command_add_to_script(char **script_body, char *new_str) |
| { |
| char *orig_script = *script_body; |
| char *new_script, *sep, save_char; |
| char *tmp_str = NULL; |
| int i; |
| |
| if (!new_str || (new_str[0] == '\0')) |
| return; /* Nothing to prepend */ |
| |
| if (!orig_script) { |
| *script_body = xstrdup(new_str); |
| return; |
| } |
| |
| tmp_str = xstrdup(new_str); |
| i = strlen(tmp_str) - 1; |
| if (tmp_str[i] != '\n') /* Append new line as needed */ |
| xstrcat(tmp_str, "\n"); |
| |
| if (orig_script[0] != '#') { |
| /* Prepend new lines */ |
| new_script = xstrdup(tmp_str); |
| xstrcat(new_script, orig_script); |
| xfree(*script_body); |
| *script_body = new_script; |
| xfree(tmp_str); |
| return; |
| } |
| |
| sep = strchr(orig_script, '\n'); |
| if (sep) { |
| save_char = sep[1]; |
| sep[1] = '\0'; |
| new_script = xstrdup(orig_script); |
| xstrcat(new_script, tmp_str); |
| sep[1] = save_char; |
| xstrcat(new_script, sep + 1); |
| xfree(*script_body); |
| *script_body = new_script; |
| xfree(tmp_str); |
| return; |
| } else { |
| new_script = xstrdup(orig_script); |
| xstrcat(new_script, "\n"); |
| xstrcat(new_script, tmp_str); |
| xfree(*script_body); |
| *script_body = new_script; |
| xfree(tmp_str); |
| return; |
| } |
| } |
| |
| /* used to initialize run_command module */ |
| extern void run_command_init(void) |
| { |
| command_shutdown = 0; |
| } |
| |
| /* used to terminate any outstanding commands */ |
| extern void run_command_shutdown(void) |
| { |
| command_shutdown = 1; |
| } |
| |
| /* Return count of child processes */ |
| extern int run_command_count(void) |
| { |
| int cnt; |
| |
| slurm_mutex_lock(&proc_count_mutex); |
| cnt = child_proc_count; |
| slurm_mutex_unlock(&proc_count_mutex); |
| |
| return cnt; |
| } |
| |
| |
| static int _tot_wait (struct timeval *start_time) |
| { |
| struct timeval end_time; |
| int msec_delay; |
| |
| gettimeofday(&end_time, NULL); |
| msec_delay = (end_time.tv_sec - start_time->tv_sec ) * 1000; |
| msec_delay += ((end_time.tv_usec - start_time->tv_usec + 500) / 1000); |
| return msec_delay; |
| } |
| |
| static void _kill_pg(pid_t pid) |
| { |
| killpg(pid, SIGTERM); |
| usleep(10000); |
| killpg(pid, SIGKILL); |
| } |
| |
| extern char *run_command(run_command_args_t *args) |
| { |
| pid_t cpid; |
| char *resp = NULL; |
| int pfd[2] = { -1, -1 }; |
| |
| if ((args->script_path == NULL) || (args->script_path[0] == '\0')) { |
| error("%s: no script specified", __func__); |
| *(args->status) = 127; |
| resp = xstrdup("Run command failed - configuration error"); |
| return resp; |
| } |
| if (args->script_path[0] != '/') { |
| error("%s: %s is not fully qualified pathname (%s)", |
| __func__, args->script_type, args->script_path); |
| *(args->status) = 127; |
| resp = xstrdup("Run command failed - configuration error"); |
| return resp; |
| } |
| if (access(args->script_path, R_OK | X_OK) < 0) { |
| error("%s: %s can not be executed (%s) %m", |
| __func__, args->script_type, args->script_path); |
| *(args->status) = 127; |
| resp = xstrdup("Run command failed - configuration error"); |
| return resp; |
| } |
| if (!args->turnoff_output) { |
| if (pipe(pfd) != 0) { |
| error("%s: pipe(): %m", __func__); |
| *(args->status) = 127; |
| resp = xstrdup("System error"); |
| return resp; |
| } |
| } |
| slurm_mutex_lock(&proc_count_mutex); |
| child_proc_count++; |
| slurm_mutex_unlock(&proc_count_mutex); |
| if ((cpid = fork()) == 0) { |
| /* |
| * container_g_join() needs to be called in the child process |
| * to avoid a race condition if this process makes a file |
| * before we add the pid to the container in the parent. |
| */ |
| if (args->container_join && |
| ((*(args->container_join))(args->job_id, getuid()) != |
| SLURM_SUCCESS)) |
| error("container_g_join(%u): %m", args->job_id); |
| |
| if (!args->turnoff_output) { |
| int devnull; |
| if ((devnull = open("/dev/null", O_RDWR)) < 0) { |
| error("%s: Unable to open /dev/null: %m", |
| __func__); |
| _exit(127); |
| } |
| dup2(devnull, STDIN_FILENO); |
| dup2(pfd[1], STDERR_FILENO); |
| dup2(pfd[1], STDOUT_FILENO); |
| closeall(3); |
| /* coverity[leaked_handle] */ |
| } else { |
| closeall(0); |
| } |
| setpgid(0, 0); |
| /* |
| * sync euid -> ruid, egid -> rgid to avoid issues with fork'd |
| * processes using access() or similar calls. |
| */ |
| if (setresgid(getegid(), getegid(), -1)) { |
| error("%s: Unable to setresgid()", __func__); |
| _exit(127); |
| } |
| if (setresuid(geteuid(), geteuid(), -1)) { |
| error("%s: Unable to setresuid()", __func__); |
| _exit(127); |
| } |
| if (!args->env) |
| execv(args->script_path, args->script_argv); |
| else |
| execve(args->script_path, args->script_argv, args->env); |
| error("%s: execv(%s): %m", __func__, args->script_path); |
| _exit(127); |
| } else if (cpid < 0) { |
| if (!args->turnoff_output) { |
| close(pfd[0]); |
| close(pfd[1]); |
| } |
| error("%s: fork(): %m", __func__); |
| slurm_mutex_lock(&proc_count_mutex); |
| child_proc_count--; |
| slurm_mutex_unlock(&proc_count_mutex); |
| } else if (!args->turnoff_output) { |
| close(pfd[1]); |
| if (args->tid) |
| track_script_reset_cpid(args->tid, cpid); |
| resp = run_command_poll_child(cpid, |
| args->max_wait, |
| args->orphan_on_shutdown, |
| pfd[0], |
| args->script_path, |
| args->script_type, |
| args->tid, |
| args->status, |
| args->timed_out); |
| close(pfd[0]); |
| slurm_mutex_lock(&proc_count_mutex); |
| child_proc_count--; |
| slurm_mutex_unlock(&proc_count_mutex); |
| } else { |
| if (args->tid) |
| track_script_reset_cpid(args->tid, cpid); |
| waitpid(cpid, args->status, 0); |
| } |
| |
| return resp; |
| } |
| |
| extern char *run_command_poll_child(int cpid, |
| int max_wait, |
| bool orphan_on_shutdown, |
| int read_fd, |
| const char *script_path, |
| const char *script_type, |
| pthread_t tid, |
| int *status, |
| bool *timed_out) |
| { |
| bool send_terminate = true; |
| struct pollfd fds; |
| struct timeval tstart; |
| int resp_size = 1024, resp_offset = 0; |
| int new_wait; |
| int i; |
| char *resp; |
| |
| resp = xmalloc(resp_size); |
| gettimeofday(&tstart, NULL); |
| |
| while (1) { |
| if (command_shutdown) { |
| error("%s: %s %s operation on shutdown", |
| __func__, |
| orphan_on_shutdown ? |
| "orphaning" : "killing", |
| script_type); |
| break; |
| } |
| |
| /* |
| * Pass zero as the status to just see if this script |
| * exists in track_script - if not, then we need to bail |
| * since this script was killed. |
| */ |
| if (tid && |
| track_script_killed(tid, 0, false)) |
| break; |
| |
| fds.fd = read_fd; |
| fds.events = POLLIN | POLLHUP | POLLRDHUP; |
| fds.revents = 0; |
| if (max_wait <= 0) { |
| new_wait = MAX_POLL_WAIT; |
| } else { |
| new_wait = max_wait - _tot_wait(&tstart); |
| if (new_wait <= 0) { |
| error("%s: %s poll timeout @ %d msec", |
| __func__, script_type, |
| max_wait); |
| if (timed_out) |
| *(timed_out) = true; |
| break; |
| } |
| new_wait = MIN(new_wait, MAX_POLL_WAIT); |
| } |
| i = poll(&fds, 1, new_wait); |
| |
| if (i == 0) { |
| continue; |
| } else if (i < 0) { |
| if ((errno == EAGAIN) || (errno == EINTR)) |
| continue; |
| error("%s: %s poll:%m", |
| __func__, script_type); |
| break; |
| } |
| if ((fds.revents & POLLIN) == 0) { |
| send_terminate = false; |
| break; |
| } |
| i = read(read_fd, resp + resp_offset, |
| resp_size - resp_offset); |
| if (i == 0) { |
| send_terminate = false; |
| break; |
| } else if (i < 0) { |
| if (errno == EAGAIN) |
| continue; |
| send_terminate = false; |
| error("%s: read(%s): %m", |
| __func__, |
| script_path); |
| break; |
| } else { |
| resp_offset += i; |
| if (resp_offset + 1024 >= resp_size) { |
| resp_size *= 2; |
| resp = xrealloc(resp, resp_size); |
| } |
| } |
| } |
| if (command_shutdown && orphan_on_shutdown) { |
| /* Don't kill the script on shutdown */ |
| *status = 0; |
| } else if (send_terminate) { |
| /* |
| * Kill immediately if the script isn't exiting |
| * normally. |
| */ |
| _kill_pg(cpid); |
| waitpid(cpid, status, 0); |
| } else { |
| /* |
| * If the STDOUT is closed from the script we may reach |
| * this point without any input in read_fd, so just wait |
| * for the process here until max_wait. |
| */ |
| run_command_waitpid_timeout(script_type, |
| cpid, status, |
| max_wait, |
| _tot_wait(&tstart), |
| tid, timed_out); |
| } |
| |
| return resp; |
| } |
| |
| /* |
| * run_command_waitpid_timeout() |
| * |
| * Same as waitpid(2) but kill process group for pid after timeout millisecs. |
| */ |
| extern int run_command_waitpid_timeout( |
| const char *name, pid_t pid, int *pstatus, int timeout_ms, |
| int elapsed_ms, pthread_t tid, bool *timed_out) |
| { |
| int max_delay = 1000; /* max delay between waitpid calls */ |
| int delay = 10; /* initial delay */ |
| int rc; |
| int options = WNOHANG; |
| int save_timeout_ms = timeout_ms; |
| bool killed_pg = false; |
| |
| if (timeout_ms <= 0 || timeout_ms == NO_VAL16) |
| options = 0; |
| timeout_ms -= elapsed_ms; |
| |
| while ((rc = waitpid (pid, pstatus, options)) <= 0) { |
| if (rc < 0) { |
| if (errno == EINTR) |
| continue; |
| error("%s: waitpid(%d): %m", __func__, pid); |
| return -1; |
| } else if (command_shutdown) { |
| error("%s: killing %s on shutdown", |
| __func__, name); |
| _kill_pg(pid); |
| killed_pg = true; |
| options = 0; |
| } else if (tid && track_script_killed(tid, 0, false)) { |
| /* |
| * Pass zero as the status to track_script_killed() to |
| * know if this script exists in track_script and bail |
| * if it does not. |
| */ |
| _kill_pg(pid); |
| killed_pg = true; |
| options = 0; |
| } else if (timeout_ms <= 0) { |
| error("%s%stimeout after %d ms: killing pgid %d", |
| name != NULL ? name : "", |
| name != NULL ? ": " : "", |
| save_timeout_ms, pid); |
| _kill_pg(pid); |
| killed_pg = true; |
| options = 0; |
| if (timed_out) |
| *timed_out = true; |
| } else { |
| (void) poll(NULL, 0, delay); |
| timeout_ms -= delay; |
| delay = MIN (timeout_ms, MIN(max_delay, delay*2)); |
| } |
| } |
| |
| if (!killed_pg) |
| _kill_pg(pid); /* kill children too */ |
| return rc; |
| } |