diff options
Diffstat (limited to 'exec.c')
-rw-r--r-- | exec.c | 715 |
1 files changed, 0 insertions, 715 deletions
@@ -1,715 +0,0 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2017-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ - -#include "exec.h" -#include "bftw.h" -#include "ctx.h" -#include "color.h" -#include "diag.h" -#include "dstring.h" -#include "util.h" -#include "xspawn.h" -#include <assert.h> -#include <errno.h> -#include <fcntl.h> -#include <stdarg.h> -#include <stdbool.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/resource.h> -#include <sys/wait.h> -#include <unistd.h> - -/** Print some debugging info. */ -BFS_FORMATTER(2, 3) -static void bfs_exec_debug(const struct bfs_exec *execbuf, const char *format, ...) { - const struct bfs_ctx *ctx = execbuf->ctx; - - if (!bfs_debug(ctx, DEBUG_EXEC, "${blu}")) { - return; - } - - if (execbuf->flags & BFS_EXEC_CONFIRM) { - fputs("-ok", stderr); - } else { - fputs("-exec", stderr); - } - if (execbuf->flags & BFS_EXEC_CHDIR) { - fputs("dir", stderr); - } - cfprintf(ctx->cerr, "${rs}: "); - - va_list args; - va_start(args, format); - vfprintf(stderr, format, args); - va_end(args); -} - -/** Determine the size of a single argument, for comparison to arg_max. */ -static size_t bfs_exec_arg_size(const char *arg) { - return sizeof(arg) + strlen(arg) + 1; -} - -/** Even if we can pass a bigger argument list, cap it here. */ -#define BFS_EXEC_ARG_MAX (16 << 20) - -/** Determine the maximum argv size. */ -static size_t bfs_exec_arg_max(const struct bfs_exec *execbuf) { - long arg_max = sysconf(_SC_ARG_MAX); - bfs_exec_debug(execbuf, "ARG_MAX: %ld according to sysconf()\n", arg_max); - if (arg_max < 0) { - arg_max = BFS_EXEC_ARG_MAX; - bfs_exec_debug(execbuf, "ARG_MAX: %ld assumed\n", arg_max); - } - - // We have to share space with the environment variables - extern char **environ; - for (char **envp = environ; *envp; ++envp) { - arg_max -= bfs_exec_arg_size(*envp); - } - // Account for the terminating NULL entry - arg_max -= sizeof(char *); - bfs_exec_debug(execbuf, "ARG_MAX: %ld remaining after environment variables\n", arg_max); - - // Account for the fixed arguments - for (size_t i = 0; i < execbuf->tmpl_argc - 1; ++i) { - arg_max -= bfs_exec_arg_size(execbuf->tmpl_argv[i]); - } - // Account for the terminating NULL entry - arg_max -= sizeof(char *); - bfs_exec_debug(execbuf, "ARG_MAX: %ld remaining after fixed arguments\n", arg_max); - - // Assume arguments are counted with the granularity of a single page, - // so allow a one page cushion to account for rounding up - long page_size = sysconf(_SC_PAGESIZE); - if (page_size < 4096) { - page_size = 4096; - } - arg_max -= page_size; - bfs_exec_debug(execbuf, "ARG_MAX: %ld remaining after page cushion\n", arg_max); - - // POSIX recommends an additional 2048 bytes of headroom - arg_max -= 2048; - bfs_exec_debug(execbuf, "ARG_MAX: %ld remaining after headroom\n", arg_max); - - if (arg_max < 0) { - arg_max = 0; - } else if (arg_max > BFS_EXEC_ARG_MAX) { - arg_max = BFS_EXEC_ARG_MAX; - } - - bfs_exec_debug(execbuf, "ARG_MAX: %ld final value\n", arg_max); - return arg_max; -} - -/** Highlight part of the command line as an error. */ -static void bfs_exec_parse_error(const struct bfs_ctx *ctx, const struct bfs_exec *execbuf) { - char **argv = execbuf->tmpl_argv - 1; - size_t argc = execbuf->tmpl_argc + 1; - if (argv[argc]) { - ++argc; - } - - bool args[ctx->argc]; - for (size_t i = 0; i < ctx->argc; ++i) { - args[i] = false; - } - - size_t i = argv - ctx->argv; - for (size_t j = 0; j < argc; ++j) { - args[i + j] = true; - } - - bfs_argv_error(ctx, args); -} - -struct bfs_exec *bfs_exec_parse(const struct bfs_ctx *ctx, char **argv, enum bfs_exec_flags flags) { - struct bfs_exec *execbuf = malloc(sizeof(*execbuf)); - if (!execbuf) { - bfs_perror(ctx, "malloc()"); - goto fail; - } - - execbuf->flags = flags; - execbuf->ctx = ctx; - execbuf->tmpl_argv = argv + 1; - execbuf->tmpl_argc = 0; - execbuf->argv = NULL; - execbuf->argc = 0; - execbuf->argv_cap = 0; - execbuf->arg_size = 0; - execbuf->arg_max = 0; - execbuf->arg_min = 0; - execbuf->wd_fd = -1; - execbuf->wd_path = NULL; - execbuf->wd_len = 0; - execbuf->ret = 0; - - while (true) { - const char *arg = execbuf->tmpl_argv[execbuf->tmpl_argc]; - if (!arg) { - if (execbuf->flags & BFS_EXEC_CONFIRM) { - bfs_exec_parse_error(ctx, execbuf); - bfs_error(ctx, "Expected '... ;'.\n"); - } else { - bfs_exec_parse_error(ctx, execbuf); - bfs_error(ctx, "Expected '... ;' or '... {} +'.\n"); - } - goto fail; - } else if (strcmp(arg, ";") == 0) { - break; - } else if (strcmp(arg, "+") == 0) { - const char *prev = execbuf->tmpl_argv[execbuf->tmpl_argc - 1]; - if (!(execbuf->flags & BFS_EXEC_CONFIRM) && strcmp(prev, "{}") == 0) { - execbuf->flags |= BFS_EXEC_MULTI; - break; - } - } - - ++execbuf->tmpl_argc; - } - - if (execbuf->tmpl_argc == 0) { - bfs_exec_parse_error(ctx, execbuf); - bfs_error(ctx, "Missing command.\n"); - goto fail; - } - - execbuf->argv_cap = execbuf->tmpl_argc + 1; - execbuf->argv = malloc(execbuf->argv_cap*sizeof(*execbuf->argv)); - if (!execbuf->argv) { - bfs_perror(ctx, "malloc()"); - goto fail; - } - - if (execbuf->flags & BFS_EXEC_MULTI) { - for (size_t i = 0; i < execbuf->tmpl_argc - 1; ++i) { - char *arg = execbuf->tmpl_argv[i]; - if (strstr(arg, "{}")) { - bfs_exec_parse_error(ctx, execbuf); - bfs_error(ctx, "Only one '{}' is supported.\n"); - goto fail; - } - execbuf->argv[i] = arg; - } - execbuf->argc = execbuf->tmpl_argc - 1; - - execbuf->arg_max = bfs_exec_arg_max(execbuf); - execbuf->arg_min = execbuf->arg_max; - } - - return execbuf; - -fail: - bfs_exec_free(execbuf); - return NULL; -} - -/** Format the current path for use as a command line argument. */ -static char *bfs_exec_format_path(const struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { - if (!(execbuf->flags & BFS_EXEC_CHDIR)) { - return strdup(ftwbuf->path); - } - - const char *name = ftwbuf->path + ftwbuf->nameoff; - - if (name[0] == '/') { - // Must be a root path ("/", "//", etc.) - return strdup(name); - } - - // For compatibility with GNU find, use './name' instead of just 'name' - char *path = malloc(2 + strlen(name) + 1); - if (!path) { - return NULL; - } - - strcpy(path, "./"); - strcpy(path + 2, name); - - return path; -} - -/** Format an argument, expanding "{}" to the current path. */ -static char *bfs_exec_format_arg(char *arg, const char *path) { - char *match = strstr(arg, "{}"); - if (!match) { - return arg; - } - - char *ret = dstralloc(0); - if (!ret) { - return NULL; - } - - char *last = arg; - do { - if (dstrncat(&ret, last, match - last) != 0) { - goto err; - } - if (dstrcat(&ret, path) != 0) { - goto err; - } - - last = match + 2; - match = strstr(last, "{}"); - } while (match); - - if (dstrcat(&ret, last) != 0) { - goto err; - } - - return ret; - -err: - dstrfree(ret); - return NULL; -} - -/** Free a formatted argument. */ -static void bfs_exec_free_arg(char *arg, const char *tmpl) { - if (arg != tmpl) { - dstrfree(arg); - } -} - -/** Open a file to use as the working directory. */ -static int bfs_exec_openwd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { - assert(execbuf->wd_fd < 0); - assert(!execbuf->wd_path); - - if (ftwbuf->at_fd != AT_FDCWD) { - // Rely on at_fd being the immediate parent - assert(ftwbuf->at_path == xbasename(ftwbuf->at_path)); - - execbuf->wd_fd = ftwbuf->at_fd; - if (!(execbuf->flags & BFS_EXEC_MULTI)) { - return 0; - } - - execbuf->wd_fd = dup_cloexec(execbuf->wd_fd); - if (execbuf->wd_fd < 0) { - return -1; - } - } - - execbuf->wd_len = ftwbuf->nameoff; - if (execbuf->wd_len == 0) { - if (ftwbuf->path[0] == '/') { - ++execbuf->wd_len; - } else { - // The path is something like "foo", so we're already in the right directory - return 0; - } - } - - execbuf->wd_path = strndup(ftwbuf->path, execbuf->wd_len); - if (!execbuf->wd_path) { - return -1; - } - - if (execbuf->wd_fd < 0) { - execbuf->wd_fd = open(execbuf->wd_path, O_RDONLY | O_CLOEXEC | O_DIRECTORY); - } - - if (execbuf->wd_fd < 0) { - return -1; - } - - return 0; -} - -/** Close the working directory. */ -static void bfs_exec_closewd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { - if (execbuf->wd_fd >= 0) { - if (!ftwbuf || execbuf->wd_fd != ftwbuf->at_fd) { - xclose(execbuf->wd_fd); - } - execbuf->wd_fd = -1; - } - - if (execbuf->wd_path) { - free(execbuf->wd_path); - execbuf->wd_path = NULL; - execbuf->wd_len = 0; - } -} - -/** Actually spawn the process. */ -static int bfs_exec_spawn(const struct bfs_exec *execbuf) { - if (execbuf->flags & BFS_EXEC_CONFIRM) { - for (size_t i = 0; i < execbuf->argc; ++i) { - if (fprintf(stderr, "%s ", execbuf->argv[i]) < 0) { - return -1; - } - } - if (fprintf(stderr, "? ") < 0) { - return -1; - } - - if (ynprompt() <= 0) { - errno = 0; - return -1; - } - } - - // Flush cached state for consistency with the external process - bfs_ctx_flush(execbuf->ctx); - - if (execbuf->flags & BFS_EXEC_MULTI) { - bfs_exec_debug(execbuf, "Executing '%s' ... [%zu arguments] (size %zu)\n", - execbuf->argv[0], execbuf->argc - 1, execbuf->arg_size); - } else { - bfs_exec_debug(execbuf, "Executing '%s' ... [%zu arguments]\n", execbuf->argv[0], execbuf->argc - 1); - } - - pid_t pid = -1; - int error; - - struct bfs_spawn ctx; - if (bfs_spawn_init(&ctx) != 0) { - return -1; - } - - if (bfs_spawn_setflags(&ctx, BFS_SPAWN_USEPATH) != 0) { - goto fail; - } - - // Reset RLIMIT_NOFILE, to avoid breaking applications that use select() - struct rlimit rl = { - .rlim_cur = execbuf->ctx->nofile_soft, - .rlim_max = execbuf->ctx->nofile_hard, - }; - if (bfs_spawn_addsetrlimit(&ctx, RLIMIT_NOFILE, &rl) != 0) { - goto fail; - } - - if (execbuf->wd_fd >= 0) { - if (bfs_spawn_addfchdir(&ctx, execbuf->wd_fd) != 0) { - goto fail; - } - } - - pid = bfs_spawn(execbuf->argv[0], &ctx, execbuf->argv, NULL); -fail: - error = errno; - bfs_spawn_destroy(&ctx); - if (pid < 0) { - errno = error; - return -1; - } - - int wstatus; - if (waitpid(pid, &wstatus, 0) < 0) { - return -1; - } - - int ret = -1; - - if (WIFEXITED(wstatus)) { - int status = WEXITSTATUS(wstatus); - if (status == EXIT_SUCCESS) { - ret = 0; - } else { - bfs_exec_debug(execbuf, "Command '%s' failed with status %d\n", execbuf->argv[0], status); - } - } else if (WIFSIGNALED(wstatus)) { - int sig = WTERMSIG(wstatus); - const char *str = strsignal(sig); - if (!str) { - str = "unknown"; - } - bfs_warning(execbuf->ctx, "Command '${ex}%s${rs}' terminated by signal %d (%s)\n", execbuf->argv[0], sig, str); - } else { - bfs_warning(execbuf->ctx, "Command '${ex}%s${rs}' terminated abnormally\n", execbuf->argv[0]); - } - - errno = 0; - return ret; -} - -/** exec() a command for a single file. */ -static int bfs_exec_single(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { - int ret = -1, error = 0; - - char *path = bfs_exec_format_path(execbuf, ftwbuf); - if (!path) { - goto out; - } - - size_t i; - for (i = 0; i < execbuf->tmpl_argc; ++i) { - execbuf->argv[i] = bfs_exec_format_arg(execbuf->tmpl_argv[i], path); - if (!execbuf->argv[i]) { - goto out_free; - } - } - execbuf->argv[i] = NULL; - execbuf->argc = i; - - if (execbuf->flags & BFS_EXEC_CHDIR) { - if (bfs_exec_openwd(execbuf, ftwbuf) != 0) { - goto out_free; - } - } - - ret = bfs_exec_spawn(execbuf); - -out_free: - error = errno; - - bfs_exec_closewd(execbuf, ftwbuf); - - for (size_t j = 0; j < i; ++j) { - bfs_exec_free_arg(execbuf->argv[j], execbuf->tmpl_argv[j]); - } - - free(path); - - errno = error; - -out: - return ret; -} - -/** Check if any arguments remain in the buffer. */ -static bool bfs_exec_args_remain(const struct bfs_exec *execbuf) { - return execbuf->argc >= execbuf->tmpl_argc; -} - -/** Compute the current ARG_MAX estimate for binary search. */ -static size_t bfs_exec_estimate_max(const struct bfs_exec *execbuf) { - size_t min = execbuf->arg_min; - size_t max = execbuf->arg_max; - return min + (max - min)/2; -} - -/** Update the ARG_MAX lower bound from a successful execution. */ -static void bfs_exec_update_min(struct bfs_exec *execbuf) { - if (execbuf->arg_size > execbuf->arg_min) { - execbuf->arg_min = execbuf->arg_size; - - // Don't let min exceed max - if (execbuf->arg_min > execbuf->arg_max) { - execbuf->arg_min = execbuf->arg_max; - } - - size_t estimate = bfs_exec_estimate_max(execbuf); - bfs_exec_debug(execbuf, "ARG_MAX between [%zu, %zu], trying %zu\n", - execbuf->arg_min, execbuf->arg_max, estimate); - } -} - -/** Update the ARG_MAX upper bound from a failed execution. */ -static size_t bfs_exec_update_max(struct bfs_exec *execbuf) { - bfs_exec_debug(execbuf, "Got E2BIG, shrinking argument list...\n"); - - size_t size = execbuf->arg_size; - if (size <= execbuf->arg_min) { - // Lower bound was wrong, restart binary search. - execbuf->arg_min = 0; - } - - // Trim a fraction off the max size to avoid repeated failures near the - // top end of the working range - size -= size/16; - if (size < execbuf->arg_max) { - execbuf->arg_max = size; - - // Don't let min exceed max - if (execbuf->arg_min > execbuf->arg_max) { - execbuf->arg_min = execbuf->arg_max; - } - } - - // Binary search for a more precise bound - size_t estimate = bfs_exec_estimate_max(execbuf); - bfs_exec_debug(execbuf, "ARG_MAX between [%zu, %zu], trying %zu\n", - execbuf->arg_min, execbuf->arg_max, estimate); - return estimate; -} - -/** Execute the pending command from a BFS_EXEC_MULTI execbuf. */ -static int bfs_exec_flush(struct bfs_exec *execbuf) { - int ret = 0, error = 0; - - size_t orig_argc = execbuf->argc; - while (bfs_exec_args_remain(execbuf)) { - execbuf->argv[execbuf->argc] = NULL; - ret = bfs_exec_spawn(execbuf); - error = errno; - if (ret == 0) { - bfs_exec_update_min(execbuf); - break; - } else if (error != E2BIG) { - break; - } - - // Try to recover from E2BIG by trying fewer and fewer arguments - // until they fit - size_t new_max = bfs_exec_update_max(execbuf); - while (execbuf->arg_size > new_max) { - execbuf->argv[execbuf->argc] = execbuf->argv[execbuf->argc - 1]; - execbuf->arg_size -= bfs_exec_arg_size(execbuf->argv[execbuf->argc]); - --execbuf->argc; - } - } - - size_t new_argc = execbuf->argc; - for (size_t i = execbuf->tmpl_argc - 1; i < new_argc; ++i) { - free(execbuf->argv[i]); - } - execbuf->argc = execbuf->tmpl_argc - 1; - execbuf->arg_size = 0; - - if (new_argc < orig_argc) { - // If we recovered from E2BIG, there are unused arguments at the - // end of the list - for (size_t i = new_argc + 1; i <= orig_argc; ++i) { - if (error == 0) { - execbuf->argv[execbuf->argc] = execbuf->argv[i]; - execbuf->arg_size += bfs_exec_arg_size(execbuf->argv[execbuf->argc]); - ++execbuf->argc; - } else { - free(execbuf->argv[i]); - } - } - } - - errno = error; - return ret; -} - -/** Check if we need to flush the execbuf because we're changing directories. */ -static bool bfs_exec_changed_dirs(const struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { - if (execbuf->flags & BFS_EXEC_CHDIR) { - if (ftwbuf->nameoff > execbuf->wd_len - || (execbuf->wd_path && strncmp(ftwbuf->path, execbuf->wd_path, execbuf->wd_len) != 0)) { - bfs_exec_debug(execbuf, "Changed directories, executing buffered command\n"); - return true; - } - } - - return false; -} - -/** Check if we need to flush the execbuf because we're too big. */ -static bool bfs_exec_would_overflow(const struct bfs_exec *execbuf, const char *arg) { - size_t arg_max = bfs_exec_estimate_max(execbuf); - size_t next_size = execbuf->arg_size + bfs_exec_arg_size(arg); - if (next_size > arg_max) { - bfs_exec_debug(execbuf, "Command size (%zu) would exceed maximum (%zu), executing buffered command\n", - next_size, arg_max); - return true; - } - - return false; -} - -/** Push a new argument to a BFS_EXEC_MULTI execbuf. */ -static int bfs_exec_push(struct bfs_exec *execbuf, char *arg) { - execbuf->argv[execbuf->argc] = arg; - - if (execbuf->argc + 1 >= execbuf->argv_cap) { - size_t cap = 2*execbuf->argv_cap; - char **argv = realloc(execbuf->argv, cap*sizeof(*argv)); - if (!argv) { - return -1; - } - execbuf->argv = argv; - execbuf->argv_cap = cap; - } - - ++execbuf->argc; - execbuf->arg_size += bfs_exec_arg_size(arg); - return 0; -} - -/** Handle a new path for a BFS_EXEC_MULTI execbuf. */ -static int bfs_exec_multi(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { - int ret = 0; - - char *arg = bfs_exec_format_path(execbuf, ftwbuf); - if (!arg) { - ret = -1; - goto out; - } - - if (bfs_exec_changed_dirs(execbuf, ftwbuf)) { - while (bfs_exec_args_remain(execbuf)) { - ret |= bfs_exec_flush(execbuf); - } - bfs_exec_closewd(execbuf, ftwbuf); - } else if (bfs_exec_would_overflow(execbuf, arg)) { - ret |= bfs_exec_flush(execbuf); - } - - if ((execbuf->flags & BFS_EXEC_CHDIR) && execbuf->wd_fd < 0) { - if (bfs_exec_openwd(execbuf, ftwbuf) != 0) { - ret = -1; - goto out_arg; - } - } - - if (bfs_exec_push(execbuf, arg) != 0) { - ret = -1; - goto out_arg; - } - - // arg will get cleaned up later by bfs_exec_flush() - goto out; - -out_arg: - free(arg); -out: - return ret; -} - -int bfs_exec(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) { - if (execbuf->flags & BFS_EXEC_MULTI) { - if (bfs_exec_multi(execbuf, ftwbuf) == 0) { - errno = 0; - } else { - execbuf->ret = -1; - } - // -exec ... + never returns false - return 0; - } else { - return bfs_exec_single(execbuf, ftwbuf); - } -} - -int bfs_exec_finish(struct bfs_exec *execbuf) { - if (execbuf->flags & BFS_EXEC_MULTI) { - bfs_exec_debug(execbuf, "Finishing execution, executing buffered command\n"); - while (bfs_exec_args_remain(execbuf)) { - execbuf->ret |= bfs_exec_flush(execbuf); - } - if (execbuf->ret != 0) { - bfs_exec_debug(execbuf, "One or more executions of '%s' failed\n", execbuf->argv[0]); - } - } - return execbuf->ret; -} - -void bfs_exec_free(struct bfs_exec *execbuf) { - if (execbuf) { - bfs_exec_closewd(execbuf, NULL); - free(execbuf->argv); - free(execbuf); - } -} |