| Author | Andy Green <andy@warmcat.com> 2026-09-10 07:23 UTC | | Committer | Andy Green <andy@warmcat.com> 2026-09-10 07:23 UTC | | Tree | ee9ec53a17c3d9287ed5290b7ec223225fbc7dda Raw Patch | | | server: ad-hoc single-task events seeded from an existing task | server: ad-hoc single-task events seeded from an existing task
Add a way for an admin to schedule a one-off build without pushing to a CI'd
branch: a new event is created holding a single task cloned from an existing
one, building the head of a chosen branch with a possibly-edited script.
- events gain an "adhoc" column (in the lws_struct map, so it is in the
initial schema, and ALTER TABLEd in at startup for existing databases).
Ad-hoc events are excluded from the notification dedupe on hash, so a
later real push of the same commit still gets its normal event.
- a new "pushes" table records the hash most recently pushed for each
(repo, ref) from every authenticated hook notification. Refs under
refs/heads/_ are scratch branches: the push is recorded but the
.sai.json is not scheduled for it.
- the per-task creation block in the saifile parser is factored out into
sais_task_insert() / sais_task_build_step_count() so the notification
path and the clone path mint uuids, nonces and step counts the same way.
- sais_event_clone_task() builds the new event from the seed task's event
(repo, urls) and the seed task (platform, taskname, packages, artifacts,
log limit), resolves the requested ref to a hash from the pushes table
(falling back to the newest event on that ref), and announces it with
the usual eventchange. The browser never supplies a repo or hash.
- the web link accepts com.warmcat.sai.taskclone, and reassembles
fragmented messages since a taskclone carries up to 4KiB of script and
the parse is one-shot. Strings that exactly fill their array were
truncated by lws_struct and are rejected.
Also stops leaking the decoded saifile on the "hash already has an event"
path, which returned without freeing it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
diff --git a/src/common/include/private.h b/src/common/include/private.h
index f2b1f1f..6be9e7e 100644
--- a/src/common/include/private.h
+++ b/src/common/include/private.h
@@ -415,7 +415,16 @@ typedef struct sai_event {
sai_event_state_t state;
int uid;
int sec;
-
+ /*
+ * Nonzero for an ad-hoc event: a single-task event seeded from an
+ * existing task by an admin in the web UI, rather than created by
+ * a hook notification. Ad-hoc events are excluded from the
+ * notification dedupe on hash, from the project head status badge and
+ * from anything else that treats "newest event" as "state of the
+ * branch".
+ */
+ int adhoc;
+
lws_dll2_owner_t watcher_owner; /* sai_watcher_t */
} sai_event_t;
@@ -710,6 +719,23 @@ typedef struct sai_browse_rx_branchlist {
char project[65];
} sai_browse_rx_branchlist_t;
+/*
+ * Browser -> sai-web -> sai-server: admin asks for a new ad-hoc event with a
+ * single task, seeded from an existing task (build dimension, platform, repo
+ * and so on are inherited from the seed), but building the head of "ref" as
+ * last pushed to the server, and with a possibly-edited build script.
+ *
+ * The browser never supplies the repo or its fetch url; those are taken from
+ * the seed task's event on the server side. The ref is resolved to a hash on
+ * the server from the pushes table, so the browser cannot name an arbitrary
+ * hash either.
+ */
+typedef struct sai_browse_rx_taskclone {
+ char seed_uuid[65];
+ char ref[65];
+ char build[4096];
+} sai_browse_rx_taskclone_t;
+
/* sai-power -> sai-server, tells it that a platform is being powered up */
typedef struct sai_power_state {
lws_dll2_t list; /* for parser */
@@ -878,7 +904,7 @@ extern const lws_struct_map_t
lsm_schema_sq3_map_artifact[1],
lsm_schema_map_ta[1],
lsm_schema_map_plat_simple[1],
- lsm_event[13],
+ lsm_event[14],
lsm_task[32],
lsm_log[8],
lsm_artifact[9],
@@ -919,6 +945,8 @@ extern const lws_struct_map_t
lsm_schema_pcon_energy[1],
lsm_pcon_control[2],
lsm_schema_pcon_control[1],
+ lsm_taskclone[3],
+ lsm_schema_taskclone[1],
lsm_watcher_rule[6],
lsm_watcher_ui_rule[4],
lsm_watcher_service[6],
diff --git a/src/common/struct-metadata.c b/src/common/struct-metadata.c
index 6746e45..1ff2d77 100644
--- a/src/common/struct-metadata.c
+++ b/src/common/struct-metadata.c
@@ -141,6 +141,7 @@ const lws_struct_map_t lsm_event[] = {
LSM_UNSIGNED (sai_event_t, state, "state"),
LSM_UNSIGNED (sai_event_t, last_updated, "last_updated"),
LSM_UNSIGNED (sai_event_t, sec, "sec"),
+ LSM_UNSIGNED (sai_event_t, adhoc, "adhoc"),
LSM_JO_LIST (sai_event_t, watcher_owner, sai_watcher_t, list,
NULL, lsm_watcher, "watchers"),
};
@@ -227,6 +228,19 @@ const lws_struct_map_t lsm_task_cancel[] = {
LSM_UNSIGNED (sai_cancel_t, killed, "killed"),
};
+/* browser -> sai-web -> server: ad-hoc build seeded from an existing task */
+
+const lws_struct_map_t lsm_taskclone[] = {
+ LSM_CARRAY (sai_browse_rx_taskclone_t, seed_uuid, "seed_uuid"),
+ LSM_CARRAY (sai_browse_rx_taskclone_t, ref, "ref"),
+ LSM_CARRAY (sai_browse_rx_taskclone_t, build, "build"),
+};
+
+const lws_struct_map_t lsm_schema_taskclone[] = {
+ LSM_SCHEMA (sai_browse_rx_taskclone_t, NULL, lsm_taskclone,
+ "com.warmcat.sai.taskclone")
+};
+
const lws_struct_map_t lsm_openshell[] = {
LSM_CARRAY (sai_openshell_t, builder_name, "builder_name"),
LSM_CARRAY (sai_openshell_t, task_uuid, "task_uuid"),
diff --git a/src/server/s-comms.c b/src/server/s-comms.c
index 6092c05..e1ffb51 100644
--- a/src/server/s-comms.c
+++ b/src/server/s-comms.c
@@ -228,10 +228,32 @@ s_callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user,
NULL, NULL, &err);
if (err)
sqlite3_free(err);
+
+ err = NULL;
+ sqlite3_exec(vhd->server.pdb,
+ "ALTER TABLE events ADD COLUMN adhoc integer;",
+ NULL, NULL, &err);
+ if (err)
+ sqlite3_free(err);
}
sai_sqlite3_statement(vhd->server.pdb, "CREATE UNIQUE INDEX IF NOT EXISTS idx_event_uuid ON events(uuid);", "create event index");
+ /*
+ * The hash most recently pushed for each (repo, ref) we were
+ * notified about, including scratch "_" refs we don't CI.
+ * Ad-hoc builds resolve their target branch to a hash here.
+ */
+ if (sai_sqlite3_statement(vhd->server.pdb,
+ "CREATE TABLE IF NOT EXISTS pushes ("
+ " repo_name varchar(64), ref varchar(64),"
+ " hash varchar(64), created integer,"
+ " PRIMARY KEY (repo_name, ref));",
+ "create pushes table")) {
+ lwsl_err("%s: unable to create pushes table\n", __func__);
+ return -1;
+ }
+
if (lws_struct_sq3_create_table(vhd->server.pdb,
lsm_schema_sq3_map_plat)) {
lwsl_err("%s: unable to create builders table\n", __func__);
diff --git a/src/server/s-notification.c b/src/server/s-notification.c
index 1eac80a..8ed2ef6 100644
--- a/src/server/s-notification.c
+++ b/src/server/s-notification.c
@@ -299,7 +299,6 @@ sai_saifile_lejp_cb(struct lejp_ctx *ctx, char reason)
if (reason == LEJPCB_OBJECT_END &&
ctx->path_match - 1 == LEJPNSAIF_CONFIGURATIONS_NAME &&
sn->t.taskname[0]) {
- lws_dll2_owner_t owner;
char *err;
/*
@@ -525,9 +524,6 @@ next_plat: ;
}
if (match) {
- const char *p;
- int c;
-
/*
* For this platform, we want to create a task
* associated with this event. Tasks and logs
@@ -535,19 +531,6 @@ next_plat: ;
* specific database file for scalability.
*/
- c = 2; /* git mirror and checkout */
- if (pl->build[0]) {
- c++;
- p = pl->build;
- while (*p)
- if (*p++ == '\n')
- c++;
- if (pl->build[strlen(pl->build) - 1] == '\n')
- c--;
- }
-
- pss->sn.t.build_step_count = c;
-
lws_strexp_init(&sx, sn, exp_cmake, sn->t.build,
sizeof(sn->t.build));
@@ -565,60 +548,24 @@ next_plat: ;
return -1;
}
-
- /*
- * Prepare a struct of the task object...
- * task uuid is the event uuid and another
- * random 32 chars, so you can always recover
- * the related event uuid from the task uuid
- */
-
- memcpy(pss->sn.t.uuid, pss->sn.e.uuid, 32);
- sai_uuid16_create(lws_get_context(pss->wsi),
- pss->sn.t.uuid + 32);
- strcpy(pss->sn.t.event_uuid, pss->sn.e.uuid);
- pss->sn.t.uid = pss->sn.event_task_index++;
-
- /*
- * This is basically a secret that anything
- * trying to upload an artifact for the task
- * must provide to authenticate.
- */
- sai_uuid16_create(lws_get_context(pss->wsi),
- pss->sn.t.art_up_nonce);
- /*
- * An unrelated secret that anything
- * trying to download an artifact for the task
- * must provide to identify it.
- */
- sai_uuid16_create(lws_get_context(pss->wsi),
- pss->sn.t.art_down_nonce);
-
- pss->sn.t.git_repo_url =
- pss->sn.e.repo_fetchurl;
- pss->sn.e.last_updated =
- (unsigned long long)lws_now_secs();
- pss->sn.e.state = SAIES_WAITING;
lws_strncpy(pss->sn.t.platform, pl->name,
sizeof(pss->sn.t.platform));
- // pss->sn.t.server_name = ;
- pss->sn.t.repo_name = pss->sn.e.repo_name;
- pss->sn.t.git_ref = sn->e.ref;
- pss->sn.t.git_hash = sn->e.hash;
- pss->sn.t.parallel = 2;
-
- lws_dll2_clear(&pss->sn.t.list);
- lws_dll2_owner_clear(&owner);
- lws_dll2_add_head(&pss->sn.t.list, &owner);
-
/*
- * Create the task in event-specific database
+ * Mint the uuids / nonces and create the task
+ * in the event-specific database
*/
-
- lws_struct_sq3_serialize(pdb,
- lsm_schema_sq3_map_task,
- &owner, (uint32_t)pss->sn.t.uid);
+ if (sais_task_insert(lws_get_context(pss->wsi),
+ pdb, &pss->sn.e, &pss->sn.t,
+ pss->sn.event_task_index++) < 0) {
+ lwsl_err("%s: task insert failed\n",
+ __func__);
+ sqlite3_exec(pdb, "END TRANSACTION", NULL, NULL, &err);
+ if (err)
+ sqlite3_free(err);
+ sai_event_db_close(&pss->vhd->sqlite3_cache, &pdb);
+ return -1;
+ }
}
} lws_end_foreach_dll(p);
@@ -1043,11 +990,35 @@ sai_notification_file_upload_cb(void *data, const char *name,
lwsl_notice("%s: hmac OK\n", __func__);
/*
+ * Remember the (repo, ref) -> hash we were just told about,
+ * for every authenticated notification and before deciding
+ * whether it gets CI'd: ad-hoc builds resolve "the head of
+ * this branch" from here.
+ */
+ if (sais_push_record(pss->vhd, pss->sn.e.repo_name,
+ pss->sn.e.ref, pss->sn.e.hash))
+ lwsl_warn("%s: unable to record push\n", __func__);
+
+ /*
+ * Branches whose name begins with "_" are scratch branches:
+ * the push is recorded above so an ad-hoc build can target
+ * it, but we don't schedule the whole .sai.json against it.
+ */
+ if (!strncmp(pss->sn.e.ref, "refs/heads/_", 12)) {
+ lwsl_notice("%s: scratch ref %s recorded, not scheduled\n",
+ __func__, pss->sn.e.ref);
+ goto saifile_done;
+ }
+
+ /*
* We have the notification metadata JSON parsed into pss->sn.e,
* eg, pss->sn->e.hash ... since it's common to, eg, push a tree
* in a branch and then later tag the same commit, we don't want
* to pointlessly repeat CI for the same tree multiple times,
* and need to basically dedupe.
+ *
+ * Ad-hoc events are excluded: they are single-task scratch
+ * builds of a hash that may well be pushed for real later.
*/
{
@@ -1057,7 +1028,8 @@ sai_notification_file_upload_cb(void *data, const char *name,
lws_sql_purify(esc_hash, pss->sn.e.hash, sizeof(esc_hash));
lws_snprintf(qu, sizeof(qu), "select rowid from events "
- "where hash='%s'",
+ "where hash='%s' and "
+ "ifnull(adhoc,0)=0",
esc_hash);
if (sqlite3_exec(pss->vhd->server.pdb, qu,
@@ -1068,7 +1040,7 @@ sai_notification_file_upload_cb(void *data, const char *name,
"tree hash event exists\n",
__func__);
- return 0;
+ goto saifile_done;
}
}
@@ -1165,6 +1137,13 @@ sai_notification_file_upload_cb(void *data, const char *name,
return 0;
+saifile_done:
+ /* notification accepted but nothing to schedule */
+ free(pss->sn.saifile);
+ pss->sn.saifile = NULL;
+
+ return 0;
+
saifile_bail:
free(pss->sn.saifile);
pss->sn.saifile = NULL;
diff --git a/src/server/s-private.h b/src/server/s-private.h
index 1f812b5..31bc323 100644
--- a/src/server/s-private.h
+++ b/src/server/s-private.h
@@ -87,6 +87,11 @@ typedef struct websrvss_srv {
struct lejp_ctx ctx;
struct lws_buflist *bl_srv_to_web;
+ /*
+ * Reassembly of fragmented web -> server messages (eg, a taskclone
+ * carrying an edited build script), see websrvss_ws_rx()
+ */
+ struct lws_buflist *rx_reasm;
unsigned int viewers;
struct lws_buflist *private_heads[SAI_WEBSRV_PB__COUNT];
@@ -420,6 +425,24 @@ sais_event_delete(struct vhd *vhd, const char *event_uuid);
sai_db_result_t
sais_event_reset(struct vhd *vhd, const char *event_uuid);
+sai_db_result_t
+sais_event_clone_task(struct vhd *vhd, const sai_browse_rx_taskclone_t *tc);
+
+int
+sais_push_record(struct vhd *vhd, const char *repo_name, const char *ref,
+ const char *hash);
+
+int
+sais_push_lookup(struct vhd *vhd, const char *repo_name, const char *ref,
+ char *hash, size_t hash_len);
+
+int
+sais_task_build_step_count(const char *build);
+
+int
+sais_task_insert(struct lws_context *cx, sqlite3 *pdb, sai_event_t *e,
+ sai_task_t *t, int uid);
+
int
sai_detach_builder(struct lws_dll2 *d, void *user);
diff --git a/src/server/s-task-helpers.c b/src/server/s-task-helpers.c
index 32a08f1..a5e984f 100644
--- a/src/server/s-task-helpers.c
+++ b/src/server/s-task-helpers.c
@@ -27,6 +27,83 @@
#include "s-private.h"
+/*
+ * Count the build steps the builder will split "build" into: one per line,
+ * plus the git mirror and checkout steps that precede them.
+ */
+int
+sais_task_build_step_count(const char *build)
+{
+ const char *p = build;
+ int c = 2; /* git mirror and checkout */
+
+ if (!build || !build[0])
+ return c;
+
+ c++;
+ while (*p)
+ if (*p++ == '\n')
+ c++;
+ if (build[strlen(build) - 1] == '\n')
+ c--;
+
+ return c;
+}
+
+/*
+ * Finish and insert a task as a new row in an event's database.
+ *
+ * The caller has already set the per-configuration strings (platform,
+ * taskname, build, packages, artifacts...); this fills in everything that is
+ * derived from the event or must be freshly minted per task: uuids, artifact
+ * nonces, uid, the repo / ref / hash pointers used in the offer, and the step
+ * count. Shared by the hook notification path (one task per platform per
+ * configuration) and the ad-hoc clone path (one task).
+ */
+int
+sais_task_insert(struct lws_context *cx, sqlite3 *pdb, sai_event_t *e,
+ sai_task_t *t, int uid)
+{
+ lws_dll2_owner_t owner;
+
+ /*
+ * task uuid is the event uuid and another random 32 chars, so you
+ * can always recover the related event uuid from the task uuid
+ */
+ memcpy(t->uuid, e->uuid, 32);
+ sai_uuid16_create(cx, t->uuid + 32);
+ lws_strncpy(t->event_uuid, e->uuid, sizeof(t->event_uuid));
+ t->uid = uid;
+
+ /*
+ * This is basically a secret that anything trying to upload an
+ * artifact for the task must provide to authenticate.
+ */
+ sai_uuid16_create(cx, t->art_up_nonce);
+ /*
+ * An unrelated secret that anything trying to download an artifact
+ * for the task must provide to identify it.
+ */
+ sai_uuid16_create(cx, t->art_down_nonce);
+
+ t->git_repo_url = e->repo_fetchurl;
+ t->repo_name = e->repo_name;
+ t->git_ref = e->ref;
+ t->git_hash = e->hash;
+ t->build_step_count = sais_task_build_step_count(t->build);
+ t->parallel = 2;
+
+ e->last_updated = (unsigned long long)lws_now_secs();
+ e->state = SAIES_WAITING;
+
+ lws_dll2_clear(&t->list);
+ lws_dll2_owner_clear(&owner);
+ lws_dll2_add_head(&t->list, &owner);
+
+ return lws_struct_sq3_serialize(pdb, lsm_schema_sq3_map_task, &owner,
+ (uint32_t)uid);
+}
+
void
sais_get_task_metrics_estimates(struct vhd *vhd, sai_task_t *task)
{
diff --git a/src/server/s-webops.c b/src/server/s-webops.c
index 6bf1943..2338c44 100644
--- a/src/server/s-webops.c
+++ b/src/server/s-webops.c
@@ -216,6 +216,263 @@ sais_eventchange(struct lws_ss_handle *hsrv, const char *event_uuid, int state)
}
}
+/*
+ * Remember the hash most recently pushed for (repo, ref). Updated for every
+ * authenticated hook notification, including scratch refs we don't CI, so an
+ * ad-hoc build can be pointed at "the head of this branch as last pushed".
+ */
+int
+sais_push_record(struct vhd *vhd, const char *repo_name, const char *ref,
+ const char *hash)
+{
+ sqlite3_stmt *stmt = NULL;
+ int rc;
+
+ if (sqlite3_prepare_v2(vhd->server.pdb,
+ "INSERT OR REPLACE INTO pushes "
+ "(repo_name, ref, hash, created) VALUES (?, ?, ?, ?)",
+ -1, &stmt, NULL) != SQLITE_OK) {
+ lwsl_err("%s: prepare failed: %s\n", __func__,
+ sqlite3_errmsg(vhd->server.pdb));
+ return 1;
+ }
+
+ sqlite3_bind_text(stmt, 1, repo_name, -1, SQLITE_STATIC);
+ sqlite3_bind_text(stmt, 2, ref, -1, SQLITE_STATIC);
+ sqlite3_bind_text(stmt, 3, hash, -1, SQLITE_STATIC);
+ sqlite3_bind_int64(stmt, 4, (sqlite3_int64)lws_now_secs());
+
+ rc = sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+
+ if (rc != SQLITE_DONE) {
+ lwsl_err("%s: insert failed: %s\n", __func__,
+ sqlite3_errmsg(vhd->server.pdb));
+ return 1;
+ }
+
+ return 0;
+}
+
+/*
+ * Resolve (repo, ref) to the hash we should build for it: the most recent
+ * push we were notified about, or failing that (eg, an event predating the
+ * pushes table) the newest non-deleted event on that ref.
+ */
+int
+sais_push_lookup(struct vhd *vhd, const char *repo_name, const char *ref,
+ char *hash, size_t hash_len)
+{
+ static const char * const q[] = {
+ "SELECT hash FROM pushes WHERE repo_name = ? AND ref = ?",
+ "SELECT hash FROM events WHERE repo_name = ? AND ref = ? "
+ "AND state != 7 ORDER BY created DESC LIMIT 1"
+ };
+ sqlite3_stmt *stmt = NULL;
+ int n, ret = 1;
+
+ for (n = 0; n < (int)LWS_ARRAY_SIZE(q) && ret; n++) {
+ if (sqlite3_prepare_v2(vhd->server.pdb, q[n], -1, &stmt,
+ NULL) != SQLITE_OK) {
+ lwsl_err("%s: prepare %d failed: %s\n", __func__, n,
+ sqlite3_errmsg(vhd->server.pdb));
+ continue;
+ }
+
+ sqlite3_bind_text(stmt, 1, repo_name, -1, SQLITE_STATIC);
+ sqlite3_bind_text(stmt, 2, ref, -1, SQLITE_STATIC);
+
+ if (sqlite3_step(stmt) == SQLITE_ROW) {
+ const char *h = (const char *)sqlite3_column_text(stmt, 0);
+
+ if (h && sai_is_git_hash(h)) {
+ lws_strncpy(hash, h, hash_len);
+ ret = 0;
+ }
+ }
+
+ sqlite3_finalize(stmt);
+ }
+
+ return ret;
+}
+
+/*
+ * Create a new ad-hoc event holding a single task, seeded from an existing
+ * task.
+ *
+ * The seed's event supplies the repo and its urls; the seed task supplies the
+ * platform, build dimension name (taskname), packages, artifacts and log
+ * limit, so the result looks like any other task of that dimension in the UI.
+ * The caller supplies the ref to build, which is resolved here to the hash
+ * last pushed for it, and the build script, which may have been edited.
+ *
+ * Everything else about the new task (uuids, nonces, state) is freshly
+ * minted; it is not a "run" of the seed and does not touch the seed's event.
+ */
+sai_db_result_t
+sais_event_clone_task(struct vhd *vhd, const sai_browse_rx_taskclone_t *tc)
+{
+ sai_db_result_t r = SAI_DB_RESULT_ERROR;
+ struct lwsac *ac_ev = NULL, *ac_task = NULL;
+ char seed_event_uuid[33], esc[96], filt[160];
+ lws_dll2_owner_t o_ev, o_task, owner;
+ sai_event_t *seed_e, ev;
+ sai_task_t *seed_t, *t = NULL;
+ sqlite3 *pdb = NULL;
+ char *err = NULL;
+ int n;
+
+ if (sais_validate_id(tc->seed_uuid, SAI_TASKID_LEN)) {
+ lwsl_notice("%s: bad seed uuid\n", __func__);
+ return SAI_DB_RESULT_ERROR;
+ }
+
+ /*
+ * The ref is exported into the builder's git helper script; it has
+ * to be a full, safe refname. strlen() == sizeof - 1 means lws_struct
+ * truncated it on the way in, so it can't be what the user meant.
+ */
+ if (strncmp(tc->ref, "refs/", 5) || !sai_is_safe_ref(tc->ref) ||
+ strlen(tc->ref) >= sizeof(tc->ref) - 1) {
+ lwsl_notice("%s: rejecting ref '%s'\n", __func__, tc->ref);
+ return SAI_DB_RESULT_ERROR;
+ }
+
+ if (!tc->build[0] || strlen(tc->build) >= sizeof(tc->build) - 1) {
+ lwsl_notice("%s: rejecting empty or overlong build\n", __func__);
+ return SAI_DB_RESULT_ERROR;
+ }
+
+ sai_task_uuid_to_event_uuid(seed_event_uuid, tc->seed_uuid);
+
+ /* the seed's event, from the main events db */
+
+ lws_sql_purify(esc, seed_event_uuid, sizeof(esc));
+ lws_snprintf(filt, sizeof(filt), " and uuid='%s'", esc);
+ n = lws_struct_sq3_deserialize(vhd->server.pdb, filt, NULL,
+ lsm_schema_sq3_map_event, &o_ev,
+ &ac_ev, 0, 1);
+ if (n < 0 || !o_ev.head) {
+ lwsl_notice("%s: no seed event %s\n", __func__, seed_event_uuid);
+ goto bail;
+ }
+ seed_e = lws_container_of(o_ev.head, sai_event_t, list);
+
+ /* the seed task, latest run, from its event-specific db */
+
+ if (sai_event_db_ensure_open(vhd->context, &vhd->sqlite3_cache,
+ vhd->sqlite3_path_lhs, seed_event_uuid,
+ 0, &pdb)) {
+ lwsl_notice("%s: unable to open seed event db\n", __func__);
+ goto bail;
+ }
+
+ lws_sql_purify(esc, tc->seed_uuid, sizeof(esc));
+ lws_snprintf(filt, sizeof(filt), " and uuid='%s'", esc);
+ n = lws_struct_sq3_deserialize(pdb, filt, "run desc",
+ lsm_schema_sq3_map_task, &o_task,
+ &ac_task, 0, 1);
+ sai_event_db_close(&vhd->sqlite3_cache, &pdb);
+ if (n < 0 || !o_task.head) {
+ lwsl_notice("%s: no seed task %s\n", __func__, tc->seed_uuid);
+ goto bail;
+ }
+ seed_t = lws_container_of(o_task.head, sai_task_t, list);
+
+ /* the new event: the seed's repo, but the requested ref's head */
+
+ memset(&ev, 0, sizeof(ev));
+ lws_strncpy(ev.repo_name, seed_e->repo_name, sizeof(ev.repo_name));
+ lws_strncpy(ev.repo_fetchurl, seed_e->repo_fetchurl,
+ sizeof(ev.repo_fetchurl));
+ lws_strncpy(ev.repo_weburl, seed_e->repo_weburl,
+ sizeof(ev.repo_weburl));
+ lws_strncpy(ev.ref, tc->ref, sizeof(ev.ref));
+ ev.sec = seed_e->sec;
+ ev.adhoc = 1;
+
+ if (sais_push_lookup(vhd, ev.repo_name, ev.ref, ev.hash,
+ sizeof(ev.hash))) {
+ lwsl_notice("%s: no known push of %s for %s\n", __func__,
+ ev.ref, ev.repo_name);
+ goto bail;
+ }
+
+ sai_uuid16_create(vhd->context, ev.uuid);
+ ev.created = (unsigned long long)lws_now_secs();
+ ev.state = SAIES_WAITING;
+
+ lws_dll2_clear(&ev.list);
+ lws_dll2_owner_clear(&owner);
+ lws_dll2_add_head(&ev.list, &owner);
+
+ if (lws_struct_sq3_serialize(vhd->server.pdb, lsm_schema_sq3_map_event,
+ &owner, 0) < 0) {
+ lwsl_err("%s: unable to create event\n", __func__);
+ goto bail;
+ }
+
+ /* the single task, in the new event's own db */
+
+ t = malloc(sizeof(*t));
+ if (!t)
+ goto bail;
+ memset(t, 0, sizeof(*t));
+
+ lws_strncpy(t->platform, seed_t->platform, sizeof(t->platform));
+ lws_strncpy(t->taskname, seed_t->taskname, sizeof(t->taskname));
+ lws_strncpy(t->packages, seed_t->packages, sizeof(t->packages));
+ lws_strncpy(t->artifacts, seed_t->artifacts, sizeof(t->artifacts));
+ lws_strncpy(t->build, tc->build, sizeof(t->build));
+ t->task_log_limit = seed_t->task_log_limit;
+ t->state = SAIES_WAITING;
+
+ if (sai_event_db_ensure_open(vhd->context, &vhd->sqlite3_cache,
+ vhd->sqlite3_path_lhs, ev.uuid, 1, &pdb)) {
+ lwsl_err("%s: unable to create event db\n", __func__);
+ goto bail;
+ }
+
+ sqlite3_exec(pdb, "BEGIN TRANSACTION", NULL, NULL, &err);
+ if (err)
+ sqlite3_free(err);
+
+ n = sais_task_insert(vhd->context, pdb, &ev, t, 0);
+
+ err = NULL;
+ sqlite3_exec(pdb, "END TRANSACTION", NULL, NULL, &err);
+ if (err)
+ sqlite3_free(err);
+ sai_event_db_close(&vhd->sqlite3_cache, &pdb);
+
+ if (n < 0) {
+ lwsl_err("%s: unable to create task\n", __func__);
+ goto bail;
+ }
+
+ lwsl_notice("%s: ad-hoc event %s: %s %s on %s from %s\n", __func__,
+ ev.uuid, ev.repo_name, ev.ref, t->platform, tc->seed_uuid);
+
+ /*
+ * Let sai-power / builders know there's something new, and let the
+ * sai-web instances (and so browsers) see the new event appear
+ */
+ sais_platforms_with_tasks_pending(vhd);
+ lws_sul_schedule(vhd->context, 0, &vhd->sul_central, sais_central_cb,
+ 1 * LWS_US_PER_SEC);
+ sais_eventchange(vhd->h_ss_websrv, ev.uuid, SAIES_WAITING);
+
+ r = SAI_DB_RESULT_OK;
+
+bail:
+ free(t);
+ lwsac_free(&ac_task);
+ lwsac_free(&ac_ev);
+
+ return r;
+}
+
sai_db_result_t
sais_event_reset(struct vhd *vhd, const char *event_uuid)
{
diff --git a/src/server/s-ws-web.c b/src/server/s-ws-web.c
index b7cf3f6..00eaa41 100644
--- a/src/server/s-ws-web.c
+++ b/src/server/s-ws-web.c
@@ -121,7 +121,9 @@ static const lws_struct_map_t lsm_schema_json_map[] = {
LSM_SCHEMA (sai_closeshell_t, NULL, lsm_closeshell,
"com.warmcat.sai.closeshell"),
LSM_SCHEMA (sai_ptydata_t, NULL, lsm_ptydata,
- "com.warmcat.sai.ptydata")
+ "com.warmcat.sai.ptydata"),
+ LSM_SCHEMA (sai_browse_rx_taskclone_t, NULL, lsm_taskclone,
+ "com.warmcat.sai.taskclone"),
};
enum {
@@ -144,8 +146,15 @@ enum {
SAIS_WS_WEBSRV_RX_OPENSHELL,
SAIS_WS_WEBSRV_RX_CLOSESHELL,
SAIS_WS_WEBSRV_RX_PTYDATA,
+ SAIS_WS_WEBSRV_RX_TASKCLONE,
};
+/*
+ * Cap on a reassembled web -> server message. The largest legitimate one is
+ * a taskclone: a 4KiB build script JSON-escaped, plus the small fixed fields.
+ */
+#define SAIS_WEBSRV_RX_REASM_MAX 32768
+
static int
sais_validate_builder_name(const char *id)
{
@@ -423,9 +432,9 @@ sum_viewers_cb(struct lws_ss_handle *h, void *arg)
static lws_ss_state_return_t
-websrvss_ws_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
+websrvss_ws_rx_msg(websrvss_srv_t *m, const uint8_t *buf, size_t len,
+ int flags)
{
- websrvss_srv_t *m = (websrvss_srv_t *)userobj;
sai_browse_rx_evinfo_t *ei;
sai_cancel_t *can;
lws_struct_args_t a;
@@ -694,6 +703,22 @@ websrvss_ws_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
break;
}
+ case SAIS_WS_WEBSRV_RX_TASKCLONE:
+ {
+ sai_browse_rx_taskclone_t *tc =
+ (sai_browse_rx_taskclone_t *)a.dest;
+
+ /*
+ * sai-web only forwards this from admin-authenticated
+ * browsers; validation of the fields is done in the helper
+ */
+ lwsl_ss_notice(m->ss, "SAIS_WS_WEBSRV_RX_TASKCLONE: seed %s, "
+ "ref %s", tc->seed_uuid, tc->ref);
+ if (sais_event_clone_task(m->vhd, tc) != SAI_DB_RESULT_OK)
+ lwsl_ss_err(m->ss, "taskclone failed");
+ break;
+ }
+
case SAIS_WS_WEBSRV_RX_OPENSHELL:
{
sai_openshell_t *os = (sai_openshell_t *)a.dest;
@@ -896,6 +921,64 @@ soft_error:
return 0;
}
+/*
+ * Messages from sai-web are normally small enough to arrive whole, but a
+ * taskclone carries an edited build script and can span several fragments.
+ * websrvss_ws_rx_msg() parses one-shot, so reassemble anything that isn't
+ * a complete message before handing it over.
+ */
+static lws_ss_state_return_t
+websrvss_ws_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
+{
+ websrvss_srv_t *m = (websrvss_srv_t *)userobj;
+ lws_ss_state_return_t r;
+ uint8_t *reasm;
+ size_t rl;
+
+ if ((flags & LWSSS_FLAG_SOM) && (flags & LWSSS_FLAG_EOM) &&
+ !m->rx_reasm)
+ return websrvss_ws_rx_msg(m, buf, len, flags);
+
+ if (flags & LWSSS_FLAG_SOM)
+ /* a new message while holding fragments: discard the old */
+ lws_buflist_destroy_all_segments(&m->rx_reasm);
+
+ if (lws_buflist_total_len(&m->rx_reasm) + len >
+ SAIS_WEBSRV_RX_REASM_MAX) {
+ lwsl_ss_warn(m->ss, "rx reassembly over size, dropping");
+ lws_buflist_destroy_all_segments(&m->rx_reasm);
+
+ return LWSSSSRET_OK;
+ }
+
+ if (len && lws_buflist_append_segment(&m->rx_reasm, buf, len) < 0) {
+ lwsl_ss_warn(m->ss, "rx reassembly oom, dropping");
+ lws_buflist_destroy_all_segments(&m->rx_reasm);
+
+ return LWSSSSRET_OK;
+ }
+
+ if (!(flags & LWSSS_FLAG_EOM))
+ return LWSSSSRET_OK;
+
+ rl = lws_buflist_total_len(&m->rx_reasm);
+ reasm = malloc(rl);
+ if (!reasm) {
+ lws_buflist_destroy_all_segments(&m->rx_reasm);
+
+ return LWSSSSRET_OK;
+ }
+
+ lws_buflist_linear_use(&m->rx_reasm, reasm, rl);
+ lws_buflist_destroy_all_segments(&m->rx_reasm);
+
+ r = websrvss_ws_rx_msg(m, reasm, rl,
+ LWSSS_FLAG_SOM | LWSSS_FLAG_EOM);
+ free(reasm);
+
+ return r;
+}
+
static lws_ss_state_return_t
websrvss_ws_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf,
size_t *len, int *flags)
@@ -921,6 +1004,7 @@ websrvss_srv_state(void *userobj, void *sh, lws_ss_constate_t state,
unsigned int total_viewers = 0;
lws_buflist_destroy_all_segments(&m->bl_srv_to_web);
+ lws_buflist_destroy_all_segments(&m->rx_reasm);
lws_wsmsg_destroy(m->private_heads, LWS_ARRAY_SIZE(m->private_heads));
m->viewers = 0;
|