/* Copyright 2019 The ChromiumOS Authors * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include #include #include #include #include #include #include #include "2common.h" #include "subprocess.h" #define MAX_CB_BUF_SIZE 2048 static int init_target_private(struct subprocess_target *target) { switch (target->type) { case TARGET_BUFFER: case TARGET_BUFFER_NULL_TERMINATED: case TARGET_CALLBACK: return pipe(target->priv.pipefd); default: return 0; } } static int flags_for_fd(int fd) { switch (fd) { case STDIN_FILENO: return O_RDONLY; case STDOUT_FILENO: case STDERR_FILENO: return O_WRONLY; default: return -1; } } static int connect_process_target(struct subprocess_target *target, int fd) { int target_fd; switch (target->type) { case TARGET_NULL: target_fd = open("/dev/null", flags_for_fd(fd)); break; case TARGET_FD: target_fd = target->fd; break; case TARGET_FILE: target_fd = fileno(target->file); break; case TARGET_BUFFER: case TARGET_BUFFER_NULL_TERMINATED: case TARGET_CALLBACK: switch (fd) { case STDIN_FILENO: target_fd = target->priv.pipefd[0]; close(target->priv.pipefd[1]); break; case STDOUT_FILENO: case STDERR_FILENO: target_fd = target->priv.pipefd[1]; close(target->priv.pipefd[0]); break; default: return -1; } break; default: return -1; } return dup2(target_fd, fd); } static int process_target_input_buffer(struct subprocess_target *target) { ssize_t write_rv; size_t bytes_to_write; char *buf; switch (target->type) { case TARGET_BUFFER: bytes_to_write = target->buffer.size; break; case TARGET_BUFFER_NULL_TERMINATED: bytes_to_write = strlen(target->buffer.buf); break; default: return -1; } buf = target->buffer.buf; while (bytes_to_write) { write_rv = write(target->priv.pipefd[1], buf, bytes_to_write); if (write_rv <= 0) return -1; buf += write_rv; bytes_to_write -= write_rv; } return 0; } static int process_target_input_cb(struct subprocess_target *target) { ssize_t write_rv, bytes_to_write; char buf[MAX_CB_BUF_SIZE]; char *bufptr; for (;;) { bytes_to_write = target->callback.cb(buf, MAX_CB_BUF_SIZE, target->callback.data); if (bytes_to_write < 0 || bytes_to_write > MAX_CB_BUF_SIZE) return -1; if (bytes_to_write == 0) return 0; bufptr = buf; while (bytes_to_write) { write_rv = write(target->priv.pipefd[1], bufptr, bytes_to_write); if (write_rv <= 0) return -1; bufptr += write_rv; bytes_to_write -= write_rv; } } } static int process_target_input(struct subprocess_target *target) { int rv; switch (target->type) { case TARGET_BUFFER: case TARGET_BUFFER_NULL_TERMINATED: case TARGET_CALLBACK: break; default: return 0; } close(target->priv.pipefd[0]); switch (target->type) { case TARGET_BUFFER: case TARGET_BUFFER_NULL_TERMINATED: rv = process_target_input_buffer(target); break; case TARGET_CALLBACK: rv = process_target_input_cb(target); break; default: return -1; } close(target->priv.pipefd[1]); return rv; } static int process_target_output_buffer(struct subprocess_target *target) { ssize_t read_rv; size_t bytes_remaining; switch (target->type) { case TARGET_BUFFER: bytes_remaining = target->buffer.size; break; case TARGET_BUFFER_NULL_TERMINATED: if (target->buffer.size == 0) return -1; bytes_remaining = target->buffer.size - 1; break; default: return 0; } target->buffer.bytes_consumed = 0; while (bytes_remaining) { read_rv = read( target->priv.pipefd[0], target->buffer.buf + target->buffer.bytes_consumed, bytes_remaining); if (read_rv < 0) return -1; if (read_rv == 0) break; target->buffer.bytes_consumed += read_rv; bytes_remaining -= read_rv; } if (target->type == TARGET_BUFFER_NULL_TERMINATED) target->buffer.buf[target->buffer.bytes_consumed] = '\0'; return 0; } static int process_target_output_cb(struct subprocess_target *target) { char buf[MAX_CB_BUF_SIZE]; ssize_t rv; for (;;) { rv = read(target->priv.pipefd[0], buf, MAX_CB_BUF_SIZE); if (rv < 0) return -1; if (rv == 0) break; if (target->callback.cb(buf, rv, target->callback.data) < 0) return -1; } return 0; } static int process_target_output(struct subprocess_target *target) { int rv; switch (target->type) { case TARGET_BUFFER: case TARGET_BUFFER_NULL_TERMINATED: case TARGET_CALLBACK: break; default: return 0; } close(target->priv.pipefd[1]); switch (target->type) { case TARGET_BUFFER: case TARGET_BUFFER_NULL_TERMINATED: rv = process_target_output_buffer(target); break; case TARGET_CALLBACK: rv = process_target_output_cb(target); break; default: return -1; } close(target->priv.pipefd[0]); return rv; } static bool contains_spaces(const char *s) { for (size_t i = 0; s[i]; i++) { if (isspace(s[i])) return true; } return false; } static void subprocess_log_call(const char *const argv[]) { VB2_DEBUG("Run:"); for (size_t i = 0; argv[i]; i++) { if (contains_spaces(argv[i])) VB2_DEBUG_RAW(" '%s'", argv[i]); else VB2_DEBUG_RAW(" %s", argv[i]); } VB2_DEBUG_RAW("\n"); } struct subprocess_target subprocess_null = { .type = TARGET_NULL, }; struct subprocess_target subprocess_stdin = { .type = TARGET_FD, .fd = STDIN_FILENO, }; struct subprocess_target subprocess_stdout = { .type = TARGET_FD, .fd = STDOUT_FILENO, }; struct subprocess_target subprocess_stderr = { .type = TARGET_FD, .fd = STDERR_FILENO, }; test_mockable int subprocess_run(const char *const argv[], struct subprocess_target *input, struct subprocess_target *output, struct subprocess_target *error) { int status; pid_t pid = -1; subprocess_log_call(argv); if (!input) input = &subprocess_stdin; if (!output) output = &subprocess_stdout; if (!error) error = &subprocess_stderr; if (init_target_private(input) < 0) goto fail; if (init_target_private(output) < 0) goto fail; if (init_target_private(error) < 0) goto fail; if ((pid = fork()) < 0) goto fail; if (pid == 0) { /* Child process */ if (connect_process_target(input, STDIN_FILENO) < 0) goto fail; if (connect_process_target(output, STDOUT_FILENO) < 0) goto fail; if (connect_process_target(error, STDERR_FILENO) < 0) goto fail; execvp(*argv, (char *const *)argv); goto fail; } /* Parent process */ if (process_target_input(input) < 0) goto fail; if (process_target_output(output) < 0) goto fail; if (process_target_output(error) < 0) goto fail; if (waitpid(pid, &status, 0) < 0) goto fail; if (WIFEXITED(status)) return WEXITSTATUS(status); fail: VB2_DEBUG("Failed to execute external command: %s\n", strerror(errno)); if (pid == 0) exit(127); return -1; }