| /*
* sai-builder
*
* Copyright (C) 2019 - 2025 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
*/
#if !defined(_CRT_SECURE_NO_WARNINGS)
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <libwebsockets.h>
#include <string.h>
#include <signal.h>
#include <limits.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#if !defined(WIN32)
#include <pwd.h>
#include <grp.h>
#endif
#if defined(__linux__) || defined(__APPLE__)
#include <unistd.h>
#endif
#if defined(WIN32)
#include <initguid.h>
#include <KnownFolders.h>
#include <Shlobj.h>
#include <processthreadsapi.h>
#include <handleapi.h>
#if !defined(PATH_MAX)
#define PATH_MAX MAX_PATH
#endif
#endif
#ifndef S_ISDIR
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
#endif
#include "b-private.h"
#if defined(LWS_WITH_STUB)
#include <libwebsockets/lws-stub.h>
#endif
#if defined(LWS_WITH_STUB)
static int
sai_rm_rf_cb(const char *dirpath, void *user, struct lws_dir_entry *lde)
{
char path[PATH_MAX];
if (lde->name[0] == '.' && lde->name[1] == '\0')
return 0;
if (lde->name[0] == '.' && lde->name[1] == '.' && lde->name[2] == '\0')
return 0;
lws_snprintf(path, sizeof(path), "%s/%s", dirpath, lde->name);
if (lde->type == LDOT_DIR) {
lws_dir(path, user, sai_rm_rf_cb);
if (rmdir(path))
lwsl_notice("%s: rmdir %s failed: errno %d (%s)\n", __func__, path, errno, strerror(errno));
} else {
if (unlink(path)) {
#if defined(WIN32)
SetFileAttributesA(path, FILE_ATTRIBUTE_NORMAL);
if (unlink(path))
#endif
lwsl_notice("%s: unlink %s failed: errno %d (%s)\n", __func__, path, errno, strerror(errno));
}
}
return 0;
}
struct child_conn {
struct lejp_ctx jctx;
char home_dir[PATH_MAX];
};
static signed char
child_lejp_cb(struct lejp_ctx *ctx, char reason)
{
struct child_conn *conn = (struct child_conn *)ctx->user;
if (reason == LEJPCB_VAL_STR_END && !strcmp(ctx->path, "delete")) {
struct lws_dir_info di;
char full_path[PATH_MAX];
struct stat st;
lwsl_notice("%s: received delete request for '%s'\n", __func__, ctx->buf);
lws_snprintf(full_path, sizeof(full_path), "%s/jobs/%s", conn->home_dir, ctx->buf);
if (!stat(full_path, &st)) {
memset(&di, 0, sizeof(di));
di.dirpath = full_path;
di.cb = sai_rm_rf_cb;
di.do_toplevel_cb = 1;
lwsl_notice("%s: performing rm -rf %s\n", __func__, full_path);
lws_dir_via_info(&di);
/* lws_dir_via_info returns 1 on success. Errors are logged by sai_rm_rf_cb. */
if (!stat(full_path, &st))
lwsl_notice("%s: top level dir %s still exists\n", __func__, full_path);
} else {
lwsl_notice("%s: job dir %s not found (errno %d)\n", __func__, full_path, errno);
}
}
return 0;
}
static const char * const child_paths[] = { "delete" };
static int
callback_sai_deletion_uds(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
struct child_conn *conn = (struct child_conn *)user;
switch (reason) {
case LWS_CALLBACK_RAW_ADOPT:
/* Get home_dir from vhost user data */
{
const char *vuser = (const char *)lws_get_vhost_user(lws_get_vhost(wsi));
lwsl_notice("%s: ADOPT: vhost user is '%s'\n", __func__, vuser ? vuser : "NULL");
lws_strncpy(conn->home_dir, vuser ? vuser : "", sizeof(conn->home_dir));
lwsl_notice("%s: ADOPT: conn->home_dir set to '%s'\n", __func__, conn->home_dir);
}
/* We would normally verify the secret here, but for simplicity we skip it
since it's a local UDS with 0600 perms. */
lejp_construct(&conn->jctx, child_lejp_cb, conn, child_paths, 1);
break;
case LWS_CALLBACK_RAW_RX: {
uint8_t *p = (uint8_t *)in;
while (len) {
int m = lejp_parse(&conn->jctx, p, 1);
if (m < 0 && m != LEJP_CONTINUE) {
/*
* We hit the end of a JSON object and the start of the next one,
* which lejp rejects as trailing garbage. Reset the parser and
* retry this byte!
*/
lejp_destruct(&conn->jctx);
lejp_construct(&conn->jctx, child_lejp_cb, conn, child_paths, LWS_ARRAY_SIZE(child_paths));
continue;
}
p++;
len--;
}
break;
}
case LWS_CALLBACK_RAW_CLOSE:
lejp_destruct(&conn->jctx);
lwsl_notice("%s: parent connection closed, stub exiting\\n", __func__);
lws_cancel_service(lws_get_context(wsi));
break;
default:
break;
}
return 0;
}
static struct lws_protocols protocol_deletion_uds[] = {
{
.name = "sai-deletion-uds",
.callback = callback_sai_deletion_uds,
.per_session_data_size = sizeof(struct child_conn),
.rx_buffer_size = 0,
},
{ NULL, NULL, 0, 0 }
};
#if defined(__linux__) || defined(__APPLE__)
extern void crash_handler(int signum);
#endif
int
sai_deletion_worker(const char *home_dir_unused)
{
struct lws_context_creation_info info;
struct lws_context *cx;
struct lws_vhost *vh_uds;
char uds[256];
char secret[129];
char home_dir[PATH_MAX];
size_t rx = 0;
#if defined(__linux__) || defined(__APPLE__)
signal(SIGSEGV, crash_handler);
signal(SIGABRT, crash_handler);
signal(SIGBUS, crash_handler);
signal(SIGILL, crash_handler);
signal(SIGFPE, crash_handler);
#endif
lwsl_notice("%s: deletion worker (stub) started\n", __func__);
/* 1. Read secret from stdin */
#if defined(WIN32)
_setmode(0, _O_BINARY);
#endif
while (rx < 128) {
ssize_t n = read(0, secret + rx, 128 - (unsigned int)rx);
if (n < 0 && errno == EINTR)
continue;
if (n <= 0)
break;
rx += (size_t)n;
}
if (rx < 64) {
lwsl_err("%s: Failed to read secret from stdin\n", __func__);
return 1;
}
secret[128] = '\0';
/* 2. Read home_dir from stdin */
{
ssize_t n;
do {
n = read(0, home_dir, sizeof(home_dir) - 1);
} while (n < 0 && errno == EINTR);
if (n <= 0) {
lwsl_err("%s: Failed to read home_dir\n", __func__);
return 1;
}
home_dir[n] = '\0';
}
/* 3. Setup context */
memset(&info, 0, sizeof(info));
info.port = CONTEXT_PORT_NO_LISTEN;
info.options = LWS_SERVER_OPTION_EXPLICIT_VHOSTS;
cx = lws_create_context(&info);
if (!cx)
return 1;
lws_snprintf(uds, sizeof(uds), "%s/sai-deletion.sock", home_dir);
/* 4. Create UDS server vhost */
memset(&info, 0, sizeof(info));
info.options = LWS_SERVER_OPTION_UNIX_SOCK | LWS_SERVER_OPTION_ONLY_RAW;
info.iface = uds;
info.protocols = protocol_deletion_uds;
info.vhost_name = "sai-deletion";
/* Pass the home_dir via pvo to the protocol so it can be extracted in protocol init */
/* Actually, we can just pass it via user pointer for the protocol! */
info.user = home_dir;
unlink(info.iface);
vh_uds = lws_create_vhost(cx, &info);
if (!vh_uds) {
lwsl_err("%s: Failed to create UDS vhost\n", __func__);
return 1;
}
chmod(info.iface, 0600);
lwsl_notice("STUB-READY (sai-deletion)\n");
while (lws_service(cx, 0) >= 0)
;
lws_context_destroy(cx);
return 0;
}
#endif
/*
* Periodically (eg, once per hour) we walk the jobs dir and find subdirs
* that are older than a day.
*
* These represent failed jobs that were left for inspection, but should now
* be cleaned up.
*
* We are careful not to delete anything that is part of an ongoing job.
*/
struct inactive_job {
struct inactive_job *next;
char name[32];
uint64_t age;
};
struct cleanup_ctx {
lws_dll2_owner_t active_owner;
struct lwsac *ac;
struct inactive_job *inactive_head;
int inactive_count;
};
struct active_job_uuid {
lws_dll2_t list;
char uuid[65];
};
static int
compare_age(const void *a, const void *b)
{
const struct inactive_job *ia = *(const struct inactive_job **)a;
const struct inactive_job *ib = *(const struct inactive_job **)b;
if (ia->age > ib->age)
return -1;
if (ia->age < ib->age)
return 1;
return 0;
}
int
scan_jobs_dir_cb(const char *dirpath, void *user, struct lws_dir_entry *lde)
{
struct cleanup_ctx *ctx = (struct cleanup_ctx *)user;
char path[512], path2[512];
struct stat sb, sb2;
uint64_t age;
if (lde->name[0] == '.')
return 0;
lws_start_foreach_dll(struct lws_dll2 *, p, ctx->active_owner.head) {
struct active_job_uuid *aj = lws_container_of(p, struct active_job_uuid, list);
if (!strcmp(aj->uuid, lde->name)) {
/* it's an active job, leave it alone */
lwsl_info("%s: %s is active\n", __func__, lde->name);
return 0;
}
} lws_end_foreach_dll(p);
lws_snprintf(path, sizeof(path), "%s/%s", dirpath, lde->name);
if (stat(path, &sb)) {
lwsl_notice("%s: stat failed %s: errno %d (%s)\n", __func__, path, errno, strerror(errno));
return 0;
}
if (!S_ISDIR(sb.st_mode)) {
lwsl_notice("%s: %s is not a dir\n", __func__, path);
return 0;
}
#if !defined(WIN32)
lws_snprintf(path2, sizeof(path2), "%s/git_helper.sh", path);
#else
lws_snprintf(path2, sizeof(path2), "%s/git_helper.bat", path);
#endif
if (!stat(path2, &sb2)) {
sb.st_mtime = sb2.st_mtime;
}
/* older than 24h? */
age = (uint64_t)lws_now_secs() - (uint64_t)sb.st_mtime;
if (age > SAI_CLEANUP_JOB_DIR_MIN_AGE_SECS) {
lwsl_info("%s: requesting removal of old job dir %s (age %llus)\n",
__func__, path, (unsigned long long)age);
#if defined(LWS_WITH_STUB)
if (builder.mgr_deletion) {
char json[256];
lws_snprintf(json, sizeof(json), "{\"delete\": \"%s\"}", lde->name);
lws_stub_request(builder.mgr_deletion, json, NULL, 0, NULL, NULL, NULL);
}
#endif
} else {
struct inactive_job *ij = lwsac_use_zero(&ctx->ac, sizeof(*ij), 0);
if (ij) {
lws_strncpy(ij->name, lde->name, sizeof(ij->name));
ij->age = age;
ij->next = ctx->inactive_head;
ctx->inactive_head = ij;
ctx->inactive_count++;
}
lwsl_info("%s: %s is only %llus old\n", __func__, path,
(unsigned long long)age);
}
return 0;
}
void
sul_cleanup_jobs_cb(lws_sorted_usec_list_t *sul)
{
struct sai_builder *b = lws_container_of(sul, struct sai_builder,
sul_cleanup_jobs);
struct cleanup_ctx ctx;
char path[256];
lwsl_info("%s: starting periodic cleanup\n", __func__);
memset(&ctx, 0, sizeof(ctx));
/*
* We must not delete any active job directories, find out the uuids
* of any active jobs
*/
lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1,
b->sai_plat_owner.head) {
struct sai_plat *sp = lws_container_of(d,
struct sai_plat, sai_plat_list);
lws_start_foreach_dll_safe(struct lws_dll2 *, d2, d3,
sp->nspawn_owner.head) {
struct sai_nspawn *ns = lws_container_of(d2,
struct sai_nspawn, list);
struct active_job_uuid *aj;
if (!ns->task)
continue;
aj = lwsac_use_zero(&ctx.ac, sizeof(*aj), 64);
if (!aj)
continue;
lws_strncpy(aj->uuid, ns->inp_vn, sizeof(aj->uuid));
lws_dll2_add_tail(&aj->list, &ctx.active_owner);
} lws_end_foreach_dll_safe(d2, d3);
} lws_end_foreach_dll_safe(d, d1);
/*
* Now we have the active job uuids, scan the jobs dir and check
* for old, inactive job dirs to reap
*/
lws_snprintf(path, sizeof(path), "%s/jobs", b->home);
lws_dir(path, &ctx, scan_jobs_dir_cb);
/* dynamic cleanup */
{
unsigned int free_kib = saib_get_free_disk_kib(b->home);
unsigned int target_free_kib = 3 * 1024 * 1024; /* 3GB target */
if (free_kib < target_free_kib && ctx.inactive_count) {
int n, to_delete = 1;
struct inactive_job **sorted, *ij;
if (to_delete > ctx.inactive_count) to_delete = ctx.inactive_count;
sorted = lwsac_use(&ctx.ac, sizeof(*sorted) * (unsigned int)ctx.inactive_count, 0);
if (sorted) {
n = 0;
ij = ctx.inactive_head;
while (ij) {
sorted[n++] = ij;
ij = ij->next;
}
qsort(sorted, (size_t)ctx.inactive_count, sizeof(*sorted), compare_age);
for (n = 0; n < to_delete; n++) {
lwsl_notice("%s: dyn cleanup: requesting removal of %s (age %llus, free %uMiB, tgt %uMiB)\n",
__func__, sorted[n]->name, (unsigned long long)sorted[n]->age,
free_kib / 1024, target_free_kib / 1024);
#if defined(LWS_WITH_STUB)
if (builder.mgr_deletion) {
char json[256];
lws_snprintf(json, sizeof(json), "{\"delete\": \"%s\"}", sorted[n]->name);
lws_stub_request(builder.mgr_deletion, json, NULL, 0, NULL, NULL, NULL);
}
#endif
}
}
}
}
lwsac_free(&ctx.ac);
lws_sul_schedule(b->context, 0, &b->sul_cleanup_jobs,
sul_cleanup_jobs_cb, SAI_CLEANUP_JOBS_INTERVAL_US);
}
static int
callback_sai_deletion_stdwsi(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
uint8_t buf[256];
int ilen;
switch (reason) {
case LWS_CALLBACK_RAW_CLOSE_FILE:
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)) {
return -1;
}
ilen = (int)rb;
}
#else
ilen = (int)read((int)(intptr_t)lws_get_socket_fd(wsi), buf, sizeof(buf));
if (ilen < 1) {
return -1;
}
#endif
if (ilen > 0)
lwsl_notice("[DELETION] %.*s", ilen, buf);
break;
default:
break;
}
return 0;
}
struct lws_protocols protocol_deletion_stdxxx[] = {
{
.name = "sai-deletion-stdxxx",
.callback = callback_sai_deletion_stdwsi,
.per_session_data_size = 0,
.rx_buffer_size = 0,
},
{ NULL, NULL, 0, 0 }
};
#if defined(LWS_WITH_STUB)
static void
sai_deletion_connected_cb(struct lws_stub_manager *mgr)
{
lwsl_notice("%s: scheduling initial cleanup immediately upon connection\n", __func__);
lws_sul_schedule(builder.context, 0, &builder.sul_cleanup_jobs,
sul_cleanup_jobs_cb, 1);
}
int
saib_deletion_init(const char *argv0)
{
struct lws_stub_config config;
char uds_path[256];
memset(&config, 0, sizeof(config));
lws_snprintf(uds_path, sizeof(uds_path), "%s/sai-deletion.sock", builder.home);
config.cx = builder.context;
config.vh = builder.vhost;
config.stub_name = "sai-deletion";
config.uds_path = uds_path;
/* protocol_deletion_stdxxx is in the global array pprotocols, but we pass it as a single element array for lws_stub_spawn */
config.protocols = protocol_deletion_stdxxx;
config.user = (void *)builder.home;
config.extra_payload = builder.home;
config.extra_payload_len = strlen(builder.home) + 1;
config.connected_cb = sai_deletion_connected_cb;
builder.mgr_deletion = lws_stub_spawn(&config);
if (!builder.mgr_deletion) {
lwsl_err("%s: stub spawn failed\n", __func__);
return 1;
}
return 0;
}
#else
int
saib_deletion_init(const char *argv0)
{
lwsl_err("%s: lws_stub disabled\n", __func__);
return 0;
}
#endif
|