Download
295 lines 10.7 KiBCModified 2026-06-06 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
// Logic for running system commands

#pragma once

#include <errno.h>
#include <gc.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <spawn.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <sys/param.h>
#include <sys/wait.h>
#include <unistd.h>

// This is a workaround fix for an issue on some systems that don't have `__GLIBC__` defined
// and run into problems with <unistr.h>

#ifndef __GLIBC__
#define __GLIBC__ 2
#include <unistr.h> // IWYU pragma: export
#undef __GLIBC__
#else
#include <unistr.h> // IWYU pragma: export
#endif

#define READ_END 0
#define WRITE_END 1

static void xpipe(int fd[2]) {
    if (pipe(fd) != 0) fail_text(Texts("Failed to create pipe: ", strerror(errno)));
}

int run_command(Text_t exe, List_t arg_list, Table_t env_table, OptionalList_t input_bytes, List_t *output_bytes,
                List_t *error_bytes) {
    pthread_testcancel();

    struct sigaction sa = {.sa_handler = SIG_IGN}, oldint, oldquit;
    sigaction(SIGINT, &sa, &oldint);
    sigaction(SIGQUIT, &sa, &oldquit);
    sigaddset(&sa.sa_mask, SIGCHLD);
    sigset_t old, reset;
    sigprocmask(SIG_BLOCK, &sa.sa_mask, &old);
    sigemptyset(&reset);
    if (oldint.sa_handler != SIG_IGN) sigaddset(&reset, SIGINT);
    if (oldquit.sa_handler != SIG_IGN) sigaddset(&reset, SIGQUIT);
    posix_spawnattr_t attr;
    posix_spawnattr_init(&attr);
    posix_spawnattr_setsigmask(&attr, &old);
    posix_spawnattr_setsigdefault(&attr, &reset);
    posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK);

    int child_inpipe[2], child_outpipe[2], child_errpipe[2];
    if (input_bytes.length >= 0) xpipe(child_inpipe);
    if (output_bytes) xpipe(child_outpipe);
    if (error_bytes) xpipe(child_errpipe);

    posix_spawn_file_actions_t actions;
    posix_spawn_file_actions_init(&actions);
    if (input_bytes.length >= 0) {
        posix_spawn_file_actions_adddup2(&actions, child_inpipe[READ_END], STDIN_FILENO);
        posix_spawn_file_actions_addclose(&actions, child_inpipe[WRITE_END]);
    }
    if (output_bytes) {
        posix_spawn_file_actions_adddup2(&actions, child_outpipe[WRITE_END], STDOUT_FILENO);
        posix_spawn_file_actions_addclose(&actions, child_outpipe[READ_END]);
    }
    if (error_bytes) {
        posix_spawn_file_actions_adddup2(&actions, child_errpipe[WRITE_END], STDERR_FILENO);
        posix_spawn_file_actions_addclose(&actions, child_errpipe[READ_END]);
    }

    const char *exe_str = Text$as_c_string(exe);

    List_t arg_strs = {};
    List$insert_value(&arg_strs, exe_str, I(0), sizeof(char *));
    for (int64_t i = 0; i < arg_list.length; i++)
        List$insert_value(&arg_strs, Text$as_c_string(*(Text_t *)(arg_list.data + i * arg_list.stride)), I(0),
                          sizeof(char *));
    List$insert_value(&arg_strs, NULL, I(0), sizeof(char *));
    char **args = arg_strs.data;

    extern char **environ;
    char **env = environ;
    if (env_table.entries.length > 0) {
        List_t env_list = {}; // List of const char*
        for (char **e = environ; *e; e++)
            List$insert(&env_list, e, I(0), sizeof(char *));

        for (int64_t i = 0; i < env_table.entries.length; i++) {
            struct {
                Text_t key, value;
            } *entry = env_table.entries.data + env_table.entries.stride * i;
            const char *env_entry = String(entry->key, "=", entry->value);
            List$insert(&env_list, &env_entry, I(0), sizeof(char *));
        }
        List$insert_value(&env_list, NULL, I(0), sizeof(char *));
        assert(env_list.stride == sizeof(char *));
        env = env_list.data;
    }

    pid_t pid;
    int ret = exe_str[0] == '/' ? posix_spawn(&pid, exe_str, &actions, &attr, args, env)
                                : posix_spawnp(&pid, exe_str, &actions, &attr, args, env);
    if (ret != 0) return -1;

    posix_spawnattr_destroy(&attr);
    posix_spawn_file_actions_destroy(&actions);

    if (input_bytes.length >= 0) close(child_inpipe[READ_END]);
    if (output_bytes) close(child_outpipe[WRITE_END]);
    if (error_bytes) close(child_errpipe[WRITE_END]);

    struct pollfd pollfds[3] = {};
    if (input_bytes.length >= 0) pollfds[0] = (struct pollfd){.fd = child_inpipe[WRITE_END], .events = POLLOUT};
    if (output_bytes) pollfds[1] = (struct pollfd){.fd = child_outpipe[WRITE_END], .events = POLLIN};
    if (error_bytes) pollfds[2] = (struct pollfd){.fd = child_errpipe[WRITE_END], .events = POLLIN};

    if (input_bytes.length > 0 && input_bytes.stride != 1) List$compact(&input_bytes, sizeof(char));
    if (output_bytes) *output_bytes = (List_t){.atomic = 1, .stride = 1, .length = 0};
    if (error_bytes) *error_bytes = (List_t){.atomic = 1, .stride = 1, .length = 0};

    while (input_bytes.length > 0 || output_bytes || error_bytes) {
        (void)poll(pollfds, sizeof(pollfds) / sizeof(pollfds[0]), -1); // Wait for data or readiness
        bool did_something = false;
        if (input_bytes.length >= 0 && pollfds[0].revents) {
            if (input_bytes.length > 0) {
                ssize_t written = write(child_inpipe[WRITE_END], input_bytes.data, (size_t)input_bytes.length);
                if (written > 0) {
                    input_bytes.data += written;
                    input_bytes.length -= (int64_t)written;
                    did_something = true;
                } else if (written < 0) {
                    close(child_inpipe[WRITE_END]);
                    pollfds[0].events = 0;
                }
            }
            if (input_bytes.length <= 0) {
                close(child_inpipe[WRITE_END]);
                pollfds[0].events = 0;
            }
        }
        char buf[256];
        if (output_bytes && pollfds[1].revents) {
            ssize_t n = read(child_outpipe[READ_END], buf, sizeof(buf));
            did_something = did_something || (n > 0);
            if (n <= 0) {
                close(child_outpipe[READ_END]);
                pollfds[1].events = 0;
            } else if (n > 0) {
                if (output_bytes->free < n) {
                    output_bytes->data = GC_REALLOC(output_bytes->data, (size_t)(output_bytes->length + n));
                    output_bytes->free = 0;
                }
                memcpy(output_bytes->data + output_bytes->length, buf, (size_t)n);
                output_bytes->length += n;
            }
        }
        if (error_bytes && pollfds[2].revents) {
            ssize_t n = read(child_errpipe[READ_END], buf, sizeof(buf));
            did_something = did_something || (n > 0);
            if (n <= 0) {
                close(child_errpipe[READ_END]);
                pollfds[2].events = 0;
            } else if (n > 0) {
                if (error_bytes->free < n) {
                    error_bytes->data = GC_REALLOC(error_bytes->data, (size_t)(error_bytes->length + n));
                    error_bytes->free = 0;
                }
                memcpy(error_bytes->data + error_bytes->length, buf, (size_t)n);
                error_bytes->length += n;
            }
        }
        if (!did_something) break;
    }

    int status = 0;
    if (ret == 0) {
        while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {
            if (WIFEXITED(status) || WIFSIGNALED(status)) break;
            else if (WIFSTOPPED(status)) kill(pid, SIGCONT);
        }
    }

    if (input_bytes.length >= 0) close(child_inpipe[WRITE_END]);
    if (output_bytes) close(child_outpipe[READ_END]);
    if (error_bytes) close(child_errpipe[READ_END]);

    sigaction(SIGINT, &oldint, NULL);
    sigaction(SIGQUIT, &oldquit, NULL);
    sigprocmask(SIG_SETMASK, &old, NULL);

    if (ret) errno = ret;
    return status;
}

