diff options
Diffstat (limited to 'examples/commands')
| -rw-r--r-- | examples/commands/README.md | 5 | ||||
| -rw-r--r-- | examples/commands/commands.c | 177 | ||||
| -rw-r--r-- | examples/commands/commands.tm | 63 |
3 files changed, 245 insertions, 0 deletions
diff --git a/examples/commands/README.md b/examples/commands/README.md new file mode 100644 index 00000000..040f4bd5 --- /dev/null +++ b/examples/commands/README.md @@ -0,0 +1,5 @@ +# Commands + +This module provides a way to run executable programs and get their output. You +can also feed in text to the programs' `stdin`. Think of it as `popen()` on +steroids. diff --git a/examples/commands/commands.c b/examples/commands/commands.c new file mode 100644 index 00000000..f07b5c32 --- /dev/null +++ b/examples/commands/commands.c @@ -0,0 +1,177 @@ +#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 <sys/param.h> +#include <sys/wait.h> +#include <unistd.h> + +#define READ_END 0 +#define WRITE_END 1 +public int run_command(Text_t exe, Array_t arg_array, Table_t env_table, + Array_t input_bytes, Array_t *output_bytes, Array_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]; + pipe(child_inpipe); + pipe(child_outpipe); + pipe(child_errpipe); + + posix_spawn_file_actions_t actions; + posix_spawn_file_actions_init(&actions); + posix_spawn_file_actions_adddup2(&actions, child_inpipe[READ_END], STDIN_FILENO); + posix_spawn_file_actions_addclose(&actions, child_inpipe[WRITE_END]); + posix_spawn_file_actions_adddup2(&actions, child_outpipe[WRITE_END], STDOUT_FILENO); + posix_spawn_file_actions_addclose(&actions, child_outpipe[READ_END]); + 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); + + Array_t arg_strs = {}; + Array$insert_value(&arg_strs, exe_str, I(0), sizeof(char*)); + for (int64_t i = 0; i < arg_array.length; i++) + Array$insert_value(&arg_strs, Text$as_c_string(*(Text_t*)(arg_array.data + i*arg_array.stride)), I(0), sizeof(char*)); + Array$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) { + Array_t env_array = {}; // Array of const char* + for (char **e = environ; *e; e++) + Array$insert(&env_array, 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 = heap_strf("%k=%k", &entry->key, &entry->value); + Array$insert(&env_array, &env_entry, I(0), sizeof(char*)); + } + Array$insert_value(&env_array, NULL, I(0), sizeof(char*)); + assert(env_array.stride == sizeof(char*)); + env = env_array.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); + + close(child_inpipe[READ_END]); + close(child_outpipe[WRITE_END]); + close(child_errpipe[WRITE_END]); + + struct pollfd pollfds[3] = { + {.fd=child_inpipe[WRITE_END], .events=POLLOUT}, + {.fd=child_outpipe[WRITE_END], .events=POLLIN}, + {.fd=child_errpipe[WRITE_END], .events=POLLIN}, + }; + + if (input_bytes.length > 0 && input_bytes.stride != 1) + Array$compact(&input_bytes, sizeof(char)); + if (output_bytes) + *output_bytes = (Array_t){.atomic=1, .stride=1, .length=0}; + if (error_bytes) + *error_bytes = (Array_t){.atomic=1, .stride=1, .length=0}; + + for (;;) { + (void)poll(pollfds, sizeof(pollfds)/sizeof(pollfds[0]), -1); // Wait for data or readiness + bool did_something = false; + if (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 (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 && output_bytes) { + 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 (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 && error_bytes) { + 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); + } + } + + sigaction(SIGINT, &oldint, NULL); + sigaction(SIGQUIT, &oldquit, NULL); + sigprocmask(SIG_SETMASK, &old, NULL); + + if (ret) errno = ret; + return status; +} + +#undef READ_END +#undef WRITE_END diff --git a/examples/commands/commands.tm b/examples/commands/commands.tm new file mode 100644 index 00000000..940dc3d9 --- /dev/null +++ b/examples/commands/commands.tm @@ -0,0 +1,63 @@ +# Functions for running system commands + +use ./commands.c + +extern run_command:func(exe:Text, args:[Text], env:{Text,Text}, input:[Byte], output:&[Byte], error:&[Byte] -> Int32) + +enum ExitType(Exited(status:Int32), Signaled(signal:Int32), Failed) + +struct ProgramResult(stdout:[Byte], stderr:[Byte], exit_type:ExitType): + func or_fail(r:ProgramResult -> ProgramResult): + when r.exit_type is Exited(status): + if status == 0: + return r + else: fail("Program failed: $r") + fail("Program failed: $r") + + func output_text(r:ProgramResult, trim_newline=yes -> Text?): + when r.exit_type is Exited(status): + if status == 0: + if text := Text.from_bytes(r.stdout): + if trim_newline: + text = text:trim($/{1 nl}/, trim_left=no, trim_right=yes) + return text + else: return none + return none + + func error_text(r:ProgramResult -> Text?): + when r.exit_type is Exited(status): + if status == 0: + return Text.from_bytes(r.stderr) + else: return none + return none + +struct Command(command:Text, args=[:Text], env={:Text,Text}): + func run(command:Command, input="", input_bytes=[:Byte] -> ProgramResult): + if input.length > 0: + (&input_bytes):insert_all(input:bytes()) + + stdout := [:Byte] + stderr := [:Byte] + status := run_command(command.command, command.args, command.env, input_bytes, &stdout, &stderr) + + if inline C : Bool { WIFEXITED(_$status) }: + return ProgramResult(stdout, stderr, ExitType.Exited(inline C : Int32 { WEXITSTATUS(_$status) })) + + if inline C : Bool { WIFSIGNALED(_$status) }: + return ProgramResult(stdout, stderr, ExitType.Signaled(inline C : Int32 { WTERMSIG(_$status) })) + + return ProgramResult(stdout, stderr, ExitType.Failed) + + func get_output(command:Command, input="", trim_newline=yes -> Text?): + return command:run(input=input):output_text(trim_newline=trim_newline) + + func get_output_bytes(command:Command, input="", input_bytes=[:Byte] -> [Byte]?): + result := command:run(input=input, input_bytes=input_bytes) + when result.exit_type is Exited(status): + if status == 0: return result.stdout + return none + else: return none + +func main(command:Text, args:[Text], input=""): + cmd := Command(command, args) + say(cmd:get_output(input=input)!) |
