| /*
* sai-builder
*
* Copyright (C) 2019 - 2020 Andy Green <andy@warmcat.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation:
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*
* Generic unix spawn and stdxxx pipe to wsi mapping
*/
#include <libwebsockets.h>
#include <string.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#if !defined(WIN32)
#include <unistd.h>
#else
#include <process.h>
#endif
#include <fcntl.h>
#include <assert.h>
#include "b-private.h"
#if !defined(WIN32)
static char csep = '/';
#else
static char csep = '\\';
#endif
extern struct lws_vhost *builder_vhost;
struct ws_capture_chunk *
saib_log_chunk_create(struct sai_nspawn *ns, void *buf, size_t len, int channel)
{
struct ws_capture_chunk *chunk;
if (!ns || !ns->spm)
return NULL;
chunk = malloc(sizeof(*chunk) + len);
if (!chunk)
return NULL;
memset(chunk, 0, sizeof(*chunk));
chunk->us = lws_now_usecs();
chunk->len = len;
chunk->stdfd = (uint8_t)channel;
if (len)
memcpy(&chunk[1], buf, len);
ns->chunk_cache_size += sizeof(*chunk) + len;
lws_dll2_add_head(&chunk->list, &ns->chunk_cache);
ns->spm->logs_in_flight++;
return chunk;
}
static int
callback_sai_stdwsi(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
struct saib_opaque_spawn *op =
(struct saib_opaque_spawn *)lws_get_opaque_user_data(wsi);
struct sai_nspawn *ns = op ? op->ns : NULL;
uint8_t buf[600];
int ilen;
// lwsl_warn("%s: reason %d\n", __func__, reason);
switch (reason) {
case LWS_CALLBACK_RAW_CLOSE_FILE:
lwsl_warn("%s: RAW_CLOSE_FILE at %llu, wsi %p: fd: %d, stdfd: %d\n",
__func__, (unsigned long long)lws_now_usecs(), wsi,
lws_get_socket_fd(wsi), lws_spawn_get_stdfd(wsi));
ilen = lws_snprintf((char *)buf, sizeof(buf), "Stdwsi %d close\n", lws_spawn_get_stdfd(wsi));
if (ns) {
saib_log_chunk_create(ns, buf, (size_t)ilen, 3);
if (ns->spm)
if (lws_ss_request_tx(ns->spm->ss))
lwsl_warn("%s: lws_ss_request_tx failed\n",
__func__);
}
lwsl_wsi_err(wsi, "CLOSING: op %p, op->lsp %p", op, op ? op->lsp : NULL);
if (op && op->lsp) {
lws_spawn_stdwsi_closed(op->lsp, wsi);
if (ns)
lws_cancel_service(ns->builder->context);
}
break;
case LWS_CALLBACK_RAW_RX_FILE:
#if defined(WIN32)
{
DWORD rb;
if (!ReadFile((HANDLE)lws_get_socket_fd(wsi), buf, sizeof(buf), &rb, NULL)) {
lwsl_debug("%s: read on stdwsi failed\n", __func__);
return -1;
}
ilen = (int)rb;
}
#else
ilen = (int)read((int)(intptr_t)lws_get_socket_fd(wsi), buf, sizeof(buf));
if (ilen < 1) {
lwsl_debug("%s: read on stdwsi failed\n", __func__);
return -1;
}
#endif
len = (unsigned int)ilen;
if (!op || !op->ns || !op->ns->spm) {
printf("%s: (%d) %.*s\n", __func__, (int)lws_spawn_get_stdfd(wsi), (int)len, buf);
return -1;
}
if (!saib_log_chunk_create(op->ns, buf, len, lws_spawn_get_stdfd(wsi)))
return -1;
return lws_ss_request_tx(op->ns->spm->ss) ? -1 : 0;
default:
break;
}
return 0;
}
struct lws_protocols protocol_stdxxx =
{ "sai-stdxxx", callback_sai_stdwsi, 0, 0 };
static void
sai_lsp_reap_cb(void *opaque, lws_usec_t *accounting, siginfo_t *si,
int we_killed_him)
{
struct saib_opaque_spawn *op = (struct saib_opaque_spawn *)opaque;
struct sai_nspawn *ns = op ? op->ns : NULL;
// lwsl_warn("%s: reap at %llu: we_killed_him: %d\n", __func__,
// (unsigned long long)lws_now_usecs(), we_killed_him);
saib_log_chunk_create(ns, ">saib> Reaping build process\n", 29, 3);
#if !defined(WIN32)
lwsl_notice("%s: reaped: timing %dms %dms %dms %dms\n", __func__,
(int)(accounting[0] / 1000), (int)(accounting[1] / 1000),
(int)(accounting[2] / 1000), (int)(accounting[3] / 1000));
if (we_killed_him & 1) {
lwsl_notice("%s: Process TIMED OUT by Sai\n", __func__);
ns->retcode = SAISPRF_TIMEDOUT;
goto ok;
}
if (we_killed_him & 2) {
lwsl_notice("%s: Process killed by Sai due to spew\n", __func__);
ns->retcode = SAISPRF_TERMINATED;
goto ok;
}
switch (si->si_code) {
case CLD_EXITED:
lwsl_notice("%s: Process Exited with exit code %d\n",
__func__, si->si_status);
ns->retcode = SAISPRF_EXIT | si->si_status;
if (ns->user_cancel)
ns->retcode = SAISPRF_TERMINATED;
break;
case CLD_KILLED:
case CLD_DUMPED:
lwsl_notice("%s: Process Terminated by signal %d / %d\n",
__func__, si->si_status, si->si_signo);
ns->retcode = SAISPRF_SIGNALLED | si->si_signo;
break;
default:
lwsl_notice("%s: SI code %d\n", __func__, si->si_code);
break;
}
ok:
#else
ns->retcode = SAISPRF_EXIT | (si->retcode & 0xff);
#endif
saib_task_grace(ns);
saib_set_ns_state(ns, NSSTATE_DONE);
/*
* add a final zero-length log with the retcode to the list of pending
* logs
*/
saib_log_chunk_create(ns, NULL, 0, 2);
lwsl_notice("%s: finished, waiting to drain logs (this ns %d, spm in flight %d)\n",
__func__, ns->chunk_cache.count,
ns->spm ? ns->spm->logs_in_flight : -99);
if (ns)
ns->op = NULL;
free(op);
}
#if defined(WIN32)
static const char * const runscript =
"set SAI_INSTANCE_IDX=%d\n"
"set SAI_BUILDER_RESOURCE_PROXY=%s\n"
"set SAI_LOGPROXY=%s\n"
"set SAI_LOGPROXY_TTY0=%s\n"
"set SAI_LOGPROXY_TTY1=%s\n"
"set HOME=%s\n"
"cd %s &&"
" rmdir /s /q build & "
"%s"
;
#else
static const char * const runscript =
"#!/bin/bash -x\n"
#if defined(__APPLE__)
"export PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/sbin:/usr/sbin\n"
#else
"export PATH=/usr/local/bin:$PATH\n"
#endif
"export HOME=%s\n"
"export SAI_OVN=%s\n"
"export SAI_PROJECT=%s\n"
"export SAI_REMOTE_REF=%s\n"
"export SAI_INSTANCE_IDX=%d\n"
"export SAI_BUILDER_RESOURCE_PROXY=%s\n"
"export SAI_LOGPROXY=%s\n"
"export SAI_LOGPROXY_TTY0=%s\n"
"export SAI_LOGPROXY_TTY1=%s\n"
"set -e\n"
"cd %s/jobs/$SAI_OVN/$SAI_PROJECT\n"
"rm -rf build\n"
"%s\n"
"exit $?\n"
;
#endif
int
saib_spawn(struct sai_nspawn *ns)
{
struct lws_spawn_piped_info info;
struct saib_opaque_spawn *op;
char args[290], st[2048], *p;
const char *respath = "unk";
const char * cmd[] = {
"/bin/ps",
NULL
};
const char *env[] = {
"PATH=/usr/local/bin:/usr/bin:/bin",
"LANG=en_US.UTF-8",
NULL
};
int fd, n;
#if defined(__linux__)
int in_cgroup = 1;
char cgroup[128];
#endif
lws_strncpy(st, ns->sp->name, sizeof(st));
lws_filename_purify_inplace(st);
p = st;
while ((p = strchr(p, '/')))
*p = '_';
#if defined(WIN32)
lws_snprintf(args, sizeof(args), "%s\\sai-build-script-%s-%d.bat",
builder.home, st, ns->instance_idx);
#else
lws_snprintf(args, sizeof(args), "%s/sai-build-script-%s-%d.sh",
builder.home, st, ns->instance_idx);
#endif
lwsl_hexdump_notice(ns->task->build, strlen(ns->task->build));
#if defined(WIN32)
if (_sopen_s(&fd, args, _O_CREAT | _O_TRUNC | _O_WRONLY,
_SH_DENYNO, _S_IWRITE))
fd = -1;
#else
fd = open(args, O_CREAT | O_TRUNC | O_WRONLY, 0755);
#endif
if (fd < 0) {
lwsl_err("%s: unable to open %s for write\n", __func__, args);
return 1;
}
if (builder.sai_plat_server_owner.head) {
struct sai_plat_server *cm = lws_container_of(
builder.sai_plat_server_owner.head,
sai_plat_server_t, list);
respath = cm->resproxy_path;
}
#if defined(WIN32)
n = lws_snprintf(st, sizeof(st), runscript, ns->instance_idx,
respath, ns->slp_control.sockpath,
ns->slp[0].sockpath, ns->slp[1].sockpath, builder.home,
ns->inp, ns->task->build);
#else
n = lws_snprintf(st, sizeof(st), runscript, builder.home, ns->fsm.ovname,
ns->project_name, ns->ref, ns->instance_idx,
respath, ns->slp_control.sockpath,
ns->slp[0].sockpath, ns->slp[1].sockpath,
builder.home, ns->task->build);
#endif
/* but from the script's pov, it's chrooted at /home/sai */
if (write(fd, st, (unsigned int)n) != n) {
close(fd);
lwsl_err("%s: failed to write runscript to %s\n", __func__, args);
return 1;
}
close(fd);
cmd[0] = args;
#if defined(__linux__)
lws_snprintf(cgroup, sizeof(cgroup), "inst-%u-%d", (unsigned int)getpid(), ns->instance_idx);
#endif
memset(&info, 0, sizeof(info));
info.vh = builder.vhost;
info.env_array = (const char **)env;
info.exec_array = cmd;
info.protocol_name = "sai-stdxxx";
info.max_log_lines = 10000;
info.timeout_us = 30 * 60 * LWS_US_PER_SEC;
info.reap_cb = sai_lsp_reap_cb;
#if defined(__linux__)
info.cgroup_name_suffix = cgroup;
info.p_cgroup_ret = &in_cgroup;
#endif
op = malloc(sizeof(*op));
if (!op)
return 1;
memset(op, 0, sizeof(*op));
op->ns = ns;
ns->op = op;
info.opaque = op;
info.owner = &builder.lsp_owner;
info.plsp = &op->lsp;
// lwsl_warn("%s: spawning build script at %llu\n", __func__,
// (unsigned long long)lws_now_usecs());
lws_spawn_piped(&info);
if (!op->lsp) {
/*
* op is attached to wsi and will be freed in reap cb,
* we can't free it here
*/
ns->op = NULL;
lwsl_err("%s: failed\n", __func__);
return 1;
}
// lwsl_warn("%s: build script spawn returned at %llu\n", __func__,
// (unsigned long long)lws_now_usecs());
#if defined(__linux__)
lwsl_notice("%s: lws_spawn_piped started (cgroup: %d)\n", __func__, in_cgroup);
#endif
return 0;
}
/*
* This prepares the same filesystem layout whether we can use it for overlayfs,
* or have to use it directly / with chroot
*/
int
saib_prepare_mount(struct sai_builder *b, struct sai_nspawn *ns)
{
char homedir[384];
int n, m;
/* create the top level overlays container if not already there */
n = lws_snprintf(ns->fsm.mp, sizeof(ns->fsm.mp), "%s%coverlays",
b->home, csep);
if (mkdir(ns->fsm.mp, 0770))
lwsl_notice("%s: mkdir %s failed\n", __func__, ns->fsm.mp);
/* create a subdir for our overlay pieces */
n += lws_snprintf(ns->fsm.mp + n, sizeof(ns->fsm.mp) - (unsigned int)n, "%c%s", csep,
ns->fsm.ovname);
m = mkdir(ns->fsm.mp, 0777);
if (m && errno != EEXIST)
goto bail_dir;
lws_strncpy(homedir, ns->fsm.mp, sizeof(homedir));
/* create work dir, session layer and mountpoint, retain mountpoint */
#if defined(__linux__) && 0
lws_snprintf(ns->fsm.mp + n, sizeof(ns->fsm.mp) - (unsigned int)n, "%cwork", csep);
m = mkdir(ns->fsm.mp, 0770);
if (m && errno != EEXIST)
goto bail_dir;
lws_snprintf(ns->fsm.mp + n, sizeof(ns->fsm.mp) - (unsigned int)n, "%csession", csep);
m = mkdir(ns->fsm.mp, 0777);
if (m && errno != EEXIST)
goto bail_dir;
n += lws_snprintf(ns->fsm.mp + n, sizeof(ns->fsm.mp) - (unsigned int)n, "%cmountpoint",
csep);
m = mkdir(ns->fsm.mp, 0777);
if (m && errno != EEXIST)
goto bail_dir;
lws_snprintf(ns->fsm.mp + n, sizeof(ns->fsm.mp) - (unsigned int)n, "%chome%csai",
csep, csep);
lws_strncpy(homedir, ns->fsm.mp, sizeof(homedir));
ns->fsm.mp[n] = '\0';
n = lws_fsmount_mount(&ns->fsm);
if (!n)
#endif
{
/* these are ephemeral on top of the mountpoint path, we snip
* them off later */
n = (int)strlen(ns->fsm.mp);
lws_snprintf(ns->fsm.mp + n, sizeof(ns->fsm.mp) - (unsigned int)n, "%chome",
csep);
m = mkdir(ns->fsm.mp, 0700);
if (m && errno != EEXIST)
goto bail_dir;
lws_snprintf(ns->fsm.mp + n, sizeof(ns->fsm.mp) - (unsigned int)n,
"%chome%csai", csep, csep);
lws_strncpy(homedir, ns->fsm.mp, sizeof(homedir));
m = mkdir(ns->fsm.mp, 0700);
if (m && errno != EEXIST)
goto bail_dir;
lws_snprintf(ns->fsm.mp + n, sizeof(ns->fsm.mp) - (unsigned int)n,
"%chome%csai%cgit-mirror", csep, csep, csep);
m = mkdir(ns->fsm.mp, 0755);
ns->fsm.mp[n] = '\0';
if (m && errno != EEXIST)
goto bail_dir;
return 0;
}
#if defined(__linux__) && 0
else {
lwsl_err("%s: mount err %d, errno %d\n", __func__, n, errno);
}
return n;
#else
return 0;
#endif
bail_dir:
lwsl_err("%s: failed to create dir %s: errno %d\n", __func__,
ns->fsm.mp, errno);
return 1;
}
|