typedef struct {
    pid_t pid;
    FILE *out;
} child_info_t;

static void _line_reader_cleanup(child_info_t *child) {
    if (child && child->out) {
        fclose(child->out);
        child->out = NULL;
    }
    if (child->pid) {
        kill(child->pid, SIGTERM);
        child->pid = 0;
    }
}

static Text_t _next_line(child_info_t *child) {
    if (!child || !child->out) return NONE_TEXT;

    char *line = NULL;
    size_t size = 0;
    ssize_t len = getline(&line, &size, child->out);
    if (len <= 0) {
        _line_reader_cleanup(child);
        return NONE_TEXT;
    }

    while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == '\n'))
        --len;

    if (u8_check((uint8_t *)line, (size_t)len) != NULL) fail_text(Text("Invalid UTF8!"));

    Text_t line_text = Text$from_strn(line, len);
    free(line);
    return line_text;
}

OptionalClosure_t command_by_line(Text_t exe, List_t arg_list, Table_t env_table) {
    posix_spawnattr_t attr;
    posix_spawnattr_init(&attr);

    int child_outpipe[2];
    xpipe(child_outpipe);

    posix_spawn_file_actions_t actions;
    posix_spawn_file_actions_init(&actions);
    posix_spawn_file_actions_adddup2(&actions, child_outpipe[WRITE_END], STDOUT_FILENO);
    posix_spawn_file_actions_addclose(&actions, child_outpipe[READ_END]);

    const char *exe_str = Text$as_c_string(exe);

    List_t arg_strs = {};
    List$insert_value(&arg_strs, exe_str, I(0), sizeof(char *));
    for (int64_t i = 0; i < arg_list.length; i++)
        List$insert_value(&arg_strs, Text$as_c_string(*(Text_t *)(arg_list.data + i * arg_list.stride)), I(0),
                          sizeof(char *));
    List$insert_value(&arg_strs, NULL, I(0), sizeof(char *));
    char **args = arg_strs.data;

    extern char **environ;
    char **env = environ;
    if (env_table.entries.length > 0) {
        List_t env_list = {}; // List of const char*
        for (char **e = environ; *e; e++)
            List$insert(&env_list, e, I(0), sizeof(char *));

        for (int64_t i = 0; i < env_table.entries.length; i++) {
            struct {
                Text_t key, value;
            } *entry = env_table.entries.data + env_table.entries.stride * i;
            const char *env_entry = String(entry->key, "=", entry->value);
            List$insert(&env_list, &env_entry, I(0), sizeof(char *));
        }
        List$insert_value(&env_list, NULL, I(0), sizeof(char *));
        assert(env_list.stride == sizeof(char *));
        env = env_list.data;
    }

    pid_t pid;
    int ret = exe_str[0] == '/' ? posix_spawn(&pid, exe_str, &actions, &attr, args, env)
                                : posix_spawnp(&pid, exe_str, &actions, &attr, args, env);
    if (ret != 0) return NONE_CLOSURE;

    posix_spawnattr_destroy(&attr);
    posix_spawn_file_actions_destroy(&actions);

    close(child_outpipe[WRITE_END]);

    child_info_t *child_info = GC_MALLOC(sizeof(child_info_t));
    child_info->out = fdopen(child_outpipe[READ_END], "r");
    child_info->pid = pid;
    GC_register_finalizer(child_info, (void *)_line_reader_cleanup, NULL, NULL, NULL);
    return (Closure_t){.fn = (void *)_next_line, .userdata = child_info};
}

#undef READ_END
#undef WRITE_END