cosmopolitan/tool/build/runit.c

489 lines
15 KiB
C
Raw Normal View History

2020-06-15 14:18:57 +00:00
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi
Copyright 2020 Justine Alexandra Roberts Tunney
2020-12-28 01:18:44 +00:00
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the
above copyright notice and this permission notice appear in all copies.
2020-06-15 14:18:57 +00:00
2020-12-28 01:18:44 +00:00
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.
2020-06-15 14:18:57 +00:00
*/
2021-08-26 04:35:58 +00:00
#include "libc/assert.h"
2020-06-15 14:18:57 +00:00
#include "libc/calls/calls.h"
#include "libc/calls/struct/itimerval.h"
#include "libc/calls/struct/stat.h"
2020-06-15 14:18:57 +00:00
#include "libc/dns/dns.h"
#include "libc/fmt/conv.h"
#include "libc/intrin/bits.h"
#include "libc/intrin/safemacros.internal.h"
2020-06-15 14:18:57 +00:00
#include "libc/limits.h"
#include "libc/log/check.h"
#include "libc/log/log.h"
2022-05-15 06:17:22 +00:00
#include "libc/mem/mem.h"
2021-08-26 04:35:58 +00:00
#include "libc/nexgen32e/crc32.h"
#include "libc/runtime/gc.internal.h"
#include "libc/runtime/runtime.h"
#include "libc/sock/ipclassify.internal.h"
2020-06-15 14:18:57 +00:00
#include "libc/stdio/stdio.h"
2022-06-12 16:37:17 +00:00
#include "libc/str/str.h"
2020-06-15 14:18:57 +00:00
#include "libc/sysv/consts/af.h"
#include "libc/sysv/consts/ex.h"
#include "libc/sysv/consts/ipproto.h"
#include "libc/sysv/consts/itimer.h"
#include "libc/sysv/consts/limits.h"
2021-08-07 20:22:35 +00:00
#include "libc/sysv/consts/map.h"
2020-06-15 14:18:57 +00:00
#include "libc/sysv/consts/o.h"
2021-08-07 20:22:35 +00:00
#include "libc/sysv/consts/prot.h"
2020-06-15 14:18:57 +00:00
#include "libc/sysv/consts/sock.h"
#include "libc/time/time.h"
#include "libc/x/x.h"
2021-08-14 13:17:56 +00:00
#include "net/https/https.h"
2021-08-07 20:22:35 +00:00
#include "third_party/mbedtls/ssl.h"
2022-05-15 06:17:22 +00:00
#include "third_party/zlib/zlib.h"
2021-08-07 20:22:35 +00:00
#include "tool/build/lib/eztls.h"
2021-08-14 13:17:56 +00:00
#include "tool/build/lib/psk.h"
2020-06-15 14:18:57 +00:00
#include "tool/build/runit.h"
#define MAX_WAIT_CONNECT_SECONDS 12
#define INITIAL_CONNECT_TIMEOUT 100000
2020-06-15 14:18:57 +00:00
/**
* @fileoverview Remote test runner.
*
2021-08-07 20:22:35 +00:00
* We want to scp .com binaries to remote machines and run them. The
* problem is that SSH is the slowest thing imaginable, taking about
* 300ms to connect to a host that's merely half a millisecond away.
*
* This program takes 17ms using elliptic curve diffie hellman exchange
* where we favor a 32-byte binary preshared key (~/.runit.psk) instead
* of certificates. It's how long it takes to connect, copy the binary,
* and run it. The remote daemon is deployed via SSH if it's not there.
2020-06-15 14:18:57 +00:00
*
* o/default/tool/build/runit.com \
* o/default/tool/build/runitd.com \
2022-08-13 15:32:34 +00:00
* o/default/test/libc/mem/qsort_test.com \
2020-06-15 14:18:57 +00:00
* freebsd.test.:31337:22
*
2021-08-07 20:22:35 +00:00
* APE binaries are hermetic and embed dependent files within their zip
* structure, which is why all we need is this simple test runner tool.
2020-06-15 14:18:57 +00:00
* The only thing that needs to be configured is /etc/hosts or Bind, to
* assign numbers to the officially reserved canned names. For example:
*
* 192.168.0.10 windows.test. windows
* 192.168.0.11 freebsd.test. freebsd
* 192.168.0.12 openbsd.test. openbsd
*
* Life is easiest if SSH public key authentication is configured too.
* It can be tuned as follows in ~/.ssh/config:
*
* host windows.test.
* user testacct
* host freebsd.test.
* user testacct
* host openbsd.test.
* user testacct
*
* Firewalls may need to be configured as well, to allow port tcp:31337
* from the local subnet. For example:
*
* iptables -L -vn
* iptables -I INPUT 1 -s 10.0.0.0/8 -p tcp --dport 31337 -j ACCEPT
* iptables -I INPUT 1 -s 192.168.0.0/16 -p tcp --dport 31337 -j ACCEPT
*
2021-08-07 20:22:35 +00:00
* This tool may be used in zero trust environments.
2020-06-15 14:18:57 +00:00
*/
static const struct addrinfo kResolvHints = {.ai_family = AF_INET,
.ai_socktype = SOCK_STREAM,
.ai_protocol = IPPROTO_TCP};
int g_sock;
2020-09-03 12:44:37 +00:00
char *g_prog;
2022-05-22 15:13:13 +00:00
long g_backoff;
2020-09-03 12:44:37 +00:00
char *g_runitd;
2020-06-15 14:18:57 +00:00
jmp_buf g_jmpbuf;
2020-09-03 12:44:37 +00:00
uint16_t g_sshport;
char g_hostname[128];
uint16_t g_runitdport;
volatile bool alarmed;
char g_ssh[PATH_MAX];
int __sys_execve(const char *, char *const[], char *const[]) hidden;
static void OnAlarm(int sig) {
alarmed = true;
}
2020-06-15 14:18:57 +00:00
wontreturn void ShowUsage(FILE *f, int rc) {
2020-06-15 14:18:57 +00:00
fprintf(f, "Usage: %s RUNITD PROGRAM HOSTNAME[:RUNITDPORT[:SSHPORT]]...\n",
program_invocation_name);
exit(rc);
unreachable;
}
void CheckExists(const char *path) {
if (!isregularfile(path)) {
fprintf(stderr, "error: %s: not found or irregular\n", path);
ShowUsage(stderr, EX_USAGE);
unreachable;
}
}
void Connect(void) {
2020-06-15 14:18:57 +00:00
const char *ip4;
int rc, err, expo;
long double t1, t2;
2020-06-15 14:18:57 +00:00
struct addrinfo *ai;
if ((rc = getaddrinfo(g_hostname, gc(xasprintf("%hu", g_runitdport)),
&kResolvHints, &ai)) != 0) {
2021-03-21 03:48:40 +00:00
FATALF("%s:%hu: EAI_%s %m", g_hostname, g_runitdport, gai_strerror(rc));
2020-06-15 14:18:57 +00:00
unreachable;
}
ip4 = (const char *)&ai->ai_addr4->sin_addr;
2020-06-15 14:18:57 +00:00
if (ispublicip(ai->ai_family, &ai->ai_addr4->sin_addr)) {
FATALF("%s points to %hhu.%hhu.%hhu.%hhu"
" which isn't part of a local/private/testing subnet",
g_hostname, ip4[0], ip4[1], ip4[2], ip4[3]);
unreachable;
}
DEBUGF("connecting to %d.%d.%d.%d port %d", ip4[0], ip4[1], ip4[2], ip4[3],
ntohs(ai->ai_addr4->sin_port));
CHECK_NE(-1,
(g_sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol)));
expo = INITIAL_CONNECT_TIMEOUT;
t1 = nowl();
LOGIFNEG1(sigaction(SIGALRM, &(struct sigaction){.sa_handler = OnAlarm}, 0));
DEBUGF("connecting to %s (%hhu.%hhu.%hhu.%hhu) to run %s", g_hostname, ip4[0],
ip4[1], ip4[2], ip4[3], g_prog);
TryAgain:
alarmed = false;
LOGIFNEG1(setitimer(
ITIMER_REAL,
&(const struct itimerval){{0, 0}, {expo / 1000000, expo % 1000000}},
NULL));
2020-06-15 14:18:57 +00:00
rc = connect(g_sock, ai->ai_addr, ai->ai_addrlen);
err = errno;
t2 = nowl();
2020-06-15 14:18:57 +00:00
if (rc == -1) {
if (err == EINTR) {
expo *= 1.5;
if (t2 > t1 + MAX_WAIT_CONNECT_SECONDS) {
FATALF("timeout connecting to %s (%hhu.%hhu.%hhu.%hhu:%d)", g_hostname,
ip4[0], ip4[1], ip4[2], ip4[3], ntohs(ai->ai_addr4->sin_port));
unreachable;
}
goto TryAgain;
}
2022-06-12 16:37:17 +00:00
FATALF("%s(%s:%hu): %s", "connect", g_hostname, g_runitdport,
strerror(err));
} else {
DEBUGF("connected to %s", g_hostname);
2020-06-15 14:18:57 +00:00
}
setitimer(ITIMER_REAL, &(const struct itimerval){0}, 0);
2020-06-15 14:18:57 +00:00
freeaddrinfo(ai);
}
bool Send(int tmpfd, const void *output, size_t outputsize) {
bool ok;
char *zbuf;
size_t zsize;
int rc, have;
2022-05-15 06:17:22 +00:00
static bool once;
static z_stream zs;
zsize = 32768;
zbuf = gc(malloc(zsize));
2022-05-15 06:17:22 +00:00
if (!once) {
CHECK_EQ(Z_OK, deflateInit2(&zs, 4, Z_DEFLATED, MAX_WBITS, DEF_MEM_LEVEL,
Z_DEFAULT_STRATEGY));
2022-05-15 06:17:22 +00:00
once = true;
}
zs.next_in = output;
zs.avail_in = outputsize;
ok = true;
2022-05-15 06:17:22 +00:00
do {
zs.avail_out = zsize;
2022-05-15 06:17:22 +00:00
zs.next_out = (unsigned char *)zbuf;
rc = deflate(&zs, Z_SYNC_FLUSH);
CHECK_NE(Z_STREAM_ERROR, rc);
have = zsize - zs.avail_out;
rc = write(tmpfd, zbuf, have);
if (rc != have) {
DEBUGF("write(%d, %d) → %d", tmpfd, have, rc);
ok = false;
break;
2022-05-22 15:13:13 +00:00
}
2022-05-15 06:17:22 +00:00
} while (!zs.avail_out);
return ok;
2022-05-15 06:17:22 +00:00
}
bool SendRequest(int tmpfd) {
2020-06-15 14:18:57 +00:00
int fd;
2021-08-07 20:22:35 +00:00
char *p;
size_t i;
bool okall;
2021-08-07 20:22:35 +00:00
ssize_t rc;
2021-08-26 04:35:58 +00:00
uint32_t crc;
2020-06-15 14:18:57 +00:00
struct stat st;
const char *name;
2021-08-26 04:35:58 +00:00
unsigned char *hdr, *q;
2020-06-15 14:18:57 +00:00
size_t progsize, namesize, hdrsize;
2022-05-15 06:17:22 +00:00
unsigned have;
2020-06-15 14:18:57 +00:00
CHECK_NE(-1, (fd = open(g_prog, O_RDONLY)));
CHECK_NE(-1, fstat(fd, &st));
2021-08-07 20:22:35 +00:00
CHECK_NE(MAP_FAILED, (p = mmap(0, st.st_size, PROT_READ, MAP_SHARED, fd, 0)));
2020-06-15 14:18:57 +00:00
CHECK_LE((namesize = strlen((name = basename(g_prog)))), PATH_MAX);
CHECK_LE((progsize = st.st_size), INT_MAX);
2021-08-26 04:35:58 +00:00
CHECK_NOTNULL((hdr = gc(calloc(1, (hdrsize = 17 + namesize)))));
crc = crc32_z(0, p, st.st_size);
q = hdr;
q = WRITE32BE(q, RUNITD_MAGIC);
*q++ = kRunitExecute;
q = WRITE32BE(q, namesize);
q = WRITE32BE(q, progsize);
q = WRITE32BE(q, crc);
q = mempcpy(q, name, namesize);
assert(hdrsize == q - hdr);
okall = true;
okall &= Send(tmpfd, hdr, hdrsize);
okall &= Send(tmpfd, p, progsize);
2021-08-07 20:22:35 +00:00
CHECK_NE(-1, munmap(p, st.st_size));
CHECK_NE(-1, close(fd));
return okall;
}
void RelayRequest(void) {
int i, rc, have, transferred;
char *buf = gc(malloc(PIPE_BUF));
for (transferred = 0;;) {
rc = read(13, buf, PIPE_BUF);
CHECK_NE(-1, rc);
have = rc;
if (!rc) break;
transferred += have;
for (i = 0; i < have; i += rc) {
rc = mbedtls_ssl_write(&ezssl, buf + i, have - i);
if (rc <= 0) {
TlsDie("relay request failed", rc);
}
}
}
CHECK_NE(0, transferred);
rc = EzTlsFlush(&ezbio, 0, 0);
if (rc < 0) {
TlsDie("relay request failed to flush", rc);
}
close(13);
2020-06-15 14:18:57 +00:00
}
bool Recv(unsigned char *p, size_t n) {
size_t i, rc;
for (i = 0; i < n; i += rc) {
do {
rc = mbedtls_ssl_read(&ezssl, p + i, n - i);
} while (rc == MBEDTLS_ERR_SSL_WANT_READ);
2022-06-12 14:32:14 +00:00
if (!rc) return false;
if (rc < 0) {
2021-08-14 13:17:56 +00:00
TlsDie("read response failed", rc);
}
}
return true;
}
2020-06-15 14:18:57 +00:00
int ReadResponse(void) {
int res;
ssize_t rc;
size_t n, m;
2021-08-07 20:22:35 +00:00
uint32_t size;
unsigned char b[512];
for (res = -1; res == -1;) {
if (!Recv(b, 5)) break;
Make numerous improvements - Python static hello world now 1.8mb - Python static fully loaded now 10mb - Python HTTPS client now uses MbedTLS - Python REPL now completes import stmts - Increase stack size for Python for now - Begin synthesizing posixpath and ntpath - Restore Python \N{UNICODE NAME} support - Restore Python NFKD symbol normalization - Add optimized code path for Intel SHA-NI - Get more Python unit tests passing faster - Get Python help() pagination working on NT - Python hashlib now supports MbedTLS PBKDF2 - Make memcpy/memmove/memcmp/bcmp/etc. faster - Add Mersenne Twister and Vigna to LIBC_RAND - Provide privileged __printf() for error code - Fix zipos opendir() so that it reports ENOTDIR - Add basic chmod() implementation for Windows NT - Add Cosmo's best functions to Python cosmo module - Pin function trace indent depth to that of caller - Show memory diagram on invalid access in MODE=dbg - Differentiate stack overflow on crash in MODE=dbg - Add stb_truetype and tools for analyzing font files - Upgrade to UNICODE 13 and reduce its binary footprint - COMPILE.COM now logs resource usage of build commands - Start implementing basic poll() support on bare metal - Set getauxval(AT_EXECFN) to GetModuleFileName() on NT - Add descriptions to strerror() in non-TINY build modes - Add COUNTBRANCH() macro to help with micro-optimizations - Make error / backtrace / asan / memory code more unbreakable - Add fast perfect C implementation of μ-Law and a-Law audio codecs - Make strtol() functions consistent with other libc implementations - Improve Linenoise implementation (see also github.com/jart/bestline) - COMPILE.COM now suppresses stdout/stderr of successful build commands
2021-09-28 05:58:51 +00:00
CHECK_EQ(RUNITD_MAGIC, READ32BE(b), "%#.5s", b);
switch (b[4]) {
case kRunitExit:
if (!Recv(b, 1)) break;
if ((res = *b)) {
WARNF("%s on %s exited with %d", g_prog, g_hostname, res);
}
break;
case kRunitStderr:
if (!Recv(b, 4)) break;
size = READ32BE(b);
for (; size; size -= n) {
n = MIN(size, sizeof(b));
if (!Recv(b, n)) goto drop;
CHECK_EQ(n, write(2, b, n));
}
break;
default:
fprintf(stderr, "error: received invalid runit command\n");
_exit(1);
}
2020-06-15 14:18:57 +00:00
}
drop:
close(g_sock);
2020-06-15 14:18:57 +00:00
return res;
}
static inline bool IsElf(const char *p, size_t n) {
return n >= 4 && READ32LE(p) == READ32LE("\177ELF");
}
static inline bool IsMachO(const char *p, size_t n) {
return n >= 4 && READ32LE(p) == 0xFEEDFACEu + 1;
}
2020-06-15 14:18:57 +00:00
int RunOnHost(char *spec) {
int rc;
2020-06-15 14:18:57 +00:00
char *p;
for (p = spec; *p; ++p) {
if (*p == ':') *p = ' ';
}
CHECK_GE(sscanf(spec, "%100s %hu %hu", g_hostname, &g_runitdport, &g_sshport),
1);
if (!strchr(g_hostname, '.')) strcat(g_hostname, ".test.");
DEBUGF("connecting to %s port %d", g_hostname, g_runitdport);
for (;;) {
Connect();
EzFd(g_sock);
if (!(rc = EzHandshake2())) {
break;
}
WARNF("got reset in handshake -0x%04x", rc);
close(g_sock);
}
RelayRequest();
return ReadResponse();
2020-06-15 14:18:57 +00:00
}
bool IsParallelBuild(void) {
const char *makeflags;
return (makeflags = getenv("MAKEFLAGS")) && strstr(makeflags, "-j");
}
2022-07-20 21:01:15 +00:00
bool ShouldRunInParallel(void) {
2020-06-15 14:18:57 +00:00
return !IsWindows() && IsParallelBuild();
}
int SpawnSubprocesses(int argc, char *argv[]) {
const char *tpath;
sigset_t chldmask, savemask;
int i, rc, ws, pid, tmpfd, *pids, exitcode;
struct sigaction ignore, saveint, savequit;
char *args[5] = {argv[0], argv[1], argv[2]};
// create compressed network request ahead of time
CHECK_NE(-1, (tmpfd = open(
(tpath = gc(xasprintf(
"%s/runit.%d", firstnonnull(getenv("TMPDIR"), "/tmp"),
getpid()))),
O_WRONLY | O_CREAT | O_TRUNC, 0755)));
CHECK(SendRequest(tmpfd));
CHECK_NE(-1, close(tmpfd));
// fork off 𝑛 subprocesses for each host on which we run binary.
// what's important here is htop in tree mode will report like:
//
// runit.com xnu freebsd netbsd
// ├─runit.com xnu
// ├─runit.com freebsd
// └─runit.com netbsd
//
// That way when one hangs, it's easy to know what o/s it is.
argc -= 3;
argv += 3;
exitcode = 0;
pids = calloc(argc, sizeof(int));
ignore.sa_flags = 0;
ignore.sa_handler = SIG_IGN;
sigemptyset(&ignore.sa_mask);
sigaction(SIGINT, &ignore, &saveint);
sigaction(SIGQUIT, &ignore, &savequit);
sigemptyset(&chldmask);
sigaddset(&chldmask, SIGCHLD);
sigprocmask(SIG_BLOCK, &chldmask, &savemask);
for (i = 0; i < argc; ++i) {
args[3] = argv[i];
CHECK_NE(-1, (pids[i] = vfork()));
if (!pids[i]) {
dup2(open(tpath, O_RDONLY | O_CLOEXEC), 13);
sigaction(SIGINT, &(struct sigaction){0}, 0);
sigaction(SIGQUIT, &(struct sigaction){0}, 0);
sigprocmask(SIG_SETMASK, &savemask, 0);
execve(args[0], args, environ); // for htop
_Exit(127);
2020-06-15 14:18:57 +00:00
}
}
// wait for children to terminate
for (;;) {
if ((pid = wait(&ws)) == -1) {
if (errno == EINTR) continue;
if (errno == ECHILD) break;
FATALF("wait failed");
2020-06-15 14:18:57 +00:00
}
for (i = 0; i < argc; ++i) {
if (pids[i] != pid) continue;
if (WIFEXITED(ws)) {
if (WEXITSTATUS(ws)) {
INFOF("%s exited with %d", argv[i], WEXITSTATUS(ws));
} else {
DEBUGF("%s exited with %d", argv[i], WEXITSTATUS(ws));
}
if (!exitcode) exitcode = WEXITSTATUS(ws);
} else {
INFOF("%s terminated with %s", argv[i], strsignal(WTERMSIG(ws)));
if (!exitcode) exitcode = 128 + WTERMSIG(ws);
}
break;
2020-06-15 14:18:57 +00:00
}
}
CHECK_NE(-1, unlink(tpath));
sigprocmask(SIG_SETMASK, &savemask, 0);
sigaction(SIGQUIT, &savequit, 0);
sigaction(SIGINT, &saveint, 0);
free(pids);
2020-06-15 14:18:57 +00:00
return exitcode;
}
int main(int argc, char *argv[]) {
2021-08-26 04:35:58 +00:00
ShowCrashReports();
if (getenv("DEBUG")) {
__log_level = kLogDebug;
}
2020-06-15 14:18:57 +00:00
if (argc > 1 &&
(strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) {
ShowUsage(stdout, 0);
unreachable;
}
if (argc < 3) {
ShowUsage(stderr, EX_USAGE);
unreachable;
}
2020-06-15 14:18:57 +00:00
CheckExists((g_runitd = argv[1]));
CheckExists((g_prog = argv[2]));
CHECK_NOTNULL(
commandv(firstnonnull(getenv("SSH"), "ssh"), g_ssh, sizeof(g_ssh)));
if (argc == 3) {
/* hosts list empty */
return 0;
} else if (argc == 4) {
/* TODO(jart): this is broken */
/* single host */
SetupPresharedKeySsl(MBEDTLS_SSL_IS_CLIENT, GetRunitPsk());
g_sshport = 22;
g_runitdport = RUNITD_PORT;
return RunOnHost(argv[3]);
} else {
/* multiple hosts */
return SpawnSubprocesses(argc, argv);
}
2020-06-15 14:18:57 +00:00
}