Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / assets / solaris-11.svg
Author[]Andy Green <andy@warmcat.com> 2026-09-10 07:23 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-09-10 07:23 UTC
Treeee9ec53a17c3d9287ed5290b7ec223225fbc7dda   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;
Page fetched 0s ago, creation time: 5ms (vhost etag hits: 0%, cache hits: 0%)