Interfaces to execute/manage processes in the guest. Child process' stdin/stdout/stderr can be associated with handles for communication via read/write interfaces.
Signed-off-by: Michael Roth <mdr...@linux.vnet.ibm.com> --- qapi-schema-guest.json | 55 ++++++++ qga/guest-agent-commands.c | 299 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+), 0 deletions(-) diff --git a/qapi-schema-guest.json b/qapi-schema-guest.json index 4c9f063..7bf3086 100644 --- a/qapi-schema-guest.json +++ b/qapi-schema-guest.json @@ -237,3 +237,58 @@ ## { 'command': 'guest-fsfreeze-thaw', 'returns': 'int' } + +## +# @guest-exec-status +# +# Check status of process associated with PID retrieved via guest-exec. +# Reap the process and associated metadata if it has exited. +# +# @pid: pid returned from guest-exec +# @wait: #optional whether to wait for completion or simply poll once. +# Waiting is disallowed if a stdin/stdout/stderr handle was supplied +# to guest-exec +# +# Returns: GuestExecStatus on success +# +{ 'type': 'GuestExecStatus', + 'data': { 'pid': 'int', 'exited': 'bool', 'exit-code': 'int', + 'handle_stdin': 'int', 'handle_stdout': 'int', + 'handle_stderr': 'int' } } +{ 'command': 'guest-exec-status', + 'data': { 'pid': 'int', '*wait': 'bool' }, + 'returns': 'GuestExecStatus' } + +## +# @guest-exec: +# +# Execute a command in the guest +# +# If a pipe is associated with the resulting process, the +# read/write/write sides of the process' stdin/stdout/stderr will +# be transferred automatically, so no need to close them from the +# client. If no handle is passed in for stdin/stdout/stderr, they +# will be closed before executing the command. +# +# @path: path or executable name to execute +# @params: #optional parameter list to pass to executable +# @handle_stdin: #optional handle to associate with process' stdin. +# @handle_stdout: #optional handle to associate with process' stdout +# @handle_stderr: #optional handle to associate with process' stderr +# @detach: #optional whether to detach the process or execute it +# synchronously. If synchronous, passing of stdin/stdout/stderr handles +# will be disallowed, since process can block on closing them and cause +# a deadlock in the guest agent. +# +# Returns: GuestExecStatus on success. +# +# Since: 1.0.50 +## +{ 'type': 'GuestExecParam', + 'data': { 'param': 'str' } } +{ 'command': 'guest-exec', + 'data': { 'path': 'str', '*params': ['GuestExecParam'], + '*handle_stdin': 'int', '*handle_stdout': 'int', + '*handle_stderr': 'int', + '*detach': 'bool' }, + 'returns': 'GuestExecStatus' } diff --git a/qga/guest-agent-commands.c b/qga/guest-agent-commands.c index ae77ee4..11f9d00 100644 --- a/qga/guest-agent-commands.c +++ b/qga/guest-agent-commands.c @@ -450,6 +450,304 @@ static void guest_file_init(void) QTAILQ_INIT(&guest_file_state.filehandles); } +typedef struct GuestExecInfo { + pid_t pid; + char **params; + GuestFileHandle *gfh_stdin; + GuestFileHandle *gfh_stdout; + GuestFileHandle *gfh_stderr; + QTAILQ_ENTRY(GuestExecInfo) next; +} GuestExecInfo; + +static struct { + QTAILQ_HEAD(, GuestExecInfo) processes; +} guest_exec_state; + +static void guest_exec_info_add(pid_t pid, char **params, + GuestFileHandle *in, GuestFileHandle *out, + GuestFileHandle *error, Error **err) +{ + GuestExecInfo *gei; + + gei = g_malloc0(sizeof(*gei)); + gei->pid = pid; + gei->params = params; + gei->gfh_stdin = in; + gei->gfh_stdout = out; + gei->gfh_stderr = error; + QTAILQ_INSERT_TAIL(&guest_exec_state.processes, gei, next); +} + +static GuestExecInfo *guest_exec_info_find(pid_t pid) +{ + GuestExecInfo *gei; + + QTAILQ_FOREACH(gei, &guest_exec_state.processes, next) + { + if (gei->pid == pid) { + return gei; + } + } + + return NULL; +} + +#include <sys/wait.h> +#include <sys/types.h> +GuestExecStatus *qmp_guest_exec_status(int64_t pid, bool has_wait, + bool wait, Error **err) +{ + GuestExecInfo *gei; + GuestExecStatus *ges; + int status, ret; + char **ptr; + + slog("guest-exec-status called"); + + gei = guest_exec_info_find(pid); + if (gei == NULL) { + error_set(err, QERR_INVALID_PARAMETER, "pid"); + return NULL; + } + + ret = waitpid(gei->pid, &status, WNOHANG); + if (ret == -1) { + error_set(err, QERR_UNDEFINED_ERROR); + return NULL; + } + + ges = g_malloc0(sizeof(*ges)); + ges->handle_stdin = gei->gfh_stdin ? gei->gfh_stdin->id : -1; + ges->handle_stdout = gei->gfh_stdout ? gei->gfh_stdout->id : -1; + ges->handle_stderr = gei->gfh_stderr ? gei->gfh_stderr->id : -1; + ges->pid = gei->pid; + if (ret == 0) { + ges->exited = false; + } else { + ges->exited = true; + /* reap child info once user has successfully wait()'d */ + QTAILQ_REMOVE(&guest_exec_state.processes, gei, next); + for (ptr = gei->params; ptr && *ptr != NULL; ptr++) { + g_free(*ptr); + } + g_free(gei->params); + g_free(gei); + } + return ges; +} + +static char **extract_param_array(const char *path, + const GuestExecParamList *entry) +{ + const GuestExecParamList *head; + GuestExecParam *param_container; + int count = 2; /* reserve 2 for path and NULL terminator */ + int i = 0; + char **param_array; + + for (head = entry; head != NULL; head = head->next) { + param_container = head->value; + printf("value: %s\n", param_container->param); + count++; + } + + param_array = g_malloc0((count) * sizeof(*param_array)); + param_array[i++] = strdup(path); + + for (head = entry; head != NULL; head = head->next) { + param_container = head->value; + /* NULL-terminated list, so if they passed us a NULL param dup + * in an emptry string to avoid client-induced memory leak + */ + if (param_container->param) { + param_array[i] = strdup(param_container->param); + } else { + param_array[i] = strdup(""); + } + i++; + } + + return param_array; +} + +GuestExecStatus *qmp_guest_exec(const char *path, + bool has_params, + GuestExecParamList *params, + bool has_handle_stdin, + int64_t handle_stdin, + bool has_handle_stdout, + int64_t handle_stdout, + bool has_handle_stderr, + int64_t handle_stderr, + bool has_detach, bool detach, + Error **err) +{ + int ret, fd; + Error *local_err = NULL; + GuestExecStatus *ges; + GuestFileHandle *gfh_stdin = NULL; + GuestFileHandle *gfh_stdout = NULL; + GuestFileHandle *gfh_stderr = NULL; + char **param_array; + + slog("guest-exec called"); + + if (!has_detach) { + detach = false; + } + + param_array = extract_param_array(path, has_params ? params : NULL); + + /* processes can block writing to a pipe, so don't allow redirect + * to a pipe unless we're doing detached/asynchronous exec() + */ + if (has_handle_stdin) { + gfh_stdin = guest_file_handle_find(handle_stdin); + if (!gfh_stdin) { + error_set(err, QERR_FD_NOT_FOUND, "handle_stdin"); + return NULL; + } + if (!detach && gfh_stdin->is_pipe) { + error_set(err, QERR_QGA_COMMAND_FAILED, + "can't use stdin pipe for non-detached exec()"); + return NULL; + } + } else { + handle_stdin = -1; + } + if (has_handle_stdout) { + gfh_stdout = guest_file_handle_find(handle_stdout); + if (!gfh_stdout) { + error_set(err, QERR_FD_NOT_FOUND, "handle_stdout"); + return NULL; + } + if (!detach && gfh_stdout->is_pipe) { + error_set(err, QERR_QGA_COMMAND_FAILED, + "can't use stdout pipe for non-detached exec()"); + return NULL; + } + } else { + handle_stdin = -1; + } + if (has_handle_stderr) { + gfh_stderr = guest_file_handle_find(handle_stderr); + if (!gfh_stderr) { + error_set(err, QERR_FD_NOT_FOUND, "handle_stderr"); + return NULL; + } + if (!detach && gfh_stderr->is_pipe) { + error_set(err, QERR_QGA_COMMAND_FAILED, + "can't use stderr pipe for non-detached exec()"); + return NULL; + } + } else { + handle_stderr = -1; + } + + ret = fork(); + if (ret < 0) { + error_set(err, QERR_UNDEFINED_ERROR); + return NULL; + } else if (ret == 0) { + slog("guest-exec child spawned"); + /* exec the command */ + setsid(); + if (has_handle_stdin) { + if (gfh_stdin->is_pipe) { + fclose(gfh_stdin->stream.pipe.in); /* parent writes to this */ + fd = fileno(gfh_stdin->stream.pipe.out); + } else { + fd = fileno(gfh_stdin->stream.fh); + } + dup2(fd, STDIN_FILENO); + /* processes don't seem to like O_NONBLOCK std in/out/err */ + toggle_flags(fd, O_NONBLOCK, false, err); + if (error_is_set(err)) { + return NULL; + } + } else { + fclose(stdin); + } + if (has_handle_stdout) { + if (gfh_stdout->is_pipe) { + fclose(gfh_stdout->stream.pipe.out); /* parent reads this end */ + fd = fileno(gfh_stdout->stream.pipe.in); + } else { + fd = fileno(gfh_stdout->stream.fh); + } + dup2(fd, STDOUT_FILENO); + toggle_flags(fd, O_NONBLOCK, false, err); + if (error_is_set(err)) { + return NULL; + } + } else { + fclose(stdout); + } + if (has_handle_stderr) { + if (gfh_stderr->is_pipe) { + fclose(gfh_stderr->stream.pipe.out); /* parent reads this end */ + fd = fileno(gfh_stderr->stream.pipe.in); + } else { + fd = fileno(gfh_stderr->stream.fh); + } + dup2(fd, STDERR_FILENO); + toggle_flags(fd, O_NONBLOCK, false, err); + if (error_is_set(err)) { + return NULL; + } + } else { + fclose(stderr); + } + + ret = execvp(path, (char * const*)param_array); + if (ret) { + slog("guest-exec child failed: %s", strerror(errno)); + } + exit(!!ret); + } + + if (has_handle_stdin && gfh_stdin->is_pipe) { + /* child reads from this end, not us */ + qmp_guest_file_close(gfh_stdin->id, true, GUEST_FILE_PIPE_END_R, err); + if (error_is_set(err)) { + return NULL; + } + } + if (has_handle_stdout && gfh_stdout->is_pipe) { + /* child writes to this end, not us */ + qmp_guest_file_close(gfh_stdout->id, true, GUEST_FILE_PIPE_END_W, err); + if (error_is_set(err)) { + return NULL; + } + } + if (has_handle_stderr && gfh_stderr->is_pipe) { + /* child writes to this end, not us */ + qmp_guest_file_close(gfh_stderr->id, true, GUEST_FILE_PIPE_END_W, err); + if (error_is_set(err)) { + return NULL; + } + } + guest_exec_info_add(ret, param_array, gfh_stdin, gfh_stdout, gfh_stderr, + &local_err); + if (local_err) { + error_propagate(err, local_err); + return NULL; + } + + /* return initial status, or wait for completion if !detach */ + if (!detach) { + ges = qmp_guest_exec_status(ret, true, true, err); + } else { + ges = qmp_guest_exec_status(ret, true, false, err); + } + return ges; +} + +static void guest_exec_init(void) +{ + QTAILQ_INIT(&guest_exec_state.processes); +} + #if defined(CONFIG_FSFREEZE) static void disable_logging(void) { @@ -687,4 +985,5 @@ void ga_command_state_init(GAState *s, GACommandState *cs) ga_command_state_add(cs, guest_fsfreeze_init, guest_fsfreeze_cleanup); #endif ga_command_state_add(cs, guest_file_init, NULL); + ga_command_state_add(cs, guest_exec_init, NULL); } -- 1.7.4.1