| Author | Andy Green <andy@warmcat.com> 2026-10-04 07:40 UTC | | Committer | Andy Green <andy@warmcat.com> 2026-10-04 20:05 UTC | | Tree | 8241937e4a6b2ce56ab803edf5af5366a3c01c6d Raw Patch | | | web: "projects" pvo limits which projects a vhost shows | web: "projects" pvo limits which projects a vhost shows
So the events of one sai-server can be offered as different canned views,
let each sai-web vhost list the projects it shows in a new optional
"projects" pvo, as a comma-separated list, eg, "libwebsockets, sai". Two
otherwise identical vhosts on differently-named unix sockets can then show
different projects, and the front-end proxy mounts pick a view by socket.
Without the pvo a vhost shows everything, as before.
The list is parsed with lws_tokenize (COMMA_SEP_LIST, so whitespace around
names is ignored and misplaced commas are errors) into an lwsac-backed
lws_dll2 list for checks in C, and into a TEMP table on the vhd's own events
db connection, so event queries take a fixed " and repo_name in (...)"
fragment. TEMP tables are private to their connection, and each vhd opens
its own.
On a restricted vhost the other projects don't exist:
- overview, project and branch lists, and rss feeds only query visible
projects
- taskinfo for a hidden task answers like a deleted event, so no logs or
artifacts either; artifact downloads 404 and /status badges are unknown
- task and event change notices from sai-server about hidden events are
not passed on
- taskactivity and builder load reports are decoded and serialized again
without the hidden tasks, instead of being forwarded as they came
- admin operations naming a hidden task, event or findings repo are
dropped, and the findings list skips hidden repos
A pvo that doesn't parse or lists nothing fails PROTOCOL_INIT, which leaves
the vhost serving nothing rather than everything.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
diff --git a/README.md b/README.md
index dd00400..4799dc4 100644
--- a/README.md
+++ b/README.md
@@ -182,6 +182,49 @@ local user on the host can connect to the link. Existing deployments keep
working unchanged, but should add the pvo to both confs and restart both
daemons (sai-server first, sai-web reconnects by itself).
+## Showing only some projects on a sai-web vhost ("projects")
+
+By default a sai-web vhost shows every project sai-server has built. The
+optional `projects` pvo in the vhost's `com-warmcat-sai` section limits it to
+the listed projects (repo names), as a comma-separated list:
+
+```
+ "projects": "libwebsockets, sai",
+```
+
+Whitespace around the names is ignored; names can use `A-Z a-z 0-9 _ - .`.
+On that vhost, everything behaves as if the other projects don't
+exist: the event list, project and branch lists, live updates, task logs and
+artifacts, the builders' lists of what they are building, the rss feeds,
+`/status` badges and findings. Admin actions on tasks and events of other
+projects are dropped. The builders themselves are shared, so they and their
+load are shown as usual. A `projects` pvo that can't be parsed, or that lists
+nothing, stops the protocol coming up on that vhost rather than showing it
+everything.
+
+To offer different canned views of the one sai-server, give sai-web two (or
+more) otherwise identical vhosts, each with its own `name`, its own unix socket
+`interface` path and its own `projects`, eg, `/var/run/sai` showing everything
+and `/var/run/sai-lws` showing only libwebsockets. Then point each front-end
+proxy's `/sai` mount at the socket for the view it should show. The web UI
+expects to be at `/sai`, so the views go on different front-end vhosts
+(hostnames) rather than on different paths of one, eg, on one front-end vhost
+
+```
+ { "mountpoint": "/sai",
+ "origin": "http://+/var/run/sai:/sai", ... }
+```
+
+and on another
+
+```
+ { "mountpoint": "/sai",
+ "origin": "http://+/var/run/sai-lws:/sai", ... }
+```
+
+Each sai-web vhost makes its own control link to sai-server, so they need the
+same `database` and `sockpath`.
+
## RSS feed of build events
sai-web serves a public RSS 2.0 feed of the latest 10 events at
diff --git a/etc-sai-EXAMPLE/web/conf.d/unixskt b/etc-sai-EXAMPLE/web/conf.d/unixskt
index 2a9e006..50efb3a 100644
--- a/etc-sai-EXAMPLE/web/conf.d/unixskt
+++ b/etc-sai-EXAMPLE/web/conf.d/unixskt
@@ -77,6 +77,22 @@
#
"sockpath": "/var/run/sai-websrv",
+ # Optional: the projects (repo names) this vhost shows,
+ # as a comma-separated list. Browsers, rss feeds,
+ # status badges and artifact links on this vhost then
+ # behave as if no other project exists. Without it,
+ # the vhost shows every project.
+ #
+ # To offer different canned views of the one sai-server,
+ # copy this whole vhost with its own "name", a different
+ # unix socket "interface" path and different "projects",
+ # keeping "database" and "sockpath" the same. Then
+ # point each front-end vhost's /sai proxy mount at the
+ # socket for the view it should show (the UI expects to
+ # be at /sai, so views go on different hostnames).
+ #
+ #"projects": "libwebsockets, sai",
+
# sai-web does NO auth of its own: no JWK, no JWT
# validation, no grant logic. It learns the login
# state from the x-lws-login-* headers the lws-login
diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt
index f14432a..2a6cc5e 100644
--- a/src/web/CMakeLists.txt
+++ b/src/web/CMakeLists.txt
@@ -10,6 +10,7 @@ set(SRCS
w-ws-server.c
w-ws-browser.c
w-findings.c
+ w-visible.c
../common/c-utils.c
../common/c-pool.c
../common/c-conf.c
diff --git a/src/web/w-artifact.c b/src/web/w-artifact.c
index 3732e61..8fb6b1d 100644
--- a/src/web/w-artifact.c
+++ b/src/web/w-artifact.c
@@ -90,6 +90,12 @@ saiw_get_blob(struct vhd *vhd, const char *url, sqlite3 **pdb,
return -1;
}
+ if (!saiw_event_visible(vhd, task_uuid)) {
+ lwsl_info("%s: task's project not shown on this vhost\n",
+ __func__);
+ return -1;
+ }
+
sai_task_uuid_to_event_uuid(event_uuid, task_uuid);
/* open the event-specific database object */
diff --git a/src/web/w-comms.c b/src/web/w-comms.c
index aa151b5..72eebcd 100644
--- a/src/web/w-comms.c
+++ b/src/web/w-comms.c
@@ -97,6 +97,10 @@ sai_get_head_status(struct vhd *vhd, const char *projname)
sai_event_t *e;
int state;
+ if (!saiw_project_visible(vhd, projname))
+ /* as if we never heard of it */
+ return -1;
+
/*
* The newest event of the named project decides it. Ad-hoc events
* are scratch builds seeded by an admin; they don't say anything
@@ -378,6 +382,16 @@ w_callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user,
sai_sqlite3_statement(vhd->pdb,
"CREATE TABLE IF NOT EXISTS saiweb_state (key TEXT PRIMARY KEY, val INTEGER);",
"create saiweb_state");
+
+ /*
+ * Which projects this vhost shows, if it doesn't show them
+ * all. Failing here leaves the vhost without a vhd, which
+ * serves nothing, rather than serving every project.
+ */
+ if (saiw_visible_init(vhd, in)) {
+ lws_struct_sq3_close(&vhd->pdb);
+ return -1;
+ }
{
sqlite3_stmt *stmt;
@@ -422,6 +436,7 @@ w_callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user,
goto passthru;
saiw_event_db_close_all_now(vhd);
lws_struct_sq3_close(&vhd->pdb);
+ saiw_visible_destroy(vhd);
goto passthru;
/*
diff --git a/src/web/w-findings.c b/src/web/w-findings.c
index 0a68310..77f0cf2 100644
--- a/src/web/w-findings.c
+++ b/src/web/w-findings.c
@@ -112,7 +112,8 @@ saiw_browser_send_findings(struct vhd *vhd, struct pss *pss)
int first = 1;
sqlite3 *pdb;
- if (!repo || !pool || saiw_findings_open(vhd, repo, pool, &pdb))
+ if (!repo || !pool || !saiw_project_visible(vhd, repo) ||
+ saiw_findings_open(vhd, repo, pool, &pdb))
continue;
p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
diff --git a/src/web/w-private.h b/src/web/w-private.h
index a6f067d..6d1b4b4 100644
--- a/src/web/w-private.h
+++ b/src/web/w-private.h
@@ -244,8 +244,18 @@ struct vhd {
lws_dll2_owner_t sqlite3_cache; /* sais_sqlite_cache_t */
lws_dll2_owner_t rss_waiters; /* pss held on long poll */
lws_dll2_owner_t tasklog_cache;
+
+ /*
+ * The projects this vhost shows, from the "projects" pvo; if empty,
+ * it shows all of them. See w-visible.c
+ */
+ lws_dll2_owner_t visible_projects;
+ struct lwsac *ac_visible;
};
+/* nonzero if a "projects" pvo limits the projects this vhost shows */
+#define saiw_restricted(_vhd) (!!(_vhd)->visible_projects.count)
+
typedef struct saiw_websrv {
struct lws_ss_handle *ss;
void *opaque_data;
@@ -379,6 +389,28 @@ saiw_browser_broadcast_queue_power_history(struct vhd *vhd, struct pss *pss);
extern const lws_struct_map_t lsm_schema_pcon_energy[];
+/* w-visible.c */
+
+int
+saiw_visible_init(struct vhd *vhd, void *pvo);
+
+void
+saiw_visible_destroy(struct vhd *vhd);
+
+int
+saiw_project_visible(struct vhd *vhd, const char *project);
+
+/* uuid may be an event's, or one of its tasks' */
+int
+saiw_event_visible(struct vhd *vhd, const char *uuid);
+
+/*
+ * " and ..." restricting a query on vhd->pdb's events table to the visible
+ * projects, or "" if they all are
+ */
+const char *
+saiw_visible_sql(struct vhd *vhd);
+
/* w-findings.c, for admins only */
int
diff --git a/src/web/w-rss.c b/src/web/w-rss.c
index 003d1d0..e0f7acd 100644
--- a/src/web/w-rss.c
+++ b/src/web/w-rss.c
@@ -249,27 +249,30 @@ static int
saiw_feed_query(struct vhd *vhd, struct pss *pss, sai_feed_t *f,
struct lwsac **ac)
{
+ struct lws_genhash_ctx hc;
+ sqlite3_stmt *sm = NULL;
+ uint8_t digest[32];
+ sai_feed_item_t *it;
+ const char *ref;
+ int rc, ret = 1;
+ char q[512];
+
+ memset(f, 0, sizeof(*f));
+
/*
* The project and branch come from the request url, so they must be
* bound rather than go via lws_struct_sq3_deserialize(), whose filter
- * text is spliced into the sql verbatim.
+ * text is spliced into the sql verbatim. Only the fixed fragment
+ * limiting it to the projects this vhost shows is spliced in.
*/
- static const char * const q =
+ lws_snprintf(q, sizeof(q),
"SELECT uuid, repo_name, ref, hash, created, state, "
"ifnull(adhoc,0), repo_fetchurl, weburl FROM events "
- "WHERE state != ?3 AND "
+ "WHERE state != ?3%s AND "
"(?1 IS NULL OR repo_name = ?1) AND "
"(?2 IS NULL OR ref = ?2 OR ref = 'refs/heads/' || ?2) AND "
"(?5 IS NULL OR repo_fetchurl = ?5) "
- "ORDER BY created DESC LIMIT ?4";
- struct lws_genhash_ctx hc;
- uint8_t digest[32];
- sqlite3_stmt *sm = NULL;
- sai_feed_item_t *it;
- const char *ref;
- int rc, ret = 1;
-
- memset(f, 0, sizeof(*f));
+ "ORDER BY created DESC LIMIT ?4", saiw_visible_sql(vhd));
if (lws_genhash_init(&hc, LWS_GENHASH_TYPE_SHA256))
return 1;
diff --git a/src/web/w-visible.c b/src/web/w-visible.c
new file mode 100644
index 0000000..09a4889
--- /dev/null
+++ b/src/web/w-visible.c
@@ -0,0 +1,230 @@
+/*
+ * Sai web - ./src/web/w-visible.c
+ *
+ * Copyright (C) 2019 - 2026 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
+ *
+ * Which projects a vhost shows
+ *
+ * A sai-web vhost can be given a "projects" pvo listing the projects (repo
+ * names) it shows, so the events of one sai-server can be offered as different
+ * canned views, eg, on differently-named unix sockets for the front-end proxy
+ * to mount. Without the pvo, the vhost shows every project.
+ *
+ * The list is kept twice from the one parse: as a list for checks done in C,
+ * and as a TEMP table on the vhd's own connection to the events db, so the sql
+ * listing events can be scoped by a fixed fragment. TEMP tables only exist on
+ * the connection that made them, so each vhost's list is private to it.
+ *
+ * Everything that tells browsers about, or acts on, a project goes through
+ * these, so that a restricted vhost behaves as if the other projects don't
+ * exist.
+ */
+
+#include <libwebsockets.h>
+#include <string.h>
+
+#include "w-private.h"
+
+#define SAIW_VISIBLE_SQL \
+ " and repo_name in (select name from temp.saiw_visible)"
+
+typedef struct saiw_visible_project {
+ lws_dll2_t list; /* vhd->visible_projects */
+
+ /* name over-allocated here */
+} saiw_visible_project_t;
+
+static const char *
+saiw_visible_name(const saiw_visible_project_t *vp)
+{
+ return (const char *)&vp[1];
+}
+
+static int
+saiw_visible_listed(struct vhd *vhd, const char *project)
+{
+ lws_start_foreach_dll(struct lws_dll2 *, p,
+ vhd->visible_projects.head) {
+ saiw_visible_project_t *vp = lws_container_of(p,
+ saiw_visible_project_t, list);
+
+ if (!strcmp(saiw_visible_name(vp), project))
+ return 1;
+ } lws_end_foreach_dll(p);
+
+ return 0;
+}
+
+int
+saiw_project_visible(struct vhd *vhd, const char *project)
+{
+ return !saiw_restricted(vhd) || saiw_visible_listed(vhd, project);
+}
+
+int
+saiw_event_visible(struct vhd *vhd, const char *uuid)
+{
+ char event_uuid[33];
+ sqlite3_stmt *sm;
+ int r = 0;
+
+ if (!saiw_restricted(vhd))
+ return 1;
+
+ /* a task uuid starts with its event's uuid */
+ if (strlen(uuid) < 32)
+ return 0;
+ sai_task_uuid_to_event_uuid(event_uuid, uuid);
+
+ if (sqlite3_prepare_v2(vhd->pdb, "SELECT 1 FROM events WHERE uuid = ?"
+ SAIW_VISIBLE_SQL, -1, &sm, NULL) != SQLITE_OK) {
+ lwsl_err("%s: prepare failed: %s\n", __func__,
+ sqlite3_errmsg(vhd->pdb));
+ return 0;
+ }
+
+ sqlite3_bind_text(sm, 1, event_uuid, -1, SQLITE_TRANSIENT);
+ r = sqlite3_step(sm) == SQLITE_ROW;
+ sqlite3_finalize(sm);
+
+ return r;
+}
+
+const char *
+saiw_visible_sql(struct vhd *vhd)
+{
+ return saiw_restricted(vhd) ? SAIW_VISIBLE_SQL : "";
+}
+
+static int
+saiw_visible_add(struct vhd *vhd, const char *name)
+{
+ saiw_visible_project_t *vp;
+ size_t len = strlen(name);
+ sqlite3_stmt *sm;
+
+ if (!len || saiw_visible_listed(vhd, name))
+ /* listed twice is the same as once */
+ return 0;
+
+ vp = lwsac_use_zero(&vhd->ac_visible, sizeof(*vp) + len + 1, 512);
+ if (!vp)
+ return 1;
+ memcpy((char *)&vp[1], name, len + 1);
+
+ if (sqlite3_prepare_v2(vhd->pdb, "INSERT INTO temp.saiw_visible "
+ "(name) VALUES (?)", -1, &sm, NULL) != SQLITE_OK)
+ return 1;
+ sqlite3_bind_text(sm, 1, name, -1, SQLITE_TRANSIENT);
+ if (sai_sqlite3_step_done(vhd->pdb, sm, "list visible project"))
+ return 1;
+
+ lws_dll2_add_tail(&vp->list, &vhd->visible_projects);
+
+ return 0;
+}
+
+/*
+ * Parse the "projects" pvo, a comma-separated list, eg,
+ *
+ * "projects": "libwebsockets, sai",
+ *
+ * Whitespace around the names is ignored. Names may use A-Z a-z 0-9 _ - and .
+ *
+ * Call after vhd->pdb is open. Returns 0 if OK, including when there is no
+ * pvo. If there is one but it can't be understood or lists nothing, it fails
+ * rather than show this vhost everything.
+ */
+
+int
+saiw_visible_init(struct vhd *vhd, void *pvo)
+{
+ struct lws_tokenize ts;
+ const char *list;
+ char name[65];
+
+ lws_dll2_owner_clear(&vhd->visible_projects);
+
+ if (lws_pvo_get_str(pvo, "projects", &list))
+ return 0;
+
+ if (sai_sqlite3_statement(vhd->pdb, "CREATE TEMP TABLE IF NOT EXISTS "
+ "saiw_visible (name TEXT PRIMARY KEY);",
+ "create saiw_visible"))
+ return 1;
+
+ lws_tokenize_init(&ts, list, LWS_TOKENIZE_F_COMMA_SEP_LIST |
+ LWS_TOKENIZE_F_MINUS_NONTERM |
+ LWS_TOKENIZE_F_DOT_NONTERM |
+ LWS_TOKENIZE_F_NO_INTEGERS |
+ LWS_TOKENIZE_F_NO_FLOATS);
+
+ do {
+ ts.e = (int8_t)lws_tokenize(&ts);
+
+ switch (ts.e) {
+ case LWS_TOKZE_ENDED:
+ break;
+
+ case LWS_TOKZE_TOKEN:
+ if (lws_tokenize_cstr(&ts, name, sizeof(name))) {
+ lwsl_err("%s: project name too long\n",
+ __func__);
+ goto bail;
+ }
+ if (saiw_visible_add(vhd, name))
+ goto bail;
+ break;
+
+ case LWS_TOKZE_DELIMITER:
+ /* COMMA_SEP_LIST makes sure commas are where they go */
+ if (*ts.token == ',')
+ break;
+ goto bail;
+
+ default:
+ /* eg, missing or doubled commas, or bad characters */
+ goto bail;
+ }
+ } while (ts.e > 0);
+
+ if (!saiw_restricted(vhd)) {
+ lwsl_err("%s: \"projects\" pvo lists no projects\n", __func__);
+ return 1;
+ }
+
+ lwsl_notice("%s: vhost %s shows %u projects: %s\n", __func__,
+ lws_get_vhost_name(vhd->vhost),
+ (unsigned int)vhd->visible_projects.count, list);
+
+ return 0;
+
+bail:
+ lwsl_err("%s: unable to use \"projects\" pvo '%s'\n", __func__, list);
+ saiw_visible_destroy(vhd);
+
+ return 1;
+}
+
+void
+saiw_visible_destroy(struct vhd *vhd)
+{
+ /* the TEMP table goes with the db connection */
+ lws_dll2_owner_clear(&vhd->visible_projects);
+ lwsac_free(&vhd->ac_visible);
+}
diff --git a/src/web/w-ws-browser.c b/src/web/w-ws-browser.c
index 48daf3d..98e0c18 100644
--- a/src/web/w-ws-browser.c
+++ b/src/web/w-ws-browser.c
@@ -533,9 +533,13 @@ saiw_pss_schedule_taskinfo(struct pss *pss, const char *task_uuid, int logsub, i
goto bail;
}
- /* open the event-specific database object */
+ /*
+ * Open the event-specific database object... a task in a project this
+ * vhost doesn't show is treated the same as one whose event is gone
+ */
- if (sai_event_db_ensure_open(pss->vhd->context, &pss->vhd->sqlite3_cache,
+ if (!saiw_event_visible(pss->vhd, event_uuid) ||
+ sai_event_db_ensure_open(pss->vhd->context, &pss->vhd->sqlite3_cache,
pss->vhd->sqlite3_path_lhs, event_uuid, 0, &pdb)) {
uint8_t buf[LWS_PRE + 128];
int n1 = lws_snprintf((char *)buf + LWS_PRE, sizeof(buf) - LWS_PRE,
@@ -877,6 +881,51 @@ saiw_event_state_change(struct vhd *vhd, const char *event_uuid)
}
/*
+ * Nonzero if the browser message, which has decoded into dest, is about
+ * something in a project this vhost shows, or about nothing project-specific
+ * at all (eg, builders). Taskinfo and eventinfo don't need checking here,
+ * the replies to them are already limited to the visible projects.
+ */
+
+static int
+saiw_rx_visible(struct vhd *vhd, int schema_idx, void *dest)
+{
+ switch (schema_idx) {
+ case SAIM_WS_BROWSER_RX_TASKRESET:
+ case SAIM_WS_BROWSER_RX_TASKREMOVEALLTRIES:
+ case SAIM_WS_BROWSER_RX_TASKREBUILDLASTSTEP:
+ case SAIM_WS_BROWSER_RX_EVENTRESET:
+ case SAIM_WS_BROWSER_RX_EVENTDELETE:
+ case SAIM_WS_BROWSER_RX_CLONEINFO:
+ return saiw_event_visible(vhd,
+ ((sai_browse_rx_evinfo_t *)dest)->event_hash);
+ case SAIM_WS_BROWSER_RX_TASKCANCEL:
+ return saiw_event_visible(vhd, ((sai_cancel_t *)dest)->task_uuid);
+ case SAIM_WS_BROWSER_RX_PLATRESET:
+ return saiw_event_visible(vhd,
+ ((sai_browse_rx_platreset_t *)dest)->event_uuid);
+ case SAIM_WS_BROWSER_RX_OPENSHELL:
+ return saiw_event_visible(vhd,
+ ((sai_openshell_t *)dest)->task_uuid);
+ case SAIM_WS_BROWSER_RX_CLOSESHELL:
+ return saiw_event_visible(vhd,
+ ((sai_closeshell_t *)dest)->task_uuid);
+ case SAIM_WS_BROWSER_RX_PTYDATA:
+ return saiw_event_visible(vhd,
+ ((sai_ptydata_t *)dest)->task_uuid);
+ case SAIM_WS_BROWSER_RX_TASKCLONE:
+ return saiw_event_visible(vhd,
+ ((sai_browse_rx_taskclone_t *)dest)->seed_uuid);
+ case SAIM_WS_BROWSER_RX_FINDINGGET:
+ case SAIM_WS_BROWSER_RX_FINDINGSET:
+ return saiw_project_visible(vhd,
+ ((sai_findingset_t *)dest)->repo);
+ }
+
+ return 1;
+}
+
+/*
* sai-web has sent us a request for either overview, or data on a specific
* task
*/
@@ -948,6 +997,16 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf,
goto soft_error;
}
+ /*
+ * Nobody, admin or not, can act on projects this vhost doesn't show:
+ * for it, they don't exist. Drop it, the UI never sends these.
+ */
+ if (!saiw_rx_visible(vhd, a.top_schema_index, a.dest)) {
+ lwsl_notice("%s: dropping schema %d for a project not shown "
+ "on this vhost\n", __func__, a.top_schema_index);
+ goto ok;
+ }
+
switch (a.top_schema_index) {
case SAIM_WS_BROWSER_RX_BUILDER_VISIBILITY:
@@ -1049,16 +1108,18 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf,
sqlite3_stmt *stmt = NULL;
uint8_t buf[LWS_PRE + 4096], *start = buf + LWS_PRE,
*p = start, *end = buf + sizeof(buf);
- char esc[96];
+ char esc[96], q[256];
/* first_elem = first array entry (omit leading comma);
* sent_any = have we already tx'd a ws fragment of this msg */
int first_elem = 1, sent_any = 0, rc;
- if (sqlite3_prepare_v2(vhd->pdb,
+ lws_snprintf(q, sizeof(q),
"SELECT repo_name, MAX(created) AS mc FROM events "
- "WHERE state != ? GROUP BY repo_name "
- "ORDER BY mc DESC",
- -1, &stmt, NULL) != SQLITE_OK) {
+ "WHERE state != ?%s GROUP BY repo_name "
+ "ORDER BY mc DESC", saiw_visible_sql(vhd));
+
+ if (sqlite3_prepare_v2(vhd->pdb, q, -1, &stmt, NULL) !=
+ SQLITE_OK) {
lwsl_notice("%s: projlist prepare failed\n", __func__);
goto soft_error;
}
@@ -1127,7 +1188,7 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf,
sqlite3_stmt *stmt = NULL;
uint8_t buf[LWS_PRE + 4096], *start = buf + LWS_PRE,
*p = start, *end = buf + sizeof(buf);
- char esc[96], pesc[96];
+ char esc[96], pesc[96], q[512];
int first_elem = 1, sent_any = 0, rc;
lws_dll2_owner_clear(&owner);
@@ -1139,16 +1200,23 @@ saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf,
*/
lws_sql_purify(pesc, bl->project, sizeof(pesc) - 1);
- if (sqlite3_prepare_v2(vhd->pdb,
+ /*
+ * A project this vhost doesn't show has no branches; the
+ * subquery only looks at the project the outer one found.
+ */
+ lws_snprintf(q, sizeof(q),
"SELECT ref, "
"(SELECT e2.state FROM events e2 "
" WHERE e2.ref = events.ref "
" AND e2.repo_name = ? AND e2.state != ? "
" ORDER BY e2.created DESC LIMIT 1) AS ls "
"FROM events "
- "WHERE state != ? AND repo_name = ? "
+ "WHERE state != ? AND repo_name = ?%s "
"GROUP BY ref ORDER BY MAX(created) DESC",
- -1, &stmt, NULL) != SQLITE_OK) {
+ saiw_visible_sql(vhd));
+
+ if (sqlite3_prepare_v2(vhd->pdb, q, -1, &stmt, NULL) !=
+ SQLITE_OK) {
lwsl_notice("%s: branchlist prepare failed\n",
__func__);
goto soft_error;
@@ -1795,7 +1863,7 @@ saiw_browser_queue_overview(struct vhd *vhd, struct pss *pss)
{
char buf[4096 + LWS_PRE], *start = buf + LWS_PRE, *p = start,
*end = buf + sizeof(buf);
- char esc[256], filt[256], subsequent;
+ char esc[256], filt[448], subsequent;
struct lwsac *task_ac = NULL, *ac = NULL;
lws_dll2_owner_t task_owner, owner;
unsigned int task_index = 0;
@@ -1840,7 +1908,7 @@ saiw_browser_queue_overview(struct vhd *vhd, struct pss *pss)
}
if (ev_created > 0) {
unsigned int events_newer = 0;
- lws_snprintf(q, sizeof(q), "SELECT COUNT(*) FROM events WHERE state != %d AND created > %llu", SAIES_DELETED, (unsigned long long)ev_created);
+ lws_snprintf(q, sizeof(q), "SELECT COUNT(*) FROM events WHERE state != %d AND created > %llu%s", SAIES_DELETED, (unsigned long long)ev_created, saiw_visible_sql(vhd));
if (sqlite3_prepare_v2(vhd->pdb, q, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
events_newer = (unsigned int)sqlite3_column_int(stmt, 0);
@@ -1852,6 +1920,12 @@ saiw_browser_queue_overview(struct vhd *vhd, struct pss *pss)
pss->resolved_task_offset = 1;
}
+ /*
+ * The vhost may only show some projects: that clause goes right after
+ * the first one, so if anything is going to be truncated off the end
+ * of filt, it isn't that.
+ */
+
if (pss->specific_project[0]) {
/*
* gitohashi /git/<project> URL-locked mode: lock to that one
@@ -1859,8 +1933,8 @@ saiw_browser_queue_overview(struct vhd *vhd, struct pss *pss)
*/
lws_sql_purify(esc, pss->specific_project, sizeof(esc) - 1);
lws_snprintf(filt, sizeof(filt),
- " and state != %d and repo_name='%s'",
- SAIES_DELETED, esc);
+ " and state != %d%s and repo_name='%s'",
+ SAIES_DELETED, saiw_visible_sql(vhd), esc);
n = -1;
} else {
size_t fl;
@@ -1870,8 +1944,8 @@ saiw_browser_queue_overview(struct vhd *vhd, struct pss *pss)
* " and ..." clauses here; the COUNT query below skips the
* leading " and " with filt + 5 and uses the rest verbatim.
*/
- lws_snprintf(filt, sizeof(filt), " and state != %d",
- SAIES_DELETED);
+ lws_snprintf(filt, sizeof(filt), " and state != %d%s",
+ SAIES_DELETED, saiw_visible_sql(vhd));
/*
* A specific event selection (com.warmcat.sai.eventinfo from
@@ -1915,7 +1989,7 @@ saiw_browser_queue_overview(struct vhd *vhd, struct pss *pss)
unsigned int total_events = 0;
{
- char q[256];
+ char q[64 + sizeof(filt)];
sqlite3_stmt *stmt;
lws_snprintf(q, sizeof(q), "SELECT COUNT(*) FROM events WHERE %s", filt + 5);
if (sqlite3_prepare_v2(vhd->pdb, q, -1, &stmt, NULL) == SQLITE_OK) {
diff --git a/src/web/w-ws-server.c b/src/web/w-ws-server.c
index 5d7a2c3..859b9cc 100644
--- a/src/web/w-ws-server.c
+++ b/src/web/w-ws-server.c
@@ -37,6 +37,32 @@ static lws_struct_map_t lsm_websrv_evinfo[] = {
};
/*
+ * sai-server's periodic list of the tasks that are building, and how
+ * recently each one produced logs. We only need to decode it to remove the
+ * tasks a vhost doesn't show, see saiw_reissue_activity()
+ */
+
+typedef struct saiw_activity {
+ lws_dll2_t list;
+ char uuid[65];
+ int cat;
+} saiw_activity_t;
+
+typedef struct saiw_activities {
+ lws_dll2_owner_t activity;
+} saiw_activities_t;
+
+static const lws_struct_map_t lsm_websrv_activity[] = {
+ LSM_CARRAY (saiw_activity_t, uuid, "uuid"),
+ LSM_SIGNED (saiw_activity_t, cat, "cat"),
+};
+
+static const lws_struct_map_t lsm_websrv_activities[] = {
+ LSM_LIST (saiw_activities_t, activity, saiw_activity_t, list,
+ NULL, lsm_websrv_activity, "activity"),
+};
+
+/*
* (Structs and maps removed - now in common/include/private.h and common/struct-metadata.c)
*/
@@ -52,7 +78,7 @@ const lws_struct_map_t lsm_schema_json_map[] = {
/* shares struct */ "sai-tasklogs"),
LSM_SCHEMA (sai_load_report_t, NULL, lsm_load_report_members,
"com.warmcat.sai.loadreport"),
- LSM_SCHEMA (sai_browse_rx_evinfo_t, NULL, lsm_websrv_evinfo,
+ LSM_SCHEMA (saiw_activities_t, NULL, lsm_websrv_activities,
"com.warmcat.sai.taskactivity"),
LSM_SCHEMA (sai_build_metric_t, NULL, lsm_build_metric,
"com.warmcat.sai.build-metric"),
@@ -146,6 +172,98 @@ saiw_pty_accum(saiw_websrv_t *m, const uint8_t *frag, size_t len)
return 0;
}
+/*
+ * A vhost that only shows some projects can't pass on messages that mention
+ * tasks in the others as they came. Instead it serializes the decoded
+ * object, with those tasks removed, from the same schema and queues it to its
+ * browsers (only ones showing builders, if builder_info).
+ */
+
+static void
+saiw_reissue_to_browsers(struct vhd *vhd, int schema_idx, void *obj,
+ int builder_info)
+{
+ uint8_t buf[LWS_PRE + 2048], *start = buf + LWS_PRE;
+ lws_struct_serialize_t *js;
+ int r, fi = 1;
+ size_t w;
+
+ js = lws_struct_json_serialize_create(&lsm_schema_json_map[schema_idx],
+ 1, 0, obj);
+ if (!js)
+ return;
+
+ do {
+ r = (int)lws_struct_json_serialize(js, start,
+ sizeof(buf) - LWS_PRE, &w);
+ if (r == LSJS_RESULT_ERROR) {
+ lwsl_err("%s: unable to serialize schema %d\n",
+ __func__, schema_idx);
+ break;
+ }
+
+ lws_start_foreach_dll(struct lws_dll2 *, p, vhd->browsers.head) {
+ struct pss *pss = lws_container_of(p, struct pss, same);
+
+ if (!builder_info ||
+ (!pss->is_gitohashi && pss->wants_builder_info))
+ saiw_ws_browser_queue_REQUIRES_LWS_PRE(pss,
+ start, w, lws_write_ws_flags(
+ LWS_WRITE_TEXT, fi,
+ r == LSJS_RESULT_FINISH));
+ } lws_end_foreach_dll(p);
+
+ fi = 0;
+ } while (r == LSJS_RESULT_CONTINUE);
+
+ lws_struct_json_serialize_destroy(&js);
+}
+
+static void
+saiw_reissue_activity(struct vhd *vhd, saiw_activities_t *acts)
+{
+ char last[33] = "";
+ int last_vis = 0;
+
+ /* the list comes grouped by event, so check each event once */
+
+ lws_start_foreach_dll_safe(struct lws_dll2 *, p, p1,
+ acts->activity.head) {
+ saiw_activity_t *act = lws_container_of(p, saiw_activity_t,
+ list);
+
+ if (strncmp(act->uuid, last, 32)) {
+ last_vis = saiw_event_visible(vhd, act->uuid);
+ lws_strnncpy(last, act->uuid, 32, sizeof(last));
+ }
+
+ if (!last_vis)
+ lws_dll2_remove(&act->list);
+ } lws_end_foreach_dll_safe(p, p1);
+
+ saiw_reissue_to_browsers(vhd, SAIS_WS_WEBSRV_RX_TASKACTIVITY, acts, 0);
+}
+
+static void
+saiw_reissue_loadreport(struct vhd *vhd, sai_load_report_t *lr)
+{
+ /*
+ * The builders and their load are shared by every project, only the
+ * tasks they're building belong to one
+ */
+
+ lws_start_foreach_dll_safe(struct lws_dll2 *, p, p1,
+ lr->active_tasks.head) {
+ sai_active_task_info_t *ati = lws_container_of(p,
+ sai_active_task_info_t, list);
+
+ if (!saiw_project_visible(vhd, ati->repo_name))
+ lws_dll2_remove(&ati->list);
+ } lws_end_foreach_dll_safe(p, p1);
+
+ saiw_reissue_to_browsers(vhd, SAIS_WS_WEBSRV_RX_LOADREPORT, lr, 1);
+}
+
static int
saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
{
@@ -192,7 +310,13 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
switch (m->a.top_schema_index) {
case SAIS_WS_WEBSRV_RX_TASKACTIVITY:
{
- uint8_t *tmp = malloc(LWS_PRE + rem);
+ uint8_t *tmp;
+
+ if (saiw_restricted(vhd))
+ /* reissued filtered when complete */
+ break;
+
+ tmp = malloc(LWS_PRE + rem);
if (tmp) {
memcpy(tmp + LWS_PRE, p, rem);
saiw_ws_broadcast_browsers_REQUIRES_LWS_PRE(vhd, tmp + LWS_PRE, rem,
@@ -213,7 +337,13 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
break;
case SAIS_WS_WEBSRV_RX_LOADREPORT:
{
- uint8_t *tmp = malloc(LWS_PRE + rem);
+ uint8_t *tmp;
+
+ if (saiw_restricted(vhd))
+ /* reissued filtered when complete */
+ break;
+
+ tmp = malloc(LWS_PRE + rem);
if (tmp) {
memcpy(tmp + LWS_PRE, p, rem);
lws_start_foreach_dll(struct lws_dll2 *, pt, vhd->browsers.head) {
@@ -239,10 +369,26 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
sai_browse_rx_evinfo_t *ei;
+ /*
+ * This vhost's browsers hear nothing about events in projects
+ * it doesn't show
+ */
+ if ((m->a.top_schema_index == SAIS_WS_WEBSRV_RX_TASKCHANGE ||
+ m->a.top_schema_index == SAIS_WS_WEBSRV_RX_EVENTCHANGE) &&
+ m->a.dest && !saiw_event_visible(vhd,
+ ((sai_browse_rx_evinfo_t *)m->a.dest)->event_hash))
+ goto cleanup_parse_allocs;
+
switch (m->a.top_schema_index) {
+ case SAIS_WS_WEBSRV_RX_TASKACTIVITY:
+ if (saiw_restricted(vhd)) {
+ if (m->a.dest)
+ saiw_reissue_activity(vhd, m->a.dest);
+ break;
+ }
+ /* fallthru */
case SAIS_WS_WEBSRV_RX_TASKCHANGE:
case SAIS_WS_WEBSRV_RX_EVENTCHANGE:
- case SAIS_WS_WEBSRV_RX_TASKACTIVITY:
{
uint8_t *tmp = malloc(LWS_PRE + consumed);
if (tmp) {
@@ -293,7 +439,15 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
}
case SAIS_WS_WEBSRV_RX_LOADREPORT:
{
- uint8_t *tmp = malloc(LWS_PRE + consumed);
+ uint8_t *tmp;
+
+ if (saiw_restricted(vhd)) {
+ if (m->a.dest)
+ saiw_reissue_loadreport(vhd, m->a.dest);
+ break;
+ }
+
+ tmp = malloc(LWS_PRE + consumed);
if (tmp) {
memcpy(tmp + LWS_PRE, p, consumed);
lws_start_foreach_dll(struct lws_dll2 *, pt, vhd->browsers.head) {
|