Warmcat homepage andy@warmcat.com
libwebsockets
{"schema":"libjg2-1", "vpath":"/git/", "avatar":"/git/avatar/", "alang":"", "gen_ut":1749527399, "reponame":"sai", "desc":"Sai lightweight distributed CI", "owner": { "name": "Andy Green", "email": "andy@warmcat.com", "md5": "c50933ca2aa61e0fe2c43d46bb6b59cb" },"url":"https://warmcat.com/repo/sai", "f":3, "items": [ {"schema":"libjg2-1", "cid":"b5a4cc741da2df53b20083e2f43e4300", "commit": {"type":"commit", "time": 1606657513, "time_ofs": 0, "oid_tree": { "oid": "cef0bce8ac54642e38d8fb6453616637096b117f", "alias": []}, "oid":{ "oid": "81dfe02ce7cffe733082b7be8d0a25269936cc69", "alias": []}, "msg": "Refactor master into server and web daemons", "sig_commit": { "git_time": { "time": 1606657513, "offset": 0 }, "name": "Andy Green", "email": "andy@warmcat.com", "md5": "c50933ca2aa61e0fe2c43d46bb6b59cb" }, "sig_author": { "git_time": { "time": 1599050348, "offset": 60 }, "name": "Andy Green", "email": "andy@warmcat.com", "md5": "c50933ca2aa61e0fe2c43d46bb6b59cb" }}, "body": "Refactor master into server and web daemons\n\nVarious improvements with web presentation and gitohashi integration\n" , "diff": "diff --git a/.sai.json b/.sai.json\nindex e284804..eaefd4d 100644\n--- a/.sai.json\n+++ b/.sai.json\n@@ -22,13 +22,13 @@\n \t},\n \n \t\u0022configurations\u0022: {\n-\t\t\u0022master+builder\u0022: {\n+\t\t\u0022server+builder\u0022: {\n \t\t\t\u0022cmake\u0022:\t\u0022\u0022,\n \t\t\t\u0022cpack\u0022: \u0022\u0026\u0026 cpack -C DEBUG $SAI_CPACK\u0022,\n \t\t\t\u0022artifacts\u0022:\t\u0022build/sai-*.deb, build/sai-*.rpm\u0022\n \t\t},\n \t\t\u0022builder-only\u0022: {\n-\t\t\t\u0022cmake\u0022:\t\u0022-DSAI_MASTER\u003d0 \u0022,\n+\t\t\t\u0022cmake\u0022:\t\u0022-DSAI_SERVER\u003d0 \u0022,\n \t\t\t\u0022platforms\u0022:\t\u0022windows-10/x86_64-amd/msvc, netbsd-OSX-catalina/x86_64-intel-i3/llvm\u0022,\n \t\t\t\u0022cpack\u0022: \u0022\u0026\u0026 cpack -C DEBUG $SAI_CPACK\u0022,\n \t\t\t\u0022artifacts\u0022:\t\u0022build/sai-*.deb, build/sai-*.rpm, build/sai-*.7z\u0022\ndiff --git a/CMakeLists.txt b/CMakeLists.txt\nindex 19627ef..812536a 100644\n--- a/CMakeLists.txt\n+++ b/CMakeLists.txt\n@@ -1,6 +1,9 @@\n project(sai C)\n cmake_minimum_required(VERSION 2.8)\n+find_package(libwebsockets CONFIG REQUIRED)\n+list(APPEND CMAKE_MODULE_PATH ${LWS_CMAKE_DIR})\n include(CheckCSourceCompiles)\n+include(LwsCheckRequirements)\n \n set(CMAKE_SKIP_RPATH 1)\n \n@@ -43,16 +46,16 @@ set(CPACK_DEB_COMPONENT_INSTALL 1)\n set(CPACK_DEBIAN_FILE_NAME \u0022DEB-DEFAULT\u0022)\n set(CPACK_DEBIAN_PACKAGE_COMPONENT 1)\n set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)\n-set(CPACK_DEBIAN_MASTER_PACKAGE_NAME \u0022sai-master\u0022)\n+set(CPACK_DEBIAN_SERVER_PACKAGE_NAME \u0022sai-server\u0022)\n set(CPACK_DEBIAN_BUILDER_PACKAGE_NAME \u0022sai-builder\u0022)\n \n-option(SAI_MASTER \u0022Build the master / server part\u0022 ON)\n-option(SAI_BUILDER \u0022Build the builder / client part\u0022 ON)\n+option(SAI_SERVER \u0022Build the server + web part\u0022 ON)\n+option(SAI_BUILDER \u0022Build the builder part\u0022 ON)\n \n set(LWS_OPENSSL_LIBRARIES CACHE PATH \u0022Path to the OpenSSL library\u0022)\n set(LWS_OPENSSL_INCLUDE_DIRS CACHE PATH \u0022Path to the OpenSSL include directory\u0022)\n \n-if (NOT SAI_MASTER AND NOT SAI_BUILDER)\n+if (NOT SAI_SERVER AND NOT SAI_BUILDER)\n \tmessage(FATAL_ERROR \u0022Have to build both or one or the other\u0022)\n endif()\n \n@@ -118,69 +121,10 @@ CHECK_C_SOURCE_COMPILES(\u0022#include \u003clibwebsockets.h\u003e\u005cnint\n \t\t0;\u005cn#else\u005cn fail;\u005cn#endif\u005cn return 0;\u005cn}\u005cn\u0022 HAS_LIBCAP)\n \n \n-# If we are being built as part of lws, confirm current build config supports\n-# reqconfig, else skip building ourselves.\n-#\n-# If we are being built externally, confirm installed lws was configured to\n-# support reqconfig, else error out with a helpful message about the problem.\n-#\n-MACRO(require_lws_config reqconfig _val result)\n-\n-\tif (DEFINED ${reqconfig})\n-\tif (${reqconfig})\n-\t\tset (rq 1)\n-\telse()\n-\t\tset (rq 0)\n-\tendif()\n-\telse()\n-\t\tset(rq 0)\n-\tendif()\n-\n-\tif (${_val} EQUAL ${rq})\n-\t\tset(SAME 1)\n-\telse()\n-\t\tset(SAME 0)\n-\tendif()\n-\n-\tif (LWS_WITH_MINIMAL_EXAMPLES AND NOT ${SAME})\n-\t\tif (${_val})\n-\t\t\tmessage(\u0022${SAMP}: skipping as lws being built without ${reqconfig}\u0022)\n-\t\telse()\n-\t\t\tmessage(\u0022${SAMP}: skipping as lws built with ${reqconfig}\u0022)\n-\t\tendif()\n-\t\tset(${result} 0)\n-\telse()\n-\t\tif (LWS_WITH_MINIMAL_EXAMPLES)\n-\t\t\tset(MET ${SAME})\n-\t\telse()\n-\t\t\tCHECK_C_SOURCE_COMPILES(\u0022#include \u003clibwebsockets.h\u003e\u005cnint main(void) {\u005cn#if defined(${reqconfig})\u005cn return 0;\u005cn#else\u005cn fail;\u005cn#endif\u005cn return 0;\u005cn}\u005cn\u0022 HAS_${reqconfig})\n-\t\t\tif (NOT DEFINED HAS_${reqconfig} OR NOT HAS_${reqconfig})\n-\t\t\t\tset(HAS_${reqconfig} 0)\n-\t\t\telse()\n-\t\t\t\tset(HAS_${reqconfig} 1)\n-\t\t\tendif()\n-\t\t\tif ((HAS_${reqconfig} AND ${_val}) OR (NOT HAS_${reqconfig} AND NOT ${_val}))\n-\t\t\t\tset(MET 1)\n-\t\t\telse()\n-\t\t\t\tset(MET 0)\n-\t\t\tendif()\n-\t\tendif()\n-\t\tif (NOT MET)\n-\t\t\tif (${_val})\n-\t\t\t\tmessage(FATAL_ERROR \u0022This project requires lws must have been configured with ${reqconfig}\u0022)\n-\t\t\telse()\n-\t\t\t\tmessage(FATAL_ERROR \u0022Lws configuration of ${reqconfig} is incompatible with this project\u0022)\n-\t\t\tendif()\n-\t\tendif()\n-\t\n-\tendif()\n-ENDMACRO()\n-\n-#set(CMAKE_REQUIRED_INCLUDES ${SAI_LWS_INC_PATH})\n-\n set(requirements 1)\n require_lws_config(LWS_ROLE_H1\t\t\t\t1 requirements)\n require_lws_config(LWS_ROLE_WS\t\t\t\t1 requirements)\n+require_lws_config(LWS_WITH_JOSE\t\t\t1 requirements)\n \n if (requirements)\n \n@@ -191,8 +135,9 @@ if (requirements)\n \t\tset(CMAKE_C_FLAGS \u0022-Wall -Wsign-compare -Wignored-qualifiers -Wtype-limits -Wuninitialized -Werror -Wundef ${CMAKE_C_FLAGS}\u0022 )\n \tendif()\n \n-\tif (SAI_MASTER)\n-\t\tadd_subdirectory(src/master)\n+\tif (SAI_SERVER)\n+\t\tadd_subdirectory(src/server)\n+\t\tadd_subdirectory(src/web)\n \tendif()\n \tif (SAI_BUILDER)\n \t\tadd_subdirectory(src/builder)\ndiff --git a/README.md b/README.md\nindex add3878..76e2aa4 100644\n--- a/README.md\n+++ b/README.md\n@@ -10,11 +10,12 @@ run on an RPi3-class 'buddy' to flash and collect results (eg, over serial, or\n gpio) from even smaller targets.\n \n A self-assembling constellation of Sai Builder clients make their own\n-connections to one or more Sai Masters, who then receive hook notifications,\n+connections to one or more Sai Servers, who then receive hook notifications,\n read JSON from the project describing what set of build variations and tests it\n should run on which platforms, and distributes work concurrently over idle\n-builders that have the required environment, and logs results at the master\n-accessible by a web interface.\n+builders that have the required environment. A parallel Sai-web server is\n+available usually on :443 or via a proxy to provide a live web / websockets\n+interface with synamic updates and realtime build logs.\n \n ![sai overview](./READMEs/sai-overview.png)\n \n@@ -24,17 +25,17 @@ accessible by a web interface.\n platform they want to provide builds for (ie, inside a systemd-nspawn or VM).\n \n - The `sai-builder` daemons maintain nailed-up outgoing client wss connections\n- to a central `sai-master`, which manages the ad-hoc collection of builders\n+ to a central `sai-server`, which manages the ad-hoc collection of builders\n it finds, and provides a web UI (http/2 and wss2).\n \n ![sai git flow](./READMEs/sai-ov2.png)\n \n - When a git repo that wants sai tests is updated, a push hook performs a POST\n- notiftying the Sai master of a new push event which creates an entry in an\n- event sqlite3 database. (The master does not need to access the repo that\n- sends the git notification). The hook sends the master information about the\n+ notiftying the Sai server of a new push event which creates an entry in an\n+ event sqlite3 database. (The server does not need to access the repo that\n+ sends the git notification). The hook sends the server information about the\n push and the revision of `.sai.json` from the new commit in the POST body...\n- the master parses that JSON to fill an sqlite3 database with tasks and\n+ the server parses that JSON to fill an sqlite3 database with tasks and\n commandline options for the build on platforms mentioned in `.sai.json`.\n Pushes to branches beginning with `_` are ignored by Sai, even if they have\n a valid `.sai.json`; this allows casual sharing of trees via the same git\n@@ -46,21 +47,21 @@ accessible by a web interface.\n as a starting point for git dramatically reduces the time compared to a full\n git fetch.\n \n- - The master hands out waiting tasks on connected idle builders that offer the\n+ - The server hands out waiting tasks on connected idle builders that offer the\n requested platforms, which build them concurrently. The builders may be\n inside a protected network along with the repos they connect to; both the\n builders and the repo only make outgoing https or wss connections to the\n- master.\n+ server.\n \n - On the builders, result channels (like stdout, stderr, eventually others like\n- `/dev/acm0`) with logs and results are streamed back to the master over a wss\n+ `/dev/acm0`) with logs and results are streamed back to the server over a wss\n link as the build proceeds, and are stored in an event-specific sqlite3\n database for scalability.\n \n - Builders can run CTest or other tests after the build and collect the\n results (CTest has the advantages it's lightweight and crossplatform).\n \n- - The master makes human readable current and historical results available\n+ - The server makes human readable current and historical results available\n in realtime over https web interface\n \n - One `sai-builder` daemon can be configured to offer multiple instances of\n@@ -73,10 +74,10 @@ accessible by a web interface.\n specified in a configuration file. One sai-jig instance can separately\n manage external gpio sequencing for multiple test targets.\n \n- - Largely the master is automatic, driven by git hook notifications over\n+ - Largely the server is automatic, driven by git hook notifications over\n HTTP and the UI is read-only. However there are some privileged UI operations\n like deleting a whole event, or redoing whole events or individual tasks.\n- For these, if the browser has an authentic JWT signed by the master, it can\n+ For these, if the browser has an authentic JWT signed by the server, it can\n see and operate these privileged controls.\n \n ## Build flow and support for embedded\n@@ -165,7 +166,7 @@ or other scripts are able to directly write to the device.\n \n The `sai-builder` instance opens three local listening Unix Domain Sockets for\n every platform, these accept raw data which is turned into logs on the\n-respective logging channel and passed up to the `sai-master` for storage and\n+respective logging channel and passed up to the `sai-server` for storage and\n display like the other logs.\n \n The paths of these \u0022log proxy\u0022 Unix Domain Sockets are exported as environment\n@@ -225,7 +226,7 @@ builders may be doing in parallel.\n \n ## Builder git caching\n \n-For each `\u003csaimaster-project\u003e`, the builder maintains a local git cache. This is\n+For each `\u003csaiserver-project\u003e`, the builder maintains a local git cache. This is\n updated once when the new ref appears and then the related tests check out a\n fresh image of their ref from that each time. This is very fast after the first\n update, because it doesn't even involve the network but fetching from the local\n@@ -255,16 +256,29 @@ down `/home/sai` or `\u005cUsers\u005csai`.\n - git-mirror/\n - `remote git url`_`project name` -- individual git mirrors\n - jobs/\n- - `master hostname`-`platform name`-`instance index`/\n+ - `server hostname`-`platform name`-`instance index`/\n - `project_name`/ - checkouts and builds occur in here\n \n ## Build steps\n \n-Building sai produces two different apps, `sai-master` and `sai-builder`.\n-`sai-master` is a ws and http server that coordinates activities, and\n-`sai-builder` is run on each machine that wants to offer build services to\n-one or more masters. By default, both are built, but you can use a cmake\n-option `-DSAI_MASTER\u003d0` and `-DSAI_BUILDER\u003d0` to disable one or the other.\n+Building sai produces two different sets of apps and daemons by default, for\n+running on a the server that coordinates the builds and for running on machines\n+that offer the actual builds for particular platforms to one or more servers.\n+\n+You can use cmake options `-DSAI_MASTER\u003d0` and `-DSAI_BUILDER\u003d0` to disable one\n+or the other.\n+\n+Server executables|Function\n+---|---\n+sai-server|The server that builders connect to\n+sai-web|The server that browsers connect to\n+\n+Builder executables|Function\n+---|---\n+sai-builder|The daemon that connects to sai-server and runs builds\n+sai-device|Helper that coordinates which builds wants and can use specific embedded hardware\n+sai-expect|Helper run by embedded build flow to capture serial traffic and react to keywords\n+sai-jig|Helper for embedded devices that lets another device control its buttons, reset etc as part of the embedded build flow\n \n First you must build lws with appropriate options.\n \n@@ -280,13 +294,13 @@ $ cd libwebsockets \u0026\u0026 mkdir build \u0026\u0026 cd build \u0026\u0026 \u005c\n $ make -j \u0026\u0026 sudo make -j install \u0026\u0026 sudo ldconfig\n ```\n \n-The actual cmake options needed depends on if you are building sai-master and / or\n+The actual cmake options needed depends on if you are building sai-server and / or\n sai-builder.\n \n Feature|lws options\n ---|---\n either|`-DLWS_WITH_STRUCT_JSON\u003d1` `-DLWS_WITH_SECURE_STREAMS\u003d1`\n-master|`-DLWS_UNIX_SOCK\u003d1` `-DLWS_WITH_GENCRYPTO\u003d1` `-DLWS_WITH_STRUCT_SQLITE3\u003d1` `-DLWS_WITH_JOSE\u003d1`\n+server|`-DLWS_UNIX_SOCK\u003d1` `-DLWS_WITH_GENCRYPTO\u003d1` `-DLWS_WITH_STRUCT_SQLITE3\u003d1` `-DLWS_WITH_JOSE\u003d1`\n builder + related|`-DLWS_WITH_SPAWN\u003d1` `-DLWS_WITH_THREADPOOL\u003d1`\n \n Similarly the two daemons bring in different dependencies\n@@ -294,9 +308,9 @@ Similarly the two daemons bring in different dependencies\n Feature|dependency\n ---|---\n either|libwebsockets\n-master|libsqlite3\n+server|libsqlite3\n builder|libgit2 pthreads\n-jig|libgpiod\n+jig (linux only)|libgpiod\n \n #### Unix / Linux\n \ndiff --git a/READMEs/README-auth.md b/READMEs/README-auth.md\nindex 42f4372..9cc8aaf 100644\n--- a/READMEs/README-auth.md\n+++ b/READMEs/README-auth.md\n@@ -7,14 +7,14 @@ Sai uses signed JWTs\n There's no UI at the moment for creating authorized users, everything except\n event deletion and task restart works without authorization.\n \n-## sai-master configuration for auth\n+## sai-server configuration for auth\n \n In the `vhosts|ws-protocols|com-warmcat-sai` section of the config JSON, the\n following entries define the authorization operation\n \n ```\n \u0022jwt-auth-alg\u0022: \u0022ES512\u0022,\n- \u0022jwt-auth-jwk-path\u0022: \u0022/etc/sai/master/auth.jwk\u0022,\n+ \u0022jwt-auth-jwk-path\u0022: \u0022/etc/sai/server/auth.jwk\u0022,\n \u0022jwt-iss\u0022: \u0022com.warmcat\u0022,\n \u0022jwt-aud\u0022: \u0022https://libwebsockets.org/sai\u0022,\n ```\n@@ -34,20 +34,20 @@ Use this as below to create the server JWK used for signing and validation,\n for ES512 algorithm in our case\n \n ```\n-$ sudo ./bin/lws-crypto-jwk -t EC -b512 -vP-521 --alg ES512 \u003e /etc/sai/master/auth.jwk\n+$ sudo ./bin/lws-crypto-jwk -t EC -b512 -vP-521 --alg ES512 \u003e /etc/sai/server/auth.jwk\n ```\n \n ## Defining an authorized user\n \n-Sai-master creates a separate auth database and prepares the table schema in\n-it on startup if not already existing. So you using sai-master at all already\n+Sai-server creates a separate auth database and prepares the table schema in\n+it on startup if not already existing. So you using sai-server at all already\n did most of the work.\n \n To create a user that can login via his browser and see and use the UI for the\n additional actions, currently you can create the user by hand on the server:\n \n ```\n-# sqlite3 /home/srv/sai/sai-master-auth.sqlite3\n+# sqlite3 /home/srv/sai/sai-server-auth.sqlite3\n SQLite version 3.32.3 2020-06-18 14:00:33\n Enter \u0022.help\u0022 for usage hints.\n sqlite\u003e .schema\ndiff --git a/READMEs/README-build-windows.md b/READMEs/README-build-windows.md\nindex e709a07..96b4135 100644\n--- a/READMEs/README-build-windows.md\n+++ b/READMEs/README-build-windows.md\n@@ -15,7 +15,7 @@ also used in the next steps here.\n For windows + sai-builder, the following lws cmake config is suitable\n \n ```\n-\u003e cmake .. -DLWS_HAVE_PTHREAD_H\u003d1 -DLWS_EXT_PTHREAD_INCLUDE_DIR\u003d\u0022C:\u005cProgram Files (x86)\u005cpthreads\u005cinclude\u0022 -DLWS_EXT_PTHREAD_LIBRARIES\u003d\u0022C:\u005cProgram Files (x86)\u005cpthreads\u005clib\u005cx64\u005clibpthreadGC2.a\u0022 -DLWS_WITH_MINIMAL_EXAMPLES\u003d1 -DLWS_WITH_THREADPOOL\u003d1 -DLWS_UNIX_SOCK\u003d1 -DLWS_WITH_STRUCT_JSON\u003d1 -DLWS_WITH_SPAWN\u003d1 -DLWS_WITH_SECURE_STREAMS\u003d1 -DLWS_WITH_DIR\u003d1 \n+\u003e cmake .. -DLWS_WITH_JOSE\u003d1 -DLWS_HAVE_PTHREAD_H\u003d1 -DLWS_EXT_PTHREAD_INCLUDE_DIR\u003d\u0022C:\u005cProgram Files (x86)\u005cpthreads\u005cinclude\u0022 -DLWS_EXT_PTHREAD_LIBRARIES\u003d\u0022C:\u005cProgram Files (x86)\u005cpthreads\u005clib\u005cx64\u005clibpthreadGC2.a\u0022 -DLWS_WITH_MINIMAL_EXAMPLES\u003d1 -DLWS_WITH_THREADPOOL\u003d1 -DLWS_UNIX_SOCK\u003d1 -DLWS_WITH_STRUCT_JSON\u003d1 -DLWS_WITH_SPAWN\u003d1 -DLWS_WITH_SECURE_STREAMS\u003d1 -DLWS_WITH_DIR\u003d1 \n ```\n \n ## Building libgit2\ndiff --git a/READMEs/README-sai-jig.md b/READMEs/README-sai-jig.md\nindex 86f7a2c..8bc13f9 100644\n--- a/READMEs/README-sai-jig.md\n+++ b/READMEs/README-sai-jig.md\n@@ -3,7 +3,7 @@\n ## Introduction\n \n Sai-jig is a standalone daemon that normally doesn't run on the builder or\n-master machines, but on a smaller helper close to embedded targets and\n+server machines, but on a smaller helper close to embedded targets and\n physically wired to, eg, the reset button or flash config GPIO on them.\n The helper is typically an RPi or similar machine that has networking,\n Linux and convenient GPIO.\ndiff --git a/READMEs/README-sai-json.md b/READMEs/README-sai-json.md\nindex 12a6686..bdddc25 100644\n--- a/READMEs/README-sai-json.md\n+++ b/READMEs/README-sai-json.md\n@@ -4,7 +4,7 @@ Projects can inform Sai of what build and test variations they want to run\n on push using a `.sai.json` file in the top level dir of the project.\n \n The git push hook isolated this file from the pushed tree and passes it to\n-the sai-master along with the rest of the push notification information in\n+the sai-server along with the rest of the push notification information in\n a signed JSON structure.\n \n ## Simple .sai.json file\n@@ -47,7 +47,7 @@ platform builder with the arguments\n \n In the top level `\u0022platforms\u0022` section you describe the different build\n platforms you want to build and test on... the specified platforms must have\n-builders that have connected themselves to the sai-master.\n+builders that have connected themselves to the sai-server.\n \n If you don't give a `\u0022default\u0022: false` entry in the platform definition, the\n platform is assumed to apply to all the configurations listed afterwards. If\n@@ -92,5 +92,5 @@ or to disable all default platforms and just use the specified ones:\n #### artifacts\n \n You can also give a comma-separated list of build artifacts, these are\n-arbitrary binary files which will be uploaded to sai-master and made available\n+arbitrary binary files which will be uploaded to sai-server and made available\n for download over https.\n\u005c No newline at end of file\ndiff --git a/READMEs/README-systemd-nspawn.md b/READMEs/README-systemd-nspawn.md\nindex 739c7f1..b622746 100644\n--- a/READMEs/README-systemd-nspawn.md\n+++ b/READMEs/README-systemd-nspawn.md\n@@ -1,7 +1,7 @@\n # Setting up and administering systemd-nspawn containers\n \n Sai's approach is to run `sai-builder` from inside the environment it will do\n-builds from. It reaches out to the sai-master instance and git repos, and\n+builds from. It reaches out to the sai-server instance and git repos, and\n otherwise there is no ingress into the container or VM from outside.\n \n So with `systemd-nspawn`, it means the containers should start on the host at\n@@ -319,7 +319,7 @@ For redhat type distros, you probably need to add /usr/local/lib to the\n ```\n Container # git clone https://libwebsockets.org/repo/libwebsockets\n Container # cd libwebsockets \u0026\u0026 mkdir build \u0026\u0026 cd build \u0026\u0026 \u005c\n- cmake .. -DLWS_UNIX_SOCK\u003d1 -DLWS_WITH_STRUCT_JSON\u003d1 \u005c\n+ cmake .. -DLWS_WITH_JOSE\u003d1 -DLWS_UNIX_SOCK\u003d1 -DLWS_WITH_STRUCT_JSON\u003d1 \u005c\n -DLWS_WITH_STRUCT_SQLITE3\u003d1 -DLWS_WITH_GENCRYPTO\u003d1 -DLWS_WITH_SPAWN\u003d1 \u005c\n -DLWS_WITH_SECURE_STREAMS\u003d1 -DLWS_WITH_THREADPOOL\u003d1 -DLWS_WITH_JOSE\u003d1\n Container # make -j \u0026\u0026 make -j install \u0026\u0026 ldconfig\n@@ -408,6 +408,8 @@ Make sai-builder run as a daemon, create the file `/Library/LaunchDaemons/com.wa\n \u003c/array\u003e\n \u003ckey\u003eRunAtLoad\u003c/key\u003e\n \u003ctrue/\u003e\n+ \u003ckey\u003eStandardErrorPath\u003c/key\u003e\n+ \u003cstring\u003e/var/log/sai-builder.log\u003c/string\u003e\n \u003c/dict\u003e\n \u003c/plist\u003e\n ```\ndiff --git a/READMEs/README-using-overlayfs.md b/READMEs/README-using-overlayfs.md\nindex 0c89448..630a07d 100644\n--- a/READMEs/README-using-overlayfs.md\n+++ b/READMEs/README-using-overlayfs.md\n@@ -219,7 +219,7 @@ $ sudo git init\n $ sudo git add *\n $ sudo git commit -m \u0022initial commit\u0022\n $ sudo git remote add origin ssh://git@example.com/linux-ubuntu-xenial-amd64+base\n-$ sudo git push origin +master:master\n+$ sudo git push origin +server:server\n ```\n \n \ndiff --git a/READMEs/sai-build-test-flow.png b/READMEs/sai-build-test-flow.png\nindex a3c439b..6ba4376 100644\nBinary files a/READMEs/sai-build-test-flow.png and b/READMEs/sai-build-test-flow.png differ\ndiff --git a/READMEs/sai-embedded-test.png b/READMEs/sai-embedded-test.png\nindex 03e9c70..f4c534f 100644\nBinary files a/READMEs/sai-embedded-test.png and b/READMEs/sai-embedded-test.png differ\ndiff --git a/READMEs/sai-overview.png b/READMEs/sai-overview.png\nindex 069460d..469adec 100644\nBinary files a/READMEs/sai-overview.png and b/READMEs/sai-overview.png differ\ndiff --git a/assets/index.html b/assets/index.html\nindex 20f099f..e37102f 100644\n--- a/assets/index.html\n+++ b/assets/index.html\n@@ -16,18 +16,18 @@\n \n \t\t\t\u003cdiv id\u003d\u0022login\u0022 class\u003d\u0022login\u0022\u003e\n \t\t\t\t\u003cdiv id\u003d\u0022creds\u0022 class\u003d\u0022creds hide\u0022\u003e\n-\t\t\t\t\t \u003cform class\u003d\u0022login\u0022 action\u003d\u0022login\u0022 method\u003d\u0022post\u0022\u003e\n+\t\t\t\t\t \u003cform class\u003d\u0022login\u0022\u003e\n \t\t\t\t\t \u003clabel for\u003d\u0022lname\u0022\u003eName:\u003c/label\u003e\n \t\t\t\t\t \u003cinput class\u003d\u0022crin\u0022 type\u003d\u0022text\u0022 id\u003d\u0022lname\u0022 name\u003d\u0022lname\u0022 autocomplete\u003d\u0022username\u0022\u003e\u003cbr\u003e\n \t\t\t\t\t \u003clabel for\u003d\u0022lpass\u0022\u003ePassword:\u003c/label\u003e\n \t\t\t\t\t \u003cinput class\u003d\u0022crin\u0022 type\u003d\u0022password\u0022 id\u003d\u0022lpass\u0022 name\u003d\u0022lpass\u0022 autocomplete\u003d\u0022current-password\u0022\u003e\u003cbr\u003e\n-\t\t\t\t\t \u003cinput class\u003d\u0022crinb\u0022 type\u003d\u0022submit\u0022 value\u003d\u0022Login\u0022\u003e\n+\t\t\t\t\t \u003cinput class\u003d\u0022crinb\u0022 type\u003d\u0022button\u0022 value\u003d\u0022Login\u0022 id\u003d\u0022login\u0022\u003e\n \t\t\t\t\t \u003cinput type\u003d\u0022hidden\u0022 id\u003d\u0022success_redir\u0022 name\u003d\u0022success_redir\u0022 value\u003d\u0022\u0022\u003e\n \t\t\t\t\t\u003c/form\u003e \n \t\t\t\t\u003c/div\u003e\n \t\t\t\t\u003cdiv id\u003d\u0022logout\u0022 class\u003d\u0022creds hide\u0022\u003e\n \t\t\t\t\t\u003cdiv id\u003d\u0022remauth\u0022\u003e\u003c/div\u003e\n-\t\t\t\t\t\u003cinput class\u003d\u0022crin\u0022 type\u003d\u0022submit\u0022 id\u003d\u0022logout\u0022 value\u003d\u0022Logout\u0022\u003e\n+\t\t\t\t\t\u003cinput class\u003d\u0022crin\u0022 type\u003d\u0022button\u0022 id\u003d\u0022logout\u0022 value\u003d\u0022Logout\u0022\u003e\n \t\t\t\t\u003c/div\u003e\n \t\t\t\u003c/div\u003e\n \ndiff --git a/assets/sai.css b/assets/sai.css\nindex 2f6f714..c8b3ab8 100644\n--- a/assets/sai.css\n+++ b/assets/sai.css\n@@ -85,6 +85,16 @@ td.waiting {\n \tborder-radius:5px;\n }\n \n+td.waiting_gi {\n+\tvertical-align:top;\n+\ttext-align:left;\n+\tcolor:#000000; \n+\tpadding:0px;\n+\tfont-size: 9pt;\n+\tfont-weight: bold;\n+\tborder-radius:2px;\n+}\n+\n table.comp {\n \tborder-radius:7px;\n }\n@@ -148,6 +158,12 @@ td.jumble {\n \twhite-space: nowrap;\n }\n \n+img.saicon {\n+\twidth: 24px;\n+\theight:24px;\n+\tpadding-right:3px;\n+}\n+\n span.group2 {\n \tvertical-align:middle;\n \ttext-align:center;\n@@ -183,6 +199,17 @@ span.nowrap {\n \n div.taskinfo {\n \tvertical-align: top;\n+\tposition: fixed;\n+\tbackground: #fff;\n+\tz-index: 2002;\n+\tborder-style: solid;\n+\tborder-width: 16px 16px 0px 16px;\n+\tborder-color: #fff;\n+\tmargin-top: -16px;\n+}\n+\n+table.scrollogs {\n+\tmargin-top: 64px;\n }\n \n div.taskstate {\n@@ -214,7 +241,23 @@ div.evr {\n \ttop: 16px;\n \tleft: 16px;\n }\n+div.evr_gi {\n+\tdisplay: inline-block;\n+\twidth: 16px;\n+\theight: 16px;\n+}\n \n+div.gi_popup {\n+\tbox-shadow: 3px 5px 5px 0px #444;\n+\tleft: 0px;\n+\tz-index: -2;\n+\tbackground: #ffe;\n+\topacity: 0.0;\n+\ttransition: opacity 1s ease;\n+\tdisplay: block;\n+\tposition: absolute;\n+\tmargin-top: 35px\n+}\n \n .taskstate0 {\n \tcolor:#000000;\n@@ -274,42 +317,24 @@ img.bico {\n }\n \n .taskstate1 {\n-\tbackground:#a0a0a0;\n+\tbackground: #d0d0d0;\n \tcolor:#000000;\n-\topacity:0.75;\n-\tanimation-name: stretch;\n-\tanimation-duration: 2s; \n-\tanimation-timing-function: ease-out; \n-\tanimation-delay: 0s;\n-\tanimation-direction: alternate;\n-\tanimation-iteration-count: infinite;\n-\tanimation-fill-mode: none;\n-\tanimation-play-state: running; \n+\topacity:1;\n+\ttransition: background-color 500ms linear;\n }\n \n .taskstate2 {\n \tbackground:#a0a0a0;\n \tcolor:#000000;\n-\topacity:0.75;\n-\tanimation-name: stretch;\n-\tanimation-duration: 1s; \n-\tanimation-timing-function: ease-out; \n-\tanimation-delay: 0s;\n-\tanimation-direction: alternate;\n-\tanimation-iteration-count: infinite;\n-\tanimation-fill-mode: none;\n-\tanimation-play-state: running; \n+\topacity:1;\n+\ttransition: background-color 500ms linear;\n }\n \n .taskstate3 {\n \tbackground:#01ff70;\n \tcolor:#003000;\n \topacity:1;\n-\t/*\n-\tanimation-name: fadeInOpacity;\n-\tanimation-iteration-count: 1;\n-\tanimation-timing-function: ease-in;\n-\tanimation-duration: 2s; */\n+\ttransition: background-color 500ms linear;\n }\n \n @keyframes fadeInOpacity {\n@@ -320,26 +345,32 @@ img.bico {\n \t\topacity: 1;\n \t}\n }\n-}\n+\n \n .taskstate4 {\n \tbackground:#ff851b;\n \tcolor:#300000;\n \topacity:1;\n+\ttransition: background-color 500ms linear;\n }\n \n .taskstate5 {\n \tbackground:#808080;\n \tcolor:#202020;\n \topacity:1;\n+\ttransition: background-color 500ms linear;\n }\n \n .taskstate6 {\n \tbackground:#df3030;\n \tcolor:#300000;\n \topacity:1;\n+\ttransition: background-color 500ms linear;\n }\n \n+div.stats_hidden {\n+\tdisplay: none;\n+}\n \n div.dlogs {\n \tvertical-align: top;\n@@ -385,6 +416,10 @@ div.hide {\n \tdisplay: none;\n }\n \n+td.hide {\n+\tdisplay: none;\n+}\n+\n input.crin {\n \tfont-size: 7pt;\n \twidth: 60px;\n@@ -483,6 +518,11 @@ td.e4 {\n \ttext-align: right;\n }\n \n+td.e6 {\n+\tfont-weight: normal;\n+\tfont-size: 6pt;\n+\ttext-align: right;\n+}\n \t\n table.summary {\n \twidth: 100%;\ndiff --git a/assets/sai.js b/assets/sai.js\nindex 8900d1b..e8c2430 100644\n--- a/assets/sai.js\n+++ b/assets/sai.js\n@@ -512,6 +512,9 @@ function aging()\n }\n }\n \n+\tif (next \u003c 5)\n+\t\tnext \u003d 5;\n+\n /*\n * We only need to come back when the age might have changed.\n * Eg, if everything is counted in hours already, once per\n@@ -527,7 +530,7 @@ function sai_plat_icon(plat, size)\n \t\n \ts \u003d plat.split('/');\n \tif (s[0]) {\n-\tconsole.log(\u0022plat \u0022 + plat + \u0022 plat[0] \u0022 + s[0]);\n+\t// console.log(\u0022plat \u0022 + plat + \u0022 plat[0] \u0022 + s[0]);\n \ts1 \u003d \u0022\u003cimg class\u003d\u005c\u0022ip\u0022 + size + \u0022 zup\u005c\u0022 src\u003d\u005c\u0022/sai/\u0022 + san(s[0]) +\n \t\t\u0022.svg\u005c\u0022\u003e\u0022;\n \t\n@@ -560,7 +563,7 @@ function sai_taskinfo_render(t, now_ut)\n \tvar now_ut \u003d Math.round((new Date().getTime() / 1000));\n \tvar s \u003d \u0022\u0022;\n \t\n-\ts \u003d \u0022\u003cdiv class\u003d\u005c\u0022taskinfo\u005c\u0022\u003e\u003ctable\u003e\u003ctr class\u003d\u005c\u0022nomar\u005c\u0022\u003e\u003ctd class\u003d\u005c\u0022atop\u005c\u0022\u003e\u003ctable\u003e\u0022 +\n+\ts \u003d \u0022\u003ctable\u003e\u003ctr class\u003d\u005c\u0022nomar\u005c\u0022\u003e\u003ctd class\u003d\u005c\u0022atop\u005c\u0022\u003e\u003ctable\u003e\u0022 +\n \t\tsai_event_render(t, now_ut, 0) + \u0022\u003c/table\u003e\u003c/td\u003e\u003ctd class\u003d\u005c\u0022ti\u005c\u0022\u003e\u0022 +\n \t\t\u0022\u003cspan class\u003d\u005c\u0022ti1\u005c\u0022\u003e\u0022 + sai_plat_icon(t.t.platform, 2) +\n \t\tsan(t.t.platform) + \u0022\u003c/span\u003e\u0026nbsp;\u0022;\n@@ -589,16 +592,53 @@ function sai_taskinfo_render(t, now_ut)\n \n \ts +\u003d \u0022\u003c/td\u003e\u003c/tr\u003e\u0022;\n \t\n-\ts +\u003d \u0022\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\u003c/table\u003e\u003c/div\u003e\u0022;\n+\ts +\u003d \u0022\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\u003c/table\u003e\u0022;\n \t\n \treturn s;\n }\n \n+function summarize_build_situation(event_uuid)\n+{\n+\tvar good \u003d 0, bad \u003d 0, total \u003d 0,\n+\t\troo \u003d document.getElementById(\u0022taskcont-\u0022 + event_uuid),\n+\t\tsame;\n+\t\t\n+\tif (!roo) \n+\t\treturn \u0022\u0022;\n+\t\n+\tsame \u003d roo.querySelectorAll(\u0022.taskstate\u0022);\n+\tif (same)\n+\t\ttotal \u003d same.length;\n+\tsame \u003d roo.querySelectorAll(\u0022.taskstate3\u0022);\n+\tif (same)\n+\t\tgood \u003d same.length;\n+\tsame \u003d roo.querySelectorAll(\u0022.taskstate4\u0022);\n+\tif (same)\n+\t\tbad \u003d same.length;\n+\t\t\t\t\n+\tif (good \u003d\u003d total)\n+\t\treturn \u0022All \u0022 + good + \u0022 passed\u0022;\n+\n+\tif (bad \u003d\u003d total)\n+\t\treturn \u0022All \u0022 + bad + \u0022 failed\u0022;\n+\t\t\n+\tif (!good \u0026\u0026 !bad)\n+\t\treturn total + \u0022 pending\u0022;\n+\n+\tif (!bad)\n+\t\treturn good + \u0022 passed, \u0022 + (total - good) + \u0022 pending\u0022;\n+\n+\tif (!good)\n+\t\treturn bad + \u0022 failed, \u0022 + (total - bad) + \u0022 pending\u0022;\n+\n+\treturn good + \u0022 passed, \u0022 + bad + \u0022 failed, \u0022 + (total - good - bad) + \u0022 pending\u0022;\n+}\n+\n function sai_event_summary_render(o, now_ut, reset_all_icon)\n {\n \tvar s, q, ctn \u003d \u0022\u0022, wai, s1 \u003d \u0022\u0022, n, e \u003d o.e;\n \n-\ts \u003d \u0022\u003ctr\u003e\u003ctd class\u003d\u005c\u0022waiting\u005c\u0022\u003e\u003ctable class\u003d\u005c\u0022comp\u0022;\n+\ts \u003d \u0022\u003ctable class\u003d\u005c\u0022comp\u0022;\n \tif (e.state \u003d\u003d 3)\n \t\ts +\u003d \u0022 comp_pass\u0022;\n \tif (e.state \u003d\u003d 4 || e.state \u003d\u003d 6)\n@@ -606,13 +646,18 @@ function sai_event_summary_render(o, now_ut, reset_all_icon)\n \n \ts +\u003d \u0022\u005c\u0022\u003e\u003ctr\u003e\u003ctd class\u003d\u005c\u0022jumble\u005c\u0022\u003e\u003ca href\u003d\u005c\u0022/sai/?event\u003d\u0022 + san(e.uuid) +\n \t\t\u0022\u005c\u0022\u003e\u003cimg src\u003d\u005c\u0022/sai/sai-event.svg\u005c\u0022\u0022;\n+\tif (gitohashi_integ)\n+\t\ts +\u003d \u0022 class\u003d\u005c\u0022saicon\u005c\u0022\u0022;\n \tif (e.state \u003d\u003d 3 || e.state \u003d\u003d 4)\n \t\ts +\u003d \u0022 class\u003d\u005c\u0022deemph\u005c\u0022\u0022;\n \ts +\u003d \u0022\u003e\u0022;\n+\tvar cl \u003d \u0022evr\u0022;\n+\tif (gitohashi_integ)\n+\t\tcl \u003d \u0022evr_gi\u0022;\n \tif (e.state \u003d\u003d 3)\n-\t\ts +\u003d \u0022\u003cdiv class\u003d\u005c\u0022evr\u005c\u0022\u003e\u003cimg src\u003d\u005c\u0022/sai/passed.svg\u005c\u0022\u003e\u003c/div\u003e\u0022;\n+\t\ts +\u003d \u0022\u003cdiv class\u003d\u005c\u0022\u0022 + cl + \u0022\u005c\u0022\u003e\u003cimg src\u003d\u005c\u0022/sai/passed.svg\u005c\u0022\u003e\u003c/div\u003e\u0022;\n \tif (e.state \u003d\u003d 4)\n-\t\ts +\u003d \u0022\u003cdiv class\u003d\u005c\u0022evr\u005c\u0022\u003e\u003cimg src\u003d\u005c\u0022/sai/failed.svg\u005c\u0022\u003e\u003c/div\u003e\u0022;\n+\t\ts +\u003d \u0022\u003cdiv class\u003d\u005c\u0022\u0022 + cl + \u0022\u005c\u0022\u003e\u003cimg src\u003d\u005c\u0022/sai/failed.svg\u005c\u0022\u003e\u003c/div\u003e\u0022;\n \t\t\n \ts +\u003d \u0022\u003c/a\u003e\u0022;\n \tif (reset_all_icon \u0026\u0026 !gitohashi_integ \u0026\u0026 authd) {\n@@ -621,30 +666,38 @@ function sai_event_summary_render(o, now_ut, reset_all_icon)\n \t\ts +\u003d \u0022\u003cimg class\u003d\u005c\u0022rebuild\u005c\u0022 alt\u003d\u005c\u0022delete event\u005c\u0022 src\u003d\u005c\u0022/sai/delete.png\u005c\u0022 \u0022 +\n \t\t\t\t\u0022id\u003d\u005c\u0022delete-ev-\u0022 + san(e.uuid) + \u0022\u005c\u0022\u003e\u0022;\n \t}\n-\ts +\u003d \u0022\u003c/td\u003e\u0022 +\n+\ts +\u003d \u0022\u003c/td\u003e\u0022;\n+\t\n+\tif (!gitohashi_integ) {\n+\t\ts +\u003d\n \t\t\u0022\u003ctd\u003e\u003ctable class\u003d\u005c\u0022nomar\u005c\u0022\u003e\u0022 +\n-\t\t\u0022\u003ctr\u003e\u003ctd class\u003d\u005c\u0022nomar\u005c\u0022 colspan\u003d2\u003e\u003cspan class\u003d\u005c\u0022e1\u005c\u0022\u003e\u0022 +\n-\t\tsan(e.repo_name) +\n+\t\t\u0022\u003ctr\u003e\u003ctd class\u003d\u005c\u0022nomar\u005c\u0022 colspan\u003d2\u003e\u0022 +\n+\t\t\u0022\u003cspan class\u003d\u005c\u0022e1\u005c\u0022\u003e\u0022 + san(e.repo_name) +\n \t\t\u0022\u003c/span\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd class\u003d\u005c\u0022nomar\u005c\u0022 colspan\u003d2\u003e\u003cspan class\u003d\u005c\u0022e2\u005c\u0022\u003e\u0022;\n-\n-\tif (e.ref.substr(0, 11) \u003d\u003d\u003d \u0022refs/heads/\u0022) {\n-\t\ts +\u003d \u0022\u003cimg class\u003d\u005c\u0022branch\u005c\u0022\u003e\u0022 +\n-\t\t\tsan(e.ref.substr(11));\n-\t} else\n-\t\tif (e.ref.substr(0, 10) \u003d\u003d\u003d \u0022refs/tags/\u0022) {\n-\t\t\ts +\u003d \u0022\u003cimg class\u003d\u005c\u0022tag\u005c\u0022\u003e\u0022 +\n-\t\t\t\tsan(e.ref.substr(10));\n+\t\n+\t\tif (e.ref.substr(0, 11) \u003d\u003d\u003d \u0022refs/heads/\u0022) {\n+\t\t\ts +\u003d \u0022\u003cimg class\u003d\u005c\u0022branch\u005c\u0022\u003e\u0022 +\n+\t\t\t\tsan(e.ref.substr(11));\n \t\t} else\n-\t\t\ts +\u003d san(e.ref);\n-\n-\ts +\u003d \u0022\u003c/span\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd class\u003d\u005c\u0022nomar\u005c\u0022\u003e\u003cspan class\u003d\u005c\u0022e3\u005c\u0022\u003e\u0022 +\n-\t san(e.hash.substr(0, 8)) +\n-\t \u0022\u003c/span\u003e\u003c/td\u003e\u003ctd class\u003d\u005c\u0022e4\u005c\u0022 class\u003d\u005c\u0022nomar\u005c\u0022\u003e\u0022 +\n-\t agify(now_ut, e.created) + \u0022\u003c/td\u003e\u003c/tr\u003e\u0022;\n-\t if (o.t.length \u003e 1)\n-\t \ts +\u003d \u0022\u003ctr\u003e\u003ctd class\u003d\u005c\u0022nomar\u005c\u0022\u003e\u003cspan class\u003d\u005c\u0022e3\u005c\u0022\u003e\u0022 + o.t.length + \u0022 builds\u003c/span\u003e\u003c/td\u003e\u003ctr\u003e\u0022;\n-\t s +\u003d \u0022\u003c/table\u003e\u0022 +\n-\t \u0022\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\u003c/td\u003e\u0022;\n+\t\t\tif (e.ref.substr(0, 10) \u003d\u003d\u003d \u0022refs/tags/\u0022) {\n+\t\t\t\ts +\u003d \u0022\u003cimg class\u003d\u005c\u0022tag\u005c\u0022\u003e\u0022 +\n+\t\t\t\t\tsan(e.ref.substr(10));\n+\t\t\t} else\n+\t\t\t\ts +\u003d san(e.ref);\n+\t\n+\t\ts +\u003d \u0022\u003c/span\u003e\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd class\u003d\u005c\u0022nomar e6\u005c\u0022\u003e\u0022 +\n+\t\t san(e.hash.substr(0, 8)) +\n+\t\t \u0022\u003c/td\u003e\u003ctd class\u003d\u005c\u0022e6 nomar\u005c\u0022\u003e\u0022 +\n+\t\t agify(now_ut, e.created) + \u0022\u003c/td\u003e\u003c/tr\u003e\u0022;\n+\t\t \ts +\u003d \u0022\u003ctr\u003e\u003ctd class\u003d\u005c\u0022nomar e6\u005c\u0022 colspan\u003d\u005c\u00222\u005c\u0022 id\u003d\u005c\u0022sumbs-\u0022 + e.uuid +\u0022\u005c\u0022\u003e\u0022 + summarize_build_situation(e.uuid) + \u0022\u003c/td\u003e\u003ctr\u003e\u0022;\n+\t\t s +\u003d \u0022\u003c/table\u003e\u0022 +\n+\t\t \u0022\u003c/td\u003e\u0022;\n+\t} else {\n+\t\ts +\u003d\u0022\u003ctd\u003e\u003ctable\u003e\u003ctr\u003e\u003ctd class\u003d\u005c\u0022e6 nomar\u005c\u0022\u003e\u0022 + san(e.hash.substr(0, 8)) + \u0022 \u0022 + agify(now_ut, e.created);\n+\t\t \ts +\u003d \u0022\u003c/td\u003e\u003c/tr\u003e\u003ctr\u003e\u003ctd class\u003d\u005c\u0022e6 nomar\u005c\u0022 id\u003d\u005c\u0022sumbs-\u0022 + e.uuid +\u0022\u005c\u0022\u003e\u0022 + summarize_build_situation(e.uuid);\n+\t\ts +\u003d \u0022\u003c/tr\u003e\u003c/table\u003e\u003c/td\u003e\u0022; \n+\t}\n+\ts +\u003d \u0022\u003c/tr\u003e\u003c/table\u003e\u0022;\n \t \n \treturn s;\n }\n@@ -652,8 +705,11 @@ function sai_event_summary_render(o, now_ut, reset_all_icon)\n function sai_event_render(o, now_ut, reset_all_icon)\n {\n \tvar s, q, ctn \u003d \u0022\u0022, wai, s1 \u003d \u0022\u0022, n, e \u003d o.e;\n-\n-\ts \u003d sai_event_summary_render(o, now_ut, reset_all_icon);\n+\t\n+\ts \u003d \u0022\u003ctr\u003e\u003ctd class\u003d\u005c\u0022waiting\u005c\u0022\u0022;\n+\tif (gitohashi_integ)\n+\t\ts +\u003d \u0022 id\u003d\u005c\u0022gitohashi_sai_icon\u005c\u0022\u0022;\n+\ts +\u003d \u0022\u003e\u003cdiv id\u003d\u005c\u0022esr-\u0022 + san(e.uuid) + \u0022\u005c\u0022\u003e\u003c/div\u003e\u003c/td\u003e\u0022;\n \n \tif (o.t.length) {\n \t\tvar all_good, has_bad, all_done;\n@@ -668,7 +724,11 @@ function sai_event_render(o, now_ut, reset_all_icon)\n \t\t * SAIES_BEING_BUILT_HAS_FAILURES\n \t\t */\n \t\t\n-\t\ts +\u003d \u0022\u003ctd class\u003d\u005c\u0022tasks\u005c\u0022\u003e\u003ctable\u003e\u003ctr\u003e\u003ctd class\u003d\u005c\u0022atop\u005c\u0022\u003e\u0022;\n+\t\ts +\u003d \u0022\u003ctd class\u003d\u005c\u0022tasks\u005c\u0022 id\u003d\u005c\u0022taskcont-\u0022 + san(e.uuid) + \u0022\u005c\u0022\u003e\u0022;\n+\t\tif (gitohashi_integ)\n+\t\t\ts +\u003d \u0022\u003cdiv class\u003d\u005c\u0022gi_popup\u005c\u0022 id\u003d\u005c\u0022gitohashi_sai_details\u005c\u0022\u003e\u0022;\n+\n+\t\ts +\u003d \u0022\u003ctable\u003e\u003ctr\u003e\u003ctd class\u003d\u005c\u0022atop\u005c\u0022\u003e\u0022;\n \n \t\tfor (q \u003d 0; q \u003c o.t.length; q++) {\n \t\t\tvar t \u003d o.t[q];\n@@ -683,6 +743,7 @@ function sai_event_render(o, now_ut, reset_all_icon)\n \t\t\t\t\t */\n \n \t\t\t\t\ts +\u003d \u0022\u003cdiv class\u003d\u005c\u0022\u0022;\n+\t\t\t\t\t/*\n \t\t\t\t\tif (wai)\n \t\t\t\t\t\ts +\u003d \u0022 awaiting\u0022;\n \t\t\t\t\tif (all_good \u003d\u003d\u003d 1) {\n@@ -696,12 +757,12 @@ function sai_event_render(o, now_ut, reset_all_icon)\n \t\t\t\t\t\t\tif (all_done \u003d\u003d\u003d 0)\n \t\t\t\t\t\t\t\ts +\u003d \u0022 ov_dunno\u0022;\n \t\t\t\t\t}\n+\t\t\t\t\t*/\n \n \t\t\t\t\ts +\u003d \u0022 ib\u005c\u0022\u003e\u003ctable class\u003d\u005c\u0022nomar\u005c\u0022\u003e\u0022 +\n-\t\t\t\t\t \u0022\u003ctr\u003e\u003ctd class\u003d\u005c\u0022keepline\u005c\u0022\u003e\u0022 + s1;\n-\t\t\t\t\ts +\u003d \u0022\u003c/td\u003e\u003ctd class\u003d\u005c\u0022tn\u005c\u0022\u003e\u0022 +\n-\t\t\t\t\t\tsai_stateful_taskname(st, ctn, 0) +\n-\t\t\t\t\t\t\u0022\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\u003c/div\u003e\u0022;\n+\t\t\t\t\t \u0022\u003ctr\u003e\u003ctd class\u003d\u005c\u0022tn\u005c\u0022\u003e\u0022 + ctn +\n+\t\t\t\t\t \u0022\u003c/td\u003e\u003ctd class\u003d\u005c\u0022keepline\u005c\u0022\u003e\u0022 + s1 +\n+\t\t\t\t\t \u0022\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\u003c/div\u003e\u0022;\n \t\t\t\t\ts1 \u003d \u0022\u0022;\n \t\t\t\t}\n \t\t\t\tall_good \u003d 1;\n@@ -718,7 +779,7 @@ function sai_event_render(o, now_ut, reset_all_icon)\n \t\t\tif (t.state \u003d\u003d\u003d 4 || t.state \u003d\u003d 6)\n \t\t\t\thas_bad \u003d 1;\n \n-\t\t\ts1 +\u003d \u0022\u003cdiv class\u003d\u005c\u0022taskstate taskstate\u0022 + t.state + \u0022\u005c\u0022\u003e\u0022;\n+\t\t\ts1 +\u003d \u0022\u003cdiv id\u003d\u005c\u0022taskstate_\u0022 + t.uuid + \u0022\u005c\u0022 class\u003d\u005c\u0022taskstate taskstate\u0022 + t.state + \u0022\u005c\u0022\u003e\u0022;\n \t\t\tif (t.state \u003d\u003d\u003d 0)\n \t\t\t\twai \u003d 1;\n \n@@ -732,6 +793,8 @@ function sai_event_render(o, now_ut, reset_all_icon)\n \t\t\tvar st \u003d 0;\n \n \t\t\ts +\u003d \u0022\u003cdiv class\u003d\u005c\u0022\u0022;\n+\t\t\t\n+\t\t\t/*\n \t\t\tif (wai)\n \t\t\t\ts +\u003d \u0022 awaiting\u0022;\n \t\t\tif (all_good \u003d\u003d\u003d 1) {\n@@ -745,14 +808,17 @@ function sai_event_render(o, now_ut, reset_all_icon)\n \t\t\t\t\tif (all_done \u003d\u003d\u003d 0)\n \t\t\t\t\t\ts +\u003d \u0022 ov_dunno\u0022;\n \t\t\t}\n+\t\t\t*/\n \t\t\ts +\u003d \u0022 ib\u005c\u0022\u003e\u003ctable class\u003d\u005c\u0022nomar\u005c\u0022\u003e\u0022 +\n-\t\t\t\t\u0022\u003ctr\u003e\u003ctd class\u003d\u005c\u0022keepline\u005c\u0022\u003e\u0022 + s1;\n-\t\t\ts +\u003d \u0022\u003c/td\u003e\u003ctd class\u003d\u005c\u0022tn\u005c\u0022\u003e\u0022 +\n-\t\t\t\tsai_stateful_taskname(st, ctn, 0) +\n-\t\t\t \t\t\u0022\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\u003c/div\u003e\u0022;\n+\t\t\t\t\u0022\u003ctr\u003e\u003ctd class\u003d\u005c\u0022tn\u005c\u0022\u003e\u0022 + ctn +\n+\t\t\t\t\u0022\u003ctd class\u003d\u005c\u0022keepline\u005c\u0022\u003e\u0022 + s1 +\n+\t\t\t\t\u0022\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\u003c/div\u003e\u0022;\n \t\t}\n \n-\t\ts +\u003d \u0022\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\u003c/td\u003e\u0022;\n+\t\ts +\u003d \u0022\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\u0022;\n+\t\tif (gitohashi_integ)\n+\t\t\ts +\u003d \u0022\u003c/div\u003e\u0022;\n+\t\ts +\u003d \u0022\u003c/td\u003e\u0022;\n \t}\n \n \ts +\u003d \u0022\u003c/tr\u003e\u0022;\n@@ -802,7 +868,7 @@ function render_builders(jso)\n \t\t\thost \u003d sen.split('.')[0];\n \t\t\tcia \u003d host.split('-')[1];\n \t\t\t\n-\t\t\tconsole.log(\u0022host \u0022 + host + \u0022, cia \u0022 + cia);\n+\t\t\t// console.log(\u0022host \u0022 + host + \u0022, cia \u0022 + cia);\n \t\t\t\n \t\t\tif ((nc \u003d\u003d conts.length \u0026\u0026 !cia) ||\n \t\t\t (cia \u0026\u0026 cia \u003d\u003d\u003d conts[nc])) {\n@@ -834,6 +900,27 @@ function render_builders(jso)\n \treturn s;\n }\n \n+function refresh_state(task_uuid, task_state)\n+{\n+\tvar tsi \u003d document.getElementById(\u0022taskstate_\u0022 + task_uuid);\n+\t\n+\tif (tsi) {\n+\t\ttsi.classList.remove(\u0022taskstate1\u0022);\n+\t\ttsi.classList.remove(\u0022taskstate2\u0022);\n+\t\ttsi.classList.remove(\u0022taskstate3\u0022);\n+\t\ttsi.classList.remove(\u0022taskstate4\u0022);\n+\t\ttsi.classList.remove(\u0022taskstate5\u0022);\n+\t\ttsi.classList.remove(\u0022taskstate6\u0022);\n+\t\ttsi.classList.remove(\u0022taskstate7\u0022);\n+\t\ttsi.classList.add(\u0022taskstate\u0022 + task_state);\n+\t\t// console.log(\u0022refresh_state taskstate\u0022 + task_state);\n+\t}\n+}\n+\n+function after_delete() {\n+\tlocation.reload();\n+}\n+\n function ws_open_sai()\n {\t\n \tvar s \u003d \u0022\u0022, q, qa, qi, q5, q5s;\n@@ -862,6 +949,14 @@ function ws_open_sai()\n \t\tgitohashi_integ \u003d 1;\n \t}\n \t\n+\tqi \u003d q.indexOf(\u0022?task\u003d\u0022);\n+\tif (qi !\u003d -1) {\n+\t\t/*\n+\t\t * it's a sai task details page\n+\t\t */\n+\t\ts +\u003d \u0022/specific?task\u003d\u0022 + q.substring(qi + 6);\n+\t}\n+\t\n \tvar s1 \u003d get_appropriate_ws_url() + \u0022/sai/browse\u0022 + s;\n //\tif (s1.split(\u0022?\u0022))\n //\ts1 \u003d s1.split(\u0022?\u0022)[0];\n@@ -919,15 +1014,15 @@ function ws_open_sai()\n \t\t\t \t \u0022\u005c\u0022com.warmcat.sai.taskinfo\u005c\u0022}\u0022);\n \t\t};\n \n-\t\tsai.onmessage \u003dfunction got_packet(msg) {\n+\t\tsai.onmessage \u003d function got_packet(msg) {\n \t\t\tvar u, ci, n;\n \t\t\tvar now_ut \u003d Math.round((new Date().getTime() / 1000));\n \n-\t\t\tconsole.log(msg.data);\n+\t\t//\tconsole.log(msg.data);\n \t\t//\tif (msg.data.length \u003c 10)\n \t\t//\t\treturn;\n \t\t\tjso \u003d JSON.parse(msg.data);\n-\t\t\tconsole.log(jso.schema);\n+\t\t//\tconsole.log(jso.schema);\n \t\t\t\n \t\t\tif (jso.alang) {\n \t\t\t\tvar a \u003d jso.alang.split(\u0022,\u0022), n;\n@@ -976,6 +1071,11 @@ function ws_open_sai()\n \t\t\t}\n \t\t\t\n \t\t\tif (jso.schema \u003d\u003d \u0022sai.warmcat.com.overview\u0022) {\n+\t\t\t\t/*\n+\t\t\t\t * Sent with an array of e[] to start, but also\n+\t\t\t\t * can send a single e[] if it just changed\n+\t\t\t\t * state\n+\t\t\t\t */ \n \t\t\t\ts \u003d \u0022\u003ctable\u003e\u0022;\n \t\t\t\t\n \t\t\t\tauthd \u003d jso.authorized;\n@@ -1001,22 +1101,89 @@ function ws_open_sai()\n \t\t\t\t\t\t\t\tsan(auth_user) + \u0022 \u0022 + agify(now_ut, now_ut + jso.auth_secs);\n \t\t\t\t\t}\n \t\t\t\t}\n+\t\t\t\t\n+\t\t\t\t/*\n+\t\t\t\t * Update existing?\n+\t\t\t\t */\n \t\t\t\n-\t\t\t\tif (jso.overview.length)\n-\t\t\t\t\tfor (n \u003d jso.overview.length - 1; n \u003e\u003d 0; n--)\n-\t\t\t\t\t\ts +\u003d sai_event_render(jso.overview[n], now_ut, 1);\n+\t\t\t\t// console.log(\u0022jso.overview.length \u0022 + jso.overview.length);\n \t\t\t\t\n-\t\t\t\ts \u003d s + \u0022\u003c/table\u003e\u0022;\n+\t\t\t\tif (jso.overview.length \u003d\u003d 1 \u0026\u0026\n+\t\t\t\t document.getElementById(\u0022esr-\u0022 + jso.overview[0].e.uuid)) {\n+\t\t\t\t\t/* this is just the summary box, not the tasks */\n+\t\t\t\t\tdocument.getElementById(\u0022esr-\u0022 + jso.overview[0].e.uuid).innerHTML \u003d\n+\t\t\t\t\t\tsai_event_summary_render(jso.overview[0], now_ut, 1);\n+\t\t\t\t\t\t\n+\t\t\t\t\t/* if the task status icons exist, update their state */\n+\t\t\t\t\t\t\n+\t\t\t\t\tfor (n \u003d jso.overview[0].t.length - 1; n \u003e\u003d 0; n--)\n+\t\t\t\t\t\trefresh_state(jso.overview[0].t[n].uuid, jso.overview[0].t[n].state);\n+\t\t\t\t\t\n+\t\t\t\t\tvar sumbs \u003d document.getElementById(\u0022sumbs-\u0022 + jso.overview[0].e.uuid);\n+\t\t\t\t\tif (sumbs)\n+\t\t\t\t\t\tsumbs.innerHTML \u003d summarize_build_situation(jso.overview[0].e.uuid);\n+\t\t\t\t\t\t\n+\t\t\t\t\taging();\n+\t\t\t\t} else\n+\t\t\t\t{\n+\t\t\t\t\t/*\n+\t\t\t\t\t * display events wholesale\n+\t\t\t\t\t */\n+\t\t\t\t\tif (jso.overview.length) {\n+\t\t\t\t\t\tfor (n \u003d jso.overview.length - 1; n \u003e\u003d 0; n--)\n+\t\t\t\t\t\t\ts +\u003d sai_event_render(jso.overview[n], now_ut, 1);\n \t\t\t\t\n-\t\t\t\tif (document.getElementById(\u0022sai_sticky\u0022))\n-\t\t\t\t\tdocument.getElementById(\u0022sai_sticky\u0022).innerHTML \u003d s;\n-\t\t\t\taging();\n+\t\t\t\t\t\ts \u003d s + \u0022\u003c/table\u003e\u0022;\n+\t\t\t\t\t\n+\t\t\t\t\t\tif (document.getElementById(\u0022sai_sticky\u0022))\n+\t\t\t\t\t\t\tdocument.getElementById(\u0022sai_sticky\u0022).innerHTML \u003d s;\n+\t\t\t\t\t\t\n+\t\t\t\t\t\tfor (n \u003d jso.overview.length - 1; n \u003e\u003d 0; n--) {\n+\t\t\t\t\t\t\tdocument.getElementById(\u0022esr-\u0022 + jso.overview[n].e.uuid).innerHTML \u003d\n+\t\t\t\t\t\t\t\tsai_event_summary_render(jso.overview[n], now_ut, 1);\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\t\n+\t\t\t\t\t\tif (jso.overview[0] \u0026\u0026 jso.overview[0].e) {\n+\t\t\t\t\t\t\tvar sumbs \u003d document.getElementById(\u0022sumbs-\u0022 + jso.overview[0].e.uuid);\n+\t\t\t\t\t\t\tif (sumbs)\n+\t\t\t\t\t\t\t\tsumbs.innerHTML \u003d summarize_build_situation(jso.overview[0].e.uuid);\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\taging();\n+\t\t\t\t\t}\n+\t\t\t\t\t\t\n+\t\t\t\t\tif (gitohashi_integ \u0026\u0026 document.getElementById(\u0022gitohashi_sai_icon\u0022)) {\n+\t\t\t\t\t\tvar integ_state \u003d 0;\n+\t\t\t\t\t\tdocument.getElementById(\u0022gitohashi_sai_icon\u0022).addEventListener(\u0022mouseenter\u0022, function( event ) {\n+\t\t\t\t\t\t\tdocument.getElementById(\u0022gitohashi_sai_icon\u0022).style.zIndex \u003d 1999; \n+\t\t\t\t\t\t\tdocument.getElementById(\u0022gitohashi_sai_details\u0022).style.zIndex \u003d 2000;\n+\t\t\t\t\t\t\tdocument.getElementById(\u0022gitohashi_sai_details\u0022).style.opacity \u003d 1.0; \n+\t\t\t\t\t\t\tinteg_state \u003d 1;\n+\t\t\t\t\t\t}, false);\n+\n+\t\t\t\t\t\tdocument.getElementById(\u0022gitohashi_sai_details\u0022).addEventListener(\u0022mouseout\u0022, function( event ) {\n+\t\t\t\t\t\t\tvar e \u003d event.toElement || event.relatedTarget;\n+\t\t\t\t\t\t\twhile (e \u0026\u0026 e.parentNode \u0026\u0026 e.parentNode !\u003d window) {\n+\t\t\t\t\t\t\t if (e.parentNode \u003d\u003d this || e \u003d\u003d this) {\n+\t\t\t\t\t\t\t if (e.preventDefault)\n+\t\t\t\t\t\t\t\t\te.preventDefault();\n+\t\t\t\t\t\t\t return false;\n+\t\t\t\t\t\t\t }\n+\t\t\t\t\t\t\t e \u003d e.parentNode;\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t\tdocument.getElementById(\u0022gitohashi_sai_details\u0022).style.opacity \u003d 0.0;\n+\t\t\t\t\t\t\tdocument.getElementById(\u0022gitohashi_sai_details\u0022).style.zIndex \u003d -1;\n+\t\t\t\t\t\t \tdocument.getElementById(\u0022gitohashi_sai_icon\u0022).style.zIndex \u003d 2001;\n+\t\t\t\t\t\t}, true);\n+\t\t\t\t\t\t\n+\t\t\t\t\t\taging();\n+\t\t\t\t\t}\n+\t\t\t\t}\n \t\t\t\t\n \t\t\t\tif (jso.overview.length)\n \t\t\t\t\tfor (n \u003d jso.overview.length - 1; n \u003e\u003d 0; n--) {\n \t\t\t\t\t\tif (document.getElementById(\u0022rebuild-ev-\u0022 + san(jso.overview[n].e.uuid)))\n-\t\t\t\t\t\tdocument.getElementById(\u0022rebuild-ev-\u0022 + san(jso.overview[n].e.uuid)).\n-\t\t\t\t\taddEventListener(\u0022click\u0022, function(e) {\n+\t\t\t\t\t\t\tdocument.getElementById(\u0022rebuild-ev-\u0022 + san(jso.overview[n].e.uuid)).\n+\t\t\t\t\t\t\t\taddEventListener(\u0022click\u0022, function(e) {\n \t\t\t\t\tconsole.log(e);\n \t\t\t\t\t\tvar rs\u003d \u0022{\u005c\u0022schema\u005c\u0022:\u0022 +\n \t\t\t\t \t \t \u0022\u005c\u0022com.warmcat.sai.eventreset\u005c\u0022,\u0022 +\n@@ -1028,7 +1195,7 @@ function ws_open_sai()\n \t\t\t\t\t});\n \t\t\t\t\tif (document.getElementById(\u0022delete-ev-\u0022 + san(jso.overview[n].e.uuid)))\n \t\t\t\t\t\tdocument.getElementById(\u0022delete-ev-\u0022 + san(jso.overview[n].e.uuid)).\n-\t\t\t\t\taddEventListener(\u0022click\u0022, function(e) {\n+\t\t\t\t\t\t\taddEventListener(\u0022click\u0022, function(e) {\n \t\t\t\t\tconsole.log(e);\n \t\t\t\t\t\tvar rs\u003d \u0022{\u005c\u0022schema\u005c\u0022:\u0022 +\n \t\t\t\t \t \t \u0022\u005c\u0022com.warmcat.sai.eventdelete\u005c\u0022,\u0022 +\n@@ -1037,6 +1204,7 @@ function ws_open_sai()\n \t\t\t\t \t \t \t\n \t\t\t\t \t \tconsole.log(rs);\n \t\t\t\t \t \tsai.send(rs);\n+\t\t\t\t\t\tsetTimeout(after_delete, 750);\n \t\t\t\t\t});\n \t\t\t\t} \n \t\t\t}\n@@ -1066,57 +1234,118 @@ function ws_open_sai()\n \t\t\t\t\t\t\t\tsan(auth_user) + \u0022 \u0022 + agify(now_ut, now_ut + jso.auth_secs);\n \t\t\t\t\t}\n \t\t\t\t}\n-\t\t\t\n-\t\t\t\ts \u003d sai_taskinfo_render(jso);\n-\t\t\t\t\t\tif (document.getElementById(\u0022sai_sticky\u0022))\n-\t\t\t\tdocument.getElementById(\u0022sai_sticky\u0022).innerHTML \u003d s;\n \t\t\t\t\n+\t\t\t\t/*\n+\t\t\t\t * We get told about changes to any task state,\n+\t\t\t\t * it's up to us to figure out if the page we\n+\t\t\t\t * showed should display the update and in what\n+\t\t\t\t * form.\n+\t\t\t\t *\n+\t\t\t\t * We make sure the div containing the task info\n+\t\t\t\t * has a special ID depending on if it's shown\n+\t\t\t\t * as a tuple or as extended info\n+\t\t\t\t *\n+\t\t\t\t * First see if it appears as a tuple, and if\n+\t\t\t\t * so, let's just update that\n+\t\t\t\t */\n \t\t\t\t\n-\t\ts \u003d \u0022\u003ctable\u003e\u003ctd colspan\u003d\u005c\u00223\u005c\u0022\u003e\u003cpre\u003e\u003ctable\u003e\u003ctr\u003e\u0022 +\n-\t\t\u0022\u003ctd class\u003d\u005c\u0022atop\u005c\u0022\u003e\u0022 +\n-\t\t\u0022\u003cdiv id\u003d\u005c\u0022dlogsn\u005c\u0022 class\u003d\u005c\u0022dlogsn\u005c\u0022\u003e\u0022 + lines + \u0022\u003c/div\u003e\u003c/td\u003e\u0022 +\n-\t\t\u0022\u003ctd class\u003d\u005c\u0022atop\u005c\u0022\u003e\u0022 +\n-\t\t\u0022\u003cdiv id\u003d\u005c\u0022dlogst\u005c\u0022 class\u003d\u005c\u0022dlogst\u005c\u0022\u003e\u0022 + times + \u0022\u003c/div\u003e\u003c/td\u003e\u0022 +\n-\t \u0022\u003ctd class\u003d\u005c\u0022atop\u005c\u0022\u003e\u003cdiv id\u003d\u005c\u0022dlogs\u005c\u0022 class\u003d\u005c\u0022dlogs\u005c\u0022\u003e\u0022 +\n-\t \u0022\u003cspan id\u003d\u005c\u0022logs\u005c\u0022 class\u003d\u005c\u0022nowrap\u005c\u0022\u003e\u0022 + logs +\n-\t \t\u0022\u003c/span\u003e\u0022+\n-\t\t\u0022\u003c/div\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\u003c/pre\u003e\u0022;\n+\t\t\t\tif (document.getElementById(\u0022taskstate_\u0022 + jso.t.uuid)) {\n+\t\t\t\t\tconsole.log(\u0022found taskstate_\u0022 + jso.t.uuid);\n+\t\t\t\t\trefresh_state(jso.t.uuid, jso.t.state);\n+\t\t\t\t\t\n+\t\t\t\t\tvar sumbs \u003d document.getElementById(\u0022sumbs-\u0022 + jso.t.uuid.substring(0, 32));\n+\t\t\t\t\tif (sumbs)\n+\t\t\t\t\t\tsumbs.innerHTML \u003d summarize_build_situation(jso.t.uuid.substring(0, 32));\n+\n+\t\t\t\t} else\n \t\t\t\t\n-\t\t\t\tif (document.getElementById(\u0022sai_overview\u0022))\n-\t\t\t\tdocument.getElementById(\u0022sai_overview\u0022).innerHTML \u003d s;\n-\n-\t\t\t\tif (document.getElementById(\u0022rebuild-\u0022 + san(jso.t.uuid))) {\n-\t\t\t\t\tdocument.getElementById(\u0022rebuild-\u0022 + san(jso.t.uuid)).\n-\t\t\t\t\t\taddEventListener(\u0022click\u0022, function(e) {\n-\t\t\t\t\t\t\tvar rs\u003d \u0022{\u005c\u0022schema\u005c\u0022:\u0022 +\n-\t\t\t\t\t \t \t \u0022\u005c\u0022com.warmcat.sai.taskreset\u005c\u0022,\u0022 +\n-\t\t\t\t\t \t \t \u0022\u005c\u0022uuid\u005c\u0022: \u0022 +\n-\t\t\t\t\t \t \t \tJSON.stringify(san(e.srcElement.id.substring(8))) + \u0022}\u0022;\n-\t\t\t\t\t \t \t \t\n-\t\t\t\t\t \t \tconsole.log(rs);\n-\t\t\t\t\t \t \tsai.send(rs);\n-\t\t\t\t\t \t \tdocument.getElementById(\u0022dlogsn\u0022).innerHTML \u003d \u0022\u0022;\n-\t\t\t\t\t \t \tdocument.getElementById(\u0022dlogst\u0022).innerHTML \u003d \u0022\u0022;\n-\t\t\t\t\t \t \tdocument.getElementById(\u0022logs\u0022).innerHTML \u003d \u0022\u0022;\n-\t\t\t\t\t \t \tlines \u003d times \u003d logs \u003d \u0022\u0022;\n-\t\t\t\t\t \t \ttfirst \u003d 0;\n-\t\t\t\t\t \t \tlli \u003d 1;\n-\t\t\t\t\t\t}); \n-\t\t\t\t}\n+\t\t\t\t\t/* update task summary if shown anywhere */\n+\t\t\t\t\t\n+\t\t\t\t\tif (document.getElementById(\u0022taskinfo-\u0022 + jso.t.uuid)) {\n+\t\t\t\t\t\tconsole.log(\u0022FOUND taskinfo-\u0022 + jso.t.uuid);\n+\t\t\t\t\t\tdocument.getElementById(\u0022taskinfo-\u0022 + jso.t.uuid).innerHTML \u003d sai_taskinfo_render(jso);\n+\t\t\t\t\t\tif (document.getElementById(\u0022esr-\u0022 + jso.e.uuid))\n+\t\t\t\t\t\t\tdocument.getElementById(\u0022esr-\u0022 + jso.e.uuid).innerHTML \u003d\n+\t\t\t\t\t\t\t\tsai_event_summary_render(jso.e, now_ut, 1);\n+\t\t\t\t\t} else {\n+\t\t\t\t\t\t\n+\t\t\t\t\t\tconsole.log(\u0022NO taskinfo- or taskstate_\u0022 + jso.t.uuid);\n+\t\t\t\t\t\t\n+\t\t\t\t\t\t/* \n+\t\t\t\t\t\t * Last chance if we might be\n+\t\t\t\t\t\t * on a task-specific page, and\n+\t\t\t\t\t\t * want to show the task info\n+\t\t\t\t\t\t * at the top\n+\t\t\t\t\t\t */\n+\t\t\t\t\t\n+\n+\t\t\t\t\t\tconst urlParams \u003d new URLSearchParams(window.location.search);\n+\t\t\t\t\t\tconst url_task_uuid \u003d urlParams.get('task');\n+\t\t\t\t\t\t\n+\t\t\t\t\t\tif (url_task_uuid \u003d\u003d\u003d jso.t.uuid \u0026\u0026\n+\t\t\t\t\t\t document.getElementById(\u0022sai_sticky\u0022))\n+\t\t\t\t\t\t\tdocument.getElementById(\u0022sai_sticky\u0022).innerHTML \u003d\n+\t\t\t\t\t\t\t\t\u0022\u003cdiv class\u003d\u005c\u0022taskinfo\u005c\u0022 id\u003d\u005c\u0022taskinfo-\u0022 +\n+\t\t\t\t\t\t\t\tsan(jso.t.uuid) + \u0022\u005c\u0022\u003e\u0022 +\n+\t\t\t\t\t\t\t\tsai_taskinfo_render(jso) +\n+\t\t\t\t\t\t\t\t\u0022\u003c/div\u003e\u0022;\n+\t\t\t\t\t\n \t\t\t\t\n-\t\t\t\tif (document.getElementById(\u0022stop-\u0022 + san(jso.t.uuid))) {\n-\t\t\t\t\tdocument.getElementById(\u0022stop-\u0022 + san(jso.t.uuid)).\n-\t\t\t\t\t\taddEventListener(\u0022click\u0022, function(e) {\n-\t\t\t\t\t\t\tvar rs\u003d \u0022{\u005c\u0022schema\u005c\u0022:\u0022 +\n-\t\t\t\t\t \t \t \u0022\u005c\u0022com.warmcat.sai.taskcan\u005c\u0022,\u0022 +\n-\t\t\t\t\t \t \t \u0022\u005c\u0022task_uuid\u005c\u0022: \u0022 +\n-\t\t\t\t\t \t \t \tJSON.stringify(san(e.srcElement.id.substring(5))) + \u0022}\u0022;\n-\t\t\t\t\t \t \t console.log(rs);\n-\t\t\t\t\t \t \tsai.send(rs);\n-\t\t\t\t\t\t}); \n+\t\t\t\t\t\ts \u003d \u0022\u003ctable\u003e\u003ctd colspan\u003d\u005c\u00223\u005c\u0022\u003e\u003cpre\u003e\u003ctable class\u003d\u005c\u0022scrollogs\u005c\u0022\u003e\u003ctr\u003e\u0022 +\n+\t\t\t\t\t\t\u0022\u003ctd class\u003d\u005c\u0022atop\u005c\u0022\u003e\u0022 +\n+\t\t\t\t\t\t\u0022\u003cdiv id\u003d\u005c\u0022dlogsn\u005c\u0022 class\u003d\u005c\u0022dlogsn\u005c\u0022\u003e\u0022 + lines + \u0022\u003c/div\u003e\u003c/td\u003e\u0022 +\n+\t\t\t\t\t\t\u0022\u003ctd class\u003d\u005c\u0022atop\u005c\u0022\u003e\u0022 +\n+\t\t\t\t\t\t\u0022\u003cdiv id\u003d\u005c\u0022dlogst\u005c\u0022 class\u003d\u005c\u0022dlogst\u005c\u0022\u003e\u0022 + times + \u0022\u003c/div\u003e\u003c/td\u003e\u0022 +\n+\t\t\t\t\t \u0022\u003ctd class\u003d\u005c\u0022atop\u005c\u0022\u003e\u003cdiv id\u003d\u005c\u0022dlogs\u005c\u0022 class\u003d\u005c\u0022dlogs\u005c\u0022\u003e\u0022 +\n+\t\t\t\t\t \u0022\u003cspan id\u003d\u005c\u0022logs\u005c\u0022 class\u003d\u005c\u0022nowrap\u005c\u0022\u003e\u0022 + logs +\n+\t\t\t\t\t \t\u0022\u003c/span\u003e\u0022+\n+\t\t\t\t\t\t\u0022\u003c/div\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\u003c/pre\u003e\u0022;\n+\t\t\t\t\t\n+\t\t\t\t\tif (document.getElementById(\u0022sai_overview\u0022)) {\n+\t\t\t\t\t\tdocument.getElementById(\u0022sai_overview\u0022).innerHTML \u003d s;\n+\t\t\t\t\t\n+\t\t\t\t\t\tif (document.getElementById(\u0022esr-\u0022 + jso.e.uuid))\n+\t\t\t\t\t\t\tdocument.getElementById(\u0022esr-\u0022 + jso.e.uuid).innerHTML \u003d\n+\t\t\t\t\t\t\t\tsai_event_summary_render(jso, now_ut, 1);\n+\t\t\t\t\t\t\t\t\n+\t\t\t\t\t\tvar sumbs \u003d document.getElementById(\u0022sumbs-\u0022 + jso.e.uuid);\n+\t\t\t\t\t\tif (sumbs)\n+\t\t\t\t\t\t\tsumbs.innerHTML \u003d summarize_build_situation(jso.e.uuid);\n+\t\t\t\t\t}\n+\t\n+\t\t\t\t\tif (document.getElementById(\u0022rebuild-\u0022 + san(jso.t.uuid))) {\n+\t\t\t\t\t\tdocument.getElementById(\u0022rebuild-\u0022 + san(jso.t.uuid)).\n+\t\t\t\t\t\t\taddEventListener(\u0022click\u0022, function(e) {\n+\t\t\t\t\t\t\t\tvar rs\u003d \u0022{\u005c\u0022schema\u005c\u0022:\u0022 +\n+\t\t\t\t\t\t \t \t \u0022\u005c\u0022com.warmcat.sai.taskreset\u005c\u0022,\u0022 +\n+\t\t\t\t\t\t \t \t \u0022\u005c\u0022uuid\u005c\u0022: \u0022 +\n+\t\t\t\t\t\t \t \t \tJSON.stringify(san(e.srcElement.id.substring(8))) + \u0022}\u0022;\n+\t\t\t\t\t\t \t \t \t\n+\t\t\t\t\t\t \t \tconsole.log(rs);\n+\t\t\t\t\t\t \t \tsai.send(rs);\n+\t\t\t\t\t\t \t \tdocument.getElementById(\u0022dlogsn\u0022).innerHTML \u003d \u0022\u0022;\n+\t\t\t\t\t\t \t \tdocument.getElementById(\u0022dlogst\u0022).innerHTML \u003d \u0022\u0022;\n+\t\t\t\t\t\t \t \tdocument.getElementById(\u0022logs\u0022).innerHTML \u003d \u0022\u0022;\n+\t\t\t\t\t\t \t \tlines \u003d times \u003d logs \u003d \u0022\u0022;\n+\t\t\t\t\t\t \t \ttfirst \u003d 0;\n+\t\t\t\t\t\t \t \tlli \u003d 1;\n+\t\t\t\t\t\t\t}); \n+\t\t\t\t\t}\n+\t\t\t\t\t\n+\t\t\t\t\tif (document.getElementById(\u0022stop-\u0022 + san(jso.t.uuid))) {\n+\t\t\t\t\t\tdocument.getElementById(\u0022stop-\u0022 + san(jso.t.uuid)).\n+\t\t\t\t\t\t\taddEventListener(\u0022click\u0022, function(e) {\n+\t\t\t\t\t\t\t\tvar rs\u003d \u0022{\u005c\u0022schema\u005c\u0022:\u0022 +\n+\t\t\t\t\t\t \t \t \u0022\u005c\u0022com.warmcat.sai.taskcan\u005c\u0022,\u0022 +\n+\t\t\t\t\t\t \t \t \u0022\u005c\u0022task_uuid\u005c\u0022: \u0022 +\n+\t\t\t\t\t\t \t \t \tJSON.stringify(san(e.srcElement.id.substring(5))) + \u0022}\u0022;\n+\t\t\t\t\t\t \t \t console.log(rs);\n+\t\t\t\t\t\t \t \tsai.send(rs);\n+\t\t\t\t\t\t\t}); \n+\t\t\t\t\t}\n+\t\t\t\t\t\t\t\t\t\n+\t\t\t\t\taging();\n \t\t\t\t}\n-\t\t\t\t\n-\t\t\t\taging();\n \t\t\t}\n \t\t\t\n \t\t\t\n@@ -1222,6 +1451,44 @@ function ws_open_sai()\n \t}\n }\n \n+function post_login_form()\n+{\n+\tvar xhr \u003d new XMLHttpRequest(), s \u003d\u0022\u0022, q \u003d window.location.pathname;\n+\t\n+\ts \u003d \u0022----boundo\u005cx0d\u005cx0acontent-disposition: form-data; name\u003d\u005c\u0022lname\u005c\u0022\u005cx0d\u005cx0a\u005cx0d\u005cx0a\u0022 +\n+\t\tdocument.getElementById(\u0022lname\u0022).value +\n+\t \u0022\u005cx0d\u005cx0a----boundo\u005cx0d\u005cx0acontent-disposition: form-data; name\u003d\u005c\u0022lpass\u005c\u0022\u005cx0d\u005cx0a\u005cx0d\u005cx0a\u0022 +\n+\t\tdocument.getElementById(\u0022lpass\u0022).value + \n+\t \u0022\u005cx0d\u005cx0a----boundo\u005cx0d\u005cx0acontent-disposition: form-data; name\u003d\u005c\u0022success_redir\u005c\u0022\u005cx0d\u005cx0a\u005cx0d\u005cx0a\u0022 +\n+\t\tdocument.getElementById(\u0022success_redir\u0022).value +\n+\t \u0022\u005cx0d\u005cx0a----boundo--\u0022;\n+\n+\tif (q.length \u003e 10 \u0026\u0026 q.substring(q.length - 10) \u003d\u003d \u0022index.html\u0022)\n+\t\tq \u003d q.substring(0, q.length - 10);\n+\txhr.open(\u0022POST\u0022, q + \u0022login\u0022, true);\n+\txhr.setRequestHeader( 'content-type', \u0022multipart/form-data; boundary\u003d--boundo\u0022);\n+\t\n+\tconsole.log(s.length +\u0022 \u0022 + s);\n+\n+\txhr.onload \u003d function (e) {\n+\t if (xhr.readyState \u003d\u003d\u003d 4) {\n+\t if (xhr.status \u003d\u003d\u003d 200 || xhr.status \u003d\u003d 303) {\n+\t console.log(xhr.responseText);\n+\t\tlocation.reload();\n+\t } else {\n+\t console.error(xhr.statusText);\n+\t }\n+\t }\n+\t};\n+\txhr.onerror \u003d function (e) {\n+\t console.error(xhr.statusText);\n+\t};\n+\n+\txhr.send(s);\n+\t\n+\treturn false;\n+}\n+\n /* stuff that has to be delayed until all the page assets are loaded */\n \n window.addEventListener(\u0022load\u0022, function() {\n@@ -1235,7 +1502,11 @@ window.addEventListener(\u0022load\u0022, function() {\n \t\t\twindow.location.href;\n \tws_open_sai();\n \taging();\n-\t\n+\n+\tif (document.getElementById(\u0022login\u0022)) {\n+\t\tdocument.getElementById(\u0022login\u0022).addEventListener(\u0022click\u0022, post_login_form);\n+\t\tdocument.getElementById(\u0022logout\u0022).addEventListener(\u0022click\u0022, post_login_form);\n+\t}\n \t\n \tsetInterval(function() {\n \ndiff --git a/etc-sai-EXAMPLE/builder/conf b/etc-sai-EXAMPLE/builder/conf\nindex b371b51..c6a9179 100644\n--- a/etc-sai-EXAMPLE/builder/conf\n+++ b/etc-sai-EXAMPLE/builder/conf\n@@ -1,42 +1,21 @@\n {\n-\t#\n-\t# where to find the nspawn contexts (scanned at runtime)\n-\t#\n-\t\u0022systemd-nspawn\u0022: {\n-\t\t\u0022path\u0022: \t\u0022/home/sai/sai-nspawn\u0022,\n-\t\t\u0022overlaypath\u0022: \t\u0022/home/sai/sai-nspawn\u0022,\n-\t\t\u0022instances\u0022:\t3,\n-\t\t\u0022timeout\u0022:\t1800\n-\t},\n+ \u0022perms\u0022: \u0022sai:nobody\u0022,\n+ \u0022home\u0022: \u0022/home/sai\u0022,\n+ \u0022host\u0022: \u0022bionic-noi\u0022,\n \n-\t#\n-\t# physically connected targets, like microcontroller boards\n-\t#\n-\t\u0022target\u0022:\t[\n+ \u0022platforms\u0022: [\n+ {\n+ \u0022name\u0022: \u0022linux-ubuntu-1804/x86_64-amd/gcc\u0022,\n+ \u0022instances\u0022: 3,\n+ \u0022servers\u0022: [ \u0022wss://libwebsockets.org:4444/sai/builder\u0022 ]\n+ },\n \t\t{\n-\t\t\t\u0022name\u0022:\t\t\u0022optee-hikey-arm64\u0022,\n-\t\t\t\u0022timeout\u0022:\t3600,\n-\t\t\t\u0022consoles\u0022:\t[\n-\t\t\t\t{\n-\t\t\t\t\t\u0022name\u0022:\t\t\u0022dbg\u0022,\n-\t\t\t\t\t\u0022path\u0022:\t\t\u0022/dev/by-id/usb-FTDI_FT232R_USB_UART_A50285BI-if00-port0\u0022,\n-\t\t\t\t\t\u0022baudrate\u0022:\t115200,\n-\t\t\t\t\t\u0022mode\u0022:\t\t\u0022n81\u0022\n-\t\t\t\t}\n-\t\t\t]\n+\t\t\t\u0022name\u0022:\t\t\u0022freertos-linkit/arm32-m4-mt7697-usi/gcc\u0022,\n+\t\t\t\u0022instances\u0022:\t1,\n+\t\t\t\u0022env\u0022:\t[\n+\t\t\t\t{ \u0022SAI_CROSS_BASE\u0022: \u0022/opt/linkit\u0022 }\n+\t\t\t],\n+\t\t\t\u0022servers\u0022: [ \u0022wss://libwebsockets.org:4444/sai/builder\u0022 ]\n \t\t}\n-\t],\n-\n-\t#\n-\t# masters we will accept jobs from\n-\t#\n-\t\u0022masters\u0022: [\n-\t\t{\n-#\t\t\t\u0022url\u0022:\t\t\u0022wss://warmcat.com/sai/builder\u0022,\n-\t\t\t\u0022url\u0022:\t\t\u0022wss://localhost:443/builder\u0022,\n-\t\t\t\u0022selfsigned\u0022:\t1,\n-\t\t\t\u0022priority\u0022:\t1\n-\t\t}\n-\t]\n+ ]\n }\n-\ndiff --git a/etc-sai-EXAMPLE/master/conf b/etc-sai-EXAMPLE/master/conf\ndeleted file mode 100644\nindex 58d027a..0000000\n--- a/etc-sai-EXAMPLE/master/conf\n+++ /dev/null\n@@ -1,14 +0,0 @@\n-# these are the sai server global settings\n-#\n-# stuff related to each vhosts should go in one\n-# file per vhost in ./conf.d/\n-\n-{\n- \u0022global\u0022: {\n- \u0022uid\u0022: \u002248\u0022, # after init, run as apache user\n- \u0022gid\u0022: \u002248\u0022, # after init, run as apache group\n- \u0022server-string\u0022: \u0022sai\u0022,\n- \u0022init-ssl\u0022: \u0022yes\u0022\n- }\n-}\n-\ndiff --git a/etc-sai-EXAMPLE/master/conf.d/localhost b/etc-sai-EXAMPLE/master/conf.d/localhost\ndeleted file mode 100644\nindex 7ccda44..0000000\n--- a/etc-sai-EXAMPLE/master/conf.d/localhost\n+++ /dev/null\n@@ -1,127 +0,0 @@\n-# you should create one file like this per vhost in /etc/sai/conf.d\n-\n-{\n-\t\u0022vhosts\u0022: [{\n-\t\t# this should match the external hostname for the vhost,\n-\t\t# like xyz.com\n-\t\t\u0022name\u0022: \u0022localhost\u0022,\n-\t\t# multiple vhosts can occupy the same port (SNI is used\n-\t\t# to resolve)\n-\t\t\u0022port\u0022: \u0022443\u0022,\n-\t\t# required for avatar cache\n-\t\t\u0022enable-client-ssl\u0022: \u0022on\u0022,\n-\t\t# \u0022ipv6\u0022: \u0022on\u0022,\n-\t\t\u0022interface\u0022: \u0022lo\u0022,\n-\n-#\t\t\u0022ciphers\u0022: \u0022DEFAULT\u0022,\n-#\t\t\u0022tls13-ciphers\u0022: \u0022DEFAULT\u0022,\n-\n-\t\t# You should store your real certificate key somewhere safer,\n-\t\t# eg on Fedora /etc/pki/tls/private, readable only by root. But\n-\t\t# since the right place for this differs by distro and these\n-\t\t# particular certs are not valuable, we get them from\n-\t\t# /usr/local/share where they are installed.\n-\n-\t\t\u0022host-ssl-key\u0022: \u0022/usr/local/share/sai/sai-selfsigned-key.pem\u0022,\n-\t\t\u0022host-ssl-cert\u0022: \u0022/usr/local/share/sai/sai-selfsigned-cert.pem\u0022,\n-\n-\t\t\u0022mounts\u0022: [\n-\t\t\t{\n-\t\t\t\t# static assets like js, css, fonts\n-\t\t\t\t\u0022mountpoint\u0022:\t\t\u0022/sai\u0022,\n-\t\t\t\t\u0022default\u0022:\t\t\u0022index.html\u0022,\n-\t\t\t\t\u0022origin\u0022:\t\t\u0022file:///usr/local/share/sai/assets\u0022,\n-\t\t\t\t\u0022cache-max-age\u0022: \t\u00227200\u0022,\n-\t\t\t\t\u0022cache-reuse\u0022:\t\t\u00221\u0022,\n-\t\t\t\t\u0022cache-revalidate\u0022:\t\u00220\u0022,\n-\t\t\t\t\u0022cache-intermediaries\u0022:\t\u00220\u0022,\n-\t \t\t\t\u0022extra-mimetypes\u0022: {\n-\t\t\t\t\t\u0022.zip\u0022: \u0022application/zip\u0022,\n-\t\t\t\t\t\u0022.map\u0022: \u0022application/json\u0022,\n-\t\t\t\t\t\u0022.ttf\u0022: \u0022application/x-font-ttf\u0022\n-\t\t\t\t}\n-\t\t\t},\n-\t\t\t{\n-\t\t\t\t# semi-static cached avatar icons\n-\t\t\t\t\u0022mountpoint\u0022: \u0022/sai/avatar\u0022,\n-\t\t\t\t\u0022origin\u0022: \u0022callback://avatar-proxy\u0022,\n-\t\t\t\t\u0022cache-max-age\u0022: \u00222400\u0022,\n-\t\t\t\t\u0022cache-reuse\u0022: \u00221\u0022,\n-\t\t\t\t\u0022cache-revalidate\u0022: \u00220\u0022,\n-\t\t\t\t\u0022cache-intermediaries\u0022: \u00220\u0022\n-\t\t\t}\n-\t\t],\n-\t\t\n-\t\t#\n-\t\t# these headers, which will be sent on every transaction, make\n-\t\t# recent browsers definitively ban any external script sources,\n-\t\t# no matter what might manage to get injected later in the\n-\t\t# page. It's a last line of defence against any successful XSS.\n-\t\t#\n-\t\t\n-\t\t\u0022headers\u0022: [{\n-\t\t \u0022content-security-policy\u0022: \u0022default-src 'none'; img-src 'self' data: https://travis-ci.org https://api.travis-ci.org https://ci.appveyor.com https://scan.coverity.com; script-src 'self'; font-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none';\u0022,\n-\t\t \u0022x-content-type-options\u0022: \u0022nosniff\u0022,\n-\t\t \u0022x-xss-protection\u0022: \u00221; mode\u003dblock\u0022,\n-\t\t \u0022x-frame-options\u0022: \u0022deny\u0022,\n-\t\t \u0022referrer-policy\u0022: \u0022no-referrer\u0022\n-\t\t}],\n-\n-\t\t\u0022ws-protocols\u0022: [\n-\n-\t\t{\u0022com-warmcat-sai\u0022: {\n-\t\t\t# template HTML to use for this vhost. You'd normally\n-\t\t\t# copy this to gitohashi-vhostname.html and modify it\n-\t\t\t# to show the content, logos, links, fonts, css etc for\n-\t\t\t# your vhost. It's not served directly but read from\n-\t\t\t# the filesystem. Although it's cached in memory by\n-\t\t\t# gitohashi, it checks for changes and reloads if\n-\t\t\t# changed automatically.\n-\t\t\t#\n-\t\t\t# Putting logos etc as svg in css is highly recommended,\n-\t\t\t# like fonts these can be transferred once with a loose\n-\t\t\t# caching policy. So in practice they cost very little,\n-\t\t\t# and allow the browser to compose the page without\n-\t\t\t# delay.\n-\t\t\t#\n-\t\t\t\u0022html-file\u0022:\t \u0022/usr/local/share/gitohashi/templates/gitohashi-example.html\u0022,\n-\t\t\t#\n-\t\t\t# vpath required at start of links into this vhost's\n-\t\t\t# gitohashi content for example if an external http\n-\t\t\t# server is proxying us, and has been told to direct\n-\t\t\t# URLs starting \u0022/git\u0022 to us, this should be set to\n-\t\t\t# \u0022/git/\u0022 so URLs we generate referring to our own pages\n-\t\t\t# can work.\n-\t\t\t#\n-\t\t\t\u0022vpath\u0022:\t \u0022/sai/\u0022,\n-\t\t\t#\n-\t\t\t# url mountpoint for the avatar cache\n-\t\t\t#\n-\t\t\t\u0022avatar-url\u0022:\t \u0022/sai/avatar/\u0022,\n-\t\t\t#\n-\t\t\t# libjsgit2 JSON cache... this\n-\t\t\t# should not be directly served\n-\t\t\t#\n-\t\t\t\u0022cache-base\u0022:\t\u0022/var/cache/libjsongit2\u0022,\n-\t\t\t#\n-\t\t\t# restrict the JSON cache size\n-\t\t\t# to 2GB\n-\t\t\t#\n-\t\t\t\u0022cache-size\u0022: \u00222000000000\u0022,\n-\t\t\t#\n-\t\t\t# optional flags, b0 \u003d 1 \u003d blog mode\n-\t\t\t#\n-\t\t\t\u0022flags\u0022: 0\n-\t\t\t#\u0022blog-repo-name\u0022:\t\u0022myrepo\u0022\n-\t\t},\n-\t\t\u0022avatar-proxy\u0022: {\n-\t\t\t\u0022remote-base\u0022: \u0022https://www.gravatar.com/avatar/\u0022,\n-\t\t\t#\n-\t\t\t# this dir is served via avatar-proxy\n-\t\t\t#\n-\t\t\t\u0022cache-dir\u0022: \u0022/var/cache/libjsongit2\u0022\n-\t\t}}\n-\t\t]\n-\t\t}\n-\t]\n-}\ndiff --git a/etc-sai-EXAMPLE/master/conf.d/unixskt b/etc-sai-EXAMPLE/master/conf.d/unixskt\ndeleted file mode 100644\nindex 2ab6da5..0000000\n--- a/etc-sai-EXAMPLE/master/conf.d/unixskt\n+++ /dev/null\n@@ -1,145 +0,0 @@\n-# you should create one file like this per vhost in /etc/sai/master/conf.d\n-\n-{\n-\t\u0022vhosts\u0022: [{\n-\t\t# this has no special meaning but needs to be unique\n- \u0022name\u0022: \u0022unixskt\u0022,\n- \u0022unix-socket\u0022: 1,\n-\t\t# this should be owned by sai and allow group access from\n-\t\t# whatever group your webserver runs under\n-\t\t\u0022unix-socket-perms\u0022: \u0022sai:apache\u0022,\n- # multiple vhosts can exist with different unix skt names\n-\t\t\u0022interface\u0022:\t \u0022/var/run/sai\u0022,\n-\t\t# required for avatar cache\n- \u0022enable-client-ssl\u0022: \u0022on\u0022,\n-\n-\t\t\u0022mounts\u0022: [\n-\t\t\t{\n-\t\t\t\t\u0022mountpoint\u0022:\t\t\u0022/sai\u0022,\n-\t\t\t\t\u0022default\u0022:\t\t\u0022index.html\u0022,\n-\t\t\t\t\u0022origin\u0022:\t\t\u0022file:///usr/local/share/sai/assets\u0022,\n-\t\t\t\t\u0022origin\u0022:\t\t\u0022callback://com-warmcat-sai\u0022,\n-\t\t\t\t\u0022cache-max-age\u0022: \t\u00227200\u0022,\n-\t\t\t\t\u0022cache-reuse\u0022:\t\t\u00221\u0022,\n-\t\t\t\t\u0022cache-revalidate\u0022:\t\u00220\u0022,\n-\t\t\t\t\u0022cache-intermediaries\u0022:\t\u00220\u0022,\n-\t \t\t\t\u0022extra-mimetypes\u0022: {\n-\t\t\t\t\t\u0022.zip\u0022: \u0022application/zip\u0022,\n-\t\t\t\t\t\u0022.map\u0022: \u0022application/json\u0022,\n-\t\t\t\t\t\u0022.ttf\u0022: \u0022application/x-font-ttf\u0022\n-\t\t\t\t}\n-\t\t\t},\n-\t\t\t{\n-\t\t\t\t# semi-static cached avatar icons\n-\t\t\t\t\u0022mountpoint\u0022: \u0022/sai/avatar\u0022,\n-\t\t\t\t\u0022origin\u0022: \u0022callback://avatar-proxy\u0022,\n-\t\t\t\t\u0022cache-max-age\u0022: \u00222400\u0022,\n-\t\t\t\t\u0022cache-reuse\u0022: \u00221\u0022,\n-\t\t\t\t\u0022cache-revalidate\u0022: \u00220\u0022,\n-\t\t\t\t\u0022cache-intermediaries\u0022: \u00220\u0022\n-\t\t\t}\n-\t\t],\n-\t\t\n-\t\t#\n-\t\t# these headers, which will be sent on every transaction, make\n-\t\t# recent browsers definitively ban any external script sources,\n-\t\t# no matter what might manage to get injected later in the\n-\t\t# page. It's a last line of defence against any successful XSS.\n-\t\t#\n-\t\t\n-\t\t\u0022headers\u0022: [{\n-\t\t \u0022content-security-policy\u0022: \u0022default-src 'none'; img-src 'self' data:; script-src 'self'; font-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none';\u0022,\n-\t\t \u0022x-content-type-options\u0022: \u0022nosniff\u0022,\n-\t\t \u0022x-xss-protection\u0022: \u00221; mode\u003dblock\u0022,\n-\t\t \u0022referrer-policy\u0022: \u0022no-referrer\u0022\n-\t\t}],\n-\n-\t\t\u0022ws-protocols\u0022: [\n-\t\t{\u0022com-warmcat-sai\u0022: {\n-\t\t\t# this is the lhs of the path to use, not a full filepath\n-\t\t\t# the events go in ...-events.sqlite3 and the related\n-\t\t\t# info for an event goes in its own database file in\n-\t\t\t# ...-event-xxxx.sqlite3 where xxxx is the event uuid.\n-\t\t\t#\n-\t\t\t\u0022database\u0022:\t\t\u0022/srv/sai/sai-master\u0022,\n- #\n- # the push hook notification script creates a JSON\n- # object and signs it with a secret... \u0022notification-\n- # key\u0022 should match the secret the notification hook\n- # script has that we're willing to accept.\n- #\n- # It should be a 64-char hex-ascii representation of\n- # a random 32-byte sequence, you can produce a strong\n- # random key in the correct format like this\n- #\n- # dd if\u003d/dev/random bs\u003d32 count\u003d1 | sha256sum | cut -d' ' -f1\n- #\n-\t\t\t\u0022notification-key\u0022:\t\u00221234...5678\u0022,\n-\n-\t\t\t# auth jwk path\n-\t\t\t# You can generate a suitable key like this\n-\t\t\t#\n-\t\t\t# lws-crypto-jwk -t EC -b512 -vP-521 --alg ES512 \u003e mykey.jwk\n-\t\t\t#\n-\t\t\t\u0022jwt-auth-alg\u0022:\t\t\u0022ES512\u0022,\n-\t\t\t\u0022jwt-auth-jwk-path\u0022:\t\u0022/etc/sai/master/auth.jwk\u0022,\n-\n-\t\t\t\u0022jwt-iss\u0022:\t\t\u0022com.warmcat\u0022,\n-\t\t\t\u0022jwt-aud\u0022:\t\t\u0022https://libwebsockets.org/sai\u0022,\n-\n-\t\t\t# template HTML to use for this vhost. You'd normally\n-\t\t\t# copy this to gitohashi-vhostname.html and modify it\n-\t\t\t# to show the content, logos, links, fonts, css etc for\n-\t\t\t# your vhost. It's not served directly but read from\n-\t\t\t# the filesystem. Although it's cached in memory by\n-\t\t\t# gitohashi, it checks for changes and reloads if\n-\t\t\t# changed automatically.\n-\t\t\t#\n-\t\t\t# Putting logos etc as svg in css is highly recommended,\n-\t\t\t# like fonts these can be transferred once with a loose\n-\t\t\t# caching policy. So in practice they cost very little,\n-\t\t\t# and allow the browser to compose the page without\n-\t\t\t# delay.\n-\t\t\t#\n-\t\t\t\u0022html-file\u0022:\t \u0022/usr/local/share/gitohashi/templates/gitohashi-example.html\u0022,\n-\t\t\t#\n-\t\t\t# vpath required at start of links into this vhost's\n-\t\t\t# gitohashi content for example if an external http\n-\t\t\t# server is proxying us, and has been told to direct\n-\t\t\t# URLs starting \u0022/git\u0022 to us, this should be set to\n-\t\t\t# \u0022/git/\u0022 so URLs we generate referring to our own pages\n-\t\t\t# can work.\n-\t\t\t#\n-\t\t\t\u0022vpath\u0022:\t \u0022/sai/\u0022,\n-\t\t\t#\n-\t\t\t# url mountpoint for the avatar cache\n-\t\t\t#\n-\t\t\t\u0022avatar-url\u0022:\t \u0022/sai/avatar/\u0022,\n-\t\t\t#\n-\t\t\t# libjsgit2 JSON cache... this\n-\t\t\t# should not be directly served\n-\t\t\t#\n-\t\t\t\u0022cache-base\u0022:\t\u0022/var/cache/libjsongit2\u0022,\n-\t\t\t#\n-\t\t\t# restrict the JSON cache size\n-\t\t\t# to 2GB\n-\t\t\t#\n-\t\t\t\u0022cache-size\u0022: \u00222000000000\u0022,\n-\t\t\t#\n-\t\t\t# optional flags, b0 \u003d 1 \u003d blog mode\n-\t\t\t#\n-\t\t\t\u0022flags\u0022: 0\n-\t\t\t#\u0022blog-repo-name\u0022:\t\u0022myrepo\u0022\n-\t\t},\n-\t\t\u0022avatar-proxy\u0022: {\n-\t\t\t\u0022remote-base\u0022: \u0022https://www.gravatar.com/avatar/\u0022,\n-\t\t\t#\n-\t\t\t# this dir is served via avatar-proxy\n-\t\t\t#\n-\t\t\t\u0022cache-dir\u0022: \u0022/var/cache/libjsongit2\u0022\n-\t\t}\n-\t\t}\n-\t\t]\n-\t\t}\n-\t]\n-}\ndiff --git a/etc-sai-EXAMPLE/server/conf b/etc-sai-EXAMPLE/server/conf\nnew file mode 100644\nindex 0000000..58d027a\n--- /dev/null\n+++ b/etc-sai-EXAMPLE/server/conf\n@@ -0,0 +1,14 @@\n+# these are the sai server global settings\n+#\n+# stuff related to each vhosts should go in one\n+# file per vhost in ./conf.d/\n+\n+{\n+ \u0022global\u0022: {\n+ \u0022uid\u0022: \u002248\u0022, # after init, run as apache user\n+ \u0022gid\u0022: \u002248\u0022, # after init, run as apache group\n+ \u0022server-string\u0022: \u0022sai\u0022,\n+ \u0022init-ssl\u0022: \u0022yes\u0022\n+ }\n+}\n+\ndiff --git a/etc-sai-EXAMPLE/server/conf.d/mydomain.com b/etc-sai-EXAMPLE/server/conf.d/mydomain.com\nnew file mode 100644\nindex 0000000..95c44ce\n--- /dev/null\n+++ b/etc-sai-EXAMPLE/server/conf.d/mydomain.com\n@@ -0,0 +1,143 @@\n+# you should create one file like this per vhost in /etc/sai/server/conf.d\n+\n+{\n+\t\u0022vhosts\u0022: [{\n+\t\t\u0022name\u0022: \u0022mydomain.com\u0022,\n+\t\t\u0022port\u0022: \u00224444\u0022,\n+\t\t\u0022host-ssl-key\u0022: \u0022/etc/letsencrypt/live/libwebsockets.org/privkey.pem\u0022,\n+\t\t\u0022host-ssl-cert\u0022: \u0022/etc/letsencrypt/live/libwebsockets.org/fullchain.pem\u0022,\n+\t\t\u0022access-log\u0022: \u0022/var/log/sai-server-access-log\u0022,\n+\t\t\u0022disable-no-protocol-ws-upgrades\u0022: \u0022on\u0022,\n+\t\t\u0022enable-client-ssl\u0022: \u0022on\u0022,\n+\t\t\u0022allow-non-tls\u0022: \u00221\u0022,\n+\n+\t\t\u0022mounts\u0022: [\n+\t\t\t{\n+\t\t\t\t\u0022mountpoint\u0022:\t\t\u0022/sai\u0022,\n+\t\t\t\t\u0022default\u0022:\t\t\u0022index.html\u0022,\n+\t\t\t\t\u0022origin\u0022:\t\t\u0022file:///usr/local/share/sai/assets\u0022,\n+\t\t\t\t\u0022origin\u0022:\t\t\u0022callback://com-warmcat-sai\u0022,\n+\t\t\t\t\u0022cache-max-age\u0022: \t\u00227200\u0022,\n+\t\t\t\t\u0022cache-reuse\u0022:\t\t\u00221\u0022,\n+\t\t\t\t\u0022cache-revalidate\u0022:\t\u00220\u0022,\n+\t\t\t\t\u0022cache-intermediaries\u0022:\t\u00220\u0022,\n+\t \t\t\t\u0022extra-mimetypes\u0022: {\n+\t\t\t\t\t\u0022.zip\u0022: \u0022application/zip\u0022,\n+\t\t\t\t\t\u0022.map\u0022: \u0022application/json\u0022,\n+\t\t\t\t\t\u0022.ttf\u0022: \u0022application/x-font-ttf\u0022\n+\t\t\t\t}\n+\t\t\t},\n+\t\t\t{\n+\t\t\t\t# semi-static cached avatar icons\n+\t\t\t\t\u0022mountpoint\u0022: \u0022/sai/avatar\u0022,\n+\t\t\t\t\u0022origin\u0022: \u0022callback://avatar-proxy\u0022,\n+\t\t\t\t\u0022cache-max-age\u0022: \u00222400\u0022,\n+\t\t\t\t\u0022cache-reuse\u0022: \u00221\u0022,\n+\t\t\t\t\u0022cache-revalidate\u0022: \u00220\u0022,\n+\t\t\t\t\u0022cache-intermediaries\u0022: \u00220\u0022\n+\t\t\t}\n+\t\t],\n+\t\t\n+\t\t#\n+\t\t# these headers, which will be sent on every transaction, make\n+\t\t# recent browsers definitively ban any external script sources,\n+\t\t# no matter what might manage to get injected later in the\n+\t\t# page. It's a last line of defence against any successful XSS.\n+\t\t#\n+\t\t\n+\t\t\u0022headers\u0022: [{\n+\t\t \u0022content-security-policy\u0022: \u0022default-src 'none'; img-src 'self' data:; script-src 'self'; font-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none';\u0022,\n+\t\t \u0022x-content-type-options\u0022: \u0022nosniff\u0022,\n+\t\t \u0022x-xss-protection\u0022: \u00221; mode\u003dblock\u0022,\n+\t\t \u0022referrer-policy\u0022: \u0022no-referrer\u0022\n+\t\t}],\n+\n+\t\t\u0022ws-protocols\u0022: [\n+\t\t{\u0022com-warmcat-sai\u0022: {\n+\t\t\t# this is the lhs of the path to use, not a full filepath\n+\t\t\t# the events go in ...-events.sqlite3 and the related\n+\t\t\t# info for an event goes in its own database file in\n+\t\t\t# ...-event-xxxx.sqlite3 where xxxx is the event uuid.\n+\t\t\t#\n+\t\t\t\u0022database\u0022:\t\t\u0022/srv/sai/sai-master\u0022,\n+ #\n+ # the push hook notification script creates a JSON\n+ # object and signs it with a secret... \u0022notification-\n+ # key\u0022 should match the secret the notification hook\n+ # script has that we're willing to accept.\n+ #\n+ # It should be a 64-char hex-ascii representation of\n+ # a random 32-byte sequence, you can produce a strong\n+ # random key in the correct format like this\n+ #\n+ # dd if\u003d/dev/random bs\u003d32 count\u003d1 | sha256sum | cut -d' ' -f1\n+ #\n+\t\t\t\u0022notification-key\u0022:\t\u002251b3ee2f06ef2a893cfe901972bd13065d7dbae4cf087b396ee38e7bf78f79a6\u0022,\n+\n+\t\t\t# auth jwk path\n+\t\t\t# You can generate a suitable key like this\n+\t\t\t#\n+\t\t\t# lws-crypto-jwk -t EC -b512 -vP-521 --alg ES512 \u003e mykey.jwk\n+\t\t\t#\n+\t\t\t\u0022jwt-auth-alg\u0022:\t\t\u0022ES512\u0022,\n+\t\t\t\u0022jwt-auth-jwk-path\u0022:\t\u0022/etc/sai/web/auth.jwk\u0022,\n+\n+\t\t\t\u0022jwt-iss\u0022:\t\t\u0022com.warmcat\u0022,\n+\t\t\t\u0022jwt-aud\u0022:\t\t\u0022https://mydomain.com/sai\u0022,\n+\n+\t\t\t# template HTML to use for this vhost. You'd normally\n+\t\t\t# copy this to gitohashi-vhostname.html and modify it\n+\t\t\t# to show the content, logos, links, fonts, css etc for\n+\t\t\t# your vhost. It's not served directly but read from\n+\t\t\t# the filesystem. Although it's cached in memory by\n+\t\t\t# gitohashi, it checks for changes and reloads if\n+\t\t\t# changed automatically.\n+\t\t\t#\n+\t\t\t# Putting logos etc as svg in css is highly recommended,\n+\t\t\t# like fonts these can be transferred once with a loose\n+\t\t\t# caching policy. So in practice they cost very little,\n+\t\t\t# and allow the browser to compose the page without\n+\t\t\t# delay.\n+\t\t\t#\n+\t\t\t\u0022html-file\u0022:\t \u0022/usr/local/share/gitohashi/templates/gitohashi-example.html\u0022,\n+\t\t\t#\n+\t\t\t# vpath required at start of links into this vhost's\n+\t\t\t# gitohashi content for example if an external http\n+\t\t\t# server is proxying us, and has been told to direct\n+\t\t\t# URLs starting \u0022/git\u0022 to us, this should be set to\n+\t\t\t# \u0022/git/\u0022 so URLs we generate referring to our own pages\n+\t\t\t# can work.\n+\t\t\t#\n+\t\t\t\u0022vpath\u0022:\t \u0022/sais/\u0022,\n+\t\t\t#\n+\t\t\t# url mountpoint for the avatar cache\n+\t\t\t#\n+\t\t\t\u0022avatar-url\u0022:\t \u0022/sais/avatar/\u0022,\n+\t\t\t#\n+\t\t\t# libjsgit2 JSON cache... this\n+\t\t\t# should not be directly served\n+\t\t\t#\n+\t\t\t\u0022cache-base\u0022:\t\u0022/var/cache/libjsongit2\u0022,\n+\t\t\t#\n+\t\t\t# restrict the JSON cache size\n+\t\t\t# to 2GB\n+\t\t\t#\n+\t\t\t\u0022cache-size\u0022: \u00222000000000\u0022,\n+\t\t\t#\n+\t\t\t# optional flags, b0 \u003d 1 \u003d blog mode\n+\t\t\t#\n+\t\t\t\u0022flags\u0022: 0\n+\t\t\t#\u0022blog-repo-name\u0022:\t\u0022myrepo\u0022\n+\t\t},\n+\t\t\u0022avatar-proxy\u0022: {\n+\t\t\t\u0022remote-base\u0022: \u0022https://www.gravatar.com/avatar/\u0022,\n+\t\t\t#\n+\t\t\t# this dir is served via avatar-proxy\n+\t\t\t#\n+\t\t\t\u0022cache-dir\u0022: \u0022/var/cache/libjsongit2\u0022\n+\t\t}\n+\t\t}\n+\t\t]\n+\t\t}\n+\t]\n+}\ndiff --git a/etc-sai-EXAMPLE/web/conf b/etc-sai-EXAMPLE/web/conf\nnew file mode 100644\nindex 0000000..673f250\n--- /dev/null\n+++ b/etc-sai-EXAMPLE/web/conf\n@@ -0,0 +1,9 @@\n+{\n+ \u0022global\u0022: {\n+ \u0022username\u0022: \u0022sai\u0022, # sai\n+ \u0022groupname\u0022: \u0022nobody\u0022, # nobody\n+ \u0022server-string\u0022: \u0022sai\u0022,\n+ \u0022rlimit-nofile\u0022: \u002210000\u0022,\n+ \u0022init-ssl\u0022: \u0022yes\u0022\n+ }\n+}\ndiff --git a/etc-sai-EXAMPLE/web/conf.d/unixskt b/etc-sai-EXAMPLE/web/conf.d/unixskt\nnew file mode 100644\nindex 0000000..e14702e\n--- /dev/null\n+++ b/etc-sai-EXAMPLE/web/conf.d/unixskt\n@@ -0,0 +1,133 @@\n+# this specifies how the sai-web server operates\n+\n+{\n+\t\u0022vhosts\u0022: [{\n+\t\t# this has no special meaning but needs to be unique\n+ \u0022name\u0022: \u0022unixsktw\u0022,\n+ \u0022unix-socket\u0022: 1,\n+\t\t# this should be owned by sai and allow group access from\n+\t\t# whatever group your webserver runs under\n+\t\t\u0022unix-socket-perms\u0022: \u0022sai:apache\u0022,\n+ # multiple vhosts can exist with different unix skt names\n+\t\t\u0022interface\u0022:\t \u0022/var/run/sai\u0022,\n+\t\t# required for avatar cache\n+ \u0022enable-client-ssl\u0022: \u0022on\u0022,\n+\n+\t\t\u0022mounts\u0022: [\n+\t\t\t{\n+\t\t\t\t\u0022mountpoint\u0022:\t\t\u0022/sai\u0022,\n+\t\t\t\t\u0022default\u0022:\t\t\u0022index.html\u0022,\n+\t\t\t\t\u0022origin\u0022:\t\t\u0022file:///usr/local/share/sai/assets\u0022,\n+\t\t\t\t\u0022origin\u0022:\t\t\u0022callback://com-warmcat-sai\u0022,\n+\t\t\t\t\u0022cache-max-age\u0022: \t\u00227200\u0022,\n+\t\t\t\t\u0022cache-reuse\u0022:\t\t\u00221\u0022,\n+\t\t\t\t\u0022cache-revalidate\u0022:\t\u00220\u0022,\n+\t\t\t\t\u0022cache-intermediaries\u0022:\t\u00220\u0022,\n+\t \t\t\t\u0022extra-mimetypes\u0022: {\n+\t\t\t\t\t\u0022.zip\u0022: \u0022application/zip\u0022,\n+\t\t\t\t\t\u0022.map\u0022: \u0022application/json\u0022,\n+\t\t\t\t\t\u0022.ttf\u0022: \u0022application/x-font-ttf\u0022\n+\t\t\t\t}\n+\t\t\t},\n+\t\t\t{\n+\t\t\t\t# semi-static cached avatar icons\n+\t\t\t\t\u0022mountpoint\u0022: \u0022/sai/avatar\u0022,\n+\t\t\t\t\u0022origin\u0022: \u0022callback://avatar-proxy\u0022,\n+\t\t\t\t\u0022cache-max-age\u0022: \u00222400\u0022,\n+\t\t\t\t\u0022cache-reuse\u0022: \u00221\u0022,\n+\t\t\t\t\u0022cache-revalidate\u0022: \u00220\u0022,\n+\t\t\t\t\u0022cache-intermediaries\u0022: \u00220\u0022\n+\t\t\t}\n+\t\t],\n+\t\t\n+\t\t#\n+\t\t# these headers, which will be sent on every transaction, make\n+\t\t# recent browsers definitively ban any external script sources,\n+\t\t# no matter what might manage to get injected later in the\n+\t\t# page. It's a last line of defence against any successful XSS.\n+\t\t#\n+\t\t\n+\t\t\u0022headers\u0022: [{\n+\t\t \u0022content-security-policy\u0022: \u0022default-src 'none'; img-src 'self' data:; script-src 'self'; font-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none';\u0022,\n+\t\t \u0022x-content-type-options\u0022: \u0022nosniff\u0022,\n+\t\t \u0022x-xss-protection\u0022: \u00221; mode\u003dblock\u0022,\n+\t\t \u0022referrer-policy\u0022: \u0022no-referrer\u0022\n+\t\t}],\n+\n+\t\t\u0022ws-protocols\u0022: [\n+\t\t{\u0022com-warmcat-sai\u0022: {\n+\t\t\t# this is the lhs of the path to use, not a full filepath\n+\t\t\t# the events go in ...-events.sqlite3 and the related\n+\t\t\t# info for an event goes in its own database file in\n+\t\t\t# ...-event-xxxx.sqlite3 where xxxx is the event uuid.\n+\t\t\t#\n+\t\t\t\u0022database\u0022:\t\t\u0022/srv/sai/sai-master\u0022,\n+\n+\n+\t\t\t# auth jwk path\n+\t\t\t# You can generate a suitable key like this\n+\t\t\t#\n+\t\t\t# lws-crypto-jwk -t EC -b512 -vP-521 --alg ES512 \u003e mykey.jwk\n+\t\t\t#\n+\t\t\t\u0022jwt-auth-alg\u0022:\t\t\u0022ES512\u0022,\n+\t\t\t\u0022jwt-auth-jwk-path\u0022:\t\u0022/etc/sai/web/auth.jwk\u0022,\n+\n+\t\t\t\u0022jwt-iss\u0022:\t\t\u0022com.warmcat\u0022,\n+\t\t\t\u0022jwt-aud\u0022:\t\t\u0022https://mydomain.com/sai\u0022,\n+\n+\t\t\t# template HTML to use for this vhost. You'd normally\n+\t\t\t# copy this to gitohashi-vhostname.html and modify it\n+\t\t\t# to show the content, logos, links, fonts, css etc for\n+\t\t\t# your vhost. It's not served directly but read from\n+\t\t\t# the filesystem. Although it's cached in memory by\n+\t\t\t# gitohashi, it checks for changes and reloads if\n+\t\t\t# changed automatically.\n+\t\t\t#\n+\t\t\t# Putting logos etc as svg in css is highly recommended,\n+\t\t\t# like fonts these can be transferred once with a loose\n+\t\t\t# caching policy. So in practice they cost very little,\n+\t\t\t# and allow the browser to compose the page without\n+\t\t\t# delay.\n+\t\t\t#\n+\t\t\t\u0022html-file\u0022:\t \u0022/usr/local/share/gitohashi/templates/gitohashi-example.html\u0022,\n+\t\t\t#\n+\t\t\t# vpath required at start of links into this vhost's\n+\t\t\t# gitohashi content for example if an external http\n+\t\t\t# server is proxying us, and has been told to direct\n+\t\t\t# URLs starting \u0022/git\u0022 to us, this should be set to\n+\t\t\t# \u0022/git/\u0022 so URLs we generate referring to our own pages\n+\t\t\t# can work.\n+\t\t\t#\n+\t\t\t\u0022vpath\u0022:\t \u0022/sai/\u0022,\n+\t\t\t#\n+\t\t\t# url mountpoint for the avatar cache\n+\t\t\t#\n+\t\t\t\u0022avatar-url\u0022:\t \u0022/sai/avatar/\u0022,\n+\t\t\t#\n+\t\t\t# libjsgit2 JSON cache... this\n+\t\t\t# should not be directly served\n+\t\t\t#\n+\t\t\t\u0022cache-base\u0022:\t\u0022/var/cache/libjsongit2\u0022,\n+\t\t\t#\n+\t\t\t# restrict the JSON cache size\n+\t\t\t# to 2GB\n+\t\t\t#\n+\t\t\t\u0022cache-size\u0022: \u00222000000000\u0022,\n+\t\t\t#\n+\t\t\t# optional flags, b0 \u003d 1 \u003d blog mode\n+\t\t\t#\n+\t\t\t\u0022flags\u0022: 0\n+\t\t\t#\u0022blog-repo-name\u0022:\t\u0022myrepo\u0022\n+\t\t},\n+\t\t\u0022avatar-proxy\u0022: {\n+\t\t\t\u0022remote-base\u0022: \u0022https://www.gravatar.com/avatar/\u0022,\n+\t\t\t#\n+\t\t\t# this dir is served via avatar-proxy\n+\t\t\t#\n+\t\t\t\u0022cache-dir\u0022: \u0022/var/cache/libjsongit2\u0022\n+\t\t}\n+\t\t}\n+\t\t]\n+\t\t}\n+\t]\n+}\ndiff --git a/scripts/builder-conf b/scripts/builder-conf\nindex ae69ffc..09bdc45 100644\n--- a/scripts/builder-conf\n+++ b/scripts/builder-conf\n@@ -10,7 +10,7 @@\n {\n \u0022name\u0022: \u0022linux-debian-buster/x86_64-amd/gcc\u0022,\n \u0022instances\u0022: 1,\n- \u0022masters\u0022: [ \u0022wss://warmcat.com/sai/builder\u0022 ]\n+ \u0022servers\u0022: [ \u0022wss://warmcat.com:4444/sai/builder\u0022 ]\n }\n ]\n } \ndiff --git a/scripts/sai-master.service b/scripts/sai-master.service\ndeleted file mode 100644\nindex 041c738..0000000\n--- a/scripts/sai-master.service\n+++ /dev/null\n@@ -1,13 +0,0 @@\n-[Unit]\n-Description\u003dSai Master\n-After\u003dsyslog.target\n-\n-[Service]\n-ExecStart\u003d/usr/local/bin/sai-master\n-ExecReload\u003d/usr/bin/kill -HUP $MAINPID\n-ExecStop\u003d/usr/bin/killall sai-master\n-\n-[Install]\n-WantedBy\u003dmulti-user.target\n-\n-\ndiff --git a/scripts/sai-server.service b/scripts/sai-server.service\nnew file mode 100644\nindex 0000000..6b7d949\n--- /dev/null\n+++ b/scripts/sai-server.service\n@@ -0,0 +1,13 @@\n+[Unit]\n+Description\u003dSai Server\n+After\u003dsyslog.target\n+\n+[Service]\n+ExecStart\u003d/usr/local/bin/sai-server\n+ExecReload\u003d/usr/bin/kill -HUP $MAINPID\n+ExecStop\u003d/usr/bin/killall sai-server\n+\n+[Install]\n+WantedBy\u003dmulti-user.target\n+\n+\ndiff --git a/scripts/sai-web.service b/scripts/sai-web.service\nnew file mode 100644\nindex 0000000..8e14732\n--- /dev/null\n+++ b/scripts/sai-web.service\n@@ -0,0 +1,13 @@\n+[Unit]\n+Description\u003dSai Web\n+After\u003dsyslog.target\n+\n+[Service]\n+ExecStart\u003d/usr/local/bin/sai-web\n+ExecReload\u003d/usr/bin/kill -HUP $MAINPID\n+ExecStop\u003d/usr/bin/killall sai-web\n+\n+[Install]\n+WantedBy\u003dmulti-user.target\n+\n+\ndiff --git a/src/builder/b-artifacts.c b/src/builder/b-artifacts.c\nindex a0f0bf4..08d6634 100644\n--- a/src/builder/b-artifacts.c\n+++ b/src/builder/b-artifacts.c\n@@ -18,7 +18,7 @@\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n * MA 02110-1301 USA\n *\n- * This deals with POSTing artifacts to the related sai-master, using a secret\n+ * This deals with POSTing artifacts to the related sai-server, using a secret\n * that was passed to the builder as part of the task that was allocated\n */\n \ndiff --git a/src/builder/b-comms.c b/src/builder/b-comms.c\nindex c01ea4d..e6b04ba 100644\n--- a/src/builder/b-comms.c\n+++ b/src/builder/b-comms.c\n@@ -30,7 +30,7 @@\n static lws_ss_state_return_t\n saib_m_rx(void *userobj, const uint8_t *buf, size_t len, int flags)\n {\n-\tstruct sai_plat_master *spm \u003d (struct sai_plat_master *)userobj;\n+\tstruct sai_plat_server *spm \u003d (struct sai_plat_server *)userobj;\n \t//struct sai_plat *sp \u003d (struct sai_plat *)spm-\u003esai_plat;\n \n \tlwsl_info(\u0022%s: len %d, flags: %d\u005cn\u0022, __func__, (int)len, flags);\n@@ -50,7 +50,7 @@ static int\n tp_sync_check(struct lws_dll2 *d, void *user)\n {\n \tsai_plat_t *sp \u003d lws_container_of(d, sai_plat_t, sai_plat_list);\n-\tstruct sai_plat_master *spm \u003d (struct sai_plat_master *)user;\n+\tstruct sai_plat_server *spm \u003d (struct sai_plat_server *)user;\n \tstruct sai_nspawn *ns;\n \tint n, soe;\n \tvoid *vp;\n@@ -66,7 +66,7 @@ tp_sync_check(struct lws_dll2 *d, void *user)\n \t\tsoe \u003d ns-\u003estate;\n \n \t\t/*\n-\t\t * We can't deal with nspawns bound to a different master or\n+\t\t * We can't deal with nspawns bound to a different server or\n \t\t * nspawns not with an active threadpool task\n \t\t */\n \n@@ -199,7 +199,7 @@ next:\n \n /*\n * We cover requested tx for any instance of a platform that can takes tasks\n- * from the same master... it means just by coming here, no particular\n+ * from the same server... it means just by coming here, no particular\n * platform / sai_plat is implied...\n */\n \n@@ -207,7 +207,7 @@ static lws_ss_state_return_t\n saib_m_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, size_t *len,\n \t int *flags)\n {\n-\tstruct sai_plat_master *spm \u003d (struct sai_plat_master *)userobj;\n+\tstruct sai_plat_server *spm \u003d (struct sai_plat_server *)userobj;\n \tuint8_t *start \u003d buf, *end \u003d buf + (*len) - 1, *p \u003d start;\n \tstruct ws_capture_chunk *chunk;\n \tstruct sai_plat *sp \u003d NULL;\n@@ -219,7 +219,7 @@ saib_m_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, size_t *len,\n \n \t/*\n \t * We are the ss / wsi that any threadpool instances on any platform\n-\t * with tasks for this master are trying to sync to. We need to handle\n+\t * with tasks for this server are trying to sync to. We need to handle\n \t * and resume them all.\n \t */\n \n@@ -265,7 +265,7 @@ saib_m_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, size_t *len,\n \tdefault:\n \n \t\t/*\n-\t\t * Update master with platform status\n+\t\t * Update server with platform status\n \t\t */\n \n \t\tjs \u003d lws_struct_json_serialize_create(lsm_schema_map_plat,\n@@ -301,7 +301,7 @@ saib_m_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, size_t *len,\n \n \t/*\n \t * Yes somebody has some logs... since we handle all logs on any\n-\t * platform doing tasks for the same master, we have to take care not\n+\t * platform doing tasks for the same server, we have to take care not\n \t * to favour draining logs for any busy tasks over letting others\n \t * getting a chance at the mic. If we just scan for guys with logs\n \t * from the start of the list each time, we will never deal with guys\n@@ -316,7 +316,7 @@ saib_m_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, size_t *len,\n \t * \u003d \u003d\n \t * spm-\u003elast_logging_platform : spm-\u003elast_logging_nspawn\n \t *\n-\t * ... filtered for nspawns associated with our spm / master SS link.\n+\t * ... filtered for nspawns associated with our spm / server SS link.\n \t *\n \t * The platforms and nspawns are allocated at conf-time statically.\n \t */\n@@ -448,7 +448,7 @@ sendify:\n static int\n cleanup_on_ss_destroy(struct lws_dll2 *d, void *user)\n {\n-\tstruct sai_plat_master *spm \u003d (struct sai_plat_master *)user;\n+\tstruct sai_plat_server *spm \u003d (struct sai_plat_server *)user;\n \tsai_plat_t *sp \u003d lws_container_of(d, sai_plat_t, sai_plat_list);\n \n \tlws_start_foreach_dll_safe(struct lws_dll2 *, d, d1,\n@@ -471,7 +471,7 @@ cleanup_on_ss_destroy(struct lws_dll2 *d, void *user)\n static int\n cleanup_on_ss_disconnect(struct lws_dll2 *d, void *user)\n {\n-\tstruct sai_plat_master *spm \u003d (struct sai_plat_master *)user;\n+\tstruct sai_plat_server *spm \u003d (struct sai_plat_server *)user;\n \tsai_plat_t *sp \u003d lws_container_of(d, sai_plat_t, sai_plat_list);\n \tstruct ws_capture_chunk *cc;\n \n@@ -512,7 +512,7 @@ static lws_ss_state_return_t\n saib_m_state(void *userobj, void *sh, lws_ss_constate_t state,\n \t lws_ss_tx_ordinal_t ack)\n {\n-\tstruct sai_plat_master *spm \u003d (struct sai_plat_master *)userobj;\n+\tstruct sai_plat_server *spm \u003d (struct sai_plat_server *)userobj;\n \n \tlwsl_user(\u0022%s: %s, ord 0x%x\u005cn\u0022, __func__, lws_ss_state_name(state),\n \t\t (unsigned int)ack);\n@@ -522,7 +522,7 @@ saib_m_state(void *userobj, void *sh, lws_ss_constate_t state,\n \n \t\t/*\n \t\t * If the logical SS itself is going down, every platform that\n-\t\t * used us to connect to their master and has nspawns are also\n+\t\t * used us to connect to their server and has nspawns are also\n \t\t * going down\n \t\t */\n \t\tlws_dll2_foreach_safe(\u0026builder.sai_plat_owner, spm,\n@@ -561,11 +561,11 @@ saib_m_state(void *userobj, void *sh, lws_ss_constate_t state,\n }\n \n const lws_ss_info_t ssi_sai_builder \u003d {\n-\t.handle_offset \u003d offsetof(struct sai_plat_master, ss),\n-\t.opaque_user_data_offset \u003d offsetof(struct sai_plat_master, opaque_data),\n+\t.handle_offset \u003d offsetof(struct sai_plat_server, ss),\n+\t.opaque_user_data_offset \u003d offsetof(struct sai_plat_server, opaque_data),\n \t.rx \u003d saib_m_rx,\n \t.tx \u003d saib_m_tx,\n \t.state \u003d saib_m_state,\n-\t.user_alloc \u003d sizeof(struct sai_plat_master),\n+\t.user_alloc \u003d sizeof(struct sai_plat_server),\n \t.streamtype \u003d \u0022sai_builder\u0022\n };\ndiff --git a/src/builder/b-conf.c b/src/builder/b-conf.c\nindex f037a5f..eac3467 100644\n--- a/src/builder/b-conf.c\n+++ b/src/builder/b-conf.c\n@@ -49,7 +49,7 @@ static const char * const paths[] \u003d {\n \t\u0022platforms[].instances\u0022,\n \t\u0022platforms[].env[].*\u0022,\n \t\u0022platforms[].env[]\u0022,\n-\t\u0022platforms[].masters\u0022,\n+\t\u0022platforms[].servers\u0022,\n \t\u0022platforms[]\u0022,\n };\n \n@@ -59,7 +59,7 @@ enum enum_paths {\n \tLEJPM_PLATFORMS_INSTANCES,\n \tLEJPM_PLATFORMS_ENV_ITEM,\n \tLEJPM_PLATFORMS_ENV,\n-\tLEJPM_PLATFORMS_MASTERS,\n+\tLEJPM_PLATFORMS_SERVERS,\n \tLEJPM_PLATFORMS,\n };\n \n@@ -71,7 +71,7 @@ struct jpargs {\n \n \tstruct sai_platform\t*pl;\n \n-\tint\t\t\tnext_master_index;\n+\tint\t\t\tnext_server_index;\n \tint\t\t\tnext_plat_index;\n };\n \n@@ -79,8 +79,8 @@ static signed char\n saib_conf_cb(struct lejp_ctx *ctx, char reason)\n {\n \tstruct jpargs *a \u003d (struct jpargs *)ctx-\u003euser;\n-\tsai_plat_master_ref_t *mref;\n-\tstruct sai_plat_master *cm;\n+\tsai_plat_server_ref_t *mref;\n+\tstruct sai_plat_server *cm;\n \tstruct lws_ss_handle *h;\n \tconst char **pp, *pq;\n \tchar temp[65];\n@@ -175,41 +175,41 @@ saib_conf_cb(struct lejp_ctx *ctx, char reason)\n \t\ta-\u003esai_plat-\u003eindex \u003d a-\u003enext_plat_index++;\n \t\tbreak;\n \n-\tcase LEJPM_PLATFORMS_MASTERS:\n+\tcase LEJPM_PLATFORMS_SERVERS:\n \n \t\tmref \u003d lwsac_use_zero(\u0026a-\u003ebuilder-\u003econf_head, sizeof(*mref),\n \t\t\t\t\t512);\n \n \t\t/*\n \t\t * The builder as a whole maintains only one SS connection to\n-\t\t * each master. If there are multiple platforms supported by\n-\t\t * the builder that want to accept tasks from the same master,\n-\t\t * only the first platform creates the SS to the master, and\n+\t\t * each server. If there are multiple platforms supported by\n+\t\t * the builder that want to accept tasks from the same server,\n+\t\t * only the first platform creates the SS to the server, and\n \t\t * the others just use that.\n \t\t *\n-\t\t * The sai_plat_master is instantiated as the ss userdata.\n+\t\t * The sai_plat_server is instantiated as the ss userdata.\n \t\t *\n \t\t * So let's see if we already have the connection created\n-\t\t * for this master...\n+\t\t * for this server...\n \t\t */\n \n \t\tlws_start_foreach_dll(struct lws_dll2 *, p,\n-\t\t\t\t a-\u003ebuilder-\u003esai_plat_master_owner.head) {\n-\t\t\tcm \u003d lws_container_of(p, sai_plat_master_t, list);\n+\t\t\t\t a-\u003ebuilder-\u003esai_plat_server_owner.head) {\n+\t\t\tcm \u003d lws_container_of(p, sai_plat_server_t, list);\n \n \t\t\tif (!strncmp(ctx-\u003ebuf, cm-\u003eurl, ctx-\u003enpos)) {\n \t\t\t\t/* we already have a logical connection... */\n \t\t\t\tmref-\u003espm \u003d cm;\n \t\t\t\tcm-\u003erefcount++;\n \t\t\t\tlws_dll2_add_tail(\u0026mref-\u003elist,\n-\t\t\t\t\t\t \u0026a-\u003esai_plat-\u003emasters);\n+\t\t\t\t\t\t \u0026a-\u003esai_plat-\u003eservers);\n \n \t\t\t\treturn 0;\n \t\t\t}\n \t\t} lws_end_foreach_dll(p);\n \n \t\t/*\n-\t\t * This is the first plat that wants to talk to this master,\n+\t\t * This is the first plat that wants to talk to this server,\n \t\t * we need to create the logical SS connection\n \t\t */\n \n@@ -220,10 +220,10 @@ saib_conf_cb(struct lejp_ctx *ctx, char reason)\n \t\t\treturn -1;\n \t\t}\n \n-\t\tcm \u003d lws_ss_to_user_object(h); /* the sai_plat_master object */\n-\t\tcm-\u003eindex \u003d a-\u003enext_master_index++;\n+\t\tcm \u003d lws_ss_to_user_object(h); /* the sai_plat_server object */\n+\t\tcm-\u003eindex \u003d a-\u003enext_server_index++;\n \n-\t\t/* hook the ss up to the master url */\n+\t\t/* hook the ss up to the server url */\n \n \t\tcm-\u003eurl \u003d lwsac_use(\u0026a-\u003ebuilder-\u003econf_head,\n \t\t\t\t 2 *(ctx-\u003enpos + 1), 512);\n@@ -256,13 +256,13 @@ saib_conf_cb(struct lejp_ctx *ctx, char reason)\n \t\twhile (strchr(cm-\u003ename, '/'))\n \t\t\t*strchr(cm-\u003ename, '/') \u003d '_';\n \n-\t\t/* add us to the builder list of unique masters */\n-\t\tlws_dll2_add_head(\u0026cm-\u003elist, \u0026a-\u003ebuilder-\u003esai_plat_master_owner);\n+\t\t/* add us to the builder list of unique servers */\n+\t\tlws_dll2_add_head(\u0026cm-\u003elist, \u0026a-\u003ebuilder-\u003esai_plat_server_owner);\n \n-\t\t/* add us to this platforms's list of masters it accepts */\n+\t\t/* add us to this platforms's list of servers it accepts */\n \t\tmref-\u003espm \u003d cm;\n \t\tcm-\u003erefcount++;\n-\t\tlws_dll2_add_tail(\u0026mref-\u003elist, \u0026a-\u003esai_plat-\u003emasters);\n+\t\tlws_dll2_add_tail(\u0026mref-\u003elist, \u0026a-\u003esai_plat-\u003eservers);\n \n \t\tlws_ss_client_connect(h);\n \ndiff --git a/src/builder/b-mirror.c b/src/builder/b-mirror.c\nindex 02c0df7..a9ba28d 100644\n--- a/src/builder/b-mirror.c\n+++ b/src/builder/b-mirror.c\n@@ -415,7 +415,7 @@ saib_mirror_task(void *user, enum lws_threadpool_task_status s)\n \t\t\t\t/*\n \t\t\t\t * Leave in NSSTATE_CHECKOUT and come back to\n \t\t\t\t * continue with checking out after we have\n-\t\t\t\t * sync'd with comms to master\n+\t\t\t\t * sync'd with comms to server\n \t\t\t\t */\n \n \t\t\t\tns-\u003estate \u003d NSSTATE_CHECKOUT;\ndiff --git a/src/builder/b-nspawn.c b/src/builder/b-nspawn.c\nindex e55542e..f755f41 100644\n--- a/src/builder/b-nspawn.c\n+++ b/src/builder/b-nspawn.c\n@@ -238,7 +238,7 @@ saib_spawn(struct sai_nspawn *ns)\n \t\t\u0022/bin/ps\u0022,\n \t\tNULL\n \t};\n-\tchar *env[] \u003d {\n+\tconst char *env[] \u003d {\n \t\t\u0022PATH\u003d/usr/bin:/bin\u0022,\n \t\t\u0022LANG\u003den_US.UTF-8\u0022,\n \t\tNULL\ndiff --git a/src/builder/b-private.h b/src/builder/b-private.h\nindex 7918199..ff5327d 100644\n--- a/src/builder/b-private.h\n+++ b/src/builder/b-private.h\n@@ -105,13 +105,13 @@ struct sai_nspawn {\n \tlws_sorted_usec_list_t\t\tsul_task_cancel;\n \n \tsai_plat_t\t\t\t*sp; /* the sai_plat */\n-\tstruct sai_plat_master\t\t*spm; /* the sai plat / master with the ss / wsi */\n+\tstruct sai_plat_server\t\t*spm; /* the sai plat / server with the ss / wsi */\n \n \tsize_t\t\t\t\tchunk_cache_size;\n \n-\tconst char\t\t\t*master_name;\t/* sai-master name who triggered this, eg, 'warmcat' */\n+\tconst char\t\t\t*server_name;\t/* sai-server name who triggered this, eg, 'warmcat' */\n \tconst char\t\t\t*project_name;\t/* name of the git project, eg, 'libwebsockets' */\n-\tconst char\t\t\t*ref;\t\t/* remote refname, eg 'master' */\n+\tconst char\t\t\t*ref;\t\t/* remote refname, eg 'server' */\n \tconst char\t\t\t*hash;\t\t/* remote hash */\n \tconst char\t\t\t*git_repo_url;\n \n@@ -148,7 +148,7 @@ typedef struct sai_mirror_instance {\n \n struct sai_builder {\n \tlws_dll2_owner_t\tsai_plat_owner; /* list of platforms we offer */\n-\tlws_dll2_owner_t\tsai_plat_master_owner; /* masters we connect to */\n+\tlws_dll2_owner_t\tsai_plat_server_owner; /* servers we connect to */\n \tlws_dll2_owner_t\tdevices_owner; /* sai_serial_t */\n \n \tstruct lwsac\t\t*conf_head;\n@@ -195,7 +195,7 @@ int\n saib_prepare_mount(struct sai_builder *b, struct sai_nspawn *ns);\n \n int\n-saib_ws_json_rx_builder(struct sai_plat_master *spm, const void *in, size_t len);\n+saib_ws_json_rx_builder(struct sai_plat_server *spm, const void *in, size_t len);\n \n int\n saib_generate(struct sai_plat *sp, char *buf, int len);\n@@ -216,7 +216,7 @@ struct ws_capture_chunk *\n saib_log_chunk_create(struct sai_nspawn *ns, void *buf, size_t len, int channel);\n \n int\n-saib_queue_task_status_update(sai_plat_t *sp, struct sai_plat_master *spm,\n+saib_queue_task_status_update(sai_plat_t *sp, struct sai_plat_server *spm,\n \t\t\t\tconst char *rej_task_uuid);\n \n int\ndiff --git a/src/builder/b-sai.c b/src/builder/b-sai.c\nindex b3d0b3d..0d61db2 100644\n--- a/src/builder/b-sai.c\n+++ b/src/builder/b-sai.c\n@@ -19,7 +19,7 @@\n * MA 02110-1301 USA\n *\n * Sai-builder uses a secure streams template to make the client connections to\n- * the masters listed in /etc/sai/builder/conf JSON config. The URL in the\n+ * the servers listed in /etc/sai/builder/conf JSON config. The URL in the\n * config is substituted for the endpoint URL at runtime.\n *\n * See b-comms.c for the secure stream template and callbacks for this.\n@@ -75,7 +75,7 @@ static const char * const default_ss_policy \u003d\n \n \t \u0022\u005c\u0022s\u005c\u0022: [\u0022\n \t\t/*\n-\t\t * The main connection to a master carrying events and logs\n+\t\t * The main connection to a server carrying events and logs\n \t\t */\n \t\t\u0022{\u005c\u0022sai_builder\u005c\u0022: {\u0022\n \t\t\t\u0022\u005c\u0022endpoint\u005c\u0022:\u0022\t\t\u0022\u005c\u0022${url}\u005c\u0022,\u0022\n@@ -90,7 +90,7 @@ static const char * const default_ss_policy \u003d\n \t\t\t\u0022]\u0022\n \t\t\u0022}},\u0022\n \t\t/*\n-\t\t * Ephemeral connections to the same master carrying artifact\n+\t\t * Ephemeral connections to the same server carrying artifact\n \t\t * JSON + bulk data\n \t\t */\n \t\t\u0022{\u005c\u0022sai_artifact\u005c\u0022: {\u0022\n@@ -177,7 +177,7 @@ app_system_state_nf(lws_state_manager_t *mgr, lws_state_notify_link_t *link,\n \t\t\tbreak;\n \n \t\t/*\n-\t\t * The builder JSON conf listed masters we want to connect to,\n+\t\t * The builder JSON conf listed servers we want to connect to,\n \t\t * let's collect the config, make a ss for each and add the\n \t\t * saim into an lws_dll2 list owned by\n \t\t * builder-\u003ebuilder-\u003esai_plat_owner\n@@ -456,12 +456,12 @@ int main(int argc, const char **argv)\n \n bail:\n \n-\t/* destroy the unique masters */\n+\t/* destroy the unique servers */\n \n \tlws_start_foreach_dll_safe(struct lws_dll2 *, p, p1,\n-\t\t\t\t builder.sai_plat_master_owner.head) {\n-\t\tstruct sai_plat_master *cm \u003d lws_container_of(p,\n-\t\t\t\t\tstruct sai_plat_master, list);\n+\t\t\t\t builder.sai_plat_server_owner.head) {\n+\t\tstruct sai_plat_server *cm \u003d lws_container_of(p,\n+\t\t\t\t\tstruct sai_plat_server, list);\n \n \t\tlws_dll2_remove(\u0026cm-\u003elist);\n \t\tlws_ss_destroy(\u0026cm-\u003ess);\ndiff --git a/src/builder/b-task.c b/src/builder/b-task.c\nindex d20dcdd..1635f79 100644\n--- a/src/builder/b-task.c\n+++ b/src/builder/b-task.c\n@@ -81,11 +81,11 @@ saib_set_ns_state(struct sai_nspawn *ns, int state)\n }\n \n /*\n- * update all masters we're connected to about builder status / optional reject\n+ * update all servers we're connected to about builder status / optional reject\n */\n \n int\n-saib_queue_task_status_update(sai_plat_t *sp, struct sai_plat_master *spm,\n+saib_queue_task_status_update(sai_plat_t *sp, struct sai_plat_server *spm,\n \t\t\t\tconst char *rej_task_uuid)\n {\n \tstruct sai_rejection *rej;\n@@ -164,7 +164,7 @@ saib_task_destroy(struct sai_nspawn *ns)\n \t\tns-\u003esp-\u003eongoing--;\n \n \t\t/*\n-\t\t * Schedule informing all the masters we're connected to\n+\t\t * Schedule informing all the servers we're connected to\n \t\t */\n \n \t\tsaib_queue_task_status_update(ns-\u003esp, ns-\u003espm, NULL);\n@@ -260,7 +260,7 @@ artifact_glob_cb(void *data, const char *path)\n \t/*\n \t * We need to set the metadata items for the post urlargs. spm-\u003eurl is\n \t * something like \u0022wss://warmcat.com/sai/builder\u0022... we will send JSON\n-\t * on this connection first and that will be understood by the master\n+\t * on this connection first and that will be understood by the server\n \t * as meaning the bulk data follows.\n \t */\n \n@@ -405,7 +405,7 @@ saib_sul_task_cancel(struct lws_sorted_usec_list *sul)\n }\n \n int\n-saib_ws_json_rx_builder(struct sai_plat_master *spm, const void *in, size_t len)\n+saib_ws_json_rx_builder(struct sai_plat_server *spm, const void *in, size_t len)\n {\n \tstruct lws_threadpool_create_args tca;\n \tstruct lws_threadpool_task_args tpa;\n@@ -453,7 +453,7 @@ saib_ws_json_rx_builder(struct sai_plat_master *spm, const void *in, size_t len)\n \t\t * Master is requesting that a platform adopt a task...\n \t\t *\n \t\t * Multiple platforms may be using this connection to a given\n-\t\t * master so we have to disambiguate which platform he's\n+\t\t * server so we have to disambiguate which platform he's\n \t\t * tasking first.\n \t\t */\n \n@@ -483,9 +483,9 @@ saib_ws_json_rx_builder(struct sai_plat_master *spm, const void *in, size_t len)\n \t\t/*\n \t\t * Look for a spare nspawn...\n \t\t *\n-\t\t * We may connect to multiple masters and it's asynchronous\n-\t\t * which master may have tasked us first, so it's not that\n-\t\t * unusual to reject a task the master thought we could have\n+\t\t * We may connect to multiple servers and it's asynchronous\n+\t\t * which server may have tasked us first, so it's not that\n+\t\t * unusual to reject a task the server thought we could have\n \t\t * taken\n \t\t */\n \n@@ -508,7 +508,7 @@ saib_ws_json_rx_builder(struct sai_plat_master *spm, const void *in, size_t len)\n \n \t\t\t/*\n \t\t\t * Full up... reject the task and update every\n-\t\t\t * master's model of our task load status\n+\t\t\t * server's model of our task load status\n \t\t\t */\n \n \t\t\tlwsl_notice(\u0022%s: plat '%s': no idle nspawn (of %d), \u0022\n@@ -538,10 +538,10 @@ saib_ws_json_rx_builder(struct sai_plat_master *spm, const void *in, size_t len)\n \t\t\t*p \u003d '_';\n \n \t\t/*\n-\t\t * unique for remote master name (\u0022warmcat\u0022),\n+\t\t * unique for remote server name (\u0022warmcat\u0022),\n \t\t * project name (\u0022libwebsockets\u0022)\n \t\t */\n-\t\tns-\u003emaster_name \u003d spm-\u003ename;\n+\t\tns-\u003eserver_name \u003d spm-\u003ename;\n \t\tns-\u003eproject_name \u003d task-\u003erepo_name;\n \t\tif (!strncmp(task-\u003egit_ref, \u0022refs/heads/\u0022, 11))\n \t\t\tns-\u003eref \u003d task-\u003egit_ref + 11;\n@@ -574,7 +574,7 @@ saib_ws_json_rx_builder(struct sai_plat_master *spm, const void *in, size_t len)\n \t\tlws_snprintf(ns-\u003efsm.ovname, sizeof(ns-\u003efsm.ovname), \u0022%d-%d.%d\u0022,\n \t\t\t spm-\u003eindex, ns-\u003esp-\u003eindex, ns-\u003einstance_idx);\n \n-\t\tlwsl_notice(\u0022%s: master %s\u005cn\u0022, __func__, ns-\u003emaster_name);\n+\t\tlwsl_notice(\u0022%s: server %s\u005cn\u0022, __func__, ns-\u003eserver_name);\n \t\tlwsl_notice(\u0022%s: project %s\u005cn\u0022, __func__, ns-\u003eproject_name);\n \t\tlwsl_notice(\u0022%s: ref %s\u005cn\u0022, __func__, ns-\u003eref);\n \t\tlwsl_notice(\u0022%s: hash %s\u005cn\u0022, __func__, ns-\u003ehash);\n@@ -682,7 +682,7 @@ saib_ws_json_rx_builder(struct sai_plat_master *spm, const void *in, size_t len)\n \t\t * after that needs to adjust sp-\u003eongoing accordingly\n \t\t */\n \n-\t\tlwsl_warn(\u0022%s: enqueued mirror thread\u005cn\u0022, __func__);\n+\t\tlwsl_warn(\u0022%s: enqueued mirror thread, ns-\u003etp_task %p\u005cn\u0022, __func__, ns-\u003etp_task);\n \n \t\tlwsl_notice(\u0022%s: ongoing %d -\u003e %d\u005cn\u0022, __func__, sp-\u003eongoing,\n \t\t\t sp-\u003eongoing + 1);\n@@ -692,7 +692,7 @@ saib_ws_json_rx_builder(struct sai_plat_master *spm, const void *in, size_t len)\n \t\t/*\n \t\t * Let the mirror thread get on with things...\n \t\t *\n-\t\t * When we took on a task, we should inform any masters we're\n+\t\t * When we took on a task, we should inform any servers we're\n \t\t * connected to about our change in task load status\n \t\t */\n \ndiff --git a/src/common/include/private.h b/src/common/include/private.h\nindex 09e1ff7..dfb6c43 100644\n--- a/src/common/include/private.h\n+++ b/src/common/include/private.h\n@@ -18,7 +18,7 @@\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n * MA 02110-1301 USA\n *\n- * structs common to builder and master\n+ * structs common to builder and server\n */\n \n //#include \u003csai_config_private.h\u003e\n@@ -40,6 +40,7 @@ typedef enum {\n \tSAIES_FAIL\t\t\t\t\u003d 4,\n \tSAIES_CANCELLED\t\t\t\t\u003d 5,\n \tSAIES_BEING_BUILT_HAS_FAILURES\t\t\u003d 6,\n+\tSAIES_DELETED\t\t\t\t\u003d 7,\n } sai_event_state_t;\n \n enum {\n@@ -50,7 +51,7 @@ enum {\n };\n \n /*\n- * Builder is indicating he can't take the task and master should free it up\n+ * Builder is indicating he can't take the task and server should free it up\n * and try another builder.\n */\n \n@@ -96,7 +97,7 @@ typedef struct {\n \n \tstruct lwsac\t\t*ac_task_container;\n \n-\tconst char\t\t*master_name; /* used in offer */\n+\tconst char\t\t*server_name; /* used in offer */\n \tconst char\t\t*repo_name; /* used in offer */\n \tconst char\t\t*git_ref; /* used in offer */\n \tconst char\t\t*git_hash; /* used in offer */\n@@ -119,7 +120,7 @@ typedef struct sai_event {\n \tchar\t\t\t\thash[65];\n \tchar\t\t\t\tuuid[65];\n \tchar\t\t\t\tsource_ip[32];\n-\tvoid\t\t\t\t*pdb; /* master only, sqlite3 */\n+\tvoid\t\t\t\t*pdb; /* server only, sqlite3 */\n \tuint64_t\t\t\tcreated;\n \tuint64_t\t\t\tlast_updated;\n \tsai_event_state_t\t\tstate;\n@@ -160,13 +161,13 @@ typedef struct {\n struct sai_plat;\n \n /*\n- * One SS per unique master the builder connects to; one of these as the SS\n+ * One SS per unique server the builder connects to; one of these as the SS\n * userdata object\n *\n- * May be in use by multiple plats offered by same builder to same master\n+ * May be in use by multiple plats offered by same builder to same server\n */\n \n-typedef struct sai_plat_master {\n+typedef struct sai_plat_server {\n \tlws_dll2_t\t\tlist;\n \n \tlws_dll2_owner_t\trejection_list;\n@@ -187,7 +188,7 @@ typedef struct sai_plat_master {\n \tint\t\t\tindex; /* used to create unique build dir path */\n \n \tuint16_t\t\tretries;\n-} sai_plat_master_t;\n+} sai_plat_server_t;\n \n struct sai_env {\n \tlws_dll2_t\t\tlist;\n@@ -196,26 +197,26 @@ struct sai_env {\n \tconst char\t\t*value;\n };\n \n-typedef struct sai_plat_master_ref {\n+typedef struct sai_plat_server_ref {\n \tlws_dll2_t\t\tlist;\n-\tsai_plat_master_t\t*spm;\n-} sai_plat_master_ref_t;\n+\tsai_plat_server_t\t*spm;\n+} sai_plat_server_ref_t;\n \n /*\n * One of these instantiated per platform instance\n *\n- * It lists a sai_plat_master per master / ss that can use the platform\n+ * It lists a sai_plat_server per server / ss that can use the platform\n *\n * It contains an nspawn for each platform builder instance\n *\n- * It's also used as the object on master side that represents a builder /\n+ * It's also used as the object on server side that represents a builder /\n * platform instance and status.\n */\n \n typedef struct sai_plat {\n \tlws_dll2_t\t\tsai_plat_list;\n \n-\tlws_dll2_owner_t\tmasters; /* list of sai_plat_master_ref_t */\n+\tlws_dll2_owner_t\tservers; /* list of sai_plat_server_ref_t */\n \tlws_dll2_owner_t\tchunk_cache;\n \n \tconst char\t\t*name;\n@@ -225,7 +226,7 @@ typedef struct sai_plat {\n \tlws_dll2_owner_t\tnspawn_owner;\n \tstruct lwsac\t\t*deserialization_ac;\n \n-\tstruct lws\t\t*wsi; /* master side only */\n+\tstruct lws\t\t*wsi; /* server side only */\n \n \tlws_dll2_owner_t\tenv_head;\n \n@@ -249,6 +250,17 @@ typedef enum {\n \tSAILOGA_STARTED,\n } sai_log_action_t;\n \n+typedef struct sai_browse_rx_evinfo {\n+\tchar\t\tevent_hash[65];\n+\tint\t\tstate;\n+} sai_browse_rx_evinfo_t;\n+\n+typedef struct sai_browse_rx_taskinfo {\n+\tchar\t\ttask_hash[65];\n+\tunsigned int\tlog_start;\n+\tuint8_t\t\tlogs;\n+} sai_browse_rx_taskinfo_t;\n+\n extern const lws_struct_map_t\n \tlsm_schema_json_map_task[],\n \tlsm_schema_sq3_map_task[],\ndiff --git a/src/common/struct-metadata.c b/src/common/struct-metadata.c\nindex 8bfbdde..67ff0ed 100644\n--- a/src/common/struct-metadata.c\n+++ b/src/common/struct-metadata.c\n@@ -1,5 +1,5 @@\n /*\n- * Sai master - ./src/master/struct-metadata.c\n+ * Sai server - ./src/server/struct-metadata.c\n *\n * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n *\n@@ -18,7 +18,7 @@\n * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n * MA 02110-1301 USA\n *\n- * lws_struct metadata for structs common to builder and master\n+ * lws_struct metadata for structs common to builder and server\n */\n \n static const lws_struct_map_t lsm_plat[] \u003d {\n@@ -87,7 +87,7 @@ const lws_struct_map_t lsm_task[] \u003d {\n \tLSM_CARRAY\t(sai_task_t, event_uuid,\t\u0022event_uuid\u0022),\n \tLSM_CARRAY\t(sai_task_t, uuid,\t\t\u0022uuid\u0022),\n \tLSM_CARRAY\t(sai_task_t, builder_name,\t\u0022builder_name\u0022),\n-\tLSM_STRING_PTR\t(sai_task_t, master_name,\t\u0022master_name\u0022),\n+\tLSM_STRING_PTR\t(sai_task_t, server_name,\t\u0022server_name\u0022),\n \tLSM_STRING_PTR\t(sai_task_t, repo_name,\t\t\u0022repo_name\u0022),\n \tLSM_STRING_PTR\t(sai_task_t, git_ref,\t\t\u0022git_ref\u0022),\n \tLSM_STRING_PTR\t(sai_task_t, git_hash,\t\t\u0022git_hash\u0022),\n@@ -103,7 +103,7 @@ const lws_struct_map_t lsm_schema_sq3_map_task[] \u003d {\n \tLSM_SCHEMA_DLL2\t(sai_task_t, list, NULL, lsm_task,\t\u0022tasks\u0022),\n };\n \n-/* builder -\u003e master */\n+/* builder -\u003e server */\n \n const lws_struct_map_t lsm_task_rej[] \u003d {\n \tLSM_CARRAY\t(sai_rejection_t, host_platform, \u0022host_platform\u0022),\n@@ -117,7 +117,7 @@ const lws_struct_map_t lsm_schema_json_task_rej[] \u003d {\n \t\t\t\t\t\t \u0022com.warmcat.sai.taskrej\u0022)\n };\n \n-/* master -\u003e builder */\n+/* server -\u003e builder */\n \n const lws_struct_map_t lsm_task_cancel[] \u003d {\n \tLSM_CARRAY\t(sai_cancel_t, task_uuid,\t \u0022task_uuid\u0022),\n@@ -173,9 +173,9 @@ const lws_struct_map_t lsm_artifact[] \u003d {\n \tLSM_UNSIGNED\t(sai_artifact_t, uid,\t\t\t\u0022uid\u0022),\n \tLSM_CARRAY\t(sai_artifact_t, task_uuid,\t\t\u0022task_uuid\u0022),\n \tLSM_CARRAY\t(sai_artifact_t, blob_filename,\t\t\u0022blob_filename\u0022),\n-\t/* created master-side, sent back with valid upload, checked at master */\n+\t/* created server-side, sent back with valid upload, checked at server */\n \tLSM_CARRAY\t(sai_artifact_t, artifact_up_nonce,\t\u0022artifact_up_nonce\u0022),\n-\t/* created master-side (not sent to builder), used in artifact links we generate */\n+\t/* created server-side (not sent to builder), used in artifact links we generate */\n \tLSM_CARRAY\t(sai_artifact_t, artifact_down_nonce,\t\u0022artifact_down_nonce\u0022),\n \tLSM_BLOB_PTR\t(sai_artifact_t, blob,\t\t\t\u0022blob\u0022),\n \tLSM_UNSIGNED\t(sai_artifact_t, timestamp,\t\t\u0022timestamp\u0022),\ndiff --git a/src/device/d-sai.c b/src/device/d-sai.c\nindex 80d1b38..99237b3 100644\n--- a/src/device/d-sai.c\n+++ b/src/device/d-sai.c\n@@ -52,6 +52,8 @@\n #if defined(__linux__)\n #include \u003csys/prctl.h\u003e\n #endif\n+#include \u003csys/types.h\u003e\n+#include \u003csys/wait.h\u003e\n \n #include \u0022d-private.h\u0022\n \ndiff --git a/src/expect/e-sai.c b/src/expect/e-sai.c\nindex 56585a9..c90a796 100644\n--- a/src/expect/e-sai.c\n+++ b/src/expect/e-sai.c\n@@ -25,7 +25,7 @@\n *\n * Sai-expect opportunistically opens ttys exported by sai-device in the\n * environment, proxies logs to sai-builder (which forwards them to the sai-\n- * master that is storing them with the task), and parses the received tty\n+ * server that is storing them with the task), and parses the received tty\n * data and emits strings as requested by its arguments.\n */\n \ndiff --git a/src/master/CMakeLists.txt b/src/master/CMakeLists.txt\ndeleted file mode 100644\nindex 5c41dc7..0000000\n--- a/src/master/CMakeLists.txt\n+++ /dev/null\n@@ -1,141 +0,0 @@\n-set(SUB \u0022sai-master\u0022)\n-\n-set(CPACK_DEBIAN_MASTER_PACKAGE_NAME ${SUB})\n-\n-set(SRCS\n-\tm-sai.c\n-\tm-conf.c\n-\tm-notification.c\n-\tm-comms.c\n-\tm-ws-builder.c\n-\tm-task.c\n-\tm-central.c\n-\tm-artifact.c\n-\tm-overview.c\n-\tm-ws-browser.c\n-)\n-\n-set(requirements 1)\n-require_lws_config(LWS_WITH_STRUCT_SQLITE3\t1 requirements)\n-require_lws_config(LWS_WITH_SERVER\t\t1 requirements)\n-require_lws_config(LWS_WITH_GENCRYPTO\t\t1 requirements)\n-require_lws_config(LWS_WITH_UNIX_SOCK\t\t1 requirements)\n-\n-if (requirements)\n-\tadd_executable(${SUB} ${SRCS})\n-\tif (APPLE)\n-\t\tset_property(TARGET sai-master PROPERTY MACOSX_RPATH YES)\n-\tendif()\n-\n-\t#\n-\t# sqlite3 paths (master)\n-\t#\n-\tif (SAI_MASTER)\n-\t\tfind_path( SQLITE3_INC_PATH NAMES \u0022sqlite3.h\u0022)\n-\t\tfind_library(SQLITE3_LIB_PATH NAMES \u0022sqlite3\u0022)\n-\t\n-\t\tif (SQLITE3_INC_PATH AND SQLITE3_LIB_PATH)\n-\t\t\tinclude_directories(BEFORE \u0022${SQLITE3_INC_PATH}\u0022)\n-\t\telse()\n-\t\t\tmessage(FATAL_ERROR \u0022 Unable to find sqlite3\u0022)\n-\t\tendif()\n-\n-\t\ttarget_link_libraries(${SUB} ${SQLITE3_LIB_PATH})\n-\tendif()\n-\n- \tinclude_directories(BEFORE \u0022${SAI_LWS_INC_PATH}\u0022)\n-\n-\tCHECK_C_SOURCE_COMPILES(\u0022#include \u003clibwebsockets.h\u003e\u005cnint\n-\tmain(void) {\u005cn#if defined(LWS_HAVE_LIBCAP)\u005cn return\n-\t\t0;\u005cn#else\u005cn fail;\u005cn#endif\u005cn return 0;\u005cn}\u005cn\u0022 HAS_LIBCAP)\n-\tif (HAS_LIBCAP)\n-\t\tfind_library(CAP_LIB_PATH NAMES \u0022cap\u0022)\n-\tendif()\n-\n-\ttarget_link_libraries(${SUB} ${SAI_LWS_LIB_PATH})\n-\t\n-\tif (LWS_OPENSSL_LIBRARIES)\n-\t\ttarget_link_libraries(${SUB} ${LWS_OPENSSL_LIBRARIES})\n-\tendif()\n-\t\n-\tif (SAI_EXT_PTHREAD_LIBRARIES)\n-\t\ttarget_link_libraries(${SUB} ${SAI_EXT_PTHREAD_LIBRARIES})\n-\tendif()\n-\n-\tif (HAS_LIBCAP)\n-\t\ttarget_link_libraries(${SUB} ${CAP_LIB_PATH})\n-\tendif()\n-\n-\tif (MSVC)\n-\t\ttarget_link_libraries(${SUB} ws2_32.lib userenv.lib psapi.lib iphlpapi.lib)\n-\tendif()\n-\t\n-\tinstall(TARGETS ${SUB}\n-\t\tRUNTIME DESTINATION \u0022${BIN_DIR}\u0022 COMPONENT master)\n-\tinstall(\n-\t\tFILES ../../assets/index.html\n-\t\t ../../assets/OpenSans-Light.ttf\n-\t\t ../../assets/rebuild.png\n-\t\t ../../assets/delete.png\n-\t\t ../../assets/passed.svg\n-\t\t ../../assets/failed.svg\n-\t\t ../../assets/sai.css\n-\t\t ../../assets/sai.js\n-\t\t ../../assets/sai.svg\n-\t\t ../../assets/sai-icon.svg\n-\t\t ../../assets/builder.png\n-\t\t ../../assets/favicon.ico\n-\t\t ../../assets/strict-csp.svg\n-\t\t ../../assets/lws-common.js\n-\t\t ../../assets/gs/lwsgs.js\n-\t\t ../../assets/gs/lwsgs.css\n-\t\t\t../../assets/arch-aarch64-a72-bcm2711-rpi4.svg\n-\t\t\t../../assets/arch-aarch64.svg\n-\t\t\t../../assets/arch-arm32-m4-mt7697-usi.svg\n-\t\t\t../../assets/arch-arm32.svg\n-\t\t\t../../assets/arch-freertos.svg\n-\t\t\t../../assets/arch-riscv64-virt.svg\n-\t\t\t../../assets/arch-riscv.svg\n-\t\t\t../../assets/arch-x86_64-amd.svg\n-\t\t\t../../assets/arch-x86_64-intel-i3.svg\n-\t\t\t../../assets/arch-x86_64-intel.svg\n-\t\t\t../../assets/arch-xl6-esp32.svg\n-\t\t\t../../assets/artifact.svg\n-\t\t\t../../assets/builder.svg\n-\t\t\t../../assets/decal-2.svg\n-\t\t\t../../assets/decal-3.svg\n-\t\t\t../../assets/decal-4.svg\n-\t\t\t../../assets/decal-5.svg\n-\t\t\t../../assets/decal-6.svg\n-\t\t\t../../assets/freertos-espidf.svg\n-\t\t\t../../assets/freertos-linkit.svg\n-\t\t\t../../assets/git.svg\n-\t\t\t../../assets/jsplease.svg\n-\t\t\t../../assets/linux-android.svg\n-\t\t\t../../assets/linux-centos-8.svg\n-\t\t\t../../assets/linux-fedora-32-riscv.svg\n-\t\t\t../../assets/linux-fedora-32.svg\n-\t\t\t../../assets/linux-gentoo.svg\n-\t\t\t../../assets/linux-ubuntu-1804.svg\n-\t\t\t../../assets/linux-ubuntu-2004.svg\n-\t\t\t../../assets/linux-ubuntu-xenial-amd64.svg\n-\t\t\t../../assets/netbsd-iOS.svg\n-\t\t\t../../assets/netbsd-OSX-catalina.svg\n-\t\t\t../../assets/sai-event.svg\n-\t\t\t../../assets/sai-icon.svg\n-\t\t\t../../assets/sai.svg\n-\t\t\t../../assets/stop.svg\n-\t\t\t../../assets/strict-csp.svg\n-\t\t\t../../assets/tc-gcc.svg\n-\t\t\t../../assets/tc-llvm.svg\n-\t\t\t../../assets/tc-mingw32.svg\n-\t\t\t../../assets/tc-mingw64.svg\n-\t\t\t../../assets/tc-msvc.svg\n-\t\t\t../../assets/ubuntu-focal-aarch64.svg\n-\t\t\t../../assets/virt-qemu.svg\n-\t\t\t../../assets/windows-10.svg\n-\n-\t\tDESTINATION \u0022${DATA_DIR}/sai/assets\u0022\n-\t\tCOMPONENT master)\n-\n-endif(requirements)\ndiff --git a/src/master/m-artifact.c b/src/master/m-artifact.c\ndeleted file mode 100644\nindex 527abc2..0000000\n--- a/src/master/m-artifact.c\n+++ /dev/null\n@@ -1,144 +0,0 @@\n-/*\n- * Sai master\n- *\n- * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n- *\n- * This library is free software; you can redistribute it and/or\n- * modify it under the terms of the GNU Lesser General Public\n- * License as published by the Free Software Foundation:\n- * version 2.1 of the License.\n- *\n- * This library is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n- * Lesser General Public License for more details.\n- *\n- * You should have received a copy of the GNU Lesser General Public\n- * License along with this library; if not, write to the Free Software\n- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n- * MA 02110-1301 USA\n- */\n-\n-#include \u003clibwebsockets.h\u003e\n-#include \u003cstring.h\u003e\n-#include \u003csignal.h\u003e\n-#include \u003ctime.h\u003e\n-\n-#include \u0022m-private.h\u0022\n-\n-int\n-saim_get_blob(struct vhd *vhd, const char *url, sqlite3 **pdb,\n-\t sqlite3_blob **blob, uint64_t *length)\n-{\n-\tchar task_uuid[66], event_uuid[34], nonce[34], qu[200],\n-\t esc[66], esc1[34];\n-\tstruct lwsac *ac \u003d NULL;\n-\tlws_dll2_owner_t o;\n-\tsai_artifact_t *a;\n-\tconst char *p;\n-\tuint64_t rid \u003d 0;\n-\tint n;\n-\n-\t/*\n-\t * We get passed the RHS of a URL string like\n-\t *\n-\t * \u003ctask_uuid\u003e/\u003cdown_nonce\u003e/filename\n-\t *\n-\t * filename is not used for matching, but make sure the client saves it\n-\t * using the name generated along with the link.\n-\t *\n-\t * Extract the pieces from the URL\n-\t */\n-\n-\tn \u003d 0;\n-\tp \u003d url;\n-\twhile (*p \u0026\u0026 *p !\u003d '/' \u0026\u0026 n \u003c (int)sizeof(task_uuid) - 1)\n-\t\ttask_uuid[n++] \u003d *p++;\n-\n-\tif (n !\u003d sizeof(task_uuid) - 2 || *p !\u003d '/') {\n-\t\tlwsl_notice(\u0022%s: url layout 1: %d %s\u005cn\u0022, __func__, n, url);\n-\t\treturn -1;\n-\t}\n-\n-\ttask_uuid[n] \u003d '\u005c0';\n-\tp++; /* skip the / */\n-\n-\tn \u003d 0;\n-\twhile (*p \u0026\u0026 *p !\u003d '/' \u0026\u0026 n \u003c (int)sizeof(nonce) - 1)\n-\t\tnonce[n++] \u003d *p++;\n-\n-\tif (n !\u003d sizeof(nonce) - 2 || *p !\u003d '/') {\n-\t\tlwsl_notice(\u0022%s: url layout 2\u005cn\u0022, __func__);\n-\t\treturn -1;\n-\t}\n-\n-\tsai_task_uuid_to_event_uuid(event_uuid, task_uuid);\n-\n-\t/* open the event-specific database object */\n-\n-\tif (saim_event_db_ensure_open(vhd, event_uuid, 0, pdb)) {\n-\t\tlwsl_info(\u0022%s: unable to open event-specific database\u005cn\u0022,\n-\t\t\t\t__func__);\n-\n-\t\treturn -1;\n-\t}\n-\n-\t/*\n-\t * Query the artifact he's asking for\n-\t */\n-\n-\tlws_sql_purify(esc, task_uuid, sizeof(esc));\n-\tlws_sql_purify(esc1, nonce, sizeof(esc1));\n-\tlws_snprintf(qu, sizeof(qu),\n-\t\t \u0022 and task_uuid\u003d'%s' and artifact_down_nonce\u003d'%s'\u0022,\n-\t\t esc, esc1);\n-\tn \u003d lws_struct_sq3_deserialize(*pdb, qu, NULL,\n-\t\t\t\t lsm_schema_sq3_map_artifact,\n-\t\t\t\t \u0026o, \u0026ac, 0, 1);\n-\tif (n \u003c 0 || !o.head) {\n-\t\tlwsl_err(\u0022%s: no result from %s\u005cn\u0022, __func__, qu);\n-\t\tgoto fail;\n-\t}\n-\n-\ta \u003d (sai_artifact_t *)o.head;\n-\n-\t*length \u003d a-\u003elen;\n-\n-\t/*\n-\t * recover the rowid the blob api requires\n-\t */\n-\n-\tlws_snprintf(qu, sizeof(qu), \u0022select rowid from artifacts \u0022\n-\t\t\t\t \u0022where artifact_down_nonce\u003d'%s'\u0022, esc1);\n-\n-\tif (sqlite3_exec(*pdb, qu, sai_sql3_get_uint64_cb, \u0026rid, NULL) !\u003d SQLITE_OK) {\n-\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, qu, sqlite3_errmsg(*pdb));\n-\t\tgoto fail;\n-\t}\n-\n-\t/*\n-\t * Get a read-only handle on the blob\n-\t */\n-\n-\tif (sqlite3_blob_open(*pdb, \u0022main\u0022, \u0022artifacts\u0022, \u0022blob\u0022, rid, 0, blob) !\u003d SQLITE_OK) {\n-\t\tlwsl_err(\u0022%s: unable to open blob, rid %d\u005cn\u0022, __func__, (int)rid);\n-\t\tgoto fail;\n-\t}\n-\n-\tlwsac_free(\u0026ac); /* drop the lwsac holding that result */\n-\n-\t/*\n-\t * It was successful, *length was set, *pdb and *blob are live handles\n-\t * to the db and the blob itself.\n-\t */\n-\n-\treturn 0;\n-\n-fail:\n-\tlwsac_free(\u0026ac);\n-\tsaim_event_db_close(vhd, pdb);\n-\n-\tlwsl_notice(\u0022%s: couldn't find blob %s\u005cn\u0022, __func__, url);\n-\n-\treturn -1;\n-}\ndiff --git a/src/master/m-central.c b/src/master/m-central.c\ndeleted file mode 100644\nindex c967625..0000000\n--- a/src/master/m-central.c\n+++ /dev/null\n@@ -1,159 +0,0 @@\n-/*\n- * Sai master\n- *\n- * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n- *\n- * This library is free software; you can redistribute it and/or\n- * modify it under the terms of the GNU Lesser General Public\n- * License as published by the Free Software Foundation:\n- * version 2.1 of the License.\n- *\n- * This library is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n- * Lesser General Public License for more details.\n- *\n- * You should have received a copy of the GNU Lesser General Public\n- * License along with this library; if not, write to the Free Software\n- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n- * MA 02110-1301 USA\n- *\n- *\n- * Central dispatcher for jobs from events that made it into the database. This\n- * is done in an event-driven way in m-task.c, but management of it also has to\n- * be done in the background for when there are no events coming,\n- */\n-\n-#include \u003clibwebsockets.h\u003e\n-#include \u003cstring.h\u003e\n-#include \u003csignal.h\u003e\n-#include \u003ctime.h\u003e\n-\n-#include \u0022m-private.h\u0022\n-\n-extern struct lws_context *context;\n-\n-static void\n-saim_central_clean_abandoned(struct vhd *vhd)\n-{\n-\tsaim_sqlite_cache_t *sc;\n-\tstruct lwsac *ac \u003d NULL;\n-\tlws_usec_t now \u003d lws_now_usecs();\n-\tlws_dll2_owner_t o;\n-\tchar s[160];\n-\tint n, nzr;\n-\n-\t/*\n-\t * Sqlite cache cleaning\n-\t */\n-\n-\tnzr \u003d 0;\n-\tlws_start_foreach_dll_safe(struct lws_dll2 *, p, p1,\n-\t\t\t\t vhd-\u003esqlite3_cache.head) {\n-\t\tsc \u003d lws_container_of(p, saim_sqlite_cache_t, list);\n-\n-\t\tif (!sc-\u003erefcount \u0026\u0026\n-\t\t (now - sc-\u003eidle_since) \u003e (60 * LWS_USEC_PER_SEC)) {\n-\t\t\tlwsl_notice(\u0022%s: delayed db pool clean %s\u005cn\u0022, __func__,\n-\t\t\t\t\tsc-\u003euuid);\n-\t\t\tlws_struct_sq3_close(\u0026sc-\u003epdb);\n-\t\t\tlws_dll2_remove(\u0026sc-\u003elist);\n-\t\t\tfree(sc);\n-\t\t} else\n-\t\t\tif (sc-\u003erefcount)\n-\t\t\t\tnzr++;\n-\n-\t} lws_end_foreach_dll_safe(p, p1);\n-\n-\tif (vhd-\u003esqlite3_cache.count)\n-\t\tlwsl_notice(\u0022%s: db pool items: in-use: %d, total: %d\u005cn\u0022,\n-\t\t\t __func__, nzr, vhd-\u003esqlite3_cache.count);\n-\n-\t/*\n-\t * Collect the most recent \u003c\u003d10 events that still feel they're\n-\t * incomplete and may be running something\n-\t */\n-\n-\tn \u003d lws_struct_sq3_deserialize(vhd-\u003emaster.pdb,\n-\t\t\t\t \u0022 and (state !\u003d 3 and state !\u003d 4 and state !\u003d 5)\u0022,\n-\t\t\t\t NULL, lsm_schema_sq3_map_event, \u0026o,\n-\t\t\t\t \u0026ac, 0, 10);\n-\tif (n \u003c 0 || !o.head)\n-\t\treturn;\n-\n-\t/*\n-\t * For each of those events, look for tasks that have been running\n-\t * \u0022too long\u0022, eg, builder restarted or lost connection etc\n-\t */\n-\n-\tlws_start_foreach_dll(struct lws_dll2 *, p, o.head) {\n-\t\tsai_event_t *e \u003d lws_container_of(p, sai_event_t, list);\n-\t\tsqlite3 *pdb \u003d NULL;\n-\n-\t\tif (!saim_event_db_ensure_open(vhd, e-\u003euuid, 0, \u0026pdb)) {\n-\t\t\t/*\n-\t\t\t * Such tasks should go into CANCELLED\n-\t\t\t */\n-\n-\t\t\tlws_snprintf(s, sizeof(s),\n-\t\t\t\t \u0022update tasks set state\u003d%d where \u0022\n-\t\t\t\t \u0022(state\u003d1 OR state\u003d2) and started \u003c %llu\u0022,\n-\t\t\t\t SAIES_CANCELLED, (unsigned long long)\n-\t\t\t\t\t (lws_now_secs() - (30 * 60)));\n-\n-\t\t\tif (sqlite3_exec(pdb, s, NULL, NULL, NULL) !\u003d\n-\t\t\t\t\t SQLITE_OK)\n-\t\t\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, s,\n-\t\t\t\t\t sqlite3_errmsg(pdb));\n-\n-\t\t\tsaim_event_db_close(vhd, \u0026pdb);\n-\t\t}\n-\n-\t} lws_end_foreach_dll(p);\n-\n-\tlwsac_free(\u0026ac);\n-}\n-\n-void\n-saim_central_cb(lws_sorted_usec_list_t *sul)\n-{\n-\tstruct vhd *vhd \u003d lws_container_of(sul, struct vhd, sul_central);\n-\n-\t/*\n-\t * For each builder connected to us, see if it can handle a new task,\n-\t * and if so, try to select one matching its supported platforms\n-\t */\n-\n-\tlws_start_foreach_dll(struct lws_dll2 *, p,\n-\t\t\t vhd-\u003emaster.builder_owner.head) {\n-\t\tsai_plat_t *cb \u003d lws_container_of(p, sai_plat_t, sai_plat_list);\n-\n-\t\tlwsl_debug(\u0022%s: checking tasks %s %d %d %p\u005cn\u0022, __func__,\n-\t\t\t cb-\u003ename, cb-\u003eongoing, cb-\u003einstances, cb-\u003ewsi);\n-\n-\t\tif (cb-\u003ewsi \u0026\u0026 lws_wsi_user(cb-\u003ewsi) \u0026\u0026\n-\t\t cb-\u003eongoing \u003c cb-\u003einstances)\n-\t\t\tsaim_allocate_task(vhd,\n-\t\t\t\t\t(struct pss *)lws_wsi_user(cb-\u003ewsi),\n-\t\t\t\t\tcb, cb-\u003eplatform);\n-\n-\t} lws_end_foreach_dll(p);\n-\n-\t/*\n-\t * Need to globally check for abandoned tasks periodically\n-\t */\n-\n-\tif (!vhd-\u003elast_check_abandoned_tasks ||\n-\t lws_now_usecs() \u003e (vhd-\u003elast_check_abandoned_tasks +\n-\t\t\t (20 * LWS_USEC_PER_SEC))) {\n-\n-\t\tsaim_central_clean_abandoned(vhd);\n-\n-\t\tvhd-\u003elast_check_abandoned_tasks \u003d lws_now_usecs();\n-\t}\n-\n-\t/* check again in 1s */\n-\n-\tlws_sul_schedule(context, 0, \u0026vhd-\u003esul_central, saim_central_cb,\n-\t\t\t 1 * LWS_US_PER_SEC);\n-}\ndiff --git a/src/master/m-comms.c b/src/master/m-comms.c\ndeleted file mode 100644\nindex de18157..0000000\n--- a/src/master/m-comms.c\n+++ /dev/null\n@@ -1,1220 +0,0 @@\n-/*\n- * Sai master\n- *\n- * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n- *\n- * This library is free software; you can redistribute it and/or\n- * modify it under the terms of the GNU Lesser General Public\n- * License as published by the Free Software Foundation:\n- * version 2.1 of the License.\n- *\n- * This library is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n- * Lesser General Public License for more details.\n- *\n- * You should have received a copy of the GNU Lesser General Public\n- * License along with this library; if not, write to the Free Software\n- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n- * MA 02110-1301 USA\n- *\n- * The same ws interface is connected-to by builders (on path /builder), and\n- * provides the query transport for browsers (on path /browse).\n- *\n- * There's a single master slite3 database containing events, and a separate\n- * sqlite3 database file for each event, it only contains tasks and logs for\n- * the event and can be deleted when the event record associated with it is\n- * deleted. This is to keep is scalable when there may be thousands of events\n- * and related tasks and logs stored.\n- */\n-\n-#include \u003clibwebsockets.h\u003e\n-#include \u003cstring.h\u003e\n-#include \u003csignal.h\u003e\n-#include \u003ctime.h\u003e\n-#include \u003cstdio.h\u003e\n-#include \u003cfcntl.h\u003e\n-\n-#include \u0022m-private.h\u0022\n-\n-#include \u0022../common/struct-metadata.c\u0022\n-\n-typedef enum {\n-\tSJS_CLONING,\n-\tSJS_ASSIGNING,\n-\tSJS_WAITING,\n-\tSJS_DONE\n-} sai_job_state_t;\n-\n-typedef struct sai_job {\n-\tstruct lws_dll2 jobs_list;\n-\tchar reponame[64];\n-\tchar ref[64];\n-\tchar head[64];\n-\n-\ttime_t requested;\n-\n-\tsai_job_state_t state;\n-\n-} sai_job_t;\n-\n-const lws_struct_map_t lsm_schema_map_ta[] \u003d {\n-\tLSM_SCHEMA (sai_task_t,\t NULL, lsm_task, \u0022com-warmcat-sai-ta\u0022),\n-};\n-\n-typedef struct sai_auth {\n-\tlws_dll2_t\t\tlist;\n-\tchar\t\t\tname[33];\n-\tchar\t\t\tpassphrase[65];\n-\tunsigned long\t\tsince;\n-\tunsigned long\t\tlast_updated;\n-} sai_auth_t;\n-\n-const lws_struct_map_t lsm_auth[] \u003d {\n-\tLSM_CARRAY\t(sai_auth_t, name,\t\t\u0022name\u0022),\n-\tLSM_CARRAY\t(sai_auth_t, passphrase,\t\u0022passphrase\u0022),\n-\tLSM_UNSIGNED\t(sai_auth_t, since,\t\t\u0022since\u0022),\n-\tLSM_UNSIGNED\t(sai_auth_t, last_updated,\t\u0022last_updated\u0022),\n-};\n-\n-const lws_struct_map_t lsm_schema_sq3_map_auth[] \u003d {\n-\tLSM_SCHEMA_DLL2\t(sai_auth_t, list, NULL, lsm_auth,\t\u0022auth\u0022),\n-};\n-\n-extern const lws_struct_map_t lsm_schema_sq3_map_event[];\n-\n-static int\n-sai_destroy_builder(struct lws_dll2 *d, void *user)\n-{\n-//\tsaib_t *b \u003d lws_container_of(d, saib_t, c.builder_list);\n-\n-\tlws_dll2_remove(d);\n-\n-\treturn 0;\n-}\n-\n-static void\n-saim_master_destroy(saim_t *master)\n-{\n-\tlws_dll2_foreach_safe(\u0026master-\u003ebuilder_owner, NULL, sai_destroy_builder);\n-\n-\tlws_struct_sq3_close(\u0026master-\u003epdb);\n-}\n-\n-/* len is typically 16 (event uuid is 32 chars + NUL)\n- * But eg, task uuid is concatenated 32-char eventid and 32-char taskid\n- */\n-\n-int\n-sai_uuid16_create(struct lws_context *context, char *dest33)\n-{\n-\treturn lws_hex_random(context, dest33, 33);\n-}\n-\n-int\n-sai_sqlite3_statement(sqlite3 *pdb, const char *cmd, const char *desc)\n-{\n-\tsqlite3_stmt *sm;\n-\tint n;\n-\n-\tif (sqlite3_prepare_v2(pdb, cmd, -1, \u0026sm, NULL) !\u003d SQLITE_OK) {\n-\t\tlwsl_err(\u0022%s: Unable to %s: %s\u005cn\u0022,\n-\t\t\t __func__, desc, sqlite3_errmsg(pdb));\n-\n-\t\treturn 1;\n-\t}\n-\n-\tn \u003d sqlite3_step(sm);\n-\tsqlite3_reset(sm);\n-\tsqlite3_finalize(sm);\n-\tif (n !\u003d SQLITE_DONE) {\n-\t\tlwsl_err(\u0022%s: %d: Unable to perform \u005c\u0022%s\u005c\u0022: %s\u005cn\u0022, __func__,\n-\t\t\t sqlite3_extended_errcode(pdb), desc,\n-\t\t\t sqlite3_errmsg(pdb));\n-\t\tputs(cmd);\n-\n-\t\treturn 1;\n-\t}\n-\n-\treturn 0;\n-}\n-\n-int\n-saim_event_db_ensure_open(struct vhd *vhd, const char *event_uuid,\n-\t\t\t char create_if_needed, sqlite3 **ppdb)\n-{\n-\tchar filepath[256], saf[33];\n-\tsaim_sqlite_cache_t *sc;\n-\n-\tif (*ppdb)\n-\t\treturn 0;\n-\n-\t/* do we have this guy cached? */\n-\n-\tlws_start_foreach_dll(struct lws_dll2 *, p, vhd-\u003esqlite3_cache.head) {\n-\t\tsc \u003d lws_container_of(p, saim_sqlite_cache_t, list);\n-\n-\t\tif (!strcmp(event_uuid, sc-\u003euuid)) {\n-\t\t\tsc-\u003erefcount++;\n-\t\t\t*ppdb \u003d sc-\u003epdb;\n-\t\t\treturn 0;\n-\t\t}\n-\n-\t} lws_end_foreach_dll(p);\n-\n-\t/* ... nope, well, let's open and cache him then... */\n-\n-\tlws_strncpy(saf, event_uuid, sizeof(saf));\n-\tlws_filename_purify_inplace(saf);\n-\n-\tlws_snprintf(filepath, sizeof(filepath), \u0022%s-event-%s.sqlite3\u0022,\n-\t\t vhd-\u003esqlite3_path_lhs, saf);\n-\n-\tif (lws_struct_sq3_open(vhd-\u003econtext, filepath, create_if_needed, ppdb)) {\n-\t\tlwsl_err(\u0022%s: Unable to open db %s: %s\u005cn\u0022, __func__,\n-\t\t\t filepath, sqlite3_errmsg(*ppdb));\n-\n-\t\treturn 1;\n-\t}\n-\n-\t/* create / add to the schema for the tables we will have in here */\n-\n-\tif (lws_struct_sq3_create_table(*ppdb, lsm_schema_sq3_map_task))\n-\t\treturn 1;\n-\n-\tsai_sqlite3_statement(*ppdb, \u0022PRAGMA journal_mode\u003dWAL;\u0022, \u0022set WAL\u0022);\n-\n-\tif (lws_struct_sq3_create_table(*ppdb, lsm_schema_sq3_map_log))\n-\t\treturn 1;\n-\n-\tif (lws_struct_sq3_create_table(*ppdb, lsm_schema_sq3_map_artifact))\n-\t\treturn 1;\n-\n-\tsc \u003d malloc(sizeof(*sc));\n-\tmemset(sc, 0, sizeof(*sc));\n-\tif (!sc) {\n-\t\tlws_struct_sq3_close(ppdb);\n-\t\t*ppdb \u003d NULL;\n-\t\treturn 1;\n-\t}\n-\n-\tlws_strncpy(sc-\u003euuid, event_uuid, sizeof(sc-\u003euuid));\n-\tsc-\u003erefcount \u003d 1;\n-\tsc-\u003epdb \u003d *ppdb;\n-\tlws_dll2_add_tail(\u0026sc-\u003elist, \u0026vhd-\u003esqlite3_cache);\n-\n-\treturn 0;\n-}\n-\n-void\n-saim_event_db_close(struct vhd *vhd, sqlite3 **ppdb)\n-{\n-\tsaim_sqlite_cache_t *sc;\n-\n-\tif (!*ppdb)\n-\t\treturn;\n-\n-\t/* look for him in the cache */\n-\n-\tlws_start_foreach_dll(struct lws_dll2 *, p, vhd-\u003esqlite3_cache.head) {\n-\t\tsc \u003d lws_container_of(p, saim_sqlite_cache_t, list);\n-\n-\t\tif (sc-\u003epdb \u003d\u003d *ppdb) {\n-\t\t\t*ppdb \u003d NULL;\n-\t\t\tif (--sc-\u003erefcount) {\n-\t\t\t\tlwsl_notice(\u0022%s: zero refcount to idle\u005cn\u0022, __func__);\n-\t\t\t\t/*\n-\t\t\t\t * He's not currently in use then... don't\n-\t\t\t\t * close him immediately, m-central.c has a\n-\t\t\t\t * timer that closes and removes sqlite3\n-\t\t\t\t * cache entries idle for longer than 60s\n-\t\t\t\t */\n-\t\t\t\tsc-\u003eidle_since \u003d lws_now_usecs();\n-\t\t\t}\n-\n-\t\t\treturn;\n-\t\t}\n-\n-\t} lws_end_foreach_dll(p);\n-\n-\tlws_struct_sq3_close(ppdb);\n-\t*ppdb \u003d NULL;\n-}\n-\n-int\n-saim_event_db_delete_database(struct vhd *vhd, const char *event_uuid)\n-{\n-\tchar filepath[256], saf[33];\n-\n-\tlws_strncpy(saf, event_uuid, sizeof(saf));\n-\tlws_filename_purify_inplace(saf);\n-\n-\tlws_snprintf(filepath, sizeof(filepath), \u0022%s-event-%s.sqlite3\u0022,\n-\t\t vhd-\u003esqlite3_path_lhs, saf);\n-\n-\treturn unlink(filepath);\n-}\n-\n-\n-#if 0\n-static void\n-saim_all_browser_on_writable(struct vhd *vhd)\n-{\n-\tlws_start_foreach_dll(struct lws_dll2 *, mp, vhd-\u003ebrowsers.head) {\n-\t\tstruct pss *pss \u003d lws_container_of(mp, struct pss, same);\n-\n-\t\tlws_callback_on_writable(pss-\u003ewsi);\n-\t} lws_end_foreach_dll(mp);\n-}\n-#endif\n-\n-typedef enum {\n-\tSHMUT_NONE \u003d -1,\n-\tSHMUT_HOOK,\n-\tSHMUT_BROWSE,\n-\tSHMUT_STATUS,\n-\tSHMUT_ARTIFACTS,\n-\tSHMUT_LOGIN\n-} sai_http_murl_t;\n-\n-static const char * const well_known[] \u003d {\n-\t\u0022/update-hook\u0022,\n-\t\u0022/sai/browse\u0022,\n-\t\u0022/status\u0022,\n-\t\u0022/artifacts/\u0022, /* HTTP api for accessing build artifacts */\n-\t\u0022/login\u0022\n-};\n-\n-static const char *hmac_names[] \u003d {\n-\t\u0022sai sha256\u003d\u0022,\n-\t\u0022sai sha384\u003d\u0022,\n-\t\u0022sai sha512\u003d\u0022\n-};\n-\n-void\n-mark_pending(struct pss *pss, ws_state state)\n-{\n-\tpss-\u003epending |\u003d 1 \u003c\u003c state;\n-\tlws_callback_on_writable(pss-\u003ewsi);\n-}\n-\n-static int\n-browser_upd(struct lws_dll2 *d, void *user)\n-{\n-\tstruct pss *pss \u003d lws_container_of(d, struct pss, same);\n-\tws_state state \u003d (ws_state)(intptr_t)user;\n-\n-\tmark_pending(pss, state);\n-\n-\treturn 0;\n-}\n-\n-int\n-sai_get_head_status(struct vhd *vhd, const char *projname)\n-{\n-\tstruct lwsac *ac \u003d NULL;\n-\tlws_dll2_owner_t o;\n-\tsai_event_t *e;\n-\tint state;\n-\n-\tif (lws_struct_sq3_deserialize(vhd-\u003emaster.pdb, NULL, \u0022created \u0022,\n-\t\t\tlsm_schema_sq3_map_event, \u0026o, \u0026ac, 0, -1))\n-\t\treturn -1;\n-\n-\tif (!o.head)\n-\t\treturn -1;\n-\n-\te \u003d lws_container_of(o.head, sai_event_t, list);\n-\tstate \u003d e-\u003estate;\n-\n-\tlwsac_free(\u0026ac);\n-\n-\treturn state;\n-}\n-\n-\n-static int\n-sai_login_cb(void *data, const char *name, const char *filename,\n-\t char *buf, int len, enum lws_spa_fileupload_states state)\n-{\n-\treturn 0;\n-}\n-\n-static const char * const auth_param_names[] \u003d {\n-\t\u0022lname\u0022,\n-\t\u0022lpass\u0022,\n-\t\u0022success_redir\u0022,\n-};\n-\n-enum enum_param_names {\n-\tEPN_LNAME,\n-\tEPN_LPASS,\n-\tEPN_SUCCESS_REDIR,\n-};\n-\n-static int\n-callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user,\n-\t void *in, size_t len)\n-{\n-\tstruct vhd *vhd \u003d (struct vhd *)lws_protocol_vh_priv_get(\n-\t\t\t\tlws_get_vhost(wsi), lws_get_protocol(wsi));\n-\tuint8_t buf[LWS_PRE + 8192], *start \u003d \u0026buf[LWS_PRE], *p \u003d start,\n-\t\t*end \u003d \u0026buf[sizeof(buf) - LWS_PRE - 1];\n-\tstruct pss *pss \u003d (struct pss *)user;\n-\tstruct lws_jwt_sign_set_cookie ck;\n-\tsai_http_murl_t mu \u003d SHMUT_NONE;\n-\tchar projname[64];\n-\tint n, resp, r;\n-\tconst char *cp;\n-\tsize_t cml;\n-\n-\t(void)end;\n-\t(void)p;\n-\n-\tswitch (reason) {\n-\tcase LWS_CALLBACK_PROTOCOL_INIT:\n-\t\tvhd \u003d lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),\n-\t\t\t\t\t\t lws_get_protocol(wsi),\n-\t\t\t\t\t\t sizeof(struct vhd));\n-\t\tif (!vhd)\n-\t\t\treturn -1;\n-\n-\t\tvhd-\u003econtext \u003d lws_get_context(wsi);\n-\t\tvhd-\u003evhost \u003d lws_get_vhost(wsi);\n-\n-\t\tif (lws_pvo_get_str(in, \u0022notification-key\u0022,\n-\t\t\t\t \u0026vhd-\u003enotification_key)) {\n-\t\t\tlwsl_err(\u0022%s: notification_key pvo required\u005cn\u0022, __func__);\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tif (lws_pvo_get_str(in, \u0022database\u0022, \u0026vhd-\u003esqlite3_path_lhs)) {\n-\t\t\tlwsl_err(\u0022%s: database pvo required\u005cn\u0022, __func__);\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tlws_snprintf((char *)buf, sizeof(buf), \u0022%s-events.sqlite3\u0022,\n-\t\t\t\tvhd-\u003esqlite3_path_lhs);\n-\n-\t\tif (lws_struct_sq3_open(vhd-\u003econtext, (char *)buf, 1,\n-\t\t\t\t\t\u0026vhd-\u003emaster.pdb)) {\n-\t\t\tlwsl_err(\u0022%s: Unable to open session db %s: %s\u005cn\u0022,\n-\t\t\t\t __func__, vhd-\u003esqlite3_path_lhs, sqlite3_errmsg(\n-\t\t\t\t\t\t vhd-\u003emaster.pdb));\n-\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tsai_sqlite3_statement(vhd-\u003emaster.pdb,\n-\t\t\t\t \u0022PRAGMA journal_mode\u003dWAL;\u0022, \u0022set WAL\u0022);\n-\n-\t\tif (lws_struct_sq3_create_table(vhd-\u003emaster.pdb,\n-\t\t\t\t\t\tlsm_schema_sq3_map_event)) {\n-\t\t\tlwsl_err(\u0022%s: unable to create event table\u005cn\u0022, __func__);\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\t/* auth database */\n-\n-\t\tlws_snprintf((char *)buf, sizeof(buf), \u0022%s-auth.sqlite3\u0022,\n-\t\t\t\tvhd-\u003esqlite3_path_lhs);\n-\n-\t\tif (lws_struct_sq3_open(vhd-\u003econtext, (char *)buf, 1,\n-\t\t\t\t\t\u0026vhd-\u003emaster.pdb_auth)) {\n-\t\t\tlwsl_err(\u0022%s: Unable to open auth db %s: %s\u005cn\u0022,\n-\t\t\t\t __func__, vhd-\u003esqlite3_path_lhs, sqlite3_errmsg(\n-\t\t\t\t\t\t vhd-\u003emaster.pdb));\n-\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tif (lws_struct_sq3_create_table(vhd-\u003emaster.pdb_auth,\n-\t\t\t\t\t\tlsm_schema_sq3_map_auth)) {\n-\t\t\tlwsl_err(\u0022%s: unable to create auth table\u005cn\u0022, __func__);\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\t/*\n-\t\t * jwt-iss\n-\t\t */\n-\n-\t\tif (lws_pvo_get_str(in, \u0022jwt-iss\u0022, \u0026vhd-\u003ejwt_issuer)) {\n-\t\t\tlwsl_err(\u0022%s: jwt-iss required\u005cn\u0022, __func__);\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\t/*\n-\t\t * jwt-aud\n-\t\t */\n-\n-\t\tif (lws_pvo_get_str(in, \u0022jwt-aud\u0022, \u0026vhd-\u003ejwt_audience)) {\n-\t\t\tlwsl_err(\u0022%s: jwt-aud required\u005cn\u0022, __func__);\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\t/*\n-\t\t * auth-alg\n-\t\t */\n-\n-\t\tif (lws_pvo_get_str(in, \u0022jwt-auth-alg\u0022, \u0026cp)) {\n-\t\t\tlwsl_err(\u0022%s: jwt-auth-alg required\u005cn\u0022, __func__);\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tlws_strncpy(vhd-\u003ejwt_auth_alg, cp, sizeof(vhd-\u003ejwt_auth_alg));\n-\n-\t\t/*\n-\t\t * auth-jwk-path\n-\t\t */\n-\n-\t\tif (lws_pvo_get_str(in, \u0022jwt-auth-jwk-path\u0022, \u0026cp)) {\n-\t\t\tlwsl_err(\u0022%s: jwt-auth-jwk-path required\u005cn\u0022, __func__);\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tn \u003d open(cp, LWS_O_RDONLY);\n-\t\tif (!n) {\n-\t\t\tlwsl_err(\u0022%s: can't open auth JWK %s\u005cn\u0022, __func__, cp);\n-\t\t\treturn -1;\n-\t\t}\n-\t\tr \u003d read(n, buf, sizeof(buf));\n-\t\tclose(n);\n-\t\tif (r \u003c 0) {\n-\t\t\tlwsl_err(\u0022%s: can't read auth JWK %s\u005cn\u0022, __func__, cp);\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tif (lws_jwk_import(\u0026vhd-\u003ejwt_jwk_auth, NULL, NULL,\n-\t\t\t\t (const char *)buf, r)) {\n-\t\t\tlwsl_notice(\u0022%s: Failed to parse JWK key\u005cn\u0022, __func__);\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tlwsl_notice(\u0022%s: Auth JWK type %d\u005cn\u0022, __func__,\n-\t\t\t\t\t\tvhd-\u003ejwt_jwk_auth.kty);\n-\n-\t\tlws_sul_schedule(vhd-\u003econtext, 0, \u0026vhd-\u003esul_central,\n-\t\t\t\t saim_central_cb, 500 * LWS_US_PER_MS);\n-\n-\t\tbreak;\n-\n-\tcase LWS_CALLBACK_PROTOCOL_DESTROY:\n-\t\tsaim_master_destroy(\u0026vhd-\u003emaster);\n-\t\tgoto passthru;\n-\n-\t/*\n-\t * receive http hook notifications\n-\t */\n-\n-\tcase LWS_CALLBACK_HTTP:\n-\n-\t\tresp \u003d HTTP_STATUS_FORBIDDEN;\n-\t\tpss-\u003evhd \u003d vhd;\n-\n-\t\t/*\n-\t\t * What's the situation with a JWT cookie? Normal users won't\n-\t\t * have any, but privileged users will have one, and we should\n-\t\t * try to confirm it and set the pss auth level accordingly\n-\t\t */\n-\n-\t\tmemset(\u0026ck, 0, sizeof(ck));\n-\t\tck.jwk \u003d \u0026vhd-\u003ejwt_jwk_auth;\n-\t\tck.alg \u003d vhd-\u003ejwt_auth_alg;\n-\t\tck.iss \u003d vhd-\u003ejwt_issuer;\n-\t\tck.aud \u003d vhd-\u003ejwt_audience;\n-\t\tck.cookie_name \u003d \u0022__Host-sai_jwt\u0022;\n-\n-\t\tcml \u003d sizeof(buf);\n-\t\tif (!lws_jwt_get_http_cookie_validate_jwt(wsi, \u0026ck,\n-\t\t\t\t\t\t\t (char *)buf, \u0026cml) \u0026\u0026\n-\t\t ck.extra_json \u0026\u0026\n-\t\t !lws_json_simple_strcmp(ck.extra_json, ck.extra_json_len,\n-\t\t\t\t\t \u0022\u005c\u0022authorized\u005c\u0022:\u0022, \u00221\u0022)) {\n-\t\t\t\t/* the token allows him to manage us */\n-\t\t\t\tpss-\u003eauthorized \u003d 1;\n-\t\t\t\tpss-\u003eexpiry_unix_time \u003d ck.expiry_unix_time;\n-\t\t\t\tlws_strncpy(pss-\u003eauth_user, ck.sub,\n-\t\t\t\t\t sizeof(pss-\u003eauth_user));\n-\t\t} else\n-\t\t\tlwsl_err(\u0022%s: cookie rejected\u005cn\u0022, __func__);\n-\n-\t\tfor (n \u003d 0; n \u003c (int)LWS_ARRAY_SIZE(well_known); n++)\n-\t\t\tif (!strncmp((const char *)in, well_known[n],\n-\t\t\t\t strlen(well_known[n]))) {\n-\t\t\t\tmu \u003d n;\n-\t\t\t\tbreak;\n-\t\t\t}\n-\n-\t\tpss-\u003eour_form \u003d 0;\n-\n-\t\tlwsl_notice(\u0022%s: HTTP: xmu \u003d %d\u005cn\u0022, __func__, n);\n-\n-\t\tswitch (mu) {\n-\n-\t\tcase SHMUT_NONE:\n-\t\t\tgoto passthru;\n-\n-\t\tcase SHMUT_HOOK:\n-\t\t\tpss-\u003eour_form \u003d 1;\n-\t\t\tlwsl_notice(\u0022LWS_CALLBACK_HTTP: sees hook\u005cn\u0022);\n-\t\t\treturn 0;\n-\n-\t\tcase SHMUT_STATUS:\n-\t\t\t/*\n-\t\t\t * in is a string like /libwebsockets/status.svg\n-\t\t\t */\n-\t\t\tcp \u003d ((const char *)in) + 7;\n-\t\t\twhile (*cp \u003d\u003d '/')\n-\t\t\t\tcp++;\n-\t\t\tn \u003d 0;\n-\t\t\twhile (*cp !\u003d '/' \u0026\u0026 *cp \u0026\u0026 (size_t)n \u003c sizeof(projname) - 1)\n-\t\t\t\tprojname[n++] \u003d *cp++;\n-\t\t\tprojname[n] \u003d '\u005c0';\n-\n-\t\t\tlwsl_notice(\u0022%s: status %s\u005cn\u0022, __func__, projname);\n-\n-\t\t\tr \u003d sai_get_head_status(vhd, projname);\n-\t\t\tif (r \u003c 2)\n-\t\t\t\tr \u003d 2;\n-\t\t\tn \u003d lws_snprintf(projname, sizeof(projname),\n-\t\t\t\t \u0022../decal-%d.svg\u0022, r);\n-\n-\t\t\tif (lws_http_redirect(wsi, 307,\n-\t\t\t\t\t (unsigned char *)projname, n,\n-\t\t\t\t\t \u0026p, end) \u003c 0)\n-\t\t\t\treturn -1;\n-\n-\t\t\tgoto passthru;\n-\n-\t\tcase SHMUT_LOGIN:\n-\t\t\tpss-\u003elogin_form \u003d 1;\n-\t\t\tlwsl_notice(\u0022LWS_CALLBACK_HTTP: sees login\u005cn\u0022);\n-\t\t\treturn 0;\n-\n-\t\tcase SHMUT_ARTIFACTS:\n-\t\t\t/*\n-\t\t\t * HTTP Bulk GET interface for artifact download\n-\t\t\t *\n-\t\t\t * /artifacts/\u003ctaskhash\u003e/\u003cdown_nonce\u003e/filename\n-\t\t\t */\n-\t\t\tlwsl_notice(\u0022%s: SHMUT_ARTIFACTS\u005cn\u0022, __func__);\n-\t\t\tpss-\u003eartifact_offset \u003d 0;\n-\t\t\tif (saim_get_blob(vhd, (const char *)in + 11,\n-\t\t\t\t\t \u0026pss-\u003epdb_artifact,\n-\t\t\t\t\t \u0026pss-\u003eblob_artifact,\n-\t\t\t\t\t \u0026pss-\u003eartifact_length)) {\n-\t\t\t\tlwsl_notice(\u0022%s: get_blob failed\u005cn\u0022, __func__);\n-\t\t\t\tresp \u003d 404;\n-\t\t\t\tgoto http_resp;\n-\t\t\t}\n-\n-\t\t\t/*\n-\t\t\t * Well, it seems what he wanted exists..\n-\t\t\t */\n-\n-\t\t\tif (lws_add_http_header_status(wsi, 200, \u0026p, end))\n-\t\t\t\tgoto bail;\n-\t\t\tif (lws_add_http_header_content_length(wsi,\n-\t\t\t\t\t(unsigned long)pss-\u003eartifact_length,\n-\t\t\t\t\t\u0026p, end))\n-\t\t\t\tgoto bail;\n-\n-\t\t\tif (lws_add_http_header_by_token(wsi,\n-\t\t\t\t\tWSI_TOKEN_HTTP_CONTENT_TYPE,\n-\t\t\t\t\t(uint8_t *)\u0022application/octet-stream\u0022,\n-\t\t\t\t\t24, \u0026p, end))\n-\t\t\t\tgoto bail;\n-\t\t\tif (lws_finalize_write_http_header(wsi, start, \u0026p, end))\n-\t\t\t\tgoto bail;\n-\n-\t\t\tlwsl_notice(\u0022%s: started artifact transaction %d\u005cn\u0022, __func__,\n-\t\t\t\t\t(int)pss-\u003eartifact_length);\n-\n-\t\t\tlws_callback_on_writable(wsi);\n-\t\t\treturn 0;\n-\n-\t\tdefault:\n-\t\t\tlwsl_notice(\u0022%s: DEFAULT!!!\u005cn\u0022, __func__);\n-\t\t\treturn 0;\n-\t\t}\n-\n-\t\tresp \u003d HTTP_STATUS_OK;\n-\n-\t\t/* faillthru */\n-\n-http_resp:\n-\t\tif (lws_add_http_header_status(wsi, resp, \u0026p, end))\n-\t\t\tgoto bail;\n-\t\tif (lws_add_http_header_content_length(wsi, 0, \u0026p, end))\n-\t\t\tgoto bail;\n-\t\tif (lws_finalize_write_http_header(wsi, start, \u0026p, end))\n-\t\t\tgoto bail;\n-\t\tgoto try_to_reuse;\n-\n-\n-\tcase LWS_CALLBACK_HTTP_WRITEABLE:\n-\n-\t\tlwsl_notice(\u0022%s: HTTP_WRITEABLE\u005cn\u0022, __func__);\n-\n-\t\tif (!pss || !pss-\u003eblob_artifact)\n-\t\t\tbreak;\n-\n-\t\tn \u003d lws_ptr_diff(end, start);\n-\t\tif ((int)(pss-\u003eartifact_length - pss-\u003eartifact_offset) \u003c n)\n-\t\t\tn \u003d (int)(pss-\u003eartifact_length - pss-\u003eartifact_offset);\n-\n-\t\tif (sqlite3_blob_read(pss-\u003eblob_artifact, start, n,\n-\t\t\t\t pss-\u003eartifact_offset)) {\n-\t\t\tlwsl_err(\u0022%s: blob read failed\u005cn\u0022, __func__);\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tpss-\u003eartifact_offset +\u003d n;\n-\n-\t\tif (lws_write(wsi, start, n,\n-\t\t\t\tpss-\u003eartifact_offset !\u003d pss-\u003eartifact_length ?\n-\t\t\t\t\tLWS_WRITE_HTTP : LWS_WRITE_HTTP_FINAL) !\u003d n)\n-\t\t\treturn -1;\n-\n-\t\tif (pss-\u003eartifact_offset !\u003d pss-\u003eartifact_length)\n-\t\t\tlws_callback_on_writable(wsi);\n-\n-\t\tbreak;\n-\n-\t/*\n-\t * Notifcation POSTs\n-\t */\n-\n-\tcase LWS_CALLBACK_HTTP_BODY:\n-\n-\t\tif (pss-\u003elogin_form) {\n-\n-\t\t\tif (!pss-\u003espa) {\n-\t\t\t\tpss-\u003espa \u003d lws_spa_create(wsi, auth_param_names,\n-\t\t\t\t\t\tLWS_ARRAY_SIZE(auth_param_names),\n-\t\t\t\t\t\t1024, sai_login_cb, pss);\n-\t\t\t\tif (!pss-\u003espa) {\n-\t\t\t\t\tlwsl_err(\u0022failed to create spa\u005cn\u0022);\n-\t\t\t\t\treturn -1;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tgoto spa_process;\n-\n-\t\t}\n-\n-\t\tif (!pss-\u003eour_form) {\n-\t\t\tlwsl_notice(\u0022%s: not our form\u005cn\u0022, __func__);\n-\t\t\tgoto passthru;\n-\t\t}\n-\n-\t\tlwsl_user(\u0022LWS_CALLBACK_HTTP_BODY: %d\u005cn\u0022, (int)len);\n-\t\t/* create the POST argument parser if not already existing */\n-\n-\t\tif (!pss-\u003espa) {\n-\t\t\tpss-\u003ewsi \u003d wsi;\n-\t\t\tif (lws_hdr_copy(wsi, pss-\u003enotification_sig,\n-\t\t\t\t\t sizeof(pss-\u003enotification_sig),\n-\t\t\t\t\t WSI_TOKEN_HTTP_AUTHORIZATION) \u003c 0) {\n-\t\t\t\tlwsl_err(\u0022%s: failed to get signature hdr\u005cn\u0022,\n-\t\t\t\t\t __func__);\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\n-\t\t\tif (lws_hdr_copy(wsi, pss-\u003esn.e.source_ip,\n-\t\t\t\t\t sizeof(pss-\u003esn.e.source_ip),\n-\t\t\t\t\t WSI_TOKEN_X_FORWARDED_FOR) \u003c 0)\n-\t\t\t\tlws_get_peer_simple(wsi, pss-\u003esn.e.source_ip,\n-\t\t\t\t\t\tsizeof(pss-\u003esn.e.source_ip));\n-\n-\t\t\tpss-\u003espa \u003d lws_spa_create(wsi, NULL, 0, 1024,\n-\t\t\t\t\tsai_notification_file_upload_cb, pss);\n-\t\t\tif (!pss-\u003espa) {\n-\t\t\t\tlwsl_err(\u0022failed to create spa\u005cn\u0022);\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\n-\t\t\t/* find out the hmac used to sign it */\n-\n-\t\t\tpss-\u003ehmac_type \u003d LWS_GENHMAC_TYPE_UNKNOWN;\n-\t\t\tfor (n \u003d 0; n \u003c (int)LWS_ARRAY_SIZE(hmac_names); n++)\n-\t\t\t\tif (!strncmp(pss-\u003enotification_sig,\n-\t\t\t\t\t hmac_names[n],\n-\t\t\t\t\t strlen(hmac_names[n]))) {\n-\t\t\t\t\tpss-\u003ehmac_type \u003d n + 1;\n-\t\t\t\t\tbreak;\n-\t\t\t\t}\n-\n-\t\t\tif (pss-\u003ehmac_type \u003d\u003d LWS_GENHMAC_TYPE_UNKNOWN) {\n-\t\t\t\tlwsl_notice(\u0022%s: unknown sig hash type\u005cn\u0022,\n-\t\t\t\t\t\t__func__);\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\n-\t\t\t/* convert it to binary */\n-\n-\t\t\tn \u003d lws_hex_to_byte_array(\n-\t\t\t\tpss-\u003enotification_sig + strlen(hmac_names[n]),\n-\t\t\t\t(uint8_t *)pss-\u003enotification_sig, 64);\n-\n-\t\t\tif (n !\u003d (int)lws_genhmac_size(pss-\u003ehmac_type)) {\n-\t\t\t\tlwsl_notice(\u0022%s: notifcation hash bad length\u005cn\u0022,\n-\t\t\t\t\t\t__func__);\n-\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t}\n-\n-spa_process:\n-\n-\t\t/* let it parse the POST data */\n-\n-\t\tif (!pss-\u003espa_failed \u0026\u0026\n-\t\t lws_spa_process(pss-\u003espa, in, (int)len))\n-\t\t\t/*\n-\t\t\t * mark it as failed, and continue taking body until\n-\t\t\t * completion, and return error there\n-\t\t\t */\n-\t\t\tpss-\u003espa_failed \u003d 1;\n-\n-\t\tbreak;\n-\n-\tcase LWS_CALLBACK_HTTP_BODY_COMPLETION:\n-\t\tlwsl_user(\u0022%s: LWS_CALLBACK_HTTP_BODY_COMPLETION: %d\u005cn\u0022,\n-\t\t\t __func__, (int)len);\n-\n-\t\tif (!pss-\u003eour_form \u0026\u0026 !pss-\u003elogin_form) {\n-\t\t\tlwsl_user(\u0022%s: no sai form\u005cn\u0022, __func__);\n-\t\t\tgoto passthru;\n-\t\t}\n-\n-\t\t/* inform the spa no more payload data coming */\n-\t\tif (pss-\u003espa)\n-\t\t\tlws_spa_finalize(pss-\u003espa);\n-\n-\t\tif (pss-\u003elogin_form) {\n-\t\t\tconst char *un, *pw, *sr;\n-\t\t\tlws_dll2_owner_t o;\n-\t\t\tstruct lwsac *ac \u003d NULL;\n-\n-\t\t\tif (lws_add_http_header_status(wsi,\n-\t\t\t\t\t\tHTTP_STATUS_SEE_OTHER, \u0026p, end))\n-\t\t\t\tgoto clean_spa;\n-\t\t\tif (lws_add_http_header_content_length(wsi, 0, \u0026p, end))\n-\t\t\t\tgoto clean_spa;\n-\n-\t\t\tif (pss-\u003espa_failed)\n-\t\t\t\tgoto final;\n-\n-\t\t\tun \u003d lws_spa_get_string(pss-\u003espa, EPN_LNAME);\n-\t\t\tpw \u003d lws_spa_get_string(pss-\u003espa, EPN_LPASS);\n-\t\t\tsr \u003d lws_spa_get_string(pss-\u003espa, EPN_SUCCESS_REDIR);\n-\n-\t\t\tif (!un || !pw || !sr) {\n-\t\t\t\tpss-\u003espa_failed \u003d 1;\n-\t\t\t\tgoto final;\n-\t\t\t}\n-\n-\t\t\tlwsl_notice(\u0022%s: login attempt %s %s %s\u005cn\u0022,\n-\t\t\t\t\t__func__, un, pw, sr);\n-\n-\t\t\t/*\n-\t\t\t * Try to look up his credentials\n-\t\t\t */\n-\n-\t\t\tlws_sql_purify((char *)buf + 512, un, 34);\n-\t\t\tlws_sql_purify((char *)buf + 768, pw, 66);\n-\t\t\tlws_snprintf((char *)buf + 256, 256,\n-\t\t\t\t\t\u0022 and name\u003d'%s' and passphrase\u003d'%s'\u0022,\n-\t\t\t\t\t(const char *)buf + 512,\n-\t\t\t\t\t(const char *)buf + 768);\n-\t\t\tlws_dll2_owner_clear(\u0026o);\n-\t\t\tn \u003d lws_struct_sq3_deserialize(pss-\u003evhd-\u003emaster.pdb_auth,\n-\t\t\t\t\t\t (const char *)buf + 256,\n-\t\t\t\t\t\t NULL,\n-\t\t\t\t\t\t lsm_schema_sq3_map_auth,\n-\t\t\t\t\t\t \u0026o, \u0026ac, 0, 1);\n-\t\t\tif (n \u003c 0 || !o.head) {\n-\t\t\t\t/* no results, failed */\n-\t\t\t\tlwsl_notice(\u0022%s: login attempt %s failed %d\u005cn\u0022,\n-\t\t\t\t\t\t__func__, (const char *)buf, n);\n-\t\t\t\tlwsac_free(\u0026ac);\n-\t\t\t\tpss-\u003espa_failed \u003d 1;\n-\t\t\t\tgoto final;\n-\t\t\t}\n-\n-\t\t\t/* any result in o means a successful match */\n-\n-\t\t\tlwsac_free(\u0026ac);\n-\n-\t\t\t/*\n-\t\t\t * Produce a signed JWT allowing managing this Sai\n-\t\t\t * instance for a short time, and redirect ourselves\n-\t\t\t * back to the page we were on\n-\t\t\t */\n-\n-\n-\n-\t\t\tlwsl_notice(\u0022%s: setting cookie\u005cn\u0022, __func__);\n-\t\t\t/* un is invalidated by destroying the spa */\n-\t\t\tmemset(\u0026ck, 0, sizeof(ck));\n-\t\t\tlws_strncpy(ck.sub, un, sizeof(ck.sub));\n-\t\t\tck.jwk \u003d \u0026vhd-\u003ejwt_jwk_auth;\n-\t\t\tck.alg \u003d vhd-\u003ejwt_auth_alg;\n-\t\t\tck.iss \u003d vhd-\u003ejwt_issuer;\n-\t\t\tck.aud \u003d vhd-\u003ejwt_audience;\n-\t\t\tck.cookie_name \u003d \u0022sai_jwt\u0022;\n-\t\t\tck.extra_json \u003d \u0022\u005c\u0022authorized\u005c\u0022: 1\u0022;\n-\t\t\tck.expiry_unix_time \u003d 20 * 60;\n-\n-\t\t\tif (lws_jwt_sign_token_set_http_cookie(wsi, \u0026ck, \u0026p, end))\n-\t\t\t\tgoto clean_spa;\n-\n-\t\t\t/*\n-\t\t\t * Auth succeeded, go to the page the form was on\n-\t\t\t */\n-\n-\t\t\tif (lws_add_http_header_by_token(wsi,\n-\t\t\t\t\t\tWSI_TOKEN_HTTP_LOCATION,\n-\t\t\t\t\t\t(unsigned char *)sr,\n-\t\t\t\t\t\tstrlen((const char *)sr),\n-\t\t\t\t\t\t\u0026p, end)) {\n-\t\t\t\tgoto clean_spa;\n-\t\t\t}\n-\n-\t\t\tif (pss-\u003espa) {\n-\t\t\t\tlws_spa_destroy(pss-\u003espa);\n-\t\t\t\tpss-\u003espa \u003d NULL;\n-\t\t\t}\n-\n-\t\t\tif (lws_finalize_write_http_header(wsi, start, \u0026p, end))\n-\t\t\t\tgoto bail;\n-\n-\t\t\tlwsl_notice(\u0022%s: setting cookie OK\u005cn\u0022, __func__);\n-\t\t\tlwsl_hexdump_notice(start, lws_ptr_diff(p, start));\n-\t\t\treturn 0;\n-\n-final:\n-\t\t\t/*\n-\t\t\t * Auth failed, go back to /\n-\t\t\t */\n-\t\t\tif (lws_add_http_header_by_token(wsi,\n-\t\t\t\t\t\tWSI_TOKEN_HTTP_LOCATION,\n-\t\t\t\t\t\t(unsigned char *)\u0022/\u0022, 1,\n-\t\t\t\t\t\t\u0026p, end)) {\n-\t\t\t\tgoto clean_spa;\n-\t\t\t}\n-\t\t\tif (lws_finalize_write_http_header(wsi, start, \u0026p, end))\n-\t\t\t\tgoto bail;\n-\t\t\treturn 0;\n-\t\t}\n-\n-\t\tif (pss-\u003espa) {\n-\t\t\tlws_spa_destroy(pss-\u003espa);\n-\t\t\tpss-\u003espa \u003d NULL;\n-\t\t}\n-\n-\t\tif (pss-\u003espa_failed)\n-\t\t\tlwsl_notice(\u0022%s: notification failed\u005cn\u0022, __func__);\n-\t\telse {\n-\t\t\tlwsl_notice(\u0022%s: notification: %d %s %s %s\u005cn\u0022, __func__,\n-\t\t\t\t pss-\u003esn.action, pss-\u003esn.e.hash,\n-\t\t\t\t pss-\u003esn.e.ref, pss-\u003esn.e.repo_name);\n-\n-\t\t\t/* update connected browsers */\n-\n-\t\t\tlws_dll2_foreach_safe(\u0026vhd-\u003ebrowsers,\n-\t\t\t\t(void *)WSS_PREPARE_OVERVIEW, browser_upd);\n-\t\t}\n-\n-\t\tif (lws_return_http_status(wsi,\n-\t\t\t\tpss-\u003espa_failed ? HTTP_STATUS_FORBIDDEN :\n-\t\t\t\t\t\t HTTP_STATUS_OK,\n-\t\t\t\tNULL) \u003c 0)\n-\t\t\treturn -1;\n-\t\tbreak;\n-\n-clean_spa:\n-\t\tif (pss-\u003espa) {\n-\t\t\tlws_spa_destroy(pss-\u003espa);\n-\t\t\tpss-\u003espa \u003d NULL;\n-\t\t}\n-\t\tpss-\u003espa_failed \u003d 1;\n-\t\tgoto final;\n-\n-\t/*\n-\t * ws connections from builders and browsers\n-\t */\n-\n-\tcase LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:\n-\t\tn \u003d lws_hdr_copy(wsi, (char *)buf, sizeof(buf) - 1,\n-\t\t\t\t WSI_TOKEN_GET_URI);\n-\t\tif (!n)\n-\t\t\tbuf[0] \u003d '\u005c0';\n-\t\t//lwsl_notice(\u0022%s: checking with lwsgs for ws conn: %s\u005cn\u0022,\n-\t\t//\t __func__, (const char *)buf);\n-\n-\t\t/*\n-\t\t * Builders don't authenticate using sessions...\n-\t\t */\n-\n-\t\tif (n \u003e\u003d 8 \u0026\u0026 !strncmp((const char *)buf + n - 8,\n-\t\t\t\t\t\u0022/builder\u0022, 8))\n-\t\t\treturn 0;\n-\n-\t\treturn 0;\n-#if 0\n-\t\t/* but everything else does */\n-\n-\t\tcar_args.max_len \u003d LWSGS_AUTH_LOGGED_IN | LWSGS_AUTH_VERIFIED;\n-\t\tcar_args.final \u003d 0;\n-\t\tcar_args.chunked \u003d 1; /* ie, we are ws */\n-\n-\t\tin \u003d \u0026car_args;\n-\t\tgoto passthru;\n-#endif\n-\n-\tcase LWS_CALLBACK_ESTABLISHED:\n-\n-\t\t/*\n-\t\t * What's the situation with a JWT cookie? Normal users won't\n-\t\t * have any, but privileged users will have one, and we should\n-\t\t * try to confirm it and set the pss auth level accordingly\n-\t\t */\n-\n-\t\tmemset(\u0026ck, 0, sizeof(ck));\n-\t\tck.jwk \u003d \u0026vhd-\u003ejwt_jwk_auth;\n-\t\tck.alg \u003d vhd-\u003ejwt_auth_alg;\n-\t\tck.iss \u003d vhd-\u003ejwt_issuer;\n-\t\tck.aud \u003d vhd-\u003ejwt_audience;\n-\t\tck.cookie_name \u003d \u0022__Host-sai_jwt\u0022;\n-\n-\t\tcml \u003d sizeof(buf);\n-\t\tif (!lws_jwt_get_http_cookie_validate_jwt(wsi, \u0026ck,\n-\t\t\t\t\t\t\t (char *)buf, \u0026cml) \u0026\u0026\n-\t\t ck.extra_json \u0026\u0026\n-\t\t !lws_json_simple_strcmp(ck.extra_json, ck.extra_json_len,\n-\t\t\t\t\t \u0022\u005c\u0022authorized\u005c\u0022:\u0022, \u00221\u0022)) {\n-\t\t\t/* the token allows him to manage us */\n-\t\t\tpss-\u003eauthorized \u003d 1;\n-\t\t\tpss-\u003eexpiry_unix_time \u003d ck.expiry_unix_time;\n-\t\t\tlws_strncpy(pss-\u003eauth_user, ck.sub,\n-\t\t\t\t sizeof(pss-\u003eauth_user));\n-\t\t} else\n-\t\t\tlwsl_err(\u0022%s: cookie rejected\u005cn\u0022, __func__);\n-\t\tpss-\u003ewsi \u003d wsi;\n-\t\tpss-\u003evhd \u003d vhd;\n-\t\tpss-\u003ealang[0] \u003d '\u005c0';\n-\t\tlws_hdr_copy(wsi, pss-\u003ealang, sizeof(pss-\u003ealang),\n-\t\t\t WSI_TOKEN_HTTP_ACCEPT_LANGUAGE);\n-\n-\t\tif (lws_hdr_total_length(wsi, WSI_TOKEN_GET_URI)) {\n-\t\t\tif (lws_hdr_copy(wsi, (char *)start, 64,\n-\t\t\t\t\t WSI_TOKEN_GET_URI) \u003c 0)\n-\t\t\t\treturn -1;\n-\t\t}\n-#if defined(LWS_ROLE_H2)\n-\t\telse\n-\t\t\tif (lws_hdr_copy(wsi, (char *)start, 64,\n-\t\t\t\t\t WSI_TOKEN_HTTP_COLON_PATH) \u003c 0)\n-\t\t\t\treturn -1;\n-#endif\n-\n-\t\tif (!memcmp((char *)start, \u0022/sai\u0022, 4))\n-\t\t\tstart +\u003d 4;\n-\n-\t\tif (!strncmp((char *)start, \u0022/browse/specific\u0022, 16)) {\n-\t\t\tconst char *spe;\n-\n-\t\t\tlwsl_info(\u0022%s: ESTABLISHED: browser (specific)\u005cn\u0022, __func__);\n-\t\t\tpss-\u003etype \u003d SWT_BROWSE;\n-\t\t\tpss-\u003ewsi \u003d wsi;\n-\t\t\tpss-\u003especific_project[0] \u003d '\u005c0';\n-\t\t\tspe \u003d (const char *)start + 16;\n-\t\t\twhile(*spe \u003d\u003d '/')\n-\t\t\t\tspe++;\n-\t\t\tn \u003d 0;\n-\t\t\twhile(*spe \u0026\u0026 *spe !\u003d '/' \u0026\u0026\n-\t\t\t (size_t)n \u003c sizeof(pss-\u003especific_project) - 2)\n-\t\t\t\tpss-\u003especific_project[n++] \u003d *spe++;\n-\n-\t\t\tpss-\u003especific_project[n] \u003d '\u005c0';\n-\n-\t\t\tstrncpy(pss-\u003especific, \u0022refs/heads/\u0022,\n-\t\t\t\t\tsizeof(pss-\u003especific));\n-\t\t\tif (lws_get_urlarg_by_name(wsi, \u0022h\u0022, pss-\u003especific + 9,\n-\t\t\t\t\t\t sizeof(pss-\u003especific) - 9)) {\n-\t\t\t\tmemcpy(pss-\u003especific, \u0022refs/heads/\u0022, 11);\n-\t\t\t\tpss-\u003especificity \u003d SAIM_SPECIFIC_H;\n-\t\t\t} else\n-\t\t\t\tif (lws_get_urlarg_by_name(wsi, \u0022id\u0022, (char *)buf,\n-\t\t\t\t\t\t\t sizeof(buf))) {\n-\t\t\t\t\tlws_strncpy(pss-\u003especific, (const char *)buf + 3,\n-\t\t\t\t\t\t\tsizeof(pss-\u003especific));\n-\t\t\t\t\tpss-\u003especificity \u003d SAIM_SPECIFIC_ID;\n-\t\t\t\t} else {\n-\t\t\t\t\tpss-\u003especificity \u003d SAIM_SPECIFIC_H;\n-\t\t\t\t\tlws_strncpy(pss-\u003especific,\n-\t\t\t\t\t\t\t\u0022refs/heads/master\u0022,\n-\t\t\t\t\t\t\tsizeof(pss-\u003especific));\n-\t\t\t\t}\n-\n-\t\t\t\t//lwsl_notice(\u0022%s: spec %d, '%s'\u005cn\u0022, __func__,\n-\t\t\t\t//\tpss-\u003especificity, pss-\u003especific);\n-\t\t\tbreak;\n-\t\t}\n-\n-\t\tif (!strcmp((char *)start, \u0022/browse\u0022)) {\n-\t\t\tlwsl_info(\u0022%s: ESTABLISHED: browser\u005cn\u0022, __func__);\n-\t\t\tpss-\u003etype \u003d SWT_BROWSE;\n-\t\t\tpss-\u003ewsi \u003d wsi;\n-\t\t\tbreak;\n-\t\t}\n-\t\tif (!strcmp((char *)start, \u0022/builder\u0022)) {\n-\t\t\tlwsl_info(\u0022%s: ESTABLISHED: builder\u005cn\u0022, __func__);\n-\t\t\tpss-\u003etype \u003d SWT_BUILDER;\n-\t\t\tpss-\u003ewsi \u003d wsi;\n-\t\t\t/*\n-\t\t\t * this adds our pss part, but not the logical builder\n-\t\t\t * yet, until we get the ws rx\n-\t\t\t */\n-\t\t\tlws_dll2_add_head(\u0026pss-\u003esame, \u0026vhd-\u003ebuilders);\n-\t\t\tbreak;\n-\t\t}\n-\n-\t\tlwsl_err(\u0022%s: unknown URL '%s'\u005cn\u0022, __func__, start);\n-\n-\t\treturn -1;\n-\n-\tcase LWS_CALLBACK_CLOSED:\n-\t\tlwsac_free(\u0026pss-\u003equery_ac);\n-\t\tswitch (pss-\u003etype) {\n-\t\tcase SWT_BROWSE:\n-\t\t\tlwsl_info(\u0022%s: CLOSED browse conn\u005cn\u0022, __func__);\n-\t\t\tlws_dll2_remove(\u0026pss-\u003esame);\n-\t\t\tlws_dll2_remove(\u0026pss-\u003esubs_list);\n-\t\t\tlwsac_free(\u0026pss-\u003etask_ac);\n-\t\t\tlwsac_free(\u0026pss-\u003elogs_ac);\n-\t\t\tbreak;\n-\n-\t\tcase SWT_BUILDER:\n-\t\t\tlwsl_user(\u0022%s: CLOSED builder conn\u005cn\u0022, __func__);\n-\t\t\t/* remove pss from vhd-\u003ebuilders */\n-\t\t\tlws_dll2_remove(\u0026pss-\u003esame);\n-\n-\t\t\t/*\n-\t\t\t * Destroy any the builder-tracking objects that\n-\t\t\t * were using this departing connection\n-\t\t\t */\n-\n-\t\t\tlws_start_foreach_dll_safe(struct lws_dll2 *, p, p1,\n-\t\t\t\t\t vhd-\u003emaster.builder_owner.head) {\n-\t\t\t\tsai_plat_t *cb \u003d lws_container_of(p, sai_plat_t,\n-\t\t\t\t\t\t\t\t sai_plat_list);\n-\n-\t\t\t\tif (cb-\u003ewsi \u003d\u003d wsi) {\n-\t\t\t\t\t/* remove builder object itself from master list */\n-\t\t\t\t\tcb-\u003ewsi \u003d NULL;\n-\t\t\t\t\tlws_dll2_remove(\u0026cb-\u003esai_plat_list);\n-\t\t\t\t\t/*\n-\t\t\t\t\t * free the deserialized builder object,\n-\t\t\t\t\t * everything he pointed to was overallocated\n-\t\t\t\t\t * when his deep copy was made\n-\t\t\t\t\t */\n-\t\t\t\t\tfree(cb);\n-\t\t\t\t}\n-\n-\t\t\t} lws_end_foreach_dll_safe(p, p1);\n-\n-\t\t\tif (pss-\u003eblob_artifact) {\n-\t\t\t\tsqlite3_blob_close(pss-\u003eblob_artifact);\n-\t\t\t\tpss-\u003eblob_artifact \u003d NULL;\n-\t\t\t}\n-\n-\t\t\tif (pss-\u003epdb_artifact) {\n-\t\t\t\tsaim_event_db_close(pss-\u003evhd, \u0026pss-\u003epdb_artifact);\n-\t\t\t\tpss-\u003epdb_artifact \u003d NULL;\n-\t\t\t}\n-\n-\t\t\t/* update the browsers about the builder removal */\n-\t\t\tlws_dll2_foreach_safe(\u0026vhd-\u003ebrowsers,\n-\t\t\t\t(void *)WSS_PREPARE_BUILDER_SUMMARY, browser_upd);\n-\t\t\tbreak;\n-\t\t}\n-\t\tbreak;\n-\n-\tcase LWS_CALLBACK_RECEIVE:\n-\n-\t\tif (!pss-\u003evhd)\n-\t\t\tpss-\u003evhd \u003d vhd;\n-\n-\t\tswitch (pss-\u003etype) {\n-\t\tcase SWT_BROWSE:\n-\t\t\t// lwsl_user(\u0022SWT_BROWSE RX: %d\u005cn\u0022, (int)len);\n-\t\t\t/*\n-\t\t\t * Browser UI sent us something on websockets\n-\t\t\t */\n-\t\t\tif (saim_ws_json_rx_browser(vhd, pss, in, len))\n-\t\t\t\treturn -1;\n-\t\t\tbreak;\n-\n-\t\tcase SWT_BUILDER:\n-\t\t\tlwsl_info(\u0022SWT_BUILDER RX: %d\u005cn\u0022, (int)len);\n-\t\t\t/*\n-\t\t\t * Builder sent us something on websockets\n-\t\t\t */\n-\t\t\tpss-\u003ewsi \u003d wsi;\n-\t\t\tif (saim_ws_json_rx_builder(vhd, pss, in, len))\n-\t\t\t\treturn -1;\n-\t\t\tif (!pss-\u003eannounced) {\n-\t\t\t\tlws_dll2_foreach_safe(\u0026vhd-\u003ebrowsers,\n-\t\t\t\t\t\t(void *)WSS_PREPARE_BUILDER_SUMMARY,\n-\t\t\t\t\t\tbrowser_upd);\n-\t\t\t\tpss-\u003eannounced \u003d 1;\n-\t\t\t}\n-\t\t\tbreak;\n-\t\tdefault:\n-\t\t\tbreak;\n-\t\t}\n-\t\tbreak;\n-\n-\tcase LWS_CALLBACK_SERVER_WRITEABLE:\n-\t\tif (!vhd) {\n-\t\t\tlwsl_notice(\u0022%s: no vhd\u005cn\u0022, __func__);\n-\t\t\tbreak;\n-\t\t}\n-\t\tswitch (pss-\u003etype) {\n-\t\tcase SWT_BROWSE:\n-\t\t\treturn saim_ws_json_tx_browser(vhd, pss, buf, sizeof(buf));\n-\t\t\tbreak;\n-\n-\t\tcase SWT_BUILDER:\n-\t\t\treturn saim_ws_json_tx_builder(vhd, pss, buf, sizeof(buf));\n-\t\t\tbreak;\n-\t\tdefault:\n-\t\t\tbreak;\n-\t\t}\n-\t\tbreak;\n-\n-\tdefault:\n-passthru:\n-\t//\tif (!pss || !vhd)\n-\t\t\tbreak;\n-\n-\t//\treturn vhd-\u003egsp-\u003ecallback(wsi, reason, pss-\u003epss_gs, in, len);\n-\t}\n-\n-\treturn lws_callback_http_dummy(wsi, reason, user, in, len);\n-\n-bail:\n-\treturn 1;\n-\n-try_to_reuse:\n-\tif (lws_http_transaction_completed(wsi))\n-\t\treturn -1;\n-\n-\treturn 0;\n-}\n-\n-const struct lws_protocols protocol_ws \u003d\n-\t{ \u0022com-warmcat-sai\u0022, callback_ws, sizeof(struct pss), 0 };\ndiff --git a/src/master/m-conf.c b/src/master/m-conf.c\ndeleted file mode 100644\nindex 498378f..0000000\n--- a/src/master/m-conf.c\n+++ /dev/null\n@@ -1,82 +0,0 @@\n-/*\n- * Sai master src/master/conf.c\n- *\n- * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n- *\n- * This library is free software; you can redistribute it and/or\n- * modify it under the terms of the GNU Lesser General Public\n- * License as published by the Free Software Foundation:\n- * version 2.1 of the License.\n- *\n- * This library is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n- * Lesser General Public License for more details.\n- *\n- * You should have received a copy of the GNU Lesser General Public\n- * License along with this library; if not, write to the Free Software\n- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n- * MA 02110-1301 USA\n- */\n-#include \u003clibwebsockets.h\u003e\n-#include \u003cstring.h\u003e\n-#include \u003csignal.h\u003e\n-#include \u003ctime.h\u003e\n-\n-#define SAI_CONFIG_STRING_SIZE (16 * 1024)\n-\n-struct lws_context *\n-sai_lws_context_from_json(const char *config_dir,\n-\t\t\t struct lws_context_creation_info *info,\n-\t\t\t const struct lws_protocols **pprotocols)\n-{\n-\tint cs_len \u003d SAI_CONFIG_STRING_SIZE - 1;\n-\tstruct lws_context *context;\n-\tchar *cs, *config_strings;\n-\n-\tcs \u003d config_strings \u003d malloc(SAI_CONFIG_STRING_SIZE);\n-\tif (!config_strings) {\n-\t\tlwsl_err(\u0022Unable to allocate config strings heap\u005cn\u0022);\n-\n-\t\treturn NULL;\n-\t}\n-\n-\tmemset(info, 0, sizeof(*info));\n-\n-\tinfo-\u003eexternal_baggage_free_on_destroy \u003d config_strings;\n-\tinfo-\u003ept_serv_buf_size \u003d 8192;\n-\tinfo-\u003eoptions \u003d LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT |\n-\t\t LWS_SERVER_OPTION_EXPLICIT_VHOSTS |\n-\t\t LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE |\n-\t\t LWS_SERVER_OPTION_VALIDATE_UTF8;\n-\n-\tlwsl_notice(\u0022Using config dir: \u005c\u0022%s\u005c\u0022\u005cn\u0022, config_dir);\n-\n-\t/*\n-\t * first go through the config for creating the outer context\n-\t */\n-\tif (lwsws_get_config_globals(info, config_dir, \u0026cs, \u0026cs_len))\n-\t\tgoto init_failed;\n-\n-\tcontext \u003d lws_create_context(info);\n-\tif (context \u003d\u003d NULL) {\n-\t\t/* config_strings freed as 'external baggage' */\n-\t\treturn NULL;\n-\t}\n-\n-\tinfo-\u003epprotocols \u003d pprotocols;\n-\n-\tif (lwsws_get_config_vhosts(context, info, config_dir, \u0026cs, \u0026cs_len)) {\n-\t\tlwsl_err(\u0022%s: sai_lws_context_from_json failed\u005cn\u0022, __func__);\n-\t\tlws_context_destroy(context);\n-\n-\t\treturn NULL;\n-\t}\n-\n-\treturn context;\n-\n-init_failed:\n-\tfree(config_strings);\n-\n-\treturn NULL;\n-}\ndiff --git a/src/master/m-notification.c b/src/master/m-notification.c\ndeleted file mode 100644\nindex 207e097..0000000\n--- a/src/master/m-notification.c\n+++ /dev/null\n@@ -1,968 +0,0 @@\n-/*\n- * Sai master - src/master/notification.c\n- *\n- * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n- *\n- * This library is free software; you can redistribute it and/or\n- * modify it under the terms of the GNU Lesser General Public\n- * License as published by the Free Software Foundation:\n- * version 2.1 of the License.\n- *\n- * This library is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n- * Lesser General Public License for more details.\n- *\n- * You should have received a copy of the GNU Lesser General Public\n- * License along with this library; if not, write to the Free Software\n- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n- * MA 02110-1301 USA\n- *\n- * The same ws interface is connected-to by builders (on path /builder), and\n- * provides the query transport for browsers (on path /browse).\n- */\n-\n-#include \u003clibwebsockets.h\u003e\n-#include \u0022m-private.h\u0022\n-\n-/* starts from +1 of sai_notification_action_t */\n-\n-static const char *notifaction_action_names[] \u003d {\n-\t\u0022repo-update\u0022\n-};\n-\n-static const char * const paths[] \u003d {\n-\t\u0022schema\u0022,\n-\t\u0022action\u0022,\n-\t\u0022repository.name\u0022,\n-\t\u0022repository.fetchurl\u0022,\n-\t\u0022ref\u0022,\n-\t\u0022hash\u0022,\n-\t\u0022nonce\u0022,\n-\t\u0022saifile_len\u0022,\n-\t\u0022saifile\u0022,\n-};\n-\n-enum enum_paths {\n-\tLEJPN_SCHEMA,\n-\tLEJPN_ACTION,\n-\tLEJPN_REPOSITORY_NAME,\n-\tLEJPN_REPOSITORY_FETCHURL,\n-\tLEJPN_REF,\n-\tLEJPN_HASH,\n-\tLEJPN_NONCE,\n-\tLEJPN_SAIFILE_LEN,\n-\tLEJPN_SAIFILE,\n-};\n-\n-/*\n- * Saifile parser\n- */\n-\n-static const char * const saifile_paths[] \u003d {\n-\t\u0022schema\u0022,\n-\t\u0022platforms.*.build\u0022,\n-\t\u0022platforms.*.default\u0022,\n-\t\u0022platforms.*\u0022,\n-\t\u0022configurations.*.prep\u0022,\n-\t\u0022configurations.*.cmake\u0022,\n-\t\u0022configurations.*.deps\u0022,\n-\t\u0022configurations.*.platforms\u0022,\n-\t\u0022configurations.*.artifacts\u0022,\n-\t\u0022configurations.*.cpack\u0022,\n-\t\u0022configurations.*\u0022,\n-};\n-\n-enum enum_saifile_paths {\n-\tLEJPNSAIF_SCHEMA,\n-\tLEJPNSAIF_PLAT_BUILD,\n-\tLEJPNSAIF_PLAT_DEFAULT,\n-\tLEJPNSAIF_PLAT_NAME,\n-\tLEJPNSAIF_CONFIGURATIONS_PREP,\n-\tLEJPNSAIF_CONFIGURATIONS_CMAKE,\n-\tLEJPNSAIF_CONFIGURATIONS_DEPS,\n-\tLEJPNSAIF_CONFIGURATIONS_PLATFORMS,\n-\tLEJPNSAIF_CONFIGURATIONS_ARTIFACTS,\n-\tLEJPNSAIF_CONFIGURATIONS_CPACK,\n-\tLEJPNSAIF_CONFIGURATIONS_NAME,\n-};\n-\n-/* do the string subst for ${cmake} etc */\n-\n-static int\n-exp_cmake(void *priv, const char *name, char *out, size_t *pos, size_t olen,\n-\t size_t *exp_ofs)\n-{\n-\tsai_notification_t *sn \u003d (sai_notification_t *)priv;\n-\tconst char *replace \u003d NULL;\n-\tsize_t replace_len, rem_out;\n-\n-\tif (!strcmp(name, \u0022prep\u0022)) {\n-\t\treplace \u003d sn-\u003et.prep;\n-\t\treplace_len \u003d strlen(sn-\u003et.prep);\n-\t\tgoto expand;\n-\t}\n-\n-\tif (!strcmp(name, \u0022cmake\u0022)) {\n-\t\treplace \u003d sn-\u003et.cmake;\n-\t\treplace_len \u003d strlen(sn-\u003et.cmake);\n-\t\tgoto expand;\n-\t}\n-\n-\tif (!strcmp(name, \u0022cpack\u0022)) {\n-\t\treplace \u003d sn-\u003et.cpack;\n-\t\treplace_len \u003d strlen(sn-\u003et.cpack);\n-\t\tgoto expand;\n-\t}\n-\n-\treturn LSTRX_FATAL_NAME_UNKNOWN;\n-\n-expand:\n-\trem_out \u003d olen - *pos;\t\t/* remaining rhs */\n-\treplace_len -\u003d *exp_ofs;\n-\tif (replace_len \u003c rem_out)\n-\t\trem_out \u003d replace_len;\n-\n-\tmemcpy(out + *pos, replace + (*exp_ofs), rem_out);\n-\t*exp_ofs +\u003d rem_out;\n-\t*pos +\u003d rem_out;\n-\n-\tif (rem_out \u003d\u003d replace_len)\n-\t\treturn LSTRX_DONE;\n-\n-\treturn LSTRX_FILLED_OUT;\n-}\n-\n-static int\n-arg_to_bool(const char *s)\n-{\n-\tstatic const char * const on[] \u003d { \u0022on\u0022, \u0022yes\u0022, \u0022true\u0022 };\n-\tint n \u003d atoi(s);\n-\n-\tif (n)\n-\t\treturn 1;\n-\n-\tfor (n \u003d 0; n \u003c (int)LWS_ARRAY_SIZE(on); n++)\n-\t\tif (!strcasecmp(s, on[n]))\n-\t\t\treturn 1;\n-\n-\treturn 0;\n-}\n-\n-static int\n-sai_tuple_element_compare(const char *e1, const char *e2)\n-{\n-\tchar *p;\n-\n-\t// lwsl_notice(\u0022%s: comp '%s' '%s'\u005cn\u0022, __func__, e1, e2);\n-\n-\tif (strlen(e1) \u003e strlen(e2))\n-\t\t/* can't match if req is longer than plat */\n-\t\treturn -1;\n-\n-\tp \u003d strchr(e2, '/');\n-\tif (p \u0026\u0026 lws_ptr_diff(p, e2) \u003c (int)strlen(e1))\n-\t\t/* e2 contains a / section terminal inside e1 scope */\n-\t\treturn 1;\n-\n-\treturn strncmp(e1, e2, strlen(e1));\n-}\n-\n-static int\n-sai_tuple_compare(const char *req, int req_len, const char *plat)\n-{\n-\tconst char *pos \u003d req;\n-\tchar e1[96];\n-\tint n;\n-\n-\tdo {\n-\t\tn \u003d 0;\n-\t\twhile (n \u003c (int)sizeof(e1) - 1 \u0026\u0026 req_len \u0026\u0026 *pos !\u003d '/') {\n-\t\t\te1[n++] \u003d *pos++;\n-\t\t\treq_len--;\n-\t\t}\n-\t\te1[n] \u003d '\u005c0';\n-\t\tif (*pos \u003d\u003d '/' \u0026\u0026 req_len) {\n-\t\t\treq_len--;\n-\t\t\tpos++;\n-\t\t}\n-\n-\t\tif (n \u003d\u003d sizeof(e1) - 1) {\n-\t\t\t// lwsl_notice(\u0022%s: NOMATCH 3 '%s' '%s'\u005cn\u0022, __func__, req, plat);\n-\t\t\t/* element too long */\n-\t\t\treturn 1;\n-\t\t}\n-\n-\t\tif (n \u0026\u0026 /* let it pass if empty match section, eg, linux//gcc */\n-\t\t sai_tuple_element_compare(e1, plat)) {\n-\t\t\t// lwsl_notice(\u0022%s: NOMATCH 2 '%s' '%s'\u005cn\u0022, __func__, req, plat);\n-\t\t\t/* section does not match */\n-\t\t\treturn 1;\n-\t\t}\n-\n-\t\twhile (*plat \u0026\u0026 *plat !\u003d '/')\n-\t\t\tplat++;\n-\t\tif (*plat \u003d\u003d '/')\n-\t\t\tplat++;\n-\n-\t\tif (!*plat \u0026\u0026 req_len) {\n-\t\t\t// lwsl_notice(\u0022%s: NOMATCH 1 '%s' '%s'\u005cn\u0022, __func__, req, plat);\n-\t\t\t/* FAIL: req had more than we had */\n-\t\t\treturn 1;\n-\t\t}\n-\n-\t\tif (!req_len) {\n-\t\t\t// lwsl_notice(\u0022%s: MATCH '%s' '%s'\u005cn\u0022, __func__, req, plat);\n-\t\t\t/* MATCH: we matched at least as far as we had */\n-\t\t\treturn 0;\n-\t\t}\n-\n-\t} while (1);\n-\n-\treturn 1;\n-}\n-\n-/*\n- * We parse the saifile JSON\n- */\n-\n-static signed char\n-sai_saifile_lejp_cb(struct lejp_ctx *ctx, char reason)\n-{\n-\tstruct pss *pss \u003d (struct pss *)ctx-\u003euser;\n-\tsai_notification_t *sn \u003d (sai_notification_t *)\u0026pss-\u003esn;\n-\tsqlite3 *pdb \u003d NULL;\n-\tsize_t n;\n-\n-\t// lwsl_notice(\u0022%s: reason %d, %s\u005cn\u0022, __func__, reason, ctx-\u003epath);\n-\n-\tif (reason \u003d\u003d LEJPCB_COMPLETE) {\n-\n-\t\tif (!pss-\u003edry)\n-\t\t\tlwsl_notice(\u0022%s: saifile decode completed\u005cn\u0022, __func__);\n-\n-\t\treturn 0;\n-\t}\n-\n-\tif (reason \u003d\u003d LEJPCB_OBJECT_START \u0026\u0026\n-\t ctx-\u003epath_match - 1 \u003d\u003d LEJPNSAIF_CONFIGURATIONS_NAME) {\n-\t\t/*\n-\t\t * We're at the testname part of \u0022testname\u0022 : { }\n-\t\t */\n-\t\tlws_strncpy(sn-\u003et.taskname, \u0026ctx-\u003epath[15],\n-\t\t\t sizeof(sn-\u003et.taskname));\n-\t\tsn-\u003et.prep[0] \u003d '\u005c0';\n-\t\tsn-\u003et.packages[0] \u003d '\u005c0';\n-\t\tsn-\u003et.cmake[0] \u003d '\u005c0';\n-\t\tsn-\u003et.artifacts[0] \u003d '\u005c0';\n-\t\tsn-\u003eexplicit_platforms[0] \u003d '\u005c0';\n-\t\treturn 0;\n-\t}\n-\n-\tif (reason \u003d\u003d LEJPCB_OBJECT_END \u0026\u0026\n-\t ctx-\u003epath_match - 1 \u003d\u003d LEJPNSAIF_CONFIGURATIONS_NAME \u0026\u0026\n-\t sn-\u003et.taskname[0]) {\n-\t\tlws_dll2_owner_t owner;\n-\n-\t\t/*\n-\t\t * We're at the testname part of \u0022testname\u0022 : { }\n-\t\t */\n-\t\tif (pss-\u003edry) {\n-\t\t\tsn-\u003et.taskname[0] \u003d '\u005c0';\n-\n-\t\t\treturn 0;\n-\t\t}\n-\n-\t\t/*\n-\t\t * Iterate through each platform creating task entries for this\n-\t\t * configuration customized for each platform... first time just\n-\t\t * check the string subst for all of them is OK...\n-\t\t */\n-\n-\t\tlwsl_notice(\u0022%s: Creating task entries for notification\u005cn\u0022, __func__);\n-\n-\t\tlws_start_foreach_dll(struct lws_dll2 *, p,\n-\t\t\t\t\t pss-\u003eplatform_owner.head) {\n-\t\t\tsai_platform_t *pl \u003d\n-\t\t\t\tlws_container_of(p, sai_platform_t, list);\n-\t\t\tsize_t used_in, used_out;\n-\t\t\tstruct lws_tokenize ts;\n-\t\t\tint n, match \u003d 0;\n-\t\t\tlws_strexp_t sx;\n-\n-\t\t\tif (!pl-\u003enondefault)\n-\t\t\t\t/*\n-\t\t\t\t * If it's a default platform, then match\n-\t\t\t\t * by default\n-\t\t\t\t */\n-\t\t\t\tmatch \u003d 1;\n-\n-\t\t\tif (sn-\u003eexplicit_platforms[0]) {\n-\t\t\t\tchar not \u003d 0;\n-\n-\t\t\t\t/*\n-\t\t\t\t * If the configuration gives explicit\n-\t\t\t\t * platforms, we have to filter the platform\n-\t\t\t\t * against the comma-separated list.\n-\t\t\t\t *\n-\t\t\t\t * \u0022none\u0022 - don't match any plats just because\n-\t\t\t\t *\t they are default\n-\t\t\t\t *\n-\t\t\t\t * \u0022not plat\u0022 - disallow specific plat \u0022plat\u0022,\n-\t\t\t\t *\t\timplies you're not using \u0022none\u0022\n-\t\t\t\t *\n-\t\t\t\t * \u0022plat\u0022 - allow specific plat \u0022plat\u0022\n-\t\t\t\t */\n-\t\t\t\tmemset(\u0026ts, 0, sizeof(ts));\n-\t\t\t\tts.start \u003d (char *)sn-\u003eexplicit_platforms;\n-\t\t\t\tts.len \u003d strlen(ts.start);\n-\t\t\t\tts.flags \u003d LWS_TOKENIZE_F_DOT_NONTERM |\n-\t\t\t\t\t LWS_TOKENIZE_F_SLASH_NONTERM |\n-\t\t\t\t\t LWS_TOKENIZE_F_MINUS_NONTERM;\n-\t\t\t\tdo {\n-\t\t\t\t\tts.e \u003d lws_tokenize(\u0026ts);\n-\t\t\t\t\tif (ts.e !\u003d LWS_TOKZE_TOKEN)\n-\t\t\t\t\t\tcontinue;\n-\n-\t\t\t\t\tlwsl_notice(\u0022%s: check %.*s\u005cn\u0022, __func__,\n-\t\t\t\t\t\t (int)ts.token_len, ts.token);\n-\n-\t\t\t\t\tif (!strncmp(ts.token, \u0022none\u0022,\n-\t\t\t\t\t\t ts.token_len)) {\n-\t\t\t\t\t\tnot \u003d match \u003d 0;\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\t\t\t\t\tif (!strncmp(ts.token, \u0022not\u0022,\n-\t\t\t\t\t\t ts.token_len)) {\n-\t\t\t\t\t\tnot \u003d 1;\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\t\t\t\t\t/*\n-\t\t\t\t\t * We need to check with per-slash\n-\t\t\t\t\t * minimal matching, ie, \u0022linux\u0022\n-\t\t\t\t\t * matches \u0022linux-ubuntu/x86_64/gcc\u0022\n-\t\t\t\t\t */\n-\t\t\t\t\tif (!sai_tuple_compare(ts.token,\n-\t\t\t\t\t\t ts.token_len, pl-\u003ename)) {\n-\t\t\t\t\t\tmatch \u003d !not;\n-\t\t\t\t\t\tif (match)\n-\t\t\t\t\t\t\tbreak;\n-\t\t\t\t\t}\n-\t\t\t\t\tnot \u003d 0;\n-\t\t\t\t} while (ts.e \u003e 0);\n-\t\t\t}\n-\n-\t\t\tif (match) {\n-\n-\t\t\t\t/*\n-\t\t\t\t * The platform build string (pl-\u003ebuild) is the\n-\t\t\t\t * base, with optional entries like ${cmake}\n-\t\t\t\t * that are filled in with the corresponding\n-\t\t\t\t * info from the specific configuration's\n-\t\t\t\t * \u0022cmake\u0022 entry (sn-\u003et.cmake)...\n-\t\t\t\t *\n-\t\t\t\t * sn-\u003et.build becomes the string-substituted\n-\t\t\t\t * copy that is serialized\n-\t\t\t\t */\n-\n-\t\t\t\tlws_strexp_init(\u0026sx, sn, exp_cmake, sn-\u003et.build,\n-\t\t\t\t\t\tsizeof(sn-\u003et.build));\n-\n-\t\t\t\tn \u003d lws_strexp_expand(\u0026sx, pl-\u003ebuild,\n-\t\t\t\t\t\t strlen(pl-\u003ebuild),\n-\t\t\t\t\t\t \u0026used_in, \u0026used_out);\n-\t\t\t\tif (n !\u003d LSTRX_DONE) {\n-\t\t\t\t\tlwsl_notice(\u0022%s: strsubst fail n\u003d%d %s %s\u005cn\u0022,\n-\t\t\t\t\t\t __func__, n, pl-\u003ebuild,\n-\t\t\t\t\t\t sn-\u003et.cmake);\n-\t\t\t\t\treturn -1;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t} lws_end_foreach_dll(p);\n-\n-\t\t/*\n-\t\t * ...now we know the string handling will go well, create the\n-\t\t * event database file and create task entries in there for this\n-\t\t * configuration's tasks for each platform\n-\t\t */\n-\n-\t\tif (saim_event_db_ensure_open(pss-\u003evhd, pss-\u003esn.e.uuid, 1, \u0026pdb)) {\n-\t\t\tlwsl_err(\u0022%s: unable to open event-specific db\u005cn\u0022, __func__);\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tlws_start_foreach_dll(struct lws_dll2 *, p,\n-\t\t\t\t\t pss-\u003eplatform_owner.head) {\n-\t\t\tsai_platform_t *pl \u003d\n-\t\t\t\tlws_container_of(p, sai_platform_t, list);\n-\t\t\tsize_t used_in, used_out;\n-\t\t\tstruct lws_tokenize ts;\n-\t\t\tint n, match \u003d 0;\n-\t\t\tlws_strexp_t sx;\n-\n-\t\t\tif (!pl-\u003enondefault)\n-\t\t\t\t/*\n-\t\t\t\t * If it's a nondefault platform, then match\n-\t\t\t\t * by default\n-\t\t\t\t */\n-\t\t\t\tmatch \u003d 1;\n-\n-\t\t\tif (sn-\u003eexplicit_platforms[0]) {\n-\t\t\t\tchar not \u003d 0;\n-\n-\t\t\t\t/*\n-\t\t\t\t * If the configuration gives explicit\n-\t\t\t\t * platforms, we have to filter the platform\n-\t\t\t\t * against the comma-separated list.\n-\t\t\t\t *\n-\t\t\t\t * \u0022none\u0022 - don't match any plats just because\n-\t\t\t\t *\t they are default\n-\t\t\t\t *\n-\t\t\t\t * \u0022not plat\u0022 - disallow specific plat \u0022plat\u0022,\n-\t\t\t\t *\t\timplies you're not using \u0022none\u0022\n-\t\t\t\t *\n-\t\t\t\t * \u0022plat\u0022 - allow specific plat \u0022plat\u0022\n-\t\t\t\t */\n-\n-\t\t\t\tmemset(\u0026ts, 0, sizeof(ts));\n-\t\t\t\tts.start \u003d (char *)sn-\u003eexplicit_platforms;\n-\t\t\t\tts.len \u003d strlen(ts.start);\n-\t\t\t\tts.flags \u003d LWS_TOKENIZE_F_DOT_NONTERM |\n-\t\t\t\t\t LWS_TOKENIZE_F_SLASH_NONTERM |\n-\t\t\t\t\t LWS_TOKENIZE_F_MINUS_NONTERM;\n-\n-\t\t\t\tdo {\n-\t\t\t\t\tts.e \u003d lws_tokenize(\u0026ts);\n-\t\t\t\t\tif (ts.e !\u003d LWS_TOKZE_TOKEN)\n-\t\t\t\t\t\tcontinue;\n-\n-\t\t\t\t\tlwsl_notice(\u0022%s: check %.*s\u005cn\u0022, __func__,\n-\t\t\t\t\t\t (int)ts.token_len, ts.token);\n-\n-\t\t\t\t\tif (!strncmp(ts.token, \u0022none\u0022,\n-\t\t\t\t\t\t ts.token_len)) {\n-\t\t\t\t\t\tnot \u003d match \u003d 0;\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\t\t\t\t\tif (!strncmp(ts.token, \u0022not\u0022,\n-\t\t\t\t\t\t ts.token_len)) {\n-\t\t\t\t\t\tnot \u003d 1;\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\t\t\t\t\tif (!sai_tuple_compare(ts.token,\n-\t\t\t\t\t\t ts.token_len, pl-\u003ename)) {\n-\t\t\t\t\t\tmatch \u003d !not;\n-\t\t\t\t\t\tif (match)\n-\t\t\t\t\t\t\tbreak;\n-\t\t\t\t\t}\n-\t\t\t\t\tnot \u003d 0;\n-\t\t\t\t} while (ts.e \u003e 0);\n-\t\t\t}\n-\n-\t\t\tif (match) {\n-\t\t\t\t/*\n-\t\t\t\t * For this platform, we want to create a task\n-\t\t\t\t * associated with this event. Tasks and logs\n-\t\t\t\t * associated with an event go in an event-\n-\t\t\t\t * specific database file for scalability.\n-\t\t\t\t */\n-\n-\t\t\t\tlws_strexp_init(\u0026sx, sn, exp_cmake, sn-\u003et.build,\n-\t\t\t\t\t\tsizeof(sn-\u003et.build));\n-\n-\t\t\t\tn \u003d lws_strexp_expand(\u0026sx, pl-\u003ebuild,\n-\t\t\t\t\t\t strlen(pl-\u003ebuild),\n-\t\t\t\t\t\t \u0026used_in, \u0026used_out);\n-\t\t\t\tif (n !\u003d LSTRX_DONE) {\n-\t\t\t\t\tlwsl_notice(\u0022%s: strsubst failed %s %s\u005cn\u0022,\n-\t\t\t\t\t\t __func__, pl-\u003ebuild,\n-\t\t\t\t\t\t sn-\u003et.cmake);\n-\t\t\t\t\tsaim_event_db_close(pss-\u003evhd, \u0026pdb);\n-\t\t\t\t\treturn -1;\n-\t\t\t\t}\n-\n-\t\t\t\t/*\n-\t\t\t\t * Prepare a struct of the task object...\n-\t\t\t\t * task uuid is the event uuid and another\n-\t\t\t\t * random 32 chars, so you can always recover\n-\t\t\t\t * the related event uuid from the task uuid\n-\t\t\t\t */\n-\n-\t\t\t\tmemcpy(pss-\u003esn.t.uuid, pss-\u003esn.e.uuid, 32);\n-\t\t\t\tsai_uuid16_create(lws_get_context(pss-\u003ewsi),\n-\t\t\t\t\t\t pss-\u003esn.t.uuid + 32);\n-\t\t\t\tstrcpy(pss-\u003esn.t.event_uuid, pss-\u003esn.e.uuid);\n-\t\t\t\tpss-\u003esn.t.uid \u003d pss-\u003esn.event_task_index++;\n-\n-\t\t\t\t/*\n-\t\t\t\t * This is basically a secret that anything\n-\t\t\t\t * trying to upload an artifact for the task\n-\t\t\t\t * must provide to authenticate.\n-\t\t\t\t */\n-\t\t\t\tsai_uuid16_create(lws_get_context(pss-\u003ewsi),\n-\t\t\t\t\t\t pss-\u003esn.t.art_up_nonce);\n-\t\t\t\t/*\n-\t\t\t\t * An unrelated secret that anything\n-\t\t\t\t * trying to download an artifact for the task\n-\t\t\t\t * must provide to identify it.\n-\t\t\t\t */\n-\t\t\t\tsai_uuid16_create(lws_get_context(pss-\u003ewsi),\n-\t\t\t\t\t\t pss-\u003esn.t.art_down_nonce);\n-\n-\t\t\t\tpss-\u003esn.t.git_repo_url \u003d\n-\t\t\t\t\t\tpss-\u003esn.e.repo_fetchurl;\n-\t\t\t\tpss-\u003esn.e.last_updated \u003d\n-\t\t\t\t\t(unsigned long long)lws_now_secs();\n-\t\t\t\tpss-\u003esn.e.state \u003d 0;\n-\t\t\t\tlws_strncpy(pss-\u003esn.t.platform, pl-\u003ename,\n-\t\t\t\t\t sizeof(pss-\u003esn.t.platform));\n-\n-\t\t\t\tmemset(\u0026pss-\u003esn.t.list, 0, sizeof(pss-\u003esn.t.list));\n-\t\t\t\tlws_dll2_owner_clear(\u0026owner);\n-\t\t\t\tlws_dll2_add_head(\u0026pss-\u003esn.t.list, \u0026owner);\n-\n-\t\t\t\t/*\n-\t\t\t\t * Create the task in event-specific database\n-\t\t\t\t */\n-\n-\t\t\t\tlws_struct_sq3_serialize(pdb,\n-\t\t\t\t\t\t\t lsm_schema_sq3_map_task,\n-\t\t\t\t\t\t\t \u0026owner, pss-\u003esn.t.uid);\n-\t\t\t}\n-\n-\t\t} lws_end_foreach_dll(p);\n-\n-\t\tsaim_event_db_close(pss-\u003evhd, \u0026pdb);\n-\n-\t\tlwsl_notice(\u0022%s: New test '%s', '%s', '%s'\u005cn\u0022, __func__,\n-\t\t\t sn-\u003et.taskname, sn-\u003et.cmake, sn-\u003et.packages);\n-\n-\t\tsn-\u003et.taskname[0] \u003d '\u005c0';\n-\t\treturn 0;\n-\t}\n-\n-\tif (reason \u003d\u003d LEJPCB_OBJECT_START \u0026\u0026\n-\t ctx-\u003epath_match - 1 \u003d\u003d LEJPNSAIF_PLAT_NAME) {\n-\t\t/*\n-\t\t * We're at the platformname part of \u0022platformname\u0022 : { }\n-\t\t */\n-\t\tlws_strncpy(sn-\u003eplatname, \u0026ctx-\u003epath[10], sizeof(sn-\u003eplatname));\n-\t\tsn-\u003et.prep[0] \u003d '\u005c0';\n-\t\tsn-\u003et.cmake[0] \u003d '\u005c0';\n-\t\tsn-\u003et.cpack[0] \u003d '\u005c0';\n-\t\tsn-\u003eplatbuild[0] \u003d '\u005c0';\n-\t\tsn-\u003enondefault \u003d 0;\n-\t\treturn 0;\n-\t}\n-\n-\tif (reason \u003d\u003d LEJPCB_OBJECT_END \u0026\u0026\n-\t ctx-\u003epath_match - 1 \u003d\u003d LEJPNSAIF_PLAT_NAME \u0026\u0026\n-\t sn-\u003eplatname[0] \u0026\u0026 sn-\u003eplatbuild[0]) {\n-\t\tsai_platform_t *pl;\n-\t\tuint8_t *plb;\n-\t\tsize_t pnl;\n-\n-\t\tif (pss-\u003edry) {\n-\t\t\tsn-\u003et.platform[0] \u003d '\u005c0';\n-\t\t\treturn 0;\n-\t\t}\n-\n-\t\t/*\n-\t\t * We create a platform object with space for its strings after\n-\t\t */\n-\n-\t\tpnl \u003d strlen(sn-\u003eplatname);\n-\t\tpl \u003d (sai_platform_t *)malloc(sizeof(*pl) + pnl +\n-\t\t\t\t\t strlen(sn-\u003eplatbuild) + 2);\n-\t\tif (!pl)\n-\t\t\treturn -1;\n-\n-\t\tmemset(pl, 0, sizeof(*pl));\n-\t\tplb \u003d (uint8_t *)\u0026pl[1];\n-\n-\t\t/*\n-\t\t * Platforms are malloc'd up and added to pss\n-\t\t * .platform_owner\n-\t\t */\n-\n-\t\tpl-\u003ename \u003d (const char *)plb;\n-\t\tmemcpy(plb, sn-\u003eplatname, pnl + 1);\n-\t\tplb +\u003d pnl + 1;\n-\n-\t\tpl-\u003ebuild \u003d (const char *)plb;\n-\t\tmemcpy(plb, sn-\u003eplatbuild, strlen(sn-\u003eplatbuild) + 1);\n-\n-\t\tpl-\u003enondefault \u003d sn-\u003enondefault;\n-\n-\t\tlws_dll2_add_head(\u0026pl-\u003elist, \u0026pss-\u003eplatform_owner);\n-\n-\t\tlwsl_notice(\u0022%s: New platform '%s', build '%s', notdefault %d\u005cn\u0022,\n-\t\t\t __func__, pl-\u003ename, pl-\u003ebuild, pl-\u003enondefault);\n-\n-\t\tsn-\u003eplatbuild[0] \u003d '\u005c0';\n-\t\treturn 0;\n-\t}\n-\n-\t/* we only match on the prepared path strings */\n-\tif (!(reason \u0026 LEJP_FLAG_CB_IS_VALUE) || !ctx-\u003epath_match)\n-\t\treturn 0;\n-\n-//\tif (reason !\u003d LEJPCB_VAL_STR_END)\n-//\t\treturn 0;\n-\n-\t/* only the end part of the string, where we know the length */\n-\n-\tswitch (ctx-\u003epath_match - 1) {\n-\tcase LEJPNSAIF_SCHEMA:\n-\t\tsn-\u003eevent_task_index \u003d 0;\n-\t\treturn 0;\n-\n-\tcase LEJPNSAIF_CONFIGURATIONS_PREP:\n-\t\t/* the additional cmake options for this test configuration */\n-\t\tn \u003d strlen(sn-\u003et.prep);\n-\t\tif (n \u003c sizeof(sn-\u003et.prep) - 2)\n-\t\t\tlws_strnncpy(sn-\u003et.prep + n, ctx-\u003ebuf, ctx-\u003enpos,\n-\t\t\t\t sizeof(sn-\u003et.prep) - n);\n-\t\tbreak;\n-\n-\tcase LEJPNSAIF_CONFIGURATIONS_CMAKE:\n-\t\t/* the additional cmake options for this test configuration */\n-\t\tn \u003d strlen(sn-\u003et.cmake);\n-\t\tif (n \u003c sizeof(sn-\u003et.cmake) - 2)\n-\t\t\tlws_strnncpy(sn-\u003et.cmake + n, ctx-\u003ebuf, ctx-\u003enpos,\n-\t\t\t\t sizeof(sn-\u003et.cmake) - n);\n-\t\tbreak;\n-\n-\tcase LEJPNSAIF_CONFIGURATIONS_DEPS:\n-\t\t/* the necessary dependent package strings */\n-\t\tn \u003d strlen(sn-\u003et.packages);\n-\t\tif (n \u003c sizeof(sn-\u003et.packages) - 2) {\n-\t\t\tif (n)\n-\t\t\t\tsn-\u003et.packages[n++] \u003d ',';\n-\t\t\tlws_strncpy(sn-\u003et.packages + n, ctx-\u003ebuf,\n-\t\t\t\t sizeof(sn-\u003et.packages) - n);\n-\t\t}\n-\t\tbreak;\n-\n-\tcase LEJPNSAIF_CONFIGURATIONS_PLATFORMS:\n-\t\t/* the necessary dependent package strings */\n-\t\tlws_strncpy(sn-\u003eexplicit_platforms, ctx-\u003ebuf,\n-\t\t\t\t sizeof(sn-\u003eexplicit_platforms));\n-\t\tbreak;\n-\n-\tcase LEJPNSAIF_CONFIGURATIONS_ARTIFACTS:\n-\t\tlws_strncpy(sn-\u003et.artifacts, ctx-\u003ebuf, sizeof(sn-\u003et.artifacts));\n-\t\tbreak;\n-\n-\tcase LEJPNSAIF_CONFIGURATIONS_CPACK:\n-\t\tlws_strncpy(sn-\u003et.cpack, ctx-\u003ebuf, sizeof(sn-\u003et.cpack));\n-\t\tbreak;\n-\n-\tcase LEJPNSAIF_PLAT_BUILD:\n-\t\t/*\n-\t\t * The overall build script for this platform\n-\t\t * is appended into the temp sn.platbuild\n-\t\t */\n-\t\tn \u003d strlen(sn-\u003eplatbuild);\n-\t\tif (n \u003c sizeof(sn-\u003eplatbuild) - 2)\n-\t\t\tlws_strnncpy(sn-\u003eplatbuild + n, ctx-\u003ebuf, ctx-\u003enpos,\n-\t\t\t\t sizeof(sn-\u003eplatbuild) - n);\n-\t\tbreak;\n-\n-\tcase LEJPNSAIF_PLAT_DEFAULT:\n-\t\tsn-\u003enondefault \u003d !arg_to_bool(ctx-\u003ebuf);\n-\t\tbreak;\n-\n-\tdefault:\n-\t\treturn 0;\n-\t}\n-\n-\treturn 0;\n-}\n-\n-static signed char\n-sai_notification_lejp_cb(struct lejp_ctx *ctx, char reason)\n-{\n-\tstruct pss *pss \u003d (struct pss *)ctx-\u003euser;\n-\tsai_notification_t *sn \u003d (sai_notification_t *)\u0026pss-\u003esn;\n-\tsize_t ile, ole;\n-\tint n;\n-\n-\t/* we only match on the prepared path strings */\n-\tif (!(reason \u0026 LEJP_FLAG_CB_IS_VALUE) || !ctx-\u003epath_match)\n-\t\treturn 0;\n-\n-//\tif (reason !\u003d LEJPCB_VAL_STR_END)\n-//\t\treturn 0;\n-\n-\t/* only the end part of the string, where we know the length */\n-\n-\tswitch (ctx-\u003epath_match - 1) {\n-\tcase LEJPN_SCHEMA:\n-\t\tif (strcmp(ctx-\u003ebuf, \u0022com-warmcat-sai-notification\u0022)) {\n-\t\t\tlwsl_err(\u0022%s: unknown schema '%s'\u005cn\u0022, __func__, ctx-\u003ebuf);\n-\t\t\treturn -1;\n-\t\t}\n-\t\treturn 0;\n-\n-\tcase LEJPN_ACTION:\n-\t\tfor (n \u003d 0; n \u003c (int)LWS_ARRAY_SIZE(notifaction_action_names);n++)\n-\t\t\tif (!strcmp(ctx-\u003ebuf, notifaction_action_names[n])) {\n-\t\t\t\tsn-\u003eaction \u003d n + 1;\n-\n-\t\t\t\treturn 0;\n-\t\t\t}\n-\t\tlwsl_notice(\u0022%s: unknown action '%s' ignored\u005cn\u0022,\n-\t\t\t __func__, ctx-\u003ebuf);\n-\t\treturn -1;\n-\n-\tcase LEJPN_REPOSITORY_NAME:\n-\t\tlws_strncpy(sn-\u003ee.repo_name, ctx-\u003ebuf, sizeof(sn-\u003ee.repo_name));\n-\t\tbreak;\n-\n-\tcase LEJPN_REPOSITORY_FETCHURL:\n-\t\tlws_strncpy(sn-\u003ee.repo_fetchurl, ctx-\u003ebuf, sizeof(sn-\u003ee.repo_fetchurl));\n-\t\tbreak;\n-\n-\tcase LEJPN_REF:\n-\t\tlws_strncpy(sn-\u003ee.ref, ctx-\u003ebuf, sizeof(sn-\u003ee.ref));\n-\t\tbreak;\n-\n-\tcase LEJPN_HASH:\n-\t\tlws_strncpy(sn-\u003ee.hash, ctx-\u003ebuf, sizeof(sn-\u003ee.hash));\n-\t\tbreak;\n-\n-\tcase LEJPN_NONCE:\n-\t\tbreak;\n-\n-\tcase LEJPN_SAIFILE_LEN:\n-\t\tsn-\u003esaifile_in_len \u003d atoi(ctx-\u003ebuf);\n-\t\t/* only accept sane base64 size */\n-\t\tif (sn-\u003esaifile_in_len \u003c 8 || sn-\u003esaifile_in_len \u003e 65536) {\n-\t\t\tlwsl_err(\u0022%s: bad saifile_len %u\u005cn\u0022, __func__,\n-\t\t\t\t\t(unsigned int)sn-\u003esaifile_in_len);\n-\n-\t\t\treturn -1;\n-\t\t}\n-\t\tsn-\u003esaifile_out_len \u003d (sn-\u003esaifile_in_len * 3) / 4 + 4;\n-\t\tsn-\u003esaifile \u003d malloc(sn-\u003esaifile_out_len);\n-\t\tif (!sn-\u003esaifile) {\n-\t\t\tlwsl_err(\u0022%s: OOM\u005cn\u0022, __func__);\n-\t\t\treturn -1;\n-\t\t}\n-\t\tsn-\u003esaifile_in_seen \u003d sn-\u003esaifile_out_pos \u003d 0;\n-\t\tlws_b64_decode_state_init(\u0026sn-\u003eb64);\n-\t\tbreak;\n-\n-\tcase LEJPN_SAIFILE:\n-\t\t/*\n-\t\t * Base64 encoding of the commit's .sai.json file contents... we\n-\t\t * have to treat this with caution since it can be anything,\n-\t\t * including unparsable JSON.\n-\t\t *\n-\t\t * Let's base64-decode it and collect it into an buffer first,\n-\t\t * before sn test parse and then sn second parse to extract the\n-\t\t * tasks\n-\t\t */\n-\n-\t\tsn-\u003esaifile_in_seen +\u003d ctx-\u003enpos;\n-\t\tif (sn-\u003esaifile_in_seen \u003e sn-\u003esaifile_in_len) {\n-\t\t\tlwsl_err(\u0022%s: SAIFILE: too large in %u vs %u\u005cn\u0022, __func__,\n-\t\t\t\t (unsigned int)sn-\u003esaifile_in_seen,\n-\t\t\t\t (unsigned int)sn-\u003esaifile_in_len);\n-\t\t\treturn -1;\n-\t\t}\n-\t\tile \u003d ctx-\u003enpos;\n-\t\tole \u003d sn-\u003esaifile_out_len - sn-\u003esaifile_out_pos;\n-\t\tlws_b64_decode_stateful(\u0026sn-\u003eb64, ctx-\u003ebuf, \u0026ile,\n-\t\t\t\t(uint8_t *)sn-\u003esaifile + sn-\u003esaifile_out_pos, \u0026ole,\n-\t\t\t\t\tsn-\u003esaifile_in_seen \u003d\u003d sn-\u003esaifile_in_len);\n-\n-\t\tsn-\u003esaifile_out_pos +\u003d ole;\n-\t\tbreak;\n-\n-\tdefault:\n-\t\treturn 0;\n-\t}\n-\n-\treturn 0;\n-}\n-\n-int\n-sai_notification_file_upload_cb(void *data, const char *name,\n-\t\t\t\tconst char *filename, char *buf, int len,\n-\t\t\t\tenum lws_spa_fileupload_states state)\n-{\n-\tstruct pss *pss \u003d (struct pss *)data;\n-\tstruct lejp_ctx saictx;\n-\tlws_dll2_owner_t owner;\n-\tuint8_t result[64];\n-\tint m;\n-\n-\tswitch (state) {\n-\tcase LWS_UFS_OPEN:\n-\t\tlwsl_notice(\u0022%s: LWS_UFS_OPEN\u005cn\u0022, __func__);\n-\t\tlejp_construct(\u0026pss-\u003ectx, sai_notification_lejp_cb,\n-\t\t\t pss, paths, LWS_ARRAY_SIZE(paths));\n-\n-\t\tif (lws_genhmac_init(\u0026pss-\u003ehmac, pss-\u003ehmac_type,\n-\t\t\t\t (uint8_t *)pss-\u003evhd-\u003enotification_key,\n-\t\t\t\t strlen(pss-\u003evhd-\u003enotification_key))) {\n-\t\t\tlwsl_err(\u0022%s: failed to init hmac\u005cn\u0022, __func__);\n-\n-\t\t\treturn -1;\n-\t\t}\n-\t\tbreak;\n-\tcase LWS_UFS_FINAL_CONTENT:\n-\tcase LWS_UFS_CONTENT:\n-\t\tlwsl_notice(\u0022%s: LWS_UFS_[]CONTENT: %p %p, \u005cn\u0022, __func__, pss, buf);\n-\t\tif (len \u0026\u0026 lws_genhmac_update(\u0026pss-\u003ehmac, buf, len))\n-\t\t\treturn -1;\n-\n-\t\tprintf(\u0022%.*s\u0022, (int)len, buf);\n-\n-\t\tm \u003d lejp_parse(\u0026pss-\u003ectx, (uint8_t *)buf, len);\n-\t\tif (m \u003c 0 \u0026\u0026 m !\u003d LEJP_CONTINUE) {\n-\t\t\tlwsl_notice(\u0022%s: notif JSON decode failed '%s' (%d)\u005cn\u0022,\n-\t\t\t\t\t__func__, lejp_error_to_string(m), m);\n-\t\t\treturn m;\n-\t\t}\n-\n-\t\tlwsl_notice(\u0022%s: m \u003d %d\u005cn\u0022, __func__, m);\n-\n-\t\tif (m !\u003d 1)\n-\t\t\tbreak;\n-\n-\t\tlws_genhmac_destroy(\u0026pss-\u003ehmac, result);\n-\n-\t\tif (memcmp(result, pss-\u003enotification_sig,\n-\t\t\t lws_genhmac_size(pss-\u003ehmac_type))) {\n-\t\t\tlwsl_err(\u0022%s: hmac mismatch\u005cn\u0022, __func__);\n-\n-\t\t\treturn -1;\n-\t\t}\n-\t\tlwsl_notice(\u0022%s: hmac OK\u005cn\u0022, __func__);\n-\n-\t\t\t\t/*\n-\t\t * We have the notification metadata JSON parsed into pss-\u003esn.e,\n-\t\t * eg, pss-\u003esn-\u003ee.hash ... since it's common to, eg, push a tree\n-\t\t * in a branch and then later tag the same commit, we don't want\n-\t\t * to pointlessly repeat CI for the same tree multiple times,\n-\t\t * and need to basically dedupe.\n-\t\t */\n-\n-\t\t{\n-\t\t\tuint64_t rid \u003d 0;\n-\t\t\tchar qu[192];\n-\n-\t\t\tlws_snprintf(qu, sizeof(qu), \u0022select rowid from events \u0022\n-\t\t\t\t\t\t \u0022where hash\u003d'%s'\u0022,\n-\t\t\t\t\t\t pss-\u003esn.e.hash);\n-\n-\t\t\tif (sqlite3_exec(pss-\u003evhd-\u003emaster.pdb, qu,\n-\t\t\t\t\t sai_sql3_get_uint64_cb,\n-\t\t\t\t\t \u0026rid, NULL) \u003d\u003d SQLITE_OK \u0026\u0026 rid) {\n-\t\t\t\t/* it already exists */\n-\t\t\t\tlwsl_notice(\u0022%s: ignoring notification as \u0022\n-\t\t\t\t\t \u0022tree hash event exists\u005cn\u0022,\n-\t\t\t\t\t __func__);\n-\n-\t\t\t\treturn 0;\n-\t\t\t}\n-\t\t}\n-\t\n-\t\tif (!pss-\u003esn.saifile)\n-\t\t\treturn -1;\n-\n-\t\t/*\n-\t\t * We processed the notification JSON, but we only decoded the\n-\t\t * base64'd copy of the .sai.json so far... now's the time we\n-\t\t * want to process that and break it down into tasks.\n-\t\t *\n-\t\t * We don't trust it since it's controlled by the guy who pushed\n-\t\t * the commit, there can be anything at all in there. We made\n-\t\t * sure he can't attack us until now by base64-ing it at the\n-\t\t * server hook, so he's just dumb payload.\n-\t\t *\n-\t\t * Let's try to parse it in two passes, first without acting on\n-\t\t * the content to confirm it's going to succeed...\n-\t\t */\n-\n-\t\tsai_uuid16_create(lws_get_context(pss-\u003ewsi), pss-\u003esn.e.uuid);\n-\t\tif (saim_event_db_ensure_open(pss-\u003evhd, pss-\u003esn.e.uuid, 1,\n-\t\t\t\t\t (sqlite3 **)\u0026pss-\u003esn.e.pdb)) {\n-\t\t\tlwsl_err(\u0022%s: unable to open event-specific database\u005cn\u0022,\n-\t\t\t\t\t__func__);\n-\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tpss-\u003edry \u003d 1;\n-\t\tlejp_construct(\u0026saictx, sai_saifile_lejp_cb, pss, saifile_paths,\n-\t\t\t LWS_ARRAY_SIZE(saifile_paths));\n-\t\tm \u003d lejp_parse(\u0026saictx, (uint8_t *)pss-\u003esn.saifile,\n-\t\t\t pss-\u003esn.saifile_out_pos);\n-\t\tsaim_event_db_close(pss-\u003evhd, (sqlite3 **)\u0026pss-\u003esn.e.pdb);\n-\t\tif (m \u003c 0) {\n-\t\t\tlwsl_notice(\u0022%s: saifile JSON decode failed '%s' (%d)\u005cn\u0022,\n-\t\t\t\t __func__, lejp_error_to_string(m), m);\n-\t\t\treturn m;\n-\t\t}\n-\n-\t\t/* ... then add the 32-char event object in the database ... */\n-\n-\t\tpss-\u003esn.e.created \u003d (unsigned long long)lws_now_secs();\n-\t\tpss-\u003esn.e.state \u003d SAIES_WAITING;\n-\n-\t\tmemset(\u0026pss-\u003esn.e.list, 0, sizeof(pss-\u003esn.e.list));\n-\t\tlws_dll2_owner_clear(\u0026owner);\n-\t\tlws_dll2_add_head(\u0026pss-\u003esn.e.list, \u0026owner);\n-\n-\t\t/*\n-\t\t * This is our new event going into the event database...\n-\t\t */\n-\n-\t\tlws_struct_sq3_serialize(pss-\u003evhd-\u003emaster.pdb,\n-\t\t\t\t\t lsm_schema_sq3_map_event, \u0026owner, 0);\n-\n-\t\t/*\n-\t\t * ... process the saifile JSON again creating tasks for each\n-\t\t * entry in the saifile, for each platform, against that event\n-\t\t * object...\n-\t\t */\n-\n-\t\tpss-\u003edry \u003d 0;\n-\t\tlejp_construct(\u0026saictx, sai_saifile_lejp_cb, pss, saifile_paths,\n-\t\t\t LWS_ARRAY_SIZE(saifile_paths));\n-\t\tm \u003d lejp_parse(\u0026saictx, (uint8_t *)pss-\u003esn.saifile,\n-\t\t\t pss-\u003esn.saifile_out_pos);\n-\t\tif (m \u003c 0) {\n-\t\t\tlwsl_notice(\u0022%s: saifile JSON decode failed '%s' (%d)\u005cn\u0022,\n-\t\t\t\t __func__, lejp_error_to_string(m), m);\n-\t\t\treturn m;\n-\t\t}\n-\n-\t\tfree(pss-\u003esn.saifile);\n-\t\tpss-\u003esn.saifile \u003d NULL;\n-\n-\t\tlwsl_notice(\u0022%s: notification inserted into db\u005cn\u0022, __func__);\n-\n-\t\t/*\n-\t\t * Reassess now if there's a builder we can match to a pending task\n-\t\t */\n-\n-\t\tlws_sul_schedule(pss-\u003evhd-\u003econtext, 0, \u0026pss-\u003evhd-\u003esul_central,\n-\t\t\t\t saim_central_cb, 1);\n-\n-\t\treturn 0;\n-\n-\n-\tcase LWS_UFS_CLOSE:\n-\t\t// lwsl_info(\u0022%s: LWS_UFS_CLOSE\u005cn\u0022, __func__);\n-\t\tlejp_destruct(\u0026pss-\u003ectx);\n-\t\tbreak;\n-\t}\n-\n-\treturn 0;\n-}\n-\ndiff --git a/src/master/m-overview.c b/src/master/m-overview.c\ndeleted file mode 100644\nindex e650c19..0000000\n--- a/src/master/m-overview.c\n+++ /dev/null\n@@ -1,28 +0,0 @@\n-/*\n- * Sai master - src/master/overview.c\n- *\n- * Copyright (C) 2019 Andy Green \u003candy@warmcat.com\u003e\n- *\n- * This library is free software; you can redistribute it and/or\n- * modify it under the terms of the GNU Lesser General Public\n- * License as published by the Free Software Foundation:\n- * version 2.1 of the License.\n- *\n- * This library is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n- * Lesser General Public License for more details.\n- *\n- * You should have received a copy of the GNU Lesser General Public\n- * License along with this library; if not, write to the Free Software\n- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n- * MA 02110-1301 USA\n- *\n- * The same ws interface is connected-to by builders (on path /builder), and\n- * provides the query transport for browsers (on path /browse).\n- */\n-\n-#include \u003clibwebsockets.h\u003e\n-#include \u0022m-private.h\u0022\n-\n-\ndiff --git a/src/master/m-private.h b/src/master/m-private.h\ndeleted file mode 100644\nindex b2f15c4..0000000\n--- a/src/master/m-private.h\n+++ /dev/null\n@@ -1,296 +0,0 @@\n-/*\n- * Sai master definitions src/master/private.h\n- *\n- * Copyright (C) 2019 Andy Green \u003candy@warmcat.com\u003e\n- *\n- * This library is free software; you can redistribute it and/or\n- * modify it under the terms of the GNU Lesser General Public\n- * License as published by the Free Software Foundation:\n- * version 2.1 of the License.\n- *\n- * This library is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n- * Lesser General Public License for more details.\n- *\n- * You should have received a copy of the GNU Lesser General Public\n- * License along with this library; if not, write to the Free Software\n- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n- * MA 02110-1301 USA\n- */\n-\n-#include \u0022../common/include/private.h\u0022\n-#include \u003csqlite3.h\u003e\n-#include \u003csys/stat.h\u003e\n-\n-struct sai_plat;\n-\n-typedef struct sai_platm {\n-\tstruct lws_dll2_owner builder_owner;\n-\tstruct lws_dll2_owner subs_owner;\n-\n-\tsqlite3 *pdb;\n-\tsqlite3 *pdb_auth;\n-} saim_t;\n-\n-typedef struct sai_platform {\n-\tstruct lws_dll2\t\tlist;\n-\n-\tconst char\t\t*name;\n-\tconst char\t\t*build;\n-\n-\tuint8_t\t\t\tnondefault;\n-\n-\t/* build and name over-allocated here */\n-} sai_platform_t;\n-\n-typedef enum {\n-\tSAIN_ACTION_INVALID,\n-\tSAIN_ACTION_REPO_UPDATED\n-} sai_notification_action_t;\n-\n-typedef struct {\n-\n-\tsai_event_t\t\t\te;\n-\tsai_task_t\t\t\tt;\n-\n-\tchar\t\t\t\tplatbuild[4096];\n-\tchar\t\t\t\tplatname[96];\n-\tchar\t\t\t\texplicit_platforms[2048];\n-\n-\tint\t\t\t\tevent_task_index;\n-\n-\tstruct lws_b64state\t\tb64;\n-\tchar\t\t\t\t*saifile;\n-\tuint64_t\t\t\twhen;\n-\tsize_t\t\t\t\tsaifile_in_len;\n-\tsize_t\t\t\t\tsaifile_out_len;\n-\tsize_t\t\t\t\tsaifile_out_pos;\n-\tsize_t\t\t\t\tsaifile_in_seen;\n-\tsai_notification_action_t\taction;\n-\n-\tuint8_t\t\t\t\tnondefault;\n-} sai_notification_t;\n-\n-typedef enum {\n-\tSWT_BUILDER,\n-\tSWT_BROWSE\n-} ws_type;\n-\n-typedef enum {\n-\tWSS_IDLE,\n-\tWSS_PREPARE_OVERVIEW,\n-\tWSS_SEND_OVERVIEW,\n-\tWSS_PREPARE_BUILDER_SUMMARY,\n-\tWSS_SEND_BUILDER_SUMMARY,\n-\n-\tWSS_PREPARE_TASKINFO,\n-\tWSS_SEND_ARTIFACT_INFO,\n-\n-\tWSS_PREPARE_EVENTINFO,\n-\tWSS_SEND_EVENTINFO,\n-} ws_state;\n-\n-typedef struct sai_builder {\n-\tsaim_t c;\n-} saib_t;\n-\n-struct vhd;\n-\n-enum {\n-\tSAIM_NOT_SPECIFIC,\n-\tSAIM_SPECIFIC_H,\n-\tSAIM_SPECIFIC_ID\n-};\n-\n-struct pss {\n-\tstruct vhd\t\t*vhd;\n-\tstruct lws\t\t*wsi;\n-\n-\tstruct lws_spa\t\t*spa;\n-\tstruct lejp_ctx\t\tctx;\n-\tsai_notification_t\tsn;\n-\tstruct lws_dll2\t\tsame; /* owner: vhd.builders */\n-\n-\tstruct lws_dll2\t\tsubs_list;\n-\n-\tuint64_t\t\tsub_timestamp;\n-\tchar\t\t\tsub_task_uuid[65];\n-\tchar\t\t\tspecific[65];\n-\tchar\t\t\tspecific_project[96];\n-\tchar\t\t\tauth_user[33];\n-\n-\tsqlite3\t\t\t*pdb_artifact;\n-\tsqlite3_blob\t\t*blob_artifact;\n-\n-\tlws_dll2_owner_t\tplatform_owner; /* sai_platform_t builder offers */\n-\tlws_dll2_owner_t\ttask_cancel_owner; /* sai_platform_t builder offers */\n-\tlws_dll2_owner_t\taft_owner; /* for statefully spooling artifact info */\n-\tlws_struct_args_t\ta;\n-\n-\tunion {\n-\t\tsai_plat_t\t*b;\n-\t\tsai_plat_owner_t *o;\n-\t} u;\n-\tconst char\t\t*master_name;\n-\n-\tstruct lwsac\t\t*query_ac;\n-\tstruct lwsac\t\t*task_ac;\t/* tasks for an event */\n-\tstruct lwsac\t\t*logs_ac;\n-\tlws_dll2_owner_t\tissue_task_owner; /* list of sai_task_t */\n-\tconst sai_task_t\t*one_task; /* only for browser */\n-\tconst sai_event_t\t*one_event;\n-\tlws_dll2_owner_t\tquery_owner;\n-\tlws_dll2_t\t\t*walk;\n-\n-\tint\t\t\ttask_index;\n-\tint\t\t\tlog_cache_index;\n-\tint\t\t\tlog_cache_size;\n-\tint\t\t\tauthorized;\n-\tint\t\t\tspecificity;\n-\tunsigned long\t\texpiry_unix_time;\n-\n-\t/* notification hmac information */\n-\tchar\t\t\tnotification_sig[128];\n-\tchar\t\t\talang[128];\n-\tstruct lws_genhmac_ctx\thmac;\n-\tenum lws_genhmac_types\thmac_type;\n-\tchar\t\t\tour_form;\n-\tchar\t\t\tlogin_form;\n-\n-\tuint64_t\t\tfirst_log_timestamp;\n-\tuint64_t\t\tartifact_offset;\n-\tuint64_t\t\tartifact_length;\n-\n-\tws_type\t\t\ttype;\n-\tws_state\t\tsend_state;\n-\n-\tuint32_t\t\tpending; /* bitmap of things that need sending */\n-\n-\tunsigned int\t\tspa_failed:1;\n-\tunsigned int\t\tsubsequent:1; /* for individual JSON */\n-\tunsigned int\t\tdry:1;\n-\tunsigned int\t\tquery_already_done:1;\n-\tunsigned int\t\tfrag:1;\n-\tunsigned int\t\tmark_started:1;\n-\tunsigned int\t\twants_event_updates:1;\n-\tunsigned int\t\tannounced:1;\n-\tunsigned int\t\tbulk_binary_data:1;\n-\n-\tuint8_t\t\t\tovstate; /* SOS_ substate when doing overview */\n-};\n-\n-typedef struct saim_sqlite_cache {\n-\tlws_dll2_t\tlist;\n-\tchar\t\tuuid[65];\n-\tsqlite3\t\t*pdb;\n-\tlws_usec_t\tidle_since;\n-\tint\t\trefcount;\n-} saim_sqlite_cache_t;\n-\n-struct vhd {\n-\tstruct lws_context *context;\n-\tstruct lws_vhost *vhost;\n-\n-\t/* pss lists */\n-\tstruct lws_dll2_owner browsers;\n-\tstruct lws_dll2_owner builders;\n-\n-\t/* our keys */\n-\tstruct lws_jwk\t\t\tjwt_jwk_auth;\n-\tchar\t\t\t\tjwt_auth_alg[16];\n-\tconst char\t\t\t*jwt_issuer;\n-\tconst char\t\t\t*jwt_audience;\n-\n-\tconst char *sqlite3_path_lhs;\n-\n-\tlws_dll2_owner_t sqlite3_cache; /* saim_sqlite_cache_t */\n-\tlws_dll2_owner_t tasklog_cache;\n-\tlws_sorted_usec_list_t sul_logcache;\n-\tlws_sorted_usec_list_t sul_central; /* background task allocation sul */\n-\n-\tlws_usec_t\tlast_check_abandoned_tasks;\n-\n-\tconst char *notification_key;\n-\n-\tsaim_t master;\n-};\n-\n-extern struct lws_context *\n-sai_lws_context_from_json(const char *config_dir,\n-\t\t\t struct lws_context_creation_info *info,\n-\t\t\t const struct lws_protocols **pprotocols);\n-extern const struct lws_protocols protocol_ws;\n-\n-int\n-sai_notification_file_upload_cb(void *data, const char *name,\n-\t\t\t\tconst char *filename, char *buf, int len,\n-\t\t\t\tenum lws_spa_fileupload_states state);\n-\n-int\n-sai_sqlite3_statement(sqlite3 *pdb, const char *cmd, const char *desc);\n-\n-int\n-sai_uuid16_create(struct lws_context *context, char *dest33);\n-\n-int\n-saim_event_db_ensure_open(struct vhd *vhd, const char *event_uuid, char can_create, sqlite3 **ppdb);\n-\n-void\n-saim_event_db_close(struct vhd *vhd, sqlite3 **ppdb);\n-\n-int\n-saim_event_db_delete_database(struct vhd *vhd, const char *event_uuid);\n-\n-int\n-sai_sq3_event_lookup(sqlite3 *pdb, uint64_t start, lws_struct_args_cb cb, void *ca);\n-\n-int\n-sai_sql3_get_uint64_cb(void *user, int cols, char **values, char **name);\n-\n-int\n-saim_ws_json_tx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t bl);\n-\n-int\n-saim_ws_json_rx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t bl);\n-\n-int\n-lws_struct_map_set(const lws_struct_map_t *map, char *u);\n-\n-int\n-saim_ws_json_rx_browser(struct vhd *vhd, struct pss *pss,\n-\t\t\t uint8_t *buf, size_t bl);\n-\n-void\n-sai_task_uuid_to_event_uuid(char *event_uuid33, const char *task_uuid65);\n-\n-int\n-saim_ws_json_tx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t bl);\n-\n-void\n-mark_pending(struct pss *pss, ws_state state);\n-\n-int\n-saim_subs_request_writeable(struct vhd *vhd, const char *task_uuid);\n-\n-void\n-saim_central_cb(lws_sorted_usec_list_t *sul);\n-\n-int\n-saim_task_reset(struct vhd *vhd, const char *task_uuid);\n-\n-int\n-saim_task_cancel(struct vhd *vhd, const char *task_uuid);\n-\n-int\n-saim_allocate_task(struct vhd *vhd, struct pss *pss, sai_plat_t *cb,\n-\t\t const char *cns_name);\n-\n-int\n-saim_set_task_state(struct vhd *vhd, const char *builder_name,\n-\t\t const char *builder_uuid, const char *task_uuid, int state,\n-\t\t uint64_t started, uint64_t duration);\n-\n-int\n-saim_get_blob(struct vhd *vhd, const char *url, sqlite3 **pdb,\n-\t sqlite3_blob **blob, uint64_t *length);\ndiff --git a/src/master/m-sai.c b/src/master/m-sai.c\ndeleted file mode 100644\nindex 5e753d6..0000000\n--- a/src/master/m-sai.c\n+++ /dev/null\n@@ -1,69 +0,0 @@\n-/*\n- * Sai master\n- *\n- * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n- *\n- * This library is free software; you can redistribute it and/or\n- * modify it under the terms of the GNU Lesser General Public\n- * License as published by the Free Software Foundation:\n- * version 2.1 of the License.\n- *\n- * This library is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n- * Lesser General Public License for more details.\n- *\n- * You should have received a copy of the GNU Lesser General Public\n- * License along with this library; if not, write to the Free Software\n- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n- * MA 02110-1301 USA\n- */\n-\n-#include \u003clibwebsockets.h\u003e\n-#include \u003cstring.h\u003e\n-#include \u003csignal.h\u003e\n-#include \u003ctime.h\u003e\n-\n-#include \u0022m-private.h\u0022\n-\n-static int interrupted;\n-struct lws_context *context;\n-\n-static const struct lws_protocols\n-\t*pprotocols[] \u003d { \u0026protocol_ws, NULL };\n-\n-static void sigint_handler(int sig)\n-{\n-\tinterrupted \u003d 1;\n-}\n-\n-int main(int argc, const char **argv)\n-{\n-\tint logs \u003d LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE;\n-\tconst char *p, *conf \u003d \u0022/etc/sai/master\u0022;\n-\tstruct lws_context_creation_info info;\n-\n-\tsignal(SIGINT, sigint_handler);\n-\n-\tif ((p \u003d lws_cmdline_option(argc, argv, \u0022-d\u0022)))\n-\t\tlogs \u003d atoi(p);\n-\n-\tlws_set_log_level(logs, NULL);\n-\tlwsl_user(\u0022Sai Master - Copyright (C) 2019-2020 Andy Green \u003candy@warmcat.com\u003e\u005cn\u0022);\n-\n-\tif ((p \u003d lws_cmdline_option(argc, argv, \u0022-c\u0022)))\n-\t\tconf \u003d p;\n-\n-\tcontext \u003d sai_lws_context_from_json(conf, \u0026info, pprotocols);\n-\tif (!context) {\n-\t\tlwsl_err(\u0022lws init failed\u005cn\u0022);\n-\t\treturn 1;\n-\t}\n-\n-\twhile (!lws_service(context, 0) \u0026\u0026 !interrupted)\n-\t\t;\n-\n-\tlws_context_destroy(context);\n-\n-\treturn 0;\n-}\ndiff --git a/src/master/m-task.c b/src/master/m-task.c\ndeleted file mode 100644\nindex 76ff2ae..0000000\n--- a/src/master/m-task.c\n+++ /dev/null\n@@ -1,467 +0,0 @@\n-/*\n- * Sai master\n- *\n- * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n- *\n- * This library is free software; you can redistribute it and/or\n- * modify it under the terms of the GNU Lesser General Public\n- * License as published by the Free Software Foundation:\n- * version 2.1 of the License.\n- *\n- * This library is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n- * Lesser General Public License for more details.\n- *\n- * You should have received a copy of the GNU Lesser General Public\n- * License along with this library; if not, write to the Free Software\n- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n- * MA 02110-1301 USA\n- */\n-\n-#include \u003clibwebsockets.h\u003e\n-#include \u003cstring.h\u003e\n-#include \u003csignal.h\u003e\n-#include \u003ctime.h\u003e\n-#include \u003cassert.h\u003e\n-\n-#include \u0022m-private.h\u0022\n-\n-static int\n-browser_upd(struct lws_dll2 *d, void *user)\n-{\n-\tstruct pss *pss \u003d lws_container_of(d, struct pss, same);\n-\tws_state state \u003d (ws_state)(intptr_t)user;\n-\n-\tmark_pending(pss, state);\n-\n-\treturn 0;\n-}\n-\n-static int\n-sql3_get_integer_cb(void *user, int cols, char **values, char **name)\n-{\n-\tunsigned int *pui \u003d (unsigned int *)user;\n-\n-\tlwsl_warn(\u0022%s: values[0] '%s'\u005cn\u0022, __func__, values[0]);\n-\t*pui \u003d atoi(values[0]);\n-\n-\treturn 0;\n-}\n-\n-void\n-sai_task_uuid_to_event_uuid(char *event_uuid33, const char *task_uuid65)\n-{\n-\tmemcpy(event_uuid33, task_uuid65, 32);\n-\tevent_uuid33[32] \u003d '\u005c0';\n-}\n-\n-int\n-saim_set_task_state(struct vhd *vhd, const char *builder_name,\n-\t\t const char *builder_uuid, const char *task_uuid, int state,\n-\t\t uint64_t started, uint64_t duration)\n-{\n-\tchar update[384], esc[96], esc1[96], esc2[96], esc3[32], esc4[32],\n-\t\tevent_uuid[33];\n-\tunsigned int count \u003d 0, count_good \u003d 0, count_bad \u003d 0;\n-\tsai_event_state_t oes, sta;\n-\tstruct lwsac *ac \u003d NULL;\n-\tsai_event_t *e \u003d NULL;\n-\tlws_dll2_owner_t o;\n-\tint n;\n-\n-\t/*\n-\t * Extract the event uuid from the task uuid\n-\t */\n-\n-\tsai_task_uuid_to_event_uuid(event_uuid, task_uuid);\n-\n-\t/*\n-\t * Look up the task's event in the event database...\n-\t */\n-\n-\tlws_dll2_owner_clear(\u0026o);\n-\tlws_sql_purify(esc1, event_uuid, sizeof(esc1));\n-\tlws_snprintf(esc2, sizeof(esc2), \u0022 and uuid\u003d'%s'\u0022, esc1);\n-\tn \u003d lws_struct_sq3_deserialize(vhd-\u003emaster.pdb, esc2, NULL,\n-\t\t\t\t lsm_schema_sq3_map_event, \u0026o, \u0026ac, 0, 1);\n-\tif (n \u003c 0 || !o.head) {\n-\t\tlwsl_err(\u0022%s: failed to get task_uuid %s\u005cn\u0022, __func__, esc1);\n-\t\tgoto bail;\n-\t}\n-\n-\te \u003d lws_container_of(o.head, sai_event_t, list);\n-\toes \u003d e-\u003estate;\n-\n-\t/*\n-\t * Open the event-specific database on the temporary event object\n-\t */\n-\n-\tif (saim_event_db_ensure_open(vhd, event_uuid, 0, (sqlite3 **)\u0026e-\u003epdb)) {\n-\t\tlwsl_err(\u0022%s: unable to open event-specific database\u005cn\u0022,\n-\t\t\t\t__func__);\n-\n-\t\treturn -1;\n-\t}\n-\n-\tif (builder_name)\n-\t\tlws_sql_purify(esc, builder_name, sizeof(esc));\n-\telse\n-\t\tesc[0] \u003d '\u005c0';\n-\n-\tif (builder_uuid)\n-\t\tlws_sql_purify(esc1, builder_uuid, sizeof(esc1));\n-\telse\n-\t\tesc1[0] \u003d '\u005c0';\n-\tlws_sql_purify(esc2, task_uuid, sizeof(esc2));\n-\n-\tesc3[0] \u003d esc4[0] \u003d '\u005c0';\n-\n-\tif (started) {\n-\t\tif (started \u003d\u003d 1)\n-\t\t\tstarted \u003d 0;\n-\t\tlws_snprintf(esc3, sizeof(esc3), \u0022,started\u003d%llu\u0022,\n-\t\t\t (unsigned long long)started);\n-\t}\n-\tif (duration) {\n-\t\tif (duration \u003d\u003d 1)\n-\t\t\tduration \u003d 0;\n-\t\tlws_snprintf(esc4, sizeof(esc4), \u0022,duration\u003d%llu\u0022,\n-\t\t\t (unsigned long long)duration);\n-\t}\n-\n-\t/*\n-\t * Update the task by uuid, in the event-specific database\n-\t */\n-\n-\tlws_snprintf(update, sizeof(update),\n-\t\t\u0022update tasks set state\u003d%d%s%s%s%s%s%s%s%s where uuid\u003d'%s'\u0022,\n-\t\t state, builder_uuid ? \u0022,builder\u003d'\u0022: \u0022\u0022,\n-\t\t\tbuilder_uuid ? esc1 : \u0022\u0022,\n-\t\t\tbuilder_uuid ? \u0022'\u0022 : \u0022\u0022,\n-\t\t\tbuilder_name ? \u0022,builder_name\u003d'\u0022 : \u0022\u0022,\n-\t\t\tbuilder_name ? esc : \u0022\u0022,\n-\t\t\tbuilder_name ? \u0022'\u0022 : \u0022\u0022,\n-\t\t\tesc3, esc4, esc2);\n-\n-\tif (sqlite3_exec((sqlite3 *)e-\u003epdb, update, NULL, NULL, NULL) !\u003d SQLITE_OK) {\n-\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, update,\n-\t\t\t sqlite3_errmsg(vhd-\u003emaster.pdb));\n-\t\tgoto bail;\n-\t}\n-\n-\t/*\n-\t * So, how many tasks for this event?\n-\t */\n-\n-\tif (sqlite3_exec((sqlite3 *)e-\u003epdb, \u0022select count(state) from tasks\u0022,\n-\t\t\t sql3_get_integer_cb, \u0026count, NULL) !\u003d SQLITE_OK) {\n-\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, update,\n-\t\t\t sqlite3_errmsg(vhd-\u003emaster.pdb));\n-\t\tgoto bail;\n-\t}\n-\n-\t/*\n-\t * ... how many completed well?\n-\t */\n-\n-\tif (sqlite3_exec((sqlite3 *)e-\u003epdb, \u0022select count(state) from tasks where state \u003d\u003d 3\u0022,\n-\t\t\t sql3_get_integer_cb, \u0026count_good, NULL) !\u003d SQLITE_OK) {\n-\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, update,\n-\t\t\t sqlite3_errmsg(vhd-\u003emaster.pdb));\n-\t\tgoto bail;\n-\t}\n-\n-\t/*\n-\t * ... how many failed?\n-\t */\n-\n-\tif (sqlite3_exec((sqlite3 *)e-\u003epdb, \u0022select count(state) from tasks where state \u003d\u003d 4\u0022,\n-\t\t\t sql3_get_integer_cb, \u0026count_bad, NULL) !\u003d SQLITE_OK) {\n-\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, update,\n-\t\t\t sqlite3_errmsg(vhd-\u003emaster.pdb));\n-\t\tgoto bail;\n-\t}\n-\n-\t/*\n-\t * Decide how to set the event state based on that\n-\t */\n-\n-\tlwsl_warn(\u0022%s: ev %s, count %u, count_good %u, count_bad %u\u005cn\u0022,\n-\t\t __func__, event_uuid, count, count_good, count_bad);\n-\n-\tsta \u003d SAIES_BEING_BUILT;\n-\n-\tif (count) {\n-\t\tif (count \u003d\u003d count_good)\n-\t\t\tsta \u003d SAIES_SUCCESS;\n-\t\telse\n-\t\t\tif (count \u003d\u003d count_bad)\n-\t\t\t\tsta \u003d SAIES_FAIL;\n-\t\t\telse\n-\t\t\t\tif (count_bad)\n-\t\t\t\t\tsta \u003d SAIES_BEING_BUILT_HAS_FAILURES;\n-\t}\n-\n-\tif (sta !\u003d oes) {\n-\t\tlwsl_notice(\u0022%s: event state changed\u005cn\u0022, __func__);\n-\t}\n-\n-\t/*\n-\t * Update the event\n-\t */\n-\n-\tlws_sql_purify(esc1, event_uuid, sizeof(esc1));\n-\tlws_snprintf(update, sizeof(update),\n-\t\t\u0022update events set state\u003d%d where uuid\u003d'%s'\u0022, sta, esc1);\n-\n-\tif (sqlite3_exec(vhd-\u003emaster.pdb, update, NULL, NULL, NULL) !\u003d SQLITE_OK) {\n-\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, update,\n-\t\t\t sqlite3_errmsg(vhd-\u003emaster.pdb));\n-\t\tgoto bail;\n-\t}\n-\n-\tsaim_event_db_close(vhd, (sqlite3 **)\u0026e-\u003epdb);\n-\n-\t/*\n-\t * We need to schedule sending an event info update to anyone\n-\t * subscribing to the task...\n-\t */\n-\n-\tlws_dll2_foreach_safe(\u0026vhd-\u003ebrowsers,\n-\t\t(void *)WSS_PREPARE_OVERVIEW, browser_upd);\n-\n-\tlwsac_free(\u0026ac);\n-\n-\treturn 0;\n-\n-bail:\n-\tif (e)\n-\t\tsaim_event_db_close(vhd, (sqlite3 **)\u0026e-\u003epdb);\n-\tlwsac_free(\u0026ac);\n-\n-\treturn 1;\n-}\n-\n-/*\n- * Find the most recent task that still needs doing for platform, on any event\n- *\n- * If any, the task pointed-to lives inside *pac, along with its strings etc\n- */\n-\n-static const sai_task_t *\n-saim_task_pending(struct vhd *vhd, struct lwsac **pac, const char *platform)\n-{\n-\tstruct lwsac *ac \u003d NULL;\n-\tchar esc[96], pf[96];\n-\tlws_dll2_owner_t o;\n-\tint n;\n-\n-\tlws_sql_purify(esc, platform, sizeof(esc));\n-\n-\tassert(platform);\n-\tassert(pac);\n-\n-\t/*\n-\t * Collect a list of events that still have any open tasks\n-\t *\n-\t * We don't put this list in the pac since we can dispose of it in this\n-\t * scope whether we find something or not\n-\t */\n-\n-\tn \u003d lws_struct_sq3_deserialize(vhd-\u003emaster.pdb,\n-\t\t\t\t \u0022 and (state !\u003d 3 and state !\u003d 4 and state !\u003d 5)\u0022,\n-\t\t\t\t \u0022created desc \u0022, lsm_schema_sq3_map_event, \u0026o,\n-\t\t\t\t \u0026ac, 0, 10);\n-\tif (n \u003c 0 || !o.head) {\n-\t//\tlwsl_notice(\u0022%s: all events complete\u005cn\u0022, __func__);\n-\t\t/* error, or there are no events that aren't complete */\n-\t\tgoto bail;\n-\t}\n-\n-\t/*\n-\t * Iterate through the events looking at his event-specific database\n-\t * for tasks on the specified platform...\n-\t */\n-\n-\tlws_start_foreach_dll(struct lws_dll2 *, p, o.head) {\n-\t\tsai_event_t *e \u003d lws_container_of(p, sai_event_t, list);\n-\t\tlws_dll2_owner_t ot;\n-\t\tsqlite3 *pdb \u003d NULL;\n-\n-\t\tif (!saim_event_db_ensure_open(vhd, e-\u003euuid, 0, \u0026pdb)) {\n-\t\t\tlws_snprintf(pf, sizeof(pf),\n-\t\t\t\t \u0022 and state\u003d0 and platform\u003d'%s'\u0022, esc);\n-\t\t\tn \u003d lws_struct_sq3_deserialize(pdb, pf, NULL,\n-\t\t\t\t\t\t lsm_schema_sq3_map_task,\n-\t\t\t\t\t\t \u0026ot, pac, 0, 1);\n-\t\t\tsaim_event_db_close(vhd, \u0026pdb);\n-\t\t\tif (ot.count) {\n-\n-\t\t\t\tlwsl_notice(\u0022%s: found task for %s\u005cn\u0022, __func__, esc);\n-\n-\t\t\t\tlwsac_free(\u0026ac);\n-\n-\t\t\t\treturn lws_container_of(ot.head,\n-\t\t\t\t\t\tsai_task_t, list);\n-\t\t\t}\n-\t\t}\n-\n-\t} lws_end_foreach_dll(p);\n-\n-\tlwsl_debug(\u0022%s: no free tasks matching '%s'\u005cn\u0022, __func__, esc);\n-\n-bail:\n-\tlwsac_free(\u0026ac);\n-\n-\treturn NULL;\n-}\n-\n-int\n-saim_task_cancel(struct vhd *vhd, const char *task_uuid)\n-{\n-\tsai_cancel_t *can;\n-\n-\t/*\n-\t * For every pss that we have from builders...\n-\t */\n-\tlws_start_foreach_dll(struct lws_dll2 *, p, vhd-\u003ebuilders.head) {\n-\t\tstruct pss *pss \u003d lws_container_of(p, struct pss, same);\n-\n-\n-\t\t/*\n-\t\t * ... queue the task cancel message\n-\t\t */\n-\t\tcan \u003d malloc(sizeof *can);\n-\t\tif (!can)\n-\t\t\treturn -1;\n-\t\tmemset(can, 0, sizeof(*can));\n-\n-\t\tstrncpy(can-\u003etask_uuid, task_uuid, sizeof(can-\u003etask_uuid));\n-\n-\t\tlws_dll2_add_tail(\u0026can-\u003elist, \u0026pss-\u003etask_cancel_owner);\n-\n-\t\tlws_callback_on_writable(pss-\u003ewsi);\n-\n-\t} lws_end_foreach_dll(p);\n-\n-\treturn 0;\n-}\n-\n-/*\n- * Keep the task record itself, but remove all logs and artifacts related to\n- * it and reset the task state back to WAITING.\n- */\n-\n-int\n-saim_task_reset(struct vhd *vhd, const char *task_uuid)\n-{\n-\tchar esc[96], cmd[256], event_uuid[33];\n-\tsqlite3 *pdb \u003d NULL;\n-\n-\tif (!task_uuid[0])\n-\t\treturn 0;\n-\n-\tlwsl_notice(\u0022%s: received request to reset task %s\u005cn\u0022, __func__, task_uuid);\n-\n-\tsai_task_uuid_to_event_uuid(event_uuid, task_uuid);\n-\n-\tif (saim_event_db_ensure_open(vhd, event_uuid, 0, \u0026pdb)) {\n-\t\tlwsl_err(\u0022%s: unable to open event-specific database\u005cn\u0022,\n-\t\t\t\t__func__);\n-\n-\t\treturn -1;\n-\t}\n-\n-\tlws_sql_purify(esc, task_uuid, sizeof(esc));\n-\tlws_snprintf(cmd, sizeof(cmd), \u0022delete from logs where task_uuid\u003d'%s'\u0022,\n-\t\t esc);\n-\n-\tif (sqlite3_exec(pdb, cmd, NULL, NULL, NULL) !\u003d SQLITE_OK) {\n-\t\tsaim_event_db_close(vhd, \u0026pdb);\n-\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, cmd,\n-\t\t\t sqlite3_errmsg(vhd-\u003emaster.pdb));\n-\t\treturn 1;\n-\t}\n-\tlws_snprintf(cmd, sizeof(cmd), \u0022delete from artifacts where task_uuid\u003d'%s'\u0022,\n-\t\t esc);\n-\n-\tsqlite3_exec(pdb, cmd, NULL, NULL, NULL);\n-\tsaim_event_db_close(vhd, \u0026pdb);\n-\n-\tsaim_set_task_state(vhd, NULL, NULL, task_uuid, SAIES_WAITING, 1, 1);\n-\n-\tsaim_task_cancel(vhd, task_uuid);\n-\n-\t/*\n-\t * Reassess now if there's a builder we can match to a pending task\n-\t */\n-\n-\tlws_sul_schedule(vhd-\u003econtext, 0, \u0026vhd-\u003esul_central, saim_central_cb, 1);\n-\n-\treturn 0;\n-}\n-\n-/*\n- * Look for any task on any event that needs building on platform_name\n- */\n-\n-int\n-saim_allocate_task(struct vhd *vhd, struct pss *pss, sai_plat_t *cb,\n-\t\t const char *platform_name)\n-{\n-\tchar esc1[96], esc2[96];\n-\tlws_dll2_owner_t o;\n-\tsai_task_t *task;\n-\tint n;\n-\n-\tif (cb-\u003eongoing \u003e\u003d cb-\u003einstances)\n-\t\treturn 1;\n-\n-\t/*\n-\t * Look for a task for this platform, on any event that needs building\n-\t */\n-\n-\ttask \u003d (sai_task_t *)saim_task_pending(vhd, \u0026pss-\u003ea.ac, platform_name);\n-\n-\tif (!task)\n-\t\treturn 1;\n-\n-\tlwsl_notice(\u0022%s: %s: task found %s\u005cn\u0022, __func__, platform_name, cb-\u003ename);\n-\n-\t/* yes, we will offer it to him */\n-\n-\tif (saim_set_task_state(vhd, cb-\u003ename, cb-\u003ename, task-\u003euuid,\n-\t\t\t\tSAIES_PASSED_TO_BUILDER, lws_now_secs(), 0))\n-\t\treturn -1;\n-\n-\t/* advance the task state first time we get logs */\n-\tpss-\u003emark_started \u003d 1;\n-\n-\t/* let's get ahold of his event as well */\n-\n-\tlws_sql_purify(esc1, task-\u003eevent_uuid, sizeof(esc1));\n-\tlws_snprintf(esc2, sizeof(esc2), \u0022 and uuid\u003d'%s'\u0022, esc1);\n-\tn \u003d lws_struct_sq3_deserialize(vhd-\u003emaster.pdb, esc2, NULL,\n-\t\t\t\t lsm_schema_sq3_map_event, \u0026o, \u0026pss-\u003ea.ac,\n-\t\t\t\t 0, 1);\n-\tif (n \u003c 0 || !o.head)\n-\t\treturn -1;\n-\n-\ttask-\u003eone_event \u003d lws_container_of(o.head, sai_event_t, list);\n-\n-\ttask-\u003emaster_name\t\u003d pss-\u003emaster_name;\n-\ttask-\u003erepo_name\t\t\u003d task-\u003eone_event-\u003erepo_name;\n-\ttask-\u003egit_ref\t\t\u003d task-\u003eone_event-\u003eref;\n-\ttask-\u003egit_hash\t\t\u003d task-\u003eone_event-\u003ehash;\n-\n-\t/*\n-\t * add to master's estimate of builder's ongoing tasks...\n-\t */\n-\tcb-\u003eongoing++;\n-\n-\tlws_dll2_add_tail(\u0026task-\u003epending_assign_list, \u0026pss-\u003eissue_task_owner);\n-\tlws_callback_on_writable(pss-\u003ewsi);\n-\n-\treturn 0;\n-}\ndiff --git a/src/master/m-ws-browser.c b/src/master/m-ws-browser.c\ndeleted file mode 100644\nindex 58f0e3f..0000000\n--- a/src/master/m-ws-browser.c\n+++ /dev/null\n@@ -1,1139 +0,0 @@\n-/*\n- * Sai master - ./src/master/m-ws-browser.c\n- *\n- * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n- *\n- * This library is free software; you can redistribute it and/or\n- * modify it under the terms of the GNU Lesser General Public\n- * License as published by the Free Software Foundation:\n- * version 2.1 of the License.\n- *\n- * This library is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n- * Lesser General Public License for more details.\n- *\n- * You should have received a copy of the GNU Lesser General Public\n- * License along with this library; if not, write to the Free Software\n- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n- * MA 02110-1301 USA\n- *\n- * These are ws rx and tx handlers related to browser ws connections, on\n- * /broswe URLs.\n- */\n-\n-#include \u003clibwebsockets.h\u003e\n-#include \u003cstring.h\u003e\n-#include \u003csignal.h\u003e\n-#include \u003cassert.h\u003e\n-#include \u003ctime.h\u003e\n-\n-#include \u0022m-private.h\u0022\n-\n-/*\n- * For decoding specific event data request from browser\n- */\n-\n-\n-typedef struct sai_browse_rx_evinfo {\n-\tchar\t\tevent_hash[65];\n-} sai_browse_rx_evinfo_t;\n-\n-static lws_struct_map_t lsm_browser_evinfo[] \u003d {\n-\tLSM_CARRAY\t(sai_browse_rx_evinfo_t, event_hash,\t\u0022event_hash\u0022),\n-};\n-\n-\n-/*\n- * For decoding specific task data request from browser\n- */\n-\n-typedef struct sai_browse_rx_taskinfo {\n-\tchar\t\ttask_hash[65];\n-\tunsigned int\tlog_start;\n-\tuint8_t\t\tlogs;\n-} sai_browse_rx_taskinfo_t;\n-\n-static lws_struct_map_t lsm_browser_taskinfo[] \u003d {\n-\tLSM_CARRAY\t(sai_browse_rx_taskinfo_t, task_hash,\t\u0022task_hash\u0022),\n-\tLSM_UNSIGNED\t(sai_browse_rx_taskinfo_t, logs,\t\u0022logs\u0022),\n-};\n-\n-static lws_struct_map_t lsm_browser_taskreset[] \u003d {\n-\tLSM_CARRAY\t(sai_browse_rx_evinfo_t, event_hash,\t\u0022uuid\u0022),\n-\n-};\n-\n-/*\n- * Schema list so lws_struct can pick the right object to create based on the\n- * incoming schema name\n- */\n-\n-static const lws_struct_map_t lsm_schema_json_map_bwsrx[] \u003d {\n-\tLSM_SCHEMA\t(sai_browse_rx_taskinfo_t, NULL, lsm_browser_taskinfo,\n-\t\t\t\t\t \u0022com.warmcat.sai.taskinfo\u0022),\n-\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_browser_evinfo,\n-\t\t\t\t\t \u0022com.warmcat.sai.eventinfo\u0022),\n-\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_browser_taskreset,\n-\t\t\t/* shares struct */ \u0022com.warmcat.sai.taskreset\u0022),\n-\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_browser_taskreset,\n-\t\t\t/* shares struct */ \u0022com.warmcat.sai.eventreset\u0022),\n-\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_browser_taskreset,\n-\t\t\t/* shares struct */ \u0022com.warmcat.sai.eventdelete\u0022),\n-\tLSM_SCHEMA\t(sai_cancel_t,\t\t NULL, lsm_task_cancel,\n-\t\t\t\t\t \u0022com.warmcat.sai.taskcan\u0022),\n-};\n-\n-enum {\n-\tSAIM_WS_BROWSER_RX_TASKINFO,\n-\tSAIM_WS_BROWSER_RX_EVENTINFO,\n-\tSAIM_WS_BROWSER_RX_TASKRESET,\n-\tSAIM_WS_BROWSER_RX_EVENTRESET,\n-\tSAIM_WS_BROWSER_RX_EVENTDELETE,\n-\tSAIM_WS_BROWSER_RX_TASKCANCEL\n-};\n-\n-\n-/*\n- * For issuing combined task and event data back to browser\n- */\n-\n-typedef struct sai_browse_taskreply {\n-\tconst sai_event_t\t*event;\n-\tconst sai_task_t\t*task;\n-\tchar\t\t\tauth_user[33];\n-\tint\t\t\tauthorized;\n-\tint\t\t\tauth_secs;\n-} sai_browse_taskreply_t;\n-\n-static lws_struct_map_t lsm_taskreply[] \u003d {\n-\tLSM_CHILD_PTR\t(sai_browse_taskreply_t, event,\tsai_event_t, NULL,\n-\t\t\t lsm_event, \u0022e\u0022),\n-\tLSM_CHILD_PTR\t(sai_browse_taskreply_t, task,\tsai_task_t, NULL,\n-\t\t\t lsm_task, \u0022t\u0022),\n-\tLSM_CARRAY\t(sai_browse_taskreply_t, auth_user,\t\u0022auth_user\u0022),\n-\tLSM_UNSIGNED\t(sai_browse_taskreply_t, authorized,\t\u0022authorized\u0022),\n-\tLSM_UNSIGNED\t(sai_browse_taskreply_t, auth_secs,\t\u0022auth_secs\u0022),\n-};\n-\n-const lws_struct_map_t lsm_schema_json_map_taskreply[] \u003d {\n-\tLSM_SCHEMA\t(sai_browse_taskreply_t, NULL, lsm_taskreply,\n-\t\t\t \u0022com.warmcat.sai.taskinfo\u0022),\n-};\n-\n-enum sai_overview_state {\n-\tSOS_EVENT,\n-\tSOS_TASKS,\n-};\n-\n-/* 1 \u003d\u003d authorized */\n-\n-static int\n-saim_conn_auth(struct pss *pss)\n-{\n-\tif (!pss-\u003eauthorized)\n-\t\treturn 0;\n-\tif (pss-\u003eexpiry_unix_time \u003c (unsigned long)lws_now_secs())\n-\t\treturn 0;\n-\n-\treturn 1;\n-}\n-\n-/*\n- * Ask for writeable cb on all browser connections subscribed to a particular\n- * task (so we can send them some more logs)\n- */\n-\n-int\n-saim_subs_request_writeable(struct vhd *vhd, const char *task_uuid)\n-{\n-\tlws_start_foreach_dll(struct lws_dll2 *, p,\n-\t\t\t vhd-\u003emaster.subs_owner.head) {\n-\t\tstruct pss *pss \u003d lws_container_of(p, struct pss, subs_list);\n-\n-\t\tif (!strcmp(pss-\u003esub_task_uuid, task_uuid))\n-\t\t\tlws_callback_on_writable(pss-\u003ewsi);\n-\n-\t} lws_end_foreach_dll(p);\n-\n-\treturn 0;\n-}\n-\n-static int\n-saim_pss_schedule_eventinfo(struct pss *pss, const char *event_uuid)\n-{\n-\tchar qu[80], esc[66];\n-\tint n;\n-\n-\t/*\n-\t * Just collect the event struct into pss-\u003equery_owner to dump\n-\t */\n-\n-\tlws_sql_purify(esc, event_uuid, sizeof(esc));\n-\tlws_snprintf(qu, sizeof(qu), \u0022 and uuid\u003d'%s'\u0022, esc);\n-\tn \u003d lws_struct_sq3_deserialize(pss-\u003evhd-\u003emaster.pdb, qu, NULL,\n-\t\t\t\t lsm_schema_sq3_map_event,\n-\t\t\t\t \u0026pss-\u003equery_owner, \u0026pss-\u003equery_ac, 0, 1);\n-\tif (n \u003c 0 || !pss-\u003equery_owner.head)\n-\t\treturn 1;\n-\n-\tpss-\u003equery_already_done \u003d 1;\n-\n-\tmark_pending(pss, WSS_PREPARE_OVERVIEW);\n-\tmark_pending(pss, WSS_PREPARE_BUILDER_SUMMARY);\n-\n-\treturn 0;\n-}\n-\n-static int\n-saim_pss_schedule_taskinfo(struct pss *pss, const char *task_uuid, int logsub)\n-{\n-\tchar qu[80], esc[66], event_uuid[33];\n-\tsqlite3 *pdb \u003d NULL;\n-\tlws_dll2_owner_t o;\n-\tint n;\n-\n-\tsai_task_uuid_to_event_uuid(event_uuid, task_uuid);\n-\n-\t/* open the event-specific database object */\n-\n-\tif (saim_event_db_ensure_open(pss-\u003evhd, event_uuid, 0, \u0026pdb))\n-\t\t/* no longer exists, nothing to do */\n-\t\treturn 0;\n-\n-\t/*\n-\t * get the related task object into its own ac... there might\n-\t * be a lot of related data, so we hold the ac in the pss for\n-\t * as long as needed to send it out\n-\t */\n-\n-\tlws_sql_purify(esc, task_uuid, sizeof(esc));\n-\tlws_snprintf(qu, sizeof(qu), \u0022 and uuid\u003d'%s'\u0022, esc);\n-\tn \u003d lws_struct_sq3_deserialize(pdb, qu, NULL, lsm_schema_sq3_map_task,\n-\t\t\t\t \u0026o, \u0026pss-\u003equery_ac, 0, 1);\n-\tsaim_event_db_close(pss-\u003evhd, \u0026pdb);\n-\tlwsl_info(\u0022%s: n %d, o.head %p\u005cn\u0022, __func__, n, o.head);\n-\tif (n \u003c 0 || !o.head)\n-\t\treturn 1;\n-\n-\tpss-\u003eone_task \u003d lws_container_of(o.head, sai_task_t, list);\n-\n-\tlwsl_info(\u0022%s: browser ws asked for task hash: %s, plat %s\u005cn\u0022,\n-\t\t __func__, task_uuid, pss-\u003eone_task-\u003eplatform);\n-\n-\t/* let the pss take over the task info ac and schedule sending */\n-\n-\tlws_dll2_remove((struct lws_dll2 *)\u0026pss-\u003eone_task-\u003elist);\n-\n-\t/*\n-\t * let's also get the event object the task relates to into\n-\t * its own event struct\n-\t */\n-\n-\tlws_sql_purify(esc, event_uuid, sizeof(esc));\n-\tlws_snprintf(qu, sizeof(qu), \u0022 and uuid\u003d'%s'\u0022, esc);\n-\tn \u003d lws_struct_sq3_deserialize(pss-\u003evhd-\u003emaster.pdb, qu, NULL,\n-\t\t\t\t lsm_schema_sq3_map_event, \u0026o,\n-\t\t\t\t \u0026pss-\u003equery_ac, 0, 1);\n-\tif (n \u003c 0 || !o.head)\n-\t\treturn 1;\n-\n-\t/* does he want to subscribe to logs? */\n-\tif (logsub) {\n-\t\tstrcpy(pss-\u003esub_task_uuid, pss-\u003eone_task-\u003euuid);\n-\t\tlws_dll2_add_head(\u0026pss-\u003esubs_list,\n-\t\t\t\t \u0026pss-\u003evhd-\u003emaster.subs_owner);\n-\t\tpss-\u003esub_timestamp \u003d 0; /* where we got up to */\n-\t\tlws_callback_on_writable(pss-\u003ewsi);\n-\t\tlwsl_info(\u0022%s: subscribed to logs for %s\u005cn\u0022, __func__,\n-\t\t\t pss-\u003esub_task_uuid);\n-\t}\n-\n-\tpss-\u003eone_event \u003d lws_container_of(o.head, sai_event_t, list);\n-\n-\tmark_pending(pss, WSS_PREPARE_TASKINFO);\n-\tmark_pending(pss, WSS_PREPARE_BUILDER_SUMMARY);\n-\n-\treturn 0;\n-}\n-\n-/*\n- * We need to schedule re-sending out task and event state to anyone subscribed\n- * to the task that changed or its associated event\n- */\n-\n-int\n-saim_subs_task_state_change(struct vhd *vhd, const char *task_uuid)\n-{\n-\tlws_start_foreach_dll(struct lws_dll2 *, p,\n-\t\t\t vhd-\u003emaster.subs_owner.head) {\n-\t\tstruct pss *pss \u003d lws_container_of(p, struct pss, subs_list);\n-\n-\t\tif (!strcmp(pss-\u003esub_task_uuid, task_uuid))\n-\t\t\tsaim_pss_schedule_taskinfo(pss, task_uuid, 0);\n-\n-\t} lws_end_foreach_dll(p);\n-\n-\treturn 0;\n-}\n-\n-\n-/*\n- * browser has sent us a request for either overview, or data on a specific\n- * task\n- */\n-\n-int\n-saim_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf,\n-\t\t\tsize_t bl)\n-{\n-\tsai_browse_rx_taskinfo_t *ti;\n-\tchar qu[128], esc[96], *err;\n-\tsai_browse_rx_evinfo_t *ei;\n-\tsqlite3 *pdb \u003d NULL;\n-\tlws_struct_args_t a;\n-\tlws_dll2_owner_t o;\n-\tsai_cancel_t *can;\n-\tint m, ret \u003d -1;\n-\n-\tmemset(\u0026a, 0, sizeof(a));\n-\ta.map_st[0] \u003d lsm_schema_json_map_bwsrx;\n-\ta.map_entries_st[0] \u003d LWS_ARRAY_SIZE(lsm_schema_json_map_bwsrx);\n-\ta.map_entries_st[1] \u003d LWS_ARRAY_SIZE(lsm_schema_json_map_bwsrx);\n-\ta.ac_block_size \u003d 128;\n-\n-\tlws_struct_json_init_parse(\u0026pss-\u003ectx, NULL, \u0026a);\n-\tm \u003d lejp_parse(\u0026pss-\u003ectx, (uint8_t *)buf, bl);\n-\tif (m \u003c 0 || !a.dest) {\n-\t\tlwsl_hexdump_notice(buf, bl);\n-\t\tlwsl_notice(\u0022%s: notification JSON decode failed '%s'\u005cn\u0022,\n-\t\t\t\t__func__, lejp_error_to_string(m));\n-\t\treturn m;\n-\t}\n-\n-\t/*\n-\t * Which object we ended up with depends on the schema that came in...\n-\t * a.top_schema_index is the index in lsm_schema_json_map_bwsrx it\n-\t * matched on\n-\t */\n-\n-\tswitch (a.top_schema_index) {\n-\n-\tcase SAIM_WS_BROWSER_RX_TASKINFO:\n-\t\tti \u003d (sai_browse_rx_taskinfo_t *)a.dest;\n-\n-\t\tlwsl_info(\u0022%s: schema index %d, task hash %s\u005cn\u0022, __func__,\n-\t\t\t\ta.top_schema_index, ti-\u003etask_hash);\n-\n-\t\tif (!ti-\u003etask_hash[0]) {\n-\t\t\t/*\n-\t\t\t * he's asking for the overview schema\n-\t\t\t */\n-\t\t\tlws_dll2_add_head(\u0026pss-\u003esame, \u0026vhd-\u003ebrowsers);\n-\t\t\tmark_pending(pss, WSS_PREPARE_OVERVIEW);\n-\t\t\tmark_pending(pss, WSS_PREPARE_BUILDER_SUMMARY);\n-\t\t\tret \u003d 0;\n-\t\t\tgoto bail;\n-\t\t}\n-\n-\t\t/*\n-\t\t * get the related task object into its own ac... there might\n-\t\t * be a lot of related data, so we hold the ac in the pss for\n-\t\t * as long as needed to send it out\n-\t\t */\n-\n-\t\tif (saim_pss_schedule_taskinfo(pss, ti-\u003etask_hash, !!ti-\u003elogs))\n-\t\t\tgoto soft_error;\n-\n-\t\treturn 0;\n-\n-\tcase SAIM_WS_BROWSER_RX_EVENTINFO:\n-\n-\t\tei \u003d (sai_browse_rx_evinfo_t *)a.dest;\n-\n-\t\tif (saim_pss_schedule_eventinfo(pss, ei-\u003eevent_hash))\n-\t\t\tgoto soft_error;\n-\n-\t\tlwsac_free(\u0026a.ac);\n-\n-\t\treturn 0;\n-\n-\tcase SAIM_WS_BROWSER_RX_TASKRESET:\n-\n-\t\tif (!saim_conn_auth(pss))\n-\t\t\tgoto soft_error;\n-\n-\t\t/*\n-\t\t * User is asking us to reset / rebuild this task\n-\t\t */\n-\n-\t\tei \u003d (sai_browse_rx_evinfo_t *)a.dest;\n-\n-\t\tif (saim_task_reset(vhd, ei-\u003eevent_hash))\n-\t\t\tgoto soft_error;\n-\n-\t\tif (saim_pss_schedule_taskinfo(pss, ei-\u003eevent_hash, 0))\n-\t\t\tgoto soft_error;\n-\n-\t\tlwsac_free(\u0026a.ac);\n-\n-\t\treturn 0;\n-\n-\tcase SAIM_WS_BROWSER_RX_EVENTRESET:\n-\n-\t\tif (!saim_conn_auth(pss))\n-\t\t\tgoto soft_error;\n-\n-\t\t/*\n-\t\t * User is asking us to reset / rebuild every task in the event\n-\t\t */\n-\n-\t\tei \u003d (sai_browse_rx_evinfo_t *)a.dest;\n-\n-\t\tlwsl_notice(\u0022%s: received request to reset event %s\u005cn\u0022,\n-\t\t\t __func__, ei-\u003eevent_hash);\n-\n-\t\t/* open the event-specific database object */\n-\n-\t\tif (saim_event_db_ensure_open(pss-\u003evhd, ei-\u003eevent_hash, 0, \u0026pdb)) {\n-\t\t\tlwsl_err(\u0022%s: unable to open event-specific database\u005cn\u0022,\n-\t\t\t\t\t__func__);\n-\t\t\t/*\n-\t\t\t * hanging up isn't a good way to deal with browser\n-\t\t\t * tabs left open with a live connection to a\n-\t\t\t * now-deleted task... the page will reconnect endlessly\n-\t\t\t */\n-\t\t\tgoto soft_error;\n-\t\t}\n-\n-\t\t/*\n-\t\t * Retreive all the related structs into a dll2 list\n-\t\t */\n-\n-\t\tlws_sql_purify(esc, ei-\u003eevent_hash, sizeof(esc));\n-\n-\t\tif (lws_struct_sq3_deserialize(pdb, NULL, NULL,\n-\t\t\t\t\t lsm_schema_sq3_map_task,\n-\t\t\t\t\t \u0026o, \u0026a.ac, 0, 999) \u003e\u003d 0) {\n-\n-\t\t\tsqlite3_exec(vhd-\u003emaster.pdb, \u0022BEGIN TRANSACTION\u0022,\n-\t\t\t\t NULL, NULL, \u0026err);\n-\n-\t\t\t/*\n-\t\t\t * Walk the results list resetting all the tasks\n-\t\t\t */\n-\n-\t\t\tlws_start_foreach_dll(struct lws_dll2 *, p, o.head) {\n-\t\t\t\tsai_task_t *t \u003d lws_container_of(p, sai_task_t,\n-\t\t\t\t\t\t\t\t list);\n-\n-\t\t\t\tsaim_task_reset(vhd, t-\u003euuid);\n-\n-\t\t\t} lws_end_foreach_dll(p);\n-\n-\t\t\tsqlite3_exec(vhd-\u003emaster.pdb, \u0022END TRANSACTION\u0022,\n-\t\t\t\t NULL, NULL, \u0026err);\n-\t\t}\n-\n-\t\tsaim_event_db_close(vhd, \u0026pdb);\n-\t\tlwsac_free(\u0026a.ac);\n-\n-\t\treturn 0;\n-\n-\tcase SAIM_WS_BROWSER_RX_EVENTDELETE:\n-\t\t/*\n-\t\t * User is asking us to delete the whole event\n-\t\t */\n-\n-\t\tif (!saim_conn_auth(pss))\n-\t\t\tgoto soft_error;\n-\n-\t\tei \u003d (sai_browse_rx_evinfo_t *)a.dest;\n-\n-\t\tlwsl_notice(\u0022%s: received request to delete event %s\u005cn\u0022,\n-\t\t\t __func__, ei-\u003eevent_hash);\n-\n-\t\t/* open the event-specific database object */\n-\n-\t\tif (saim_event_db_ensure_open(pss-\u003evhd, ei-\u003eevent_hash, 0, \u0026pdb)) {\n-\t\t\tlwsl_notice(\u0022%s: unable to open event-specific database\u005cn\u0022,\n-\t\t\t\t\t__func__);\n-\t\t\t/*\n-\t\t\t * hanging up isn't a good way to deal with browser\n-\t\t\t * tabs left open with a live connection to a\n-\t\t\t * now-deleted task... the page will reconnect endlessly\n-\t\t\t */\n-\n-\t\t\tgoto soft_error;\n-\t\t}\n-\n-\t\t/*\n-\t\t * Retreive all the related structs into a dll2 list\n-\t\t */\n-\n-\t\tlws_sql_purify(esc, ei-\u003eevent_hash, sizeof(esc));\n-\n-\t\tif (lws_struct_sq3_deserialize(pdb, NULL, NULL,\n-\t\t\t\t\t lsm_schema_sq3_map_task,\n-\t\t\t\t\t \u0026o, \u0026a.ac, 0, 999) \u003e\u003d 0) {\n-\n-\t\t\tsqlite3_exec(vhd-\u003emaster.pdb, \u0022BEGIN TRANSACTION\u0022,\n-\t\t\t\t NULL, NULL, \u0026err);\n-\n-\t\t\t/*\n-\t\t\t * Walk the results list cancelling all the tasks\n-\t\t\t * that look like they might be ongoing\n-\t\t\t */\n-\n-\t\t\tlws_start_foreach_dll(struct lws_dll2 *, p, o.head) {\n-\t\t\t\tsai_task_t *t \u003d lws_container_of(p, sai_task_t,\n-\t\t\t\t\t\t\t\t list);\n-\n-\t\t\t\tif (t-\u003estate !\u003d SAIES_WAITING \u0026\u0026\n-\t\t\t\t t-\u003estate !\u003d SAIES_SUCCESS \u0026\u0026\n-\t\t\t\t t-\u003estate !\u003d SAIES_FAIL \u0026\u0026\n-\t\t\t\t t-\u003estate !\u003d SAIES_CANCELLED)\n-\t\t\t\t\tsaim_task_cancel(vhd, t-\u003euuid);\n-\n-\t\t\t} lws_end_foreach_dll(p);\n-\n-\t\t\tsqlite3_exec(vhd-\u003emaster.pdb, \u0022END TRANSACTION\u0022,\n-\t\t\t\t NULL, NULL, \u0026err);\n-\t\t}\n-\n-\t\tsaim_event_db_close(vhd, \u0026pdb);\n-\n-\t\t/* delete the event iself */\n-\n-\t\tlws_sql_purify(esc, ei-\u003eevent_hash, sizeof(esc));\n-\t\tlws_snprintf(qu, sizeof(qu), \u0022delete from events where uuid\u003d'%s'\u0022,\n-\t\t\t\tesc);\n-\t\tsqlite3_exec(vhd-\u003emaster.pdb, qu, NULL, NULL, \u0026err);\n-\n-\t\t/* remove the event-specific database */\n-\n-\t\tsaim_event_db_delete_database(vhd, ei-\u003eevent_hash);\n-\n-\t\tret \u003d 0;\n-\t\tlwsac_free(\u0026a.ac);\n-\t\tmark_pending(pss, WSS_PREPARE_OVERVIEW);\n-\t\tmark_pending(pss, WSS_PREPARE_BUILDER_SUMMARY);\n-\n-\t\tgoto bail;;\n-\n-\tcase SAIM_WS_BROWSER_RX_TASKCANCEL:\n-\n-\t\tif (!saim_conn_auth(pss))\n-\t\t\tgoto soft_error;\n-\n-\t\t/*\n-\t\t * Browser is informing us of task's STOP button clicked, we\n-\t\t * need to inform any builder that might be building it\n-\t\t */\n-\t\tcan \u003d (sai_cancel_t *)a.dest;\n-\n-\t\tlwsl_notice(\u0022%s: received request to cancel task %s\u005cn\u0022,\n-\t\t\t __func__, can-\u003etask_uuid);\n-\n-\t\tsaim_task_cancel(vhd, can-\u003etask_uuid);\n-\t\tbreak;\n-\n-\tdefault:\n-\t\tassert(0);\n-\t\tbreak;\n-\t}\n-\n-\tret \u003d 0;\n-\n-bail:\n-\tlwsac_free(\u0026a.ac);\n-\n-\treturn ret;\n-\n-soft_error:\n-\tlwsac_free(\u0026a.ac);\n-\n-\treturn 0;\n-}\n-\n-\n-/*\n- * We're sending something on a browser ws connection. Returning nonzero from\n- * here drops the connection, necessary if we fail partway through a message\n- * but undesirable if a browser tab will keep reconnecting and asking for the\n- * same, no-longer-existant thing.\n- */\n-\n-int\n-saim_ws_json_tx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t bl)\n-{\n-\tuint8_t *start \u003d buf + LWS_PRE, *p \u003d start, *end \u003d p + bl - LWS_PRE - 1;\n-\tint n, flags \u003d LWS_WRITE_TEXT, first \u003d 0, iu;\n-\tsai_browse_taskreply_t task_reply;\n-\tlws_dll2_owner_t task_owner;\n-\tlws_struct_serialize_t *js;\n-\tchar esc[256], esc1[33], filt[128];\n-\tchar event_uuid[33];\n-\tsqlite3 *pdb \u003d NULL;\n-\tsai_event_t *e;\n-\tsai_task_t *t;\n-\tchar any;\n-\tsize_t w;\n-\n-again:\n-\n-\tstart \u003d buf + LWS_PRE;\n-\tp \u003d start;\n-\tend \u003d p + bl - LWS_PRE - 1;\n-\tflags \u003d LWS_WRITE_TEXT;\n-\tfirst \u003d 0;\n-\n-\t// lwsl_notice(\u0022%s: send_state %d, pss %p, wsi %p\u005cn\u0022, __func__,\n-\t// pss-\u003esend_state, pss, pss-\u003ewsi);\n-\n-\tswitch (pss-\u003esend_state) {\n-\tcase WSS_IDLE:\n-\n-\t\t/*\n-\t\t * Anything from a task log he's subscribed to?\n-\t\t */\n-\n-\t\tif (pss-\u003esubs_list.owner) {\n-\n-\t\t\tlws_snprintf(esc, sizeof(esc),\n-\t\t\t\t \u0022and task_uuid\u003d'%s' and timestamp \u003e %llu\u0022,\n-\t\t\t\t pss-\u003esub_task_uuid,\n-\t\t\t\t (unsigned long long)pss-\u003esub_timestamp);\n-\n-\t\t\t/*\n-\t\t\t * For efficiency, let's try to grab the next 100 at\n-\t\t\t * once from sqlite and work our way through sending\n-\t\t\t * them\n-\t\t\t */\n-\n-\t\t\tif (pss-\u003elog_cache_index \u003d\u003d pss-\u003elog_cache_size) {\n-\t\t\t\tint sr;\n-\n-\t\t\t\tsai_task_uuid_to_event_uuid(event_uuid,\n-\t\t\t\t\t\t\t pss-\u003esub_task_uuid);\n-\n-\t\t\t\tlws_dll2_owner_clear(\u0026task_owner);\n-\t\t\t\tlwsac_free(\u0026pss-\u003elogs_ac);\n-\n-\t\t\t\tlwsl_info(\u0022%s: collecting logs %s\u005cn\u0022,\n-\t\t\t\t\t __func__, esc);\n-\n-\t\t\t\tif (saim_event_db_ensure_open(vhd, event_uuid, 0,\n-\t\t\t\t\t\t\t \u0026pdb)) {\n-\t\t\t\t\tlwsl_notice(\u0022%s: unable to open event-specific database\u005cn\u0022,\n-\t\t\t\t\t\t\t__func__);\n-\n-\t\t\t\t\treturn 0;\n-\t\t\t\t}\n-\n-\t\t\t\tsr \u003d lws_struct_sq3_deserialize(pdb, esc,\n-\t\t\t\t\t\t\t\t\u0022uid,timestamp \u0022,\n-\t\t\t\t\t\t\t\tlsm_schema_sq3_map_log,\n-\t\t\t\t\t\t\t\t\u0026pss-\u003equery_owner,\n-\t\t\t\t\t\t\t\t\u0026pss-\u003elogs_ac, 0, 100);\n-\n-\t\t\t\tsaim_event_db_close(vhd, \u0026pdb);\n-\n-\t\t\t\tif (sr) {\n-\n-\t\t\t\t\tlwsl_err(\u0022%s: subs failed\u005cn\u0022, __func__);\n-\n-\t\t\t\t\treturn 0;\n-\t\t\t\t}\n-\n-\t\t\t\tpss-\u003elog_cache_index \u003d 0;\n-\t\t\t\tpss-\u003elog_cache_size \u003d pss-\u003equery_owner.count;\n-\t\t\t}\n-\n-\t\t\tif (pss-\u003elog_cache_index \u003c pss-\u003elog_cache_size) {\n-\t\t\t\tsai_log_t *log \u003d lws_container_of(\n-\t\t\t\t\t\tpss-\u003equery_owner.head,\n-\t\t\t\t\t\tsai_log_t, list);\n-\n-\t\t\t\tlws_dll2_remove(\u0026log-\u003elist);\n-\t\t\t\tpss-\u003elog_cache_index++;\n-\n-\t\t\t\t/*\n-\t\t\t\t * Turn it back into JSON so we can give it to\n-\t\t\t\t * the browser\n-\t\t\t\t */\n-\n-\t\t\t\tjs \u003d lws_struct_json_serialize_create(\n-\t\t\t\t\tlsm_schema_json_map_log, 1, 0, log);\n-\t\t\t\tif (!js) {\n-\t\t\t\t\tlwsl_notice(\u0022%s: json ser fail\u005cn\u0022, __func__);\n-\t\t\t\t\treturn 0;\n-\t\t\t\t}\n-\n-\t\t\t\tn \u003d lws_struct_json_serialize(js, p, end - p, \u0026w);\n-\t\t\t\tlws_struct_json_serialize_destroy(\u0026js);\n-\t\t\t\tif (n \u003d\u003d LSJS_RESULT_ERROR) {\n-\t\t\t\t\tlwsl_notice(\u0022%s: json ser error\u005cn\u0022, __func__);\n-\t\t\t\t\treturn 0;\n-\t\t\t\t}\n-\n-\t\t\t\tp +\u003d w;\n-\t\t\t\tfirst \u003d 1;\n-\t\t\t\tpss-\u003ewalk \u003d NULL;\n-\n-\t\t\t\t/*\n-\t\t\t\t * Record that this was the most recent log we\n-\t\t\t\t * saw so far\n-\t\t\t\t */\n-\t\t\t\tpss-\u003esub_timestamp \u003d log-\u003etimestamp;\n-\t\t\t\tgoto send_it;\n-\t\t\t}\n-\t\t}\n-\n-\t\tif (!pss-\u003epending)\n-\t\t\treturn 0;\n-\n-\t\tfor (n \u003d 0; n \u003c 31; n++) {\n-\t\t\tif (pss-\u003epending \u0026 (1 \u003c\u003c n)) {\n-\t\t\t\tpss-\u003epending \u0026\u003d ~(1 \u003c\u003c n);\n-\t\t\t\tpss-\u003esend_state \u003d n;\n-\t\t\t\tgoto again;\n-\t\t\t}\n-\t\t}\n-\t\treturn 0;\n-\n-\tcase WSS_PREPARE_OVERVIEW:\n-\n-\t\tfilt[0] \u003d '\u005c0';\n-\t\tif (pss-\u003especific_project[0]) {\n-\t\t\tlws_sql_purify(esc, pss-\u003especific_project, sizeof(esc) - 1);\n-\t\t\tlws_snprintf(filt, sizeof(filt), \u0022 and repo_name\u003d\u005c\u0022%s\u005c\u0022\u0022, esc);\n-\t\t}\n-\t\tpss-\u003ewants_event_updates \u003d 1;\n-\t\tif (!pss-\u003equery_already_done \u0026\u0026\n-\t\t lws_struct_sq3_deserialize(vhd-\u003emaster.pdb,\n-\t\t\t\t filt[0] ? filt : NULL, \u0022created \u0022,\n-\t\t\t\tlsm_schema_sq3_map_event, \u0026pss-\u003equery_owner,\n-\t\t\t\t\u0026pss-\u003equery_ac, 0, -8)) {\n-\t\t\tlwsl_notice(\u0022%s: OVERVIEW 2 failed\u005cn\u0022, __func__);\n-\n-\t\t\tpss-\u003esend_state \u003d WSS_IDLE;\n-\t\t\tlwsac_free(\u0026pss-\u003etask_ac);\n-\t\t\tlwsac_free(\u0026pss-\u003equery_ac);\n-\n-\t\t\treturn 0;\n-\t\t}\n-\n-\t\tpss-\u003equery_already_done \u003d 0;\n-\n-\t\t/*\n-\t\t * we get zero or more sai_event_t laid out in pss-\u003equery_ac,\n-\t\t * and listed in pss-\u003equery_owner\n-\t\t */\n-\n-\t\tlwsl_debug(\u0022%s: WSS_PREPARE_OVERVIEW: %d results %p\u005cn\u0022,\n-\t\t\t __func__, pss-\u003equery_owner.count, pss-\u003equery_ac);\n-\n-\t\tp +\u003d lws_snprintf((char *)p, end - p,\n-\t\t\t\u0022{\u005c\u0022schema\u005c\u0022:\u005c\u0022sai.warmcat.com.overview\u005c\u0022,\u0022\n-\t\t\t\u0022 \u005c\u0022alang\u005c\u0022:\u005c\u0022%s\u005c\u0022,\u0022\n-\t\t\t\u0022 \u005c\u0022authorized\u005c\u0022: %d,\u0022\n-\t\t\t\u0022 \u005c\u0022auth_secs\u005c\u0022: %ld,\u0022\n-\t\t\t\u0022 \u005c\u0022auth_user\u005c\u0022: \u005c\u0022%s\u005c\u0022,\u0022\n-\t\t\t\u0022\u005c\u0022overview\u005c\u0022:[\u0022,\n-\t\t\tlws_json_purify(esc, pss-\u003ealang, sizeof(esc) - 1, \u0026iu),\n-\t\t\tpss-\u003eauthorized, pss-\u003eexpiry_unix_time - lws_now_secs(),\n-\t\t\tlws_json_purify(esc1, pss-\u003eauth_user, sizeof(esc1) - 1, \u0026iu)\n-\t\t\t);\n-\n-\t\t/*\n-\t\t * \u0022authorized\u0022 here is used to decide whether to render the\n-\t\t * additional controls clientside. The events the controls\n-\t\t * cause if used are separately checked for coming from an\n-\t\t * authorized pss when they are received.\n-\t\t */\n-\n-\t\tif (pss-\u003especificity)\n-\t\t\tpss-\u003ewalk \u003d lws_dll2_get_head(\u0026pss-\u003equery_owner);\n-\t\telse\n-\t\t\tpss-\u003ewalk \u003d lws_dll2_get_tail(\u0026pss-\u003equery_owner);\n-\t\tpss-\u003esubsequent \u003d 0;\n-\t\tfirst \u003d 1;\n-\n-\t\tpss-\u003esend_state \u003d WSS_SEND_OVERVIEW;\n-\n-\t\tif (!pss-\u003equery_owner.count)\n-\t\t\tgoto so_finish;\n-\n-\t\t/* fallthru */\n-\n-\tcase WSS_SEND_OVERVIEW:\n-\n-\t\tif (pss-\u003eovstate \u003d\u003d SOS_TASKS)\n-\t\t\tgoto enum_tasks;\n-\n-\t\tany \u003d 0;\n-\t\twhile (end - p \u003e 2048 \u0026\u0026 pss-\u003ewalk \u0026\u0026\n-\t\t pss-\u003esend_state \u003d\u003d WSS_SEND_OVERVIEW) {\n-\n-\t\t\te \u003d lws_container_of(pss-\u003ewalk, sai_event_t, list);\n-\n-\t\t\tif (pss-\u003especificity) {\n-\t\t\t\tlwsl_notice(\u0022%s: Specificity: e-\u003ehash: %s, e-\u003eref: '%s', pss-\u003especific: '%s'\u005cn\u0022,\n-\t\t\t\t\t\t__func__, e-\u003ehash, e-\u003eref, pss-\u003especific);\n-\n-\t\t\t\tif (!strcmp(pss-\u003especific, \u0022refs/heads/master\u0022) \u0026\u0026 !strcmp(e-\u003eref, \u0022refs/heads/main\u0022)) {\n-\t\t\t\t\tlwsl_notice(\u0022master-\u003emain\u005cn\u0022);\n-\t\t\t\t\tany \u003d 1;\n-\t\t\t\t} else {\n-\n-\t\t\t\t\tif (strcmp(e-\u003ehash, pss-\u003especific) \u0026\u0026\n-\t\t\t\t\t strcmp(e-\u003eref, pss-\u003especific)) {\n-\t\t\t\t\t\tpss-\u003ewalk \u003d pss-\u003ewalk-\u003enext;\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\t\t\t\t\t//lwsl_notice(\u0022%s: match\u005cn\u0022, __func__);\n-\t\t\t\t\tany \u003d 1;\n-\t\t\t\t}\n-\t\t\t}\n-\n-\t\t\tjs \u003d lws_struct_json_serialize_create(\n-\t\t\t\tlsm_schema_json_map_event,\n-\t\t\t\tLWS_ARRAY_SIZE(lsm_schema_json_map_event), 0, e);\n-\t\t\tif (!js) {\n-\t\t\t\tlwsl_err(\u0022%s: json ser fail\u005cn\u0022, __func__);\n-\t\t\t\treturn 1;\n-\t\t\t}\n-\t\t\tif (pss-\u003esubsequent)\n-\t\t\t\t*p++ \u003d ',';\n-\t\t\tpss-\u003esubsequent \u003d 1;\n-\n-\t\t\tp +\u003d lws_snprintf((char *)p, end - p, \u0022{\u005c\u0022e\u005c\u0022:\u0022);\n-\n-\t\t\tn \u003d lws_struct_json_serialize(js, p, end - p, \u0026w);\n-\t\t\tlws_struct_json_serialize_destroy(\u0026js);\n-\t\t\tswitch (n) {\n-\t\t\tcase LSJS_RESULT_ERROR:\n-\t\t\t\tpss-\u003esend_state \u003d WSS_IDLE;\n-\t\t\t\tlwsl_err(\u0022%s: json ser error\u005cn\u0022, __func__);\n-\t\t\t\treturn 1;\n-\n-\t\t\tcase LSJS_RESULT_FINISH:\n-\t\t\tcase LSJS_RESULT_CONTINUE:\n-\t\t\t\tp +\u003d w;\n-\t\t\t\tpss-\u003eovstate \u003d SOS_TASKS;\n-\t\t\t\tpss-\u003etask_index \u003d 0;\n-\t\t\t\tp +\u003d lws_snprintf((char *)p, end - p, \u0022, \u005c\u0022t\u005c\u0022:[\u0022);\n-\t\t\t\tgoto enum_tasks;\n-\t\t\t}\n-\t\t}\n-\t\tif (!any) {\n-\t\t\tpss-\u003esend_state \u003d WSS_IDLE;\n-\t\t\tlwsac_free(\u0026pss-\u003etask_ac);\n-\t\t\tlwsac_free(\u0026pss-\u003equery_ac);\n-\n-\t\t\treturn 0;\n-\t\t}\n-\t\tbreak;\n-\n-enum_tasks:\n-\t\t/*\n-\t\t * Enumerate the tasks associated with this event... we will\n-\t\t * come back here as often as needed to dump all the tasks\n-\t\t */\n-\n-\t\te \u003d lws_container_of(pss-\u003ewalk, sai_event_t, list);\n-\n-\t\tdo {\n-\n-\t\t\tif (saim_event_db_ensure_open(vhd, e-\u003euuid, 0, \u0026pdb)) {\n-\t\t\t\tlwsl_err(\u0022%s: unable to open event-specific database\u005cn\u0022,\n-\t\t\t\t\t\t__func__);\n-\n-\t\t\t\tbreak;\n-\t\t\t}\n-\n-\t\t\tlws_dll2_owner_clear(\u0026task_owner);\n-\t\t\tif (lws_struct_sq3_deserialize(pdb, NULL, NULL,\n-\t\t\t\t\tlsm_schema_sq3_map_task, \u0026task_owner,\n-\t\t\t\t\t\u0026pss-\u003etask_ac, pss-\u003etask_index, 1)) {\n-\t\t\t\tlwsl_err(\u0022%s: OVERVIEW 1 failed\u005cn\u0022, __func__);\n-\t\t\t\tlwsac_free(\u0026pss-\u003etask_ac);\n-\t\t\t\tsaim_event_db_close(vhd, \u0026pdb);\n-\n-\t\t\t\tbreak;\n-\t\t\t}\n-\t\t\tsaim_event_db_close(vhd, \u0026pdb);\n-\n-\t\t\tif (!task_owner.count)\n-\t\t\t\tbreak;\n-\n-\t\t\tif (pss-\u003etask_index)\n-\t\t\t\t*p++ \u003d ',';\n-\n-\t\t\t/*\n-\t\t\t * We don't want to send everyone the artifact nonces...\n-\t\t\t * the up nonce is a key for uploading artifacts on to\n-\t\t\t * this task, it should only be stored in the master db\n-\t\t\t * and sent to the builder to use.\n-\t\t\t *\n-\t\t\t * The down nonce is used in generated links, but still\n-\t\t\t * you should have to acquire such a link via whatever\n-\t\t\t * auth rather than be able to cook them up yourself\n-\t\t\t * from knowing the task uuid.\n-\t\t\t */\n-\n-\t\t\tt \u003d (sai_task_t *)task_owner.head;\n-\t\t\tt-\u003eart_up_nonce[0] \u003d '\u005c0';\n-\t\t\tt-\u003eart_down_nonce[0] \u003d '\u005c0';\n-\n-\t\t\t/* only one in it at a time */\n-\t\t\tt \u003d lws_container_of(task_owner.head, sai_task_t, list);\n-\n-\t\t\tjs \u003d lws_struct_json_serialize_create(\n-\t\t\t\tlsm_schema_json_map_task,\n-\t\t\t\tLWS_ARRAY_SIZE(lsm_schema_json_map_task), 0, t);\n-\n-\t\t\tn \u003d lws_struct_json_serialize(js, p, end - p, \u0026w);\n-\t\t\tlws_struct_json_serialize_destroy(\u0026js);\n-\t\t\tlwsac_free(\u0026pss-\u003etask_ac);\n-\t\t\tp +\u003d w;\n-\n-\t\t\tpss-\u003etask_index++;\n-\t\t} while ((end - p \u003e 2048) \u0026\u0026 task_owner.count);\n-\n-\t\tif (task_owner.count)\n-\t\t\t/* may be more left to do */\n-\t\t\tbreak;\n-\n-\t\t/* none left to do, go back up a level */\n-\n-\t\tp +\u003d lws_snprintf((char *)p, end - p, \u0022]}\u0022);\n-\n-\t\tpss-\u003eovstate \u003d SOS_EVENT;\n-\t\tif (pss-\u003especificity)\n-\t\t\tpss-\u003ewalk \u003d pss-\u003ewalk-\u003enext;\n-\t\telse\n-\t\t\tpss-\u003ewalk \u003d pss-\u003ewalk-\u003eprev;\n-\t\tif (!pss-\u003ewalk || pss-\u003especificity) {\n-\t\t\twhile (pss-\u003ewalk)\n-\t\t\t\tpss-\u003ewalk \u003d pss-\u003ewalk-\u003enext;\n-\t\t\tgoto so_finish;\n-\t\t}\n-\t\tbreak;\n-\n-so_finish:\n-\t\tp +\u003d lws_snprintf((char *)p, end - p, \u0022]}\u0022);\n-\t\tpss-\u003esend_state \u003d WSS_IDLE;\n-\t\tlwsac_free(\u0026pss-\u003etask_ac);\n-\t\tlwsac_free(\u0026pss-\u003equery_ac);\n-\t\tbreak;\n-\n-\tcase WSS_PREPARE_BUILDER_SUMMARY:\n-\t\tp +\u003d lws_snprintf((char *)p, end - p,\n-\t\t\t\u0022{\u005c\u0022schema\u005c\u0022:\u005c\u0022com.warmcat.sai.builders\u005c\u0022,\u0022\n-\t\t\t\u0022 \u005c\u0022alang\u005c\u0022:\u005c\u0022%s\u005c\u0022,\u0022\n-\t\t\t\u0022 \u005c\u0022authorized\u005c\u0022:%d,\u0022\n-\t\t\t\u0022 \u005c\u0022auth_secs\u005c\u0022:%ld,\u0022\n-\t\t\t\u0022 \u005c\u0022auth_user\u005c\u0022: \u005c\u0022%s\u005c\u0022,\u0022\n-\t\t\t\u0022 \u005c\u0022builders\u005c\u0022:[\u0022,\n-\t\t\tlws_sql_purify(esc, pss-\u003ealang, sizeof(esc) - 1),\n-\t\t\tpss-\u003eauthorized, pss-\u003eexpiry_unix_time - lws_now_secs(),\n-\t\t\tlws_json_purify(esc1, pss-\u003eauth_user, sizeof(esc1) - 1, \u0026iu));\n-\n-\t\tpss-\u003ewalk \u003d lws_dll2_get_head(\u0026vhd-\u003emaster.builder_owner);\n-\t\tpss-\u003esubsequent \u003d 0;\n-\t\tpss-\u003esend_state \u003d WSS_SEND_BUILDER_SUMMARY;\n-\t\tfirst \u003d 1;\n-\n-\t\t/* fallthru */\n-\n-\tcase WSS_SEND_BUILDER_SUMMARY:\n-\t\tif (!pss-\u003ewalk)\n-\t\t\tgoto b_finish;\n-\n-\t\t/*\n-\t\t * We're going to send the browser some JSON about all the\n-\t\t * builders / platforms we feel are connected to us\n-\t\t */\n-\n-\t\twhile (end - p \u003e 512 \u0026\u0026 pss-\u003ewalk \u0026\u0026\n-\t\t pss-\u003esend_state \u003d\u003d WSS_SEND_BUILDER_SUMMARY) {\n-\n-\t\t\tsai_plat_t *b \u003d lws_container_of(pss-\u003ewalk, sai_plat_t,\n-\t\t\t\t\t\t sai_plat_list);\n-\n-\t\t\tjs \u003d lws_struct_json_serialize_create(\n-\t\t\t\tlsm_schema_map_plat_simple,\n-\t\t\t\tLWS_ARRAY_SIZE(lsm_schema_map_plat_simple),\n-\t\t\t\t0, b);\n-\t\t\tif (!js)\n-\t\t\t\treturn 1;\n-\t\t\tif (pss-\u003esubsequent)\n-\t\t\t\t*p++ \u003d ',';\n-\t\t\tpss-\u003esubsequent \u003d 1;\n-\n-\t\t\tswitch (lws_struct_json_serialize(js, p, end - p, \u0026w)) {\n-\t\t\tcase LSJS_RESULT_ERROR:\n-\t\t\t\tlws_struct_json_serialize_destroy(\u0026js);\n-\t\t\t\tpss-\u003esend_state \u003d WSS_IDLE;\n-\t\t\t\treturn 1;\n-\t\t\tcase LSJS_RESULT_FINISH:\n-\t\t\t\tp +\u003d w;\n-\t\t\t\tlws_struct_json_serialize_destroy(\u0026js);\n-\t\t\t\tpss-\u003ewalk \u003d pss-\u003ewalk-\u003enext;\n-\t\t\t\tif (!pss-\u003ewalk)\n-\t\t\t\t\tgoto b_finish;\n-\t\t\t\tbreak;\n-\n-\t\t\tcase LSJS_RESULT_CONTINUE:\n-\t\t\t\tp +\u003d w;\n-\t\t\t\tlws_struct_json_serialize_destroy(\u0026js);\n-\t\t\t\tpss-\u003ewalk \u003d pss-\u003ewalk-\u003enext;\n-\t\t\t\tif (!pss-\u003ewalk)\n-\t\t\t\t\tgoto b_finish;\n-\t\t\t\tbreak;\n-\t\t\t}\n-\t\t}\n-\t\tbreak;\n-b_finish:\n-\t\tp +\u003d lws_snprintf((char *)p, end - p, \u0022]}\u0022);\n-\t\tpss-\u003esend_state \u003d WSS_IDLE;\n-//\t\tlwsac_free(\u0026pss-\u003equery_ac);\n-\t\tlwsac_free(\u0026pss-\u003etask_ac);\n-\t\tbreak;\n-\n-\n-\tcase WSS_PREPARE_TASKINFO:\n-\t\t/*\n-\t\t * We're sending a browser the specific task info that he\n-\t\t * asked for.\n-\t\t *\n-\t\t * We already got the task struct out of the db in .one_task\n-\t\t * (all in .query_ac)\n-\t\t */\n-\n-\t\ttask_reply.event \u003d pss-\u003eone_event;\n-\t\ttask_reply.task \u003d pss-\u003eone_task;\n-\t\ttask_reply.auth_secs \u003d pss-\u003eexpiry_unix_time - lws_now_secs();\n-\t\ttask_reply.authorized \u003d pss-\u003eauthorized;\n-\t\tlws_strncpy(task_reply.auth_user, pss-\u003eauth_user,\n-\t\t\t sizeof(task_reply.auth_user));\n-\n-\t\tjs \u003d lws_struct_json_serialize_create(lsm_schema_json_map_taskreply,\n-\t\t\t\tLWS_ARRAY_SIZE(lsm_schema_json_map_taskreply),\n-\t\t\t\t0, \u0026task_reply);\n-\t\tif (!js)\n-\t\t\treturn 1;\n-\n-\t\tn \u003d lws_struct_json_serialize(js, p, end - p, \u0026w);\n-\t\tlws_struct_json_serialize_destroy(\u0026js);\n-\n-\t\t/*\n-\t\t * Let's also try to fetch any artifacts into pss-\u003eaft_owner...\n-\t\t * no db or no artifacts can also be a normal situation...\n-\t\t */\n-\n-\t\tif (pss-\u003eone_task) {\n-\n-\t\t\tsai_task_uuid_to_event_uuid(event_uuid, pss-\u003eone_task-\u003euuid);\n-\n-\t\t\tlwsl_notice(\u0022%s: ---------------- event uuid '%s'\u005cn\u0022, __func__,\n-\t\t\t\t event_uuid);\n-\n-\t\t\tlws_dll2_owner_clear(\u0026pss-\u003eaft_owner);\n-\t\t\tif (!saim_event_db_ensure_open(vhd, event_uuid, 0, \u0026pdb)) {\n-\n-\t\t\t\tlws_snprintf(filt, sizeof(filt), \u0022 and (task_uuid \u003d\u003d '%s')\u0022,\n-\t\t\t\t\t pss-\u003eone_task-\u003euuid);\n-\n-\t\t\t\tlwsl_notice(\u0022%s: ---------------- %s\u005cn\u0022, __func__, filt);\n-\n-\t\t\t\tif (lws_struct_sq3_deserialize(pdb, filt, NULL,\n-\t\t\t\t\t\t\tlsm_schema_sq3_map_artifact,\n-\t\t\t\t\t\t\t\u0026pss-\u003eaft_owner,\n-\t\t\t\t\t\t\t\u0026pss-\u003etask_ac, 0, 10)) {\n-\t\t\t\t\tlwsl_err(\u0022%s: get afcts failed\u005cn\u0022, __func__);\n-\t\t\t\t}\n-\t\t\t\tsaim_event_db_close(vhd, \u0026pdb);\n-\t\t\t}\n-\t\t}\n-\n-\t\tfirst \u003d 1;\n-\t\tpss-\u003ewalk \u003d NULL;\n-\t\tpss-\u003esend_state \u003d WSS_SEND_ARTIFACT_INFO;\n-\t\tif (!pss-\u003eaft_owner.head) {\n-\t\t\tlwsl_notice(\u0022%s: ---------------- no artifacts\u005cn\u0022, __func__);\n-\t\t\t/* there's no artifact stuff to do */\n-\t\t\tpss-\u003esend_state \u003d WSS_IDLE;\n-\t\t\tlwsac_free(\u0026pss-\u003equery_ac);\n-\t\t}\n-\t\tpss-\u003eone_task \u003d NULL;\n-\t\tif (n \u003d\u003d LSJS_RESULT_ERROR) {\n-\t\t\tlwsl_notice(\u0022%s: taskinfo: error generating json\u005cn\u0022, __func__);\n-\t\t\treturn 1;\n-\t\t}\n-\t\tp +\u003d w;\n-\t\tif (!lws_ptr_diff(p, start)) {\n-\t\t\tlwsl_notice(\u0022%s: taskinfo: empty json\u005cn\u0022, __func__);\n-\t\t\treturn 0;\n-\t\t}\n-\t\tbreak;\n-\n-\tcase WSS_SEND_ARTIFACT_INFO:\n-\t\tif (pss-\u003eaft_owner.head) {\n-\t\t\tsai_artifact_t *aft \u003d (sai_artifact_t *)pss-\u003eaft_owner.head;\n-\n-\t\t\tlws_dll2_remove(\u0026aft-\u003elist);\n-\n-\t\t\t/* we don't want to disclose this to browsers */\n-\t\t\taft-\u003eartifact_up_nonce[0] \u003d '\u005c0';\n-\n-\t\t\tjs \u003d lws_struct_json_serialize_create(lsm_schema_json_map_artifact,\n-\t\t\t\t\tLWS_ARRAY_SIZE(lsm_schema_json_map_artifact),\n-\t\t\t\t\t0, aft);\n-\t\t\tif (!js) {\n-\t\t\t\tlwsl_err(\u0022%s ----------------- failed to render artifact json\u005cn\u0022, __func__);\n-\t\t\t\treturn 1;\n-\t\t\t}\n-\n-\t\t\tn \u003d lws_struct_json_serialize(js, p, end - p, \u0026w);\n-\t\t\tlws_struct_json_serialize_destroy(\u0026js);\n-\t\t\tif (n \u003d\u003d LSJS_RESULT_ERROR) {\n-\t\t\t\tlwsl_notice(\u0022%s: taskinfo: ---------- error generating json\u005cn\u0022, __func__);\n-\t\t\t\treturn 1;\n-\t\t\t}\n-\t\t\tfirst \u003d 1;\n-\t\t\tp +\u003d w;\n-\t\t\tlwsl_warn(\u0022%s: --------------------- %.*s\u005cn\u0022, __func__, (int)w, start);\n-\t\t}\n-\n-\t\tif (!pss-\u003eaft_owner.head) {\n-\t\t\tpss-\u003esend_state \u003d WSS_IDLE;\n-\t\t\tlwsac_free(\u0026pss-\u003equery_ac);\n-\t\t}\n-\t\tbreak;\n-\n-\tdefault:\n-\t\tlwsl_err(\u0022%s: pss state %d\u005cn\u0022, __func__, pss-\u003esend_state);\n-\t\treturn 0;\n-\t}\n-\n-send_it:\n-\tflags \u003d lws_write_ws_flags(LWS_WRITE_TEXT, first, !pss-\u003ewalk);\n-\n-\tif (lws_write(pss-\u003ewsi, start, p - start, flags) \u003c 0)\n-\t\treturn -1;\n-\n-\t/*\n-\t * We get a bad ratio of reads to write when the builder spams us\n-\t * with rx... we have to try to clear as much as we can in one go.\n-\t */\n-\n-\tif (!lws_send_pipe_choked(pss-\u003ewsi))\n-\t\tgoto again;\n-\n-\tlws_callback_on_writable(pss-\u003ewsi);\n-\n-\treturn 0;\n-}\ndiff --git a/src/master/m-ws-builder.c b/src/master/m-ws-builder.c\ndeleted file mode 100644\nindex 0b717d0..0000000\n--- a/src/master/m-ws-builder.c\n+++ /dev/null\n@@ -1,728 +0,0 @@\n-/*\n- * Sai master - ./src/master/ws-json-rx.c\n- *\n- * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n- *\n- * This library is free software; you can redistribute it and/or\n- * modify it under the terms of the GNU Lesser General Public\n- * License as published by the Free Software Foundation:\n- * version 2.1 of the License.\n- *\n- * This library is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n- * Lesser General Public License for more details.\n- *\n- * You should have received a copy of the GNU Lesser General Public\n- * License along with this library; if not, write to the Free Software\n- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n- * MA 02110-1301 USA\n- *\n- * These are ws rx and tx handlers related to builder ws connections, on\n- * /builder\n- */\n-\n-#include \u003clibwebsockets.h\u003e\n-#include \u003cstring.h\u003e\n-#include \u003csignal.h\u003e\n-#include \u003ctime.h\u003e\n-\n-#include \u0022m-private.h\u0022\n-\n-enum sai_overview_state {\n-\tSOS_EVENT,\n-\tSOS_TASKS,\n-};\n-\n-typedef struct saim_logcache_pertask {\n-\tlws_dll2_t\t\tlist; /* vhd-\u003etasklog_cache is the owner */\n-\tchar\t\t\tuuid[65];\n-\tlws_dll2_owner_t\tcache; /* sai_log_t */\n-} saim_logcache_pertask_t;\n-\n-/*\n- * The Schema that may be sent to us by a builder\n- *\n- * Artifacts are sent on secondary SS connections so they don't block ongoing\n- * log delivery etc. The JSON is immediately followed by binary data to the\n- * length told in the JSON.\n- */\n-\n-static const lws_struct_map_t lsm_schema_map_ba[] \u003d {\n-\tLSM_SCHEMA_DLL2\t(sai_plat_owner_t, plat_owner, NULL, lsm_plat_list,\n-\t\t\t\t\t\t\u0022com-warmcat-sai-ba\u0022),\n-\tLSM_SCHEMA (sai_log_t,\t NULL, lsm_log,\n-\t\t\t\t\t\t\u0022com-warmcat-sai-logs\u0022),\n-\tLSM_SCHEMA (sai_event_t,\t NULL, lsm_task_rej,\n-\t\t\t\t\t\t\u0022com.warmcat.sai.taskrej\u0022),\n-\tLSM_SCHEMA (sai_artifact_t, NULL, lsm_artifact,\n-\t\t\t\t\t\t\u0022com-warmcat-sai-artifact\u0022),\n-};\n-\n-enum {\n-\tSAIM_WSSCH_BUILDER_PLATS,\n-\tSAIM_WSSCH_BUILDER_LOGS,\n-\tSAIM_WSSCH_BUILDER_TASKREJ,\n-\tSAIM_WSSCH_BUILDER_ARTIFACT\n-};\n-\n-static void\n-saim_dump_logs_to_db(lws_sorted_usec_list_t *sul)\n-{\n-\tstruct vhd *vhd \u003d lws_container_of(sul, struct vhd, sul_logcache);\n-\tsaim_logcache_pertask_t *lcpt;\n-\tchar event_uuid[33];\n-\tsqlite3 *pdb \u003d NULL;\n-\tsai_log_t *hlog;\n-\tchar *err;\n-\n-\t/*\n-\t * for each task that acquired logs in the interval\n-\t */\n-\n-\tlws_start_foreach_dll_safe(struct lws_dll2 *, p, p1,\n-\t\t\t\t vhd-\u003etasklog_cache.head) {\n-\t\tlcpt \u003d lws_container_of(p, saim_logcache_pertask_t, list);\n-\n-\t\tsai_task_uuid_to_event_uuid(event_uuid, lcpt-\u003euuid);\n-\n-\t\tif (!saim_event_db_ensure_open(vhd, event_uuid, 0, \u0026pdb)) {\n-\n-\t\t\t/*\n-\t\t\t * Empty the task-specific log cache into the event-\n-\t\t\t * specific db for the task in one go, this is much\n-\t\t\t * more efficient\n-\t\t\t */\n-\n-\t\t\tsqlite3_exec(pdb, \u0022BEGIN TRANSACTION\u0022, NULL, NULL, \u0026err);\n-\n-\t\t\tlws_struct_sq3_serialize(pdb, lsm_schema_sq3_map_log,\n-\t\t\t\t\t \u0026lcpt-\u003ecache, 0);\n-\n-\t\t\tsqlite3_exec(pdb, \u0022END TRANSACTION\u0022, NULL, NULL, \u0026err);\n-\t\t\tsaim_event_db_close(vhd, \u0026pdb);\n-\n-\t\t} else\n-\t\t\tlwsl_err(\u0022%s: unable to open event-specific database\u005cn\u0022,\n-\t\t\t\t\t__func__);\n-\n-\t\t/*\n-\t\t * Destroy the logs in the task cache and the task cache\n-\t\t */\n-\n-\t\tlws_start_foreach_dll_safe(struct lws_dll2 *, pq, pq1,\n-\t\t\t\t\t lcpt-\u003ecache.head) {\n-\t\t\thlog \u003d lws_container_of(pq, sai_log_t, list);\n-\t\t\tlws_dll2_remove(\u0026hlog-\u003elist);\n-\t\t\tfree(hlog);\n-\t\t} lws_end_foreach_dll_safe(pq, pq1);\n-\n-\t\t/*\n-\t\t * Inform anybody who's looking at this task's logs that\n-\t\t * something changed\n-\t\t */\n-\t\tsaim_subs_request_writeable(vhd, lcpt-\u003euuid);\n-\n-\t\t/*\n-\t\t * Destroy the whole task-specific cache, it will regenerate\n-\t\t * if more logs come for it\n-\t\t */\n-\n-\t\tlws_dll2_remove(\u0026lcpt-\u003elist);\n-\t\tfree(lcpt);\n-\n-\t} lws_end_foreach_dll_safe(p, p1);\n-\n-}\n-\n-/*\n- * We're going to stash these logs on a per-task list, and deal with them\n- * inside a single trasaction per task efficiently on a timer.\n- */\n-\n-static void\n-saim_log_to_db(struct vhd *vhd, sai_log_t *log)\n-{\n-\tsaim_logcache_pertask_t *lcpt \u003d NULL;\n-\tsai_log_t *hlog;\n-\n-\t/*\n-\t * find the pertask if one exists\n-\t */\n-\n-\tlws_start_foreach_dll(struct lws_dll2 *, p, vhd-\u003etasklog_cache.head) {\n-\t\tlcpt \u003d lws_container_of(p, saim_logcache_pertask_t, list);\n-\n-\t\tif (!strcmp(lcpt-\u003euuid, log-\u003etask_uuid))\n-\t\t\tbreak;\n-\t\tlcpt \u003d NULL;\n-\n-\t} lws_end_foreach_dll(p);\n-\n-\tif (!lcpt) {\n-\t\t/*\n-\t\t * Create a pertask and add it to the vhd list of them\n-\t\t */\n-\t\tlcpt \u003d malloc(sizeof(*lcpt));\n-\t\tmemset(lcpt, 0, sizeof(*lcpt));\n-\t\tlws_strncpy(lcpt-\u003euuid, log-\u003etask_uuid, sizeof(lcpt-\u003euuid));\n-\t\tlws_dll2_add_tail(\u0026lcpt-\u003elist, \u0026vhd-\u003etasklog_cache);\n-\t}\n-\n-\thlog \u003d malloc(sizeof(*hlog) + log-\u003elen + strlen(log-\u003elog) + 1);\n-\tif (!hlog)\n-\t\treturn;\n-\n-\t*hlog \u003d *log;\n-\tmemset(\u0026hlog-\u003elist, 0, sizeof(hlog-\u003elist));\n-\tmemcpy(\u0026hlog[1], log-\u003elog, strlen(log-\u003elog) + 1);\n-\thlog-\u003elog \u003d (char *)\u0026hlog[1];\n-\n-\t/*\n-\t * add our log copy to the task-specific cache\n-\t */\n-\n-\tlws_dll2_add_tail(\u0026hlog-\u003elist, \u0026lcpt-\u003ecache);\n-\n-\tif (!vhd-\u003esul_logcache.list.owner)\n-\t\t/* if not already scheduled, schedule it for 250ms */\n-\n-\t\tlws_sul_schedule(vhd-\u003econtext, 0, \u0026vhd-\u003esul_logcache,\n-\t\t\t\t saim_dump_logs_to_db, 250 * LWS_US_PER_MS);\n-}\n-\n-static sai_plat_t *\n-saim_builder_from_uuid(struct vhd *vhd, const char *hostname)\n-{\n-\tlws_start_foreach_dll(struct lws_dll2 *, p,\n-\t\t\t vhd-\u003emaster.builder_owner.head) {\n-\t\tsai_plat_t *cb \u003d lws_container_of(p, sai_plat_t,\n-\t\t\t\tsai_plat_list);\n-\n-\t\tif (!strcmp(hostname, cb-\u003ename))\n-\t\t\treturn cb;\n-\n-\t} lws_end_foreach_dll(p);\n-\n-\treturn NULL;\n-}\n-\n-int\n-sai_sql3_get_uint64_cb(void *user, int cols, char **values, char **name)\n-{\n-\tuint64_t *pui \u003d (uint64_t *)user;\n-\n-\t*pui \u003d (uint64_t)atoll(values[0]);\n-\n-\treturn 0;\n-}\n-\n-/*\n- * Master received a communication from a builder\n- */\n-\n-int\n-saim_ws_json_rx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t bl)\n-{\n-\tchar event_uuid[33], s[128], esc[96];\n-\tstruct lwsac *ac \u003d NULL;\n-\tsai_plat_t *build, *cb;\n-\tsai_rejection_t *rej;\n-\tlws_dll2_owner_t o;\n-\tsai_artifact_t *ap;\n-\tsai_task_t *task;\n-\tsai_log_t *log;\n-\tuint64_t rid;\n-\tint n, m;\n-\n-\t/*\n-\t * use the schema name on the incoming JSON to decide what kind of\n-\t * structure to instantiate\n-\t *\n-\t * We may have:\n-\t *\n-\t * - just received a fragment of the whole JSON\n-\t *\n-\t * - received the JSON and be handling appeneded blob data\n-\t */\n-\n-\tif (pss-\u003ebulk_binary_data)\n-\t\tgoto handle;\n-\n-\tif (!pss-\u003efrag) {\n-\t\tmemset(\u0026pss-\u003ea, 0, sizeof(pss-\u003ea));\n-\t\tpss-\u003ea.map_st[0] \u003d lsm_schema_map_ba;\n-\t\tpss-\u003ea.map_entries_st[0] \u003d LWS_ARRAY_SIZE(lsm_schema_map_ba);\n-\t\tpss-\u003ea.ac_block_size \u003d 4096;\n-\n-\t\tlws_struct_json_init_parse(\u0026pss-\u003ectx, NULL, \u0026pss-\u003ea);\n-\t} else\n-\t\tpss-\u003efrag \u003d 0;\n-\n-\tm \u003d lejp_parse(\u0026pss-\u003ectx, (uint8_t *)buf, bl);\n-\n-\t/*\n-\t * returns negative, or unused amount... for us, we either had a\n-\t * (negative) error, had LEJP_CONTINUE, or if 0/positive, finished\n-\t */\n-\tif (m \u003c 0 \u0026\u0026 m !\u003d LEJP_CONTINUE) {\n-\t\t/* an explicit error */\n-\t\tlwsl_hexdump_err(buf, bl);\n-\t\tlwsl_err(\u0022%s: rx JSON decode failed '%s', %d, %s, %s, %d\u005cn\u0022,\n-\t\t\t __func__, lejp_error_to_string(m), m,\n-\t\t\t pss-\u003ectx.path, pss-\u003ectx.buf, pss-\u003ectx.npos);\n-\t\treturn 1;\n-\t}\n-\n-\tif (m \u003d\u003d LEJP_CONTINUE) {\n-\t\tpss-\u003efrag \u003d 1;\n-\t\treturn 0;\n-\t}\n-\n-\tif (!pss-\u003ea.dest) {\n-\t\tlwsl_err(\u0022%s: json decode didn't make an object\u005cn\u0022, __func__);\n-\t\treturn 1;\n-\t}\n-\n-handle:\n-\tswitch (pss-\u003ea.top_schema_index) {\n-\tcase SAIM_WSSCH_BUILDER_PLATS:\n-\n-\t\tlwsl_hexdump_notice(buf, bl);\n-\n-\t\t/*\n-\t\t * builder is sending us an array of platforms it provides us\n-\t\t */\n-\n-\t\tpss-\u003eu.o \u003d (sai_plat_owner_t *)pss-\u003ea.dest;\n-\n-\t\tlwsl_notice(\u0022%s: seen platform list: count %d\u005cn\u0022, __func__,\n-\t\t\t\tpss-\u003eu.o-\u003eplat_owner.count);\n-\n-\t\tlws_start_foreach_dll(struct lws_dll2 *, pb,\n-\t\t\t\t pss-\u003eu.o-\u003eplat_owner.head) {\n-\t\t\tbuild \u003d lws_container_of(pb, sai_plat_t, sai_plat_list);\n-\n-\t\t\tlwsl_notice(\u0022%s: seeing plat %s\u005cn\u0022, __func__, build-\u003ename);\n-\n-\t\t\t/*\n-\t\t\t * ... so is this one a new guy?\n-\t\t\t */\n-\n-\t\t\tcb \u003d saim_builder_from_uuid(vhd, build-\u003ename);\n-\t\t\tif (!cb) {\n-\t\t\t\tchar *cp;\n-\n-\t\t\t\t/*\n-\t\t\t\t * We need to make a persistent, deep, copy of\n-\t\t\t\t * the (from JSON) builder object representing\n-\t\t\t\t * this client.\n-\t\t\t\t *\n-\t\t\t\t * \u0022platform\u0022 is eg \u0022linux-ubuntu-bionic-arm64\u0022\n-\t\t\t\t * and \u0022name\u0022 is \u0022hostname.\u003cplatform\u003e\u0022.\n-\t\t\t\t */\n-\n-\t\t\t\tif (!build-\u003ename || !build-\u003eplatform) {\n-\t\t\t\t\tlwsl_err(\u0022%s: missing build '%s'/hostname '%s'\u005cn\u0022,\n-\t\t\t\t\t\t__func__,\n-\t\t\t\t\t\tbuild-\u003ename ? build-\u003ename : \u0022null\u0022,\n-\t\t\t\t\t\tbuild-\u003eplatform ? build-\u003eplatform : \u0022null\u0022);\n-\t\t\t\t\treturn -1;\n-\t\t\t\t}\n-\n-\t\t\t\tcb \u003d malloc(sizeof(*cb) +\n-\t\t\t\t\t strlen(build-\u003ename) + 1 +\n-\t\t\t\t\t strlen(build-\u003eplatform) + 1);\n-\n-\t\t\t\tmemset(cb, 0, sizeof(*cb));\n-\t\t\t\tcp \u003d (char *)\u0026cb[1];\n-\n-\t\t\t\tmemcpy(cp, build-\u003ename, strlen(build-\u003ename) + 1);\n-\t\t\t\tcb-\u003ename \u003d cp;\n-\t\t\t\tcp +\u003d strlen(build-\u003ename) + 1;\n-\n-\t\t\t\tmemcpy(cp, build-\u003eplatform, strlen(build-\u003eplatform) + 1);\n-\t\t\t\tcb-\u003eplatform \u003d cp;\n-\t\t\t\tcp +\u003d strlen(build-\u003eplatform) + 1;\n-\n-\t\t\t\tcb-\u003eongoing \u003d build-\u003eongoing;\n-\t\t\t\tcb-\u003einstances \u003d build-\u003einstances;\n-\n-\t\t\t\tcb-\u003ewsi \u003d pss-\u003ewsi;\n-\n-\t\t\t\t/* Then attach the copy to the master in the vhd\n-\t\t\t\t */\n-\t\t\t\tlws_dll2_add_tail(\u0026cb-\u003esai_plat_list,\n-\t\t\t\t\t\t \u0026vhd-\u003emaster.builder_owner);\n-\t\t\t}\n-\n-\t\t\t/*\n-\t\t\t * Even if he's not new, we should use his updated info about\n-\t\t\t * builder load\n-\t\t\t */\n-\t\t\tcb-\u003eongoing \u003d build-\u003eongoing;\n-\t\t\tcb-\u003einstances \u003d build-\u003einstances;\n-\n-\t\t\tlwsl_notice(\u0022%s: builder %s reports load %d/%d\u005cn\u0022,\n-\t\t\t\t __func__, cb-\u003ename, cb-\u003eongoing,\n-\t\t\t\t cb-\u003einstances);\n-\n-\t\t} lws_end_foreach_dll(pb);\n-\n-\t\tlwsac_free(\u0026pss-\u003ea.ac);\n-\n-\t\t/*\n-\t\t * look if we should offer the builder a task, given the\n-\t\t * platforms he's offering\n-\t\t */\n-\n-\t\tif (saim_allocate_task(vhd, pss, cb, cb-\u003eplatform) \u003c 0)\n-\t\t\tgoto bail;\n-\n-\t\tbreak;\n-\n-bail:\n-\t\tlwsac_free(\u0026pss-\u003ea.ac);\n-\t\treturn -1;\n-\n-\tcase SAIM_WSSCH_BUILDER_LOGS:\n-\t\t/*\n-\t\t * builder is sending us info about task logs\n-\t\t */\n-\n-\t\tlog \u003d (sai_log_t *)pss-\u003ea.dest;\n-\t\tsaim_log_to_db(vhd, log);\n-\n-\t\tif (pss-\u003emark_started) {\n-\t\t\tpss-\u003emark_started \u003d 0;\n-\t\t\tpss-\u003efirst_log_timestamp \u003d log-\u003etimestamp;\n-\t\t\tif (saim_set_task_state(vhd, NULL, NULL, log-\u003etask_uuid,\n-\t\t\t\t\t\tSAIES_BEING_BUILT, 0, 0))\n-\t\t\t\tgoto bail;\n-\t\t}\n-\n-\t\tif (log-\u003efinished) {\n-\t\t\t/*\n-\t\t\t * We have reached the end of the logs for this task\n-\t\t\t */\n-\t\t\tlwsl_info(\u0022%s: log-\u003efinished says 0x%x, dur %lluus\u005cn\u0022,\n-\t\t\t\t __func__, log-\u003efinished, (unsigned long long)(\n-\t\t\t\t log-\u003etimestamp - pss-\u003efirst_log_timestamp));\n-\t\t\tif (log-\u003efinished \u0026 SAISPRF_EXIT) {\n-\t\t\t\tif ((log-\u003efinished \u0026 0xff) \u003d\u003d 0)\n-\t\t\t\t\tn \u003d SAIES_SUCCESS;\n-\t\t\t\telse\n-\t\t\t\t\tn \u003d SAIES_FAIL;\n-\t\t\t} else\n-\t\t\t\tif (log-\u003efinished \u0026 8192)\n-\t\t\t\t\tn \u003d SAIES_CANCELLED;\n-\t\t\t\telse\n-\t\t\t\t\tn \u003d SAIES_FAIL;\n-\n-\t\t\tif (saim_set_task_state(vhd, NULL, NULL, log-\u003etask_uuid,\n-\t\t\t\t\t\tn, 0, log-\u003etimestamp -\n-\t\t\t\t\t\t pss-\u003efirst_log_timestamp))\n-\t\t\t\tgoto bail;\n-\t\t}\n-\n-\t\tlwsac_free(\u0026pss-\u003ea.ac);\n-\n-\t\tbreak;\n-\n-\tcase SAIM_WSSCH_BUILDER_TASKREJ:\n-\n-\t\t/*\n-\t\t * builder is updating us about his status, and may be\n-\t\t * rejecting a task we tried to give him\n-\t\t */\n-\n-\t\trej \u003d (sai_rejection_t *)pss-\u003ea.dest;\n-\n-\t\tcb \u003d saim_builder_from_uuid(vhd, rej-\u003ehost_platform);\n-\t\tif (!cb) {\n-\t\t\tlwsl_info(\u0022%s: unknown builder %s rejecting\u005cn\u0022,\n-\t\t\t\t __func__, rej-\u003ehost_platform);\n-\t\t\tbreak;\n-\t\t}\n-\n-\t\t/* update our info about builder state with reality */\n-\n-\t\tcb-\u003eongoing \u003d rej-\u003eongoing;\n-\t\tcb-\u003einstances \u003d rej-\u003elimit;\n-\n-\t\tlwsl_notice(\u0022%s: builder %s reports load %d/%d (rej %s)\u005cn\u0022,\n-\t\t\t __func__, cb-\u003ename, cb-\u003eongoing, cb-\u003einstances,\n-\t\t\t rej-\u003etask_uuid[0] ? rej-\u003etask_uuid : \u0022none\u0022);\n-\n-\t\tif (rej-\u003etask_uuid[0])\n-\t\t\tsaim_task_reset(vhd, rej-\u003etask_uuid);\n-\n-\t\tbreak;\n-\n-\tcase SAIM_WSSCH_BUILDER_ARTIFACT:\n-\t\t/*\n-\t\t * We get sent a JSON object immediately followed by binary\n-\t\t * data for the artifact.\n-\t\t *\n-\t\t * We place the binary data as a blob in the sql record in the\n-\t\t * artifact table.\n-\t\t */\n-\n-\t\tlwsl_debug(\u0022%s: SAIM_WSSCH_BUILDER_ARTIFACT\u005cn\u0022, __func__);\n-\n-\t\tif (!pss-\u003ebulk_binary_data) {\n-\n-\t\t\tap \u003d (sai_artifact_t *)pss-\u003ea.dest;\n-\n-\t\t\tsai_task_uuid_to_event_uuid(event_uuid, ap-\u003etask_uuid);\n-\n-\t\t\t/*\n-\t\t\t * Open the event-specific database object... the\n-\t\t\t * handle is closed when the stream closes for whatever\n-\t\t\t * reason.\n-\t\t\t */\n-\n-\t\t\tif (saim_event_db_ensure_open(pss-\u003evhd, event_uuid, 0,\n-\t\t\t\t\t\t \u0026pss-\u003epdb_artifact)) {\n-\t\t\t\tlwsl_err(\u0022%s: unable to open event-specific \u0022\n-\t\t\t\t\t \u0022database\u005cn\u0022, __func__);\n-\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\n-\t\t\t/*\n-\t\t\t * Retreive the task object\n-\t\t\t */\n-\n-\t\t\tlws_sql_purify(esc, ap-\u003etask_uuid, sizeof(esc));\n-\t\t\tlws_snprintf(s, sizeof(s),\u0022 and uuid \u003d\u003d \u005c\u0022%s\u005c\u0022\u0022, esc);\n-\t\t\tn \u003d lws_struct_sq3_deserialize(pss-\u003epdb_artifact, s,\n-\t\t\t\t\t\t NULL, lsm_schema_sq3_map_task,\n-\t\t\t\t\t\t \u0026o, \u0026ac, 0, 1);\n-\t\t\tif (n \u003c 0 || !o.head) {\n-\t\t\t\tsaim_event_db_close(vhd, \u0026pss-\u003epdb_artifact);\n-\t\t\t\tlwsl_notice(\u0022%s: no task of that id\u005cn\u0022, __func__);\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\n-\t\t\ttask \u003d (sai_task_t *)o.head;\n-\t\t\tn \u003d strcmp(task-\u003eart_up_nonce, ap-\u003eartifact_up_nonce);\n-\n-\t\t\tif (n) {\n-\t\t\t\tlwsl_err(\u0022%s: artifact nonce mismatch\u005cn\u0022,\n-\t\t\t\t\t __func__);\n-\t\t\t\tgoto afail;\n-\t\t\t}\n-\n-\t\t\t/*\n-\t\t\t * The task the sender is sending us an artifact for\n-\t\t\t * exists. The sender knows the random upload nonce\n-\t\t\t * for that task's artifacts.\n-\t\t\t *\n-\t\t\t * Create a random download nonce unrelated to the\n-\t\t\t * random upload nonce (so knowing the download one\n-\t\t\t * won't let you upload anything).\n-\t\t\t *\n-\t\t\t * Create the artifact's entry in the event-specific\n-\t\t\t * database\n-\t\t\t */\n-\n-\t\t\tsai_uuid16_create(pss-\u003evhd-\u003econtext,\n-\t\t\t\t\t ap-\u003eartifact_down_nonce);\n-\n-\t\t\tlws_dll2_owner_clear(\u0026o);\n-\t\t\tlws_dll2_add_head(\u0026ap-\u003elist, \u0026o);\n-\n-\t\t\t/*\n-\t\t\t * Create the task in event-specific database\n-\t\t\t */\n-\n-\t\t\tif (lws_struct_sq3_serialize(pss-\u003epdb_artifact,\n-\t\t\t\t\t\t lsm_schema_sq3_map_artifact,\n-\t\t\t\t\t\t \u0026o, ap-\u003euid)) {\n-\t\t\t\tlwsl_err(\u0022%s: failed artifact struct insert\u005cn\u0022,\n-\t\t\t\t\t\t__func__);\n-\n-\t\t\t\tgoto afail;\n-\t\t\t}\n-\n-\t\t\t/*\n-\t\t\t * recover the rowid\n-\t\t\t */\n-\n-\t\t\tlws_snprintf(s, sizeof(s),\n-\t\t\t\t \u0022select rowid from artifacts \u0022\n-\t\t\t\t\t\u0022where timestamp\u003d%llu\u0022,\n-\t\t\t\t (unsigned long long)ap-\u003etimestamp);\n-\n-\t\t\tif (sqlite3_exec((sqlite3 *)pss-\u003epdb_artifact, s,\n-\t\t\t\t\tsai_sql3_get_uint64_cb, \u0026rid, NULL) !\u003d\n-\t\t\t\t\t\t\t\t SQLITE_OK) {\n-\t\t\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, s,\n-\t\t\t\t\t sqlite3_errmsg(pss-\u003epdb_artifact));\n-\t\t\t\tgoto afail;\n-\t\t\t}\n-\n-\t\t\t/*\n-\t\t\t * Set the blob size on associated row\n-\t\t\t */\n-\n-\t\t\tlws_snprintf(s, sizeof(s),\n-\t\t\t\t \u0022update artifacts set blob\u003dzeroblob(%llu) \u0022\n-\t\t\t\t\t\u0022where rowid\u003d%llu\u0022,\n-\t\t\t\t (unsigned long long)ap-\u003elen,\n-\t\t\t\t (unsigned long long)rid);\n-\n-\t\t\tif (sqlite3_exec((sqlite3 *)pss-\u003epdb_artifact, s,\n-\t\t\t\t\t NULL, NULL, NULL) !\u003d\n-\t\t\t\t\t\t\t\t SQLITE_OK) {\n-\t\t\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, s,\n-\t\t\t\t\t sqlite3_errmsg(pss-\u003epdb_artifact));\n-\t\t\t\tgoto afail;\n-\t\t\t}\n-\n-\t\t\t/*\n-\t\t\t * Open a blob on the associated row... the blob handle\n-\t\t\t * is closed when this stream closes for whatever\n-\t\t\t * reason.\n-\t\t\t */\n-\n-\t\t\tif (sqlite3_blob_open(pss-\u003epdb_artifact, \u0022main\u0022,\n-\t\t\t\t\t \u0022artifacts\u0022, \u0022blob\u0022, rid, 1,\n-\t\t\t\t\t \u0026pss-\u003eblob_artifact) !\u003d SQLITE_OK) {\n-\t\t\t\tlwsl_err(\u0022%s: unable to open blob\u005cn\u0022, __func__);\n-\t\t\t\tgoto afail;\n-\t\t\t}\n-\n-\t\t\t/*\n-\t\t\t * First time around, m \u003d\u003d number of bytes let in buf\n-\t\t\t * after JSON, (bl - m) offset\n-\t\t\t */\n-\t\t\tpss-\u003ebulk_binary_data \u003d 1;\n-\t\t\tpss-\u003eartifact_length \u003d ap-\u003elen;\n-\t\t} else\n-\t\t\tm \u003d bl;\n-\n-\t\tif (m) {\n-\t\t\tlwsl_notice(\u0022%s: blob write +%d, ofs %llu / %llu, len %d\u005cn\u0022,\n-\t\t\t\t __func__, (int)(bl - m),\n-\t\t\t\t (unsigned long long)pss-\u003eartifact_offset,\n-\t\t\t\t (unsigned long long)pss-\u003eartifact_length, m);\n-\t\t\tif (sqlite3_blob_write(pss-\u003eblob_artifact,\n-\t\t\t\t\t (uint8_t *)buf + (bl - m), (int)m,\n-\t\t\t\t\t pss-\u003eartifact_offset)) {\n-\t\t\t\tlwsl_err(\u0022%s: writing blob failed\u005cn\u0022, __func__);\n-\t\t\t\tgoto afail;\n-\t\t\t}\n-\n-\t\t\tlws_set_timeout(pss-\u003ewsi, PENDING_TIMEOUT_HTTP_CONTENT, 5);\n-\t\t\tpss-\u003eartifact_offset +\u003d (int)m;\n-\t\t}\n-\n-\t\tif (pss-\u003eartifact_offset \u003d\u003d pss-\u003eartifact_length) {\n-\t\t\tlwsl_notice(\u0022%s: blob upload finished\u005cn\u0022, __func__);\n-\n-\t\t\tgoto afail;\n-\t\t}\n-\n-\t}\n-\n-\treturn 0;\n-\n-afail:\n-\tlwsac_free(\u0026ac);\n-\tsaim_event_db_close(vhd, \u0026pss-\u003epdb_artifact);\n-\n-\treturn -1;\n-}\n-\n-/*\n- * We're sending something on a builder ws connection\n- */\n-\n-int\n-saim_ws_json_tx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf,\n-\t\t\tsize_t bl)\n-{\n-\tuint8_t *start \u003d buf + LWS_PRE, *p \u003d start, *end \u003d p + bl - LWS_PRE - 1;\n-\tint n, flags \u003d LWS_WRITE_TEXT, first \u003d 0;\n-\tlws_struct_serialize_t *js;\n-\tsai_task_t *task;\n-\tsize_t w;\n-\n-\tif (pss-\u003etask_cancel_owner.head) {\n-\t\t/*\n-\t\t * Pending cancel message to send\n-\t\t */\n-\t\tsai_cancel_t *c \u003d lws_container_of(pss-\u003etask_cancel_owner.head,\n-\t\t\t\t\t\t sai_cancel_t, list);\n-\n-\t\tjs \u003d lws_struct_json_serialize_create(lsm_schema_json_map_can,\n-\t\t\t\tLWS_ARRAY_SIZE(lsm_schema_json_map_can), 0, c);\n-\t\tif (!js)\n-\t\t\treturn 1;\n-\n-\t\tn \u003d lws_struct_json_serialize(js, p, end - p, \u0026w);\n-\t\tlws_struct_json_serialize_destroy(\u0026js);\n-\n-\t\tlws_dll2_remove(\u0026c-\u003elist);\n-\t\tfree(c);\n-\n-\t\tfirst \u003d 1;\n-\t\tpss-\u003ewalk \u003d NULL;\n-\n-\t\tgoto send_json;\n-\t}\n-\n-\tif (!pss-\u003eissue_task_owner.count)\n-\t\treturn 0; /* nothing to send */\n-\n-\t/*\n-\t * We're sending a browser the specific task info that he\n-\t * asked for.\n-\t *\n-\t * We already got the task struct out of the db in .one_task\n-\t * (all in .query_ac)\n-\t */\n-\n-\ttask \u003d lws_container_of(pss-\u003eissue_task_owner.head, sai_task_t,\n-\t\t\t\tpending_assign_list);\n-\tlws_dll2_remove(\u0026task-\u003epending_assign_list);\n-\n-\tjs \u003d lws_struct_json_serialize_create(lsm_schema_map_ta,\n-\t\t\t\t\t LWS_ARRAY_SIZE(lsm_schema_map_ta),\n-\t\t\t\t\t 0, task);\n-\tif (!js)\n-\t\treturn 1;\n-\n-\tn \u003d lws_struct_json_serialize(js, p, end - p, \u0026w);\n-\tlws_struct_json_serialize_destroy(\u0026js);\n-\n-\tfirst \u003d 1;\n-\tpss-\u003ewalk \u003d NULL;\n-\n-\t//lwsac_free(\u0026pss-\u003equery_ac);\n-\n-send_json:\n-\tp +\u003d w;\n-\tif (n \u003d\u003d LSJS_RESULT_ERROR) {\n-\t\tlwsl_notice(\u0022%s: taskinfo: error generating json\u005cn\u0022,\n-\t\t\t __func__);\n-\t\treturn 1;\n-\t}\n-\tif (!lws_ptr_diff(p, start)) {\n-\t\tlwsl_notice(\u0022%s: taskinfo: empty json\u005cn\u0022, __func__);\n-\t\treturn 0;\n-\t}\n-\n-\tflags \u003d lws_write_ws_flags(LWS_WRITE_TEXT, first, !pss-\u003ewalk);\n-\n-\tlwsl_hexdump_notice(start, p - start);\n-\n-\tif (lws_write(pss-\u003ewsi, start, p - start, flags) \u003c 0)\n-\t\treturn -1;\n-\n-\tlws_callback_on_writable(pss-\u003ewsi);\n-\n-\treturn 0;\n-}\ndiff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt\nnew file mode 100644\nindex 0000000..b256075\n--- /dev/null\n+++ b/src/server/CMakeLists.txt\n@@ -0,0 +1,73 @@\n+set(SUB \u0022sai-server\u0022)\n+\n+set(CPACK_DEBIAN_SERVER_PACKAGE_NAME ${SUB})\n+\n+set(SRCS\n+\ts-sai.c\n+\ts-conf.c\n+\ts-notification.c\n+\ts-comms.c\n+\ts-ws-builder.c\n+\ts-task.c\n+\ts-central.c\n+\ts-websrv.c\n+)\n+\n+set(requirements 1)\n+require_lws_config(LWS_WITH_STRUCT_SQLITE3\t1 requirements)\n+require_lws_config(LWS_WITH_SERVER\t\t1 requirements)\n+require_lws_config(LWS_WITH_GENCRYPTO\t\t1 requirements)\n+require_lws_config(LWS_WITH_UNIX_SOCK\t\t1 requirements)\n+\n+if (requirements)\n+\tadd_executable(${SUB} ${SRCS})\n+\tif (APPLE)\n+\t\tset_property(TARGET sai-server PROPERTY MACOSX_RPATH YES)\n+\tendif()\n+\n+\t#\n+\t# sqlite3 paths (server)\n+\t#\n+\n+\tfind_path( SQLITE3_INC_PATH NAMES \u0022sqlite3.h\u0022)\n+\tfind_library(SQLITE3_LIB_PATH NAMES \u0022sqlite3\u0022)\n+\t\n+\tif (SQLITE3_INC_PATH AND SQLITE3_LIB_PATH)\n+\t\tinclude_directories(BEFORE \u0022${SQLITE3_INC_PATH}\u0022)\n+\telse()\n+\t\tmessage(FATAL_ERROR \u0022 Unable to find sqlite3\u0022)\n+\tendif()\n+\n+\ttarget_link_libraries(${SUB} websockets ${SQLITE3_LIB_PATH})\n+\n+ \tinclude_directories(BEFORE \u0022${SAI_LWS_INC_PATH}\u0022)\n+\n+\tCHECK_C_SOURCE_COMPILES(\u0022#include \u003clibwebsockets.h\u003e\u005cnint\n+\tmain(void) {\u005cn#if defined(LWS_HAVE_LIBCAP)\u005cn return\n+\t\t0;\u005cn#else\u005cn fail;\u005cn#endif\u005cn return 0;\u005cn}\u005cn\u0022 HAS_LIBCAP)\n+\tif (HAS_LIBCAP)\n+\t\tfind_library(CAP_LIB_PATH NAMES \u0022cap\u0022)\n+\tendif()\n+\n+\ttarget_link_libraries(${SUB} ${SAI_LWS_LIB_PATH})\n+\t\n+\tif (LWS_OPENSSL_LIBRARIES)\n+\t\ttarget_link_libraries(${SUB} ${LWS_OPENSSL_LIBRARIES})\n+\tendif()\n+\t\n+\tif (SAI_EXT_PTHREAD_LIBRARIES)\n+\t\ttarget_link_libraries(${SUB} ${SAI_EXT_PTHREAD_LIBRARIES})\n+\tendif()\n+\n+\tif (HAS_LIBCAP)\n+\t\ttarget_link_libraries(${SUB} ${CAP_LIB_PATH})\n+\tendif()\n+\n+\tif (MSVC)\n+\t\ttarget_link_libraries(${SUB} ws2_32.lib userenv.lib psapi.lib iphlpapi.lib)\n+\tendif()\n+\t\n+\tinstall(TARGETS ${SUB} RUNTIME DESTINATION \u0022${BIN_DIR}\u0022 COMPONENT server)\n+\n+endif(requirements)\n+\ndiff --git a/src/server/s-central.c b/src/server/s-central.c\nnew file mode 100644\nindex 0000000..b04355e\n--- /dev/null\n+++ b/src/server/s-central.c\n@@ -0,0 +1,168 @@\n+/*\n+ * Sai server\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ *\n+ *\n+ * Central dispatcher for jobs from events that made it into the database. This\n+ * is done in an event-driven way in m-task.c, but management of it also has to\n+ * be done in the background for when there are no events coming,\n+ */\n+\n+#include \u003clibwebsockets.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003ctime.h\u003e\n+\n+#include \u0022s-private.h\u0022\n+\n+extern struct lws_context *context;\n+\n+static void\n+sais_central_clean_abandoned(struct vhd *vhd)\n+{\n+\tsais_sqlite_cache_t *sc;\n+\tstruct lwsac *ac \u003d NULL;\n+\tlws_usec_t now \u003d lws_now_usecs();\n+\tlws_dll2_owner_t o;\n+\tchar s[160];\n+\tint n, nzr;\n+\n+\t/*\n+\t * Sqlite cache cleaning\n+\t */\n+\n+\tnzr \u003d 0;\n+\tlws_start_foreach_dll_safe(struct lws_dll2 *, p, p1,\n+\t\t\t\t vhd-\u003esqlite3_cache.head) {\n+\t\tsc \u003d lws_container_of(p, sais_sqlite_cache_t, list);\n+\n+\t\tif (!sc-\u003erefcount \u0026\u0026\n+\t\t (now - sc-\u003eidle_since) \u003e (60 * LWS_USEC_PER_SEC)) {\n+\t\t\tlwsl_notice(\u0022%s: delayed db pool clean %s\u005cn\u0022, __func__,\n+\t\t\t\t\tsc-\u003euuid);\n+\t\t\tlws_struct_sq3_close(\u0026sc-\u003epdb);\n+\t\t\tlws_dll2_remove(\u0026sc-\u003elist);\n+\t\t\tfree(sc);\n+\t\t} else\n+\t\t\tif (sc-\u003erefcount)\n+\t\t\t\tnzr++;\n+\n+\t} lws_end_foreach_dll_safe(p, p1);\n+\n+\tif (vhd-\u003esqlite3_cache.count)\n+\t\tlwsl_notice(\u0022%s: db pool items: in-use: %d, total: %d\u005cn\u0022,\n+\t\t\t __func__, nzr, vhd-\u003esqlite3_cache.count);\n+\n+\t/*\n+\t * Collect the most recent \u003c\u003d10 events that still feel they're\n+\t * incomplete and may be running something\n+\t */\n+\n+\tn \u003d lws_struct_sq3_deserialize(vhd-\u003eserver.pdb,\n+\t\t\t\t \u0022 and (state !\u003d 3 and state !\u003d 4 and state !\u003d 5)\u0022,\n+\t\t\t\t NULL, lsm_schema_sq3_map_event, \u0026o,\n+\t\t\t\t \u0026ac, 0, 10);\n+\tif (n \u003c 0 || !o.head)\n+\t\treturn;\n+\n+\t/*\n+\t * For each of those events, look for tasks that have been running\n+\t * \u0022too long\u0022, eg, builder restarted or lost connection etc\n+\t */\n+\n+\tlws_start_foreach_dll(struct lws_dll2 *, p, o.head) {\n+\t\tsai_event_t *e \u003d lws_container_of(p, sai_event_t, list);\n+\t\tsqlite3 *pdb \u003d NULL;\n+\n+\t\tif (!sais_event_db_ensure_open(vhd, e-\u003euuid, 0, \u0026pdb)) {\n+\t\t\tchar *err \u003d NULL;\n+\n+\t\t\t/*\n+\t\t\t * Such tasks should go into CANCELLED\n+\t\t\t */\n+\n+\t\t\tlws_snprintf(s, sizeof(s),\n+\t\t\t\t \u0022update tasks set state\u003d%d where \u0022\n+\t\t\t\t \u0022(state\u003d1 OR state\u003d2) and started \u003c %llu\u0022,\n+\t\t\t\t SAIES_CANCELLED, (unsigned long long)\n+\t\t\t\t\t (lws_now_secs() - (30 * 60)));\n+\n+\t\t\tif (sqlite3_exec(pdb, s, NULL, NULL, \u0026err) !\u003d\n+\t\t\t\t\t SQLITE_OK) {\n+\t\t\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, s,\n+\t\t\t\t\t sqlite3_errmsg(pdb));\n+\t\t\t\tif (err)\n+\t\t\t\t\tsqlite3_free(err);\n+\t\t\t}\n+\n+\t\t\tsais_event_db_close(vhd, \u0026pdb);\n+\t\t}\n+\n+\t} lws_end_foreach_dll(p);\n+\n+\tlwsac_free(\u0026ac);\n+}\n+\n+void\n+sais_central_cb(lws_sorted_usec_list_t *sul)\n+{\n+\tstruct vhd *vhd \u003d lws_container_of(sul, struct vhd, sul_central);\n+\n+\t/*\n+\t * For each builder connected to us, see if it can handle a new task,\n+\t * and if so, try to select one matching its supported platforms\n+\t */\n+\n+\tlws_start_foreach_dll(struct lws_dll2 *, p,\n+\t\t\t vhd-\u003eserver.builder_owner.head) {\n+\t\tsai_plat_t *cb \u003d lws_container_of(p, sai_plat_t, sai_plat_list);\n+\n+\t\tlwsl_debug(\u0022%s: checking tasks %s %d %d %p\u005cn\u0022, __func__,\n+\t\t\t cb-\u003ename, cb-\u003eongoing, cb-\u003einstances, cb-\u003ewsi);\n+\n+\t\tif (cb-\u003ewsi \u0026\u0026 lws_wsi_user(cb-\u003ewsi) \u0026\u0026\n+\t\t cb-\u003eongoing \u003c cb-\u003einstances)\n+\t\t\t/*\n+\t\t\t * try to bind outstanding task to specific builder\n+\t\t\t * instance\n+\t\t\t */\n+\t\t\tsais_allocate_task(vhd,\n+\t\t\t\t\t (struct pss *)lws_wsi_user(cb-\u003ewsi),\n+\t\t\t\t\t cb, cb-\u003eplatform);\n+\n+\t} lws_end_foreach_dll(p);\n+\n+\t/*\n+\t * Need to globally check for abandoned tasks periodically\n+\t */\n+\n+\tif (!vhd-\u003elast_check_abandoned_tasks ||\n+\t lws_now_usecs() \u003e (vhd-\u003elast_check_abandoned_tasks +\n+\t\t\t (20 * LWS_USEC_PER_SEC))) {\n+\n+\t\tsais_central_clean_abandoned(vhd);\n+\n+\t\tvhd-\u003elast_check_abandoned_tasks \u003d lws_now_usecs();\n+\t}\n+\n+\t/* check again in 1s */\n+\n+\tlws_sul_schedule(context, 0, \u0026vhd-\u003esul_central, sais_central_cb,\n+\t\t\t 1 * LWS_US_PER_SEC);\n+}\ndiff --git a/src/server/s-comms.c b/src/server/s-comms.c\nnew file mode 100644\nindex 0000000..c2937c7\n--- /dev/null\n+++ b/src/server/s-comms.c\n@@ -0,0 +1,834 @@\n+/*\n+ * Sai server\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ *\n+ * The same ws interface is connected-to by builders (on path /builder), and\n+ * provides the query transport for browsers (on path /browse).\n+ *\n+ * There's a single server slite3 database containing events, and a separate\n+ * sqlite3 database file for each event, it only contains tasks and logs for\n+ * the event and can be deleted when the event record associated with it is\n+ * deleted. This is to keep is scalable when there may be thousands of events\n+ * and related tasks and logs stored.\n+ */\n+\n+#include \u003clibwebsockets.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003ctime.h\u003e\n+#include \u003cstdio.h\u003e\n+#include \u003cfcntl.h\u003e\n+\n+#include \u0022s-private.h\u0022\n+\n+#include \u0022../common/struct-metadata.c\u0022\n+\n+typedef enum {\n+\tSJS_CLONING,\n+\tSJS_ASSIGNING,\n+\tSJS_WAITING,\n+\tSJS_DONE\n+} sai_job_state_t;\n+\n+typedef struct sai_job {\n+\tstruct lws_dll2 jobs_list;\n+\tchar reponame[64];\n+\tchar ref[64];\n+\tchar head[64];\n+\n+\ttime_t requested;\n+\n+\tsai_job_state_t state;\n+\n+} sai_job_t;\n+\n+const lws_struct_map_t lsm_schema_map_ta[] \u003d {\n+\tLSM_SCHEMA (sai_task_t,\t NULL, lsm_task, \u0022com-warmcat-sai-ta\u0022),\n+};\n+\n+extern const lws_struct_map_t lsm_schema_sq3_map_event[];\n+extern const lws_ss_info_t ssi_server;\n+\n+/* len is typically 16 (event uuid is 32 chars + NUL)\n+ * But eg, task uuid is concatenated 32-char eventid and 32-char taskid\n+ */\n+\n+int\n+sai_uuid16_create(struct lws_context *context, char *dest33)\n+{\n+\treturn lws_hex_random(context, dest33, 33);\n+}\n+\n+int\n+sai_sqlite3_statement(sqlite3 *pdb, const char *cmd, const char *desc)\n+{\n+\tsqlite3_stmt *sm;\n+\tint n;\n+\n+\tif (sqlite3_prepare_v2(pdb, cmd, -1, \u0026sm, NULL) !\u003d SQLITE_OK) {\n+\t\tlwsl_err(\u0022%s: Unable to %s: %s\u005cn\u0022,\n+\t\t\t __func__, desc, sqlite3_errmsg(pdb));\n+\n+\t\treturn 1;\n+\t}\n+\n+\tn \u003d sqlite3_step(sm);\n+\tsqlite3_reset(sm);\n+\tsqlite3_finalize(sm);\n+\tif (n !\u003d SQLITE_DONE) {\n+\t\tn \u003d sqlite3_extended_errcode(pdb);\n+\t\tif (!n) {\n+\t\t\tlwsl_info(\u0022%s: failed '%s'\u005cn\u0022, __func__, cmd);\n+\t\t\treturn 0;\n+\t\t}\n+\n+\t\tlwsl_err(\u0022%s: %d: Unable to perform \u005c\u0022%s\u005c\u0022: %s\u005cn\u0022, __func__,\n+\t\t\t n, desc, sqlite3_errmsg(pdb));\n+\t\tputs(cmd);\n+\n+\t\treturn 1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+int\n+sais_event_db_ensure_open(struct vhd *vhd, const char *event_uuid,\n+\t\t\t char create_if_needed, sqlite3 **ppdb)\n+{\n+\tchar filepath[256], saf[33];\n+\tsais_sqlite_cache_t *sc;\n+\n+\tif (*ppdb)\n+\t\treturn 0;\n+\n+\t/* do we have this guy cached? */\n+\n+\tlws_start_foreach_dll(struct lws_dll2 *, p, vhd-\u003esqlite3_cache.head) {\n+\t\tsc \u003d lws_container_of(p, sais_sqlite_cache_t, list);\n+\n+\t\tif (!strcmp(event_uuid, sc-\u003euuid)) {\n+\t\t\tsc-\u003erefcount++;\n+\t\t\t*ppdb \u003d sc-\u003epdb;\n+\t\t\treturn 0;\n+\t\t}\n+\n+\t} lws_end_foreach_dll(p);\n+\n+\t/* ... nope, well, let's open and cache him then... */\n+\n+\tlws_strncpy(saf, event_uuid, sizeof(saf));\n+\tlws_filename_purify_inplace(saf);\n+\n+\tlws_snprintf(filepath, sizeof(filepath), \u0022%s-event-%s.sqlite3\u0022,\n+\t\t vhd-\u003esqlite3_path_lhs, saf);\n+\n+\tif (lws_struct_sq3_open(vhd-\u003econtext, filepath, create_if_needed, ppdb)) {\n+\t\tlwsl_err(\u0022%s: Unable to open db %s: %s\u005cn\u0022, __func__,\n+\t\t\t filepath, sqlite3_errmsg(*ppdb));\n+\n+\t\treturn 1;\n+\t}\n+\n+\t/* create / add to the schema for the tables we will have in here */\n+\n+\tif (lws_struct_sq3_create_table(*ppdb, lsm_schema_sq3_map_task))\n+\t\treturn 1;\n+\n+\tsai_sqlite3_statement(*ppdb, \u0022PRAGMA journal_mode\u003dWAL;\u0022, \u0022set WAL\u0022);\n+\n+\tif (lws_struct_sq3_create_table(*ppdb, lsm_schema_sq3_map_log))\n+\t\treturn 1;\n+\n+\tif (lws_struct_sq3_create_table(*ppdb, lsm_schema_sq3_map_artifact))\n+\t\treturn 1;\n+\n+\tsc \u003d malloc(sizeof(*sc));\n+\tmemset(sc, 0, sizeof(*sc));\n+\tif (!sc) {\n+\t\tlws_struct_sq3_close(ppdb);\n+\t\t*ppdb \u003d NULL;\n+\t\treturn 1;\n+\t}\n+\n+\tlws_strncpy(sc-\u003euuid, event_uuid, sizeof(sc-\u003euuid));\n+\tsc-\u003erefcount \u003d 1;\n+\tsc-\u003epdb \u003d *ppdb;\n+\tlws_dll2_add_tail(\u0026sc-\u003elist, \u0026vhd-\u003esqlite3_cache);\n+\n+\treturn 0;\n+}\n+\n+void\n+sais_event_db_close(struct vhd *vhd, sqlite3 **ppdb)\n+{\n+\tsais_sqlite_cache_t *sc;\n+\n+\tif (!*ppdb)\n+\t\treturn;\n+\n+\t/* look for him in the cache */\n+\n+\tlws_start_foreach_dll(struct lws_dll2 *, p, vhd-\u003esqlite3_cache.head) {\n+\t\tsc \u003d lws_container_of(p, sais_sqlite_cache_t, list);\n+\n+\t\tif (sc-\u003epdb \u003d\u003d *ppdb) {\n+\t\t\t*ppdb \u003d NULL;\n+\t\t\tif (--sc-\u003erefcount) {\n+\t\t\t\tlwsl_notice(\u0022%s: zero refcount to idle\u005cn\u0022,\n+\t\t\t\t\t\t__func__);\n+\t\t\t\t/*\n+\t\t\t\t * He's not currently in use then... don't\n+\t\t\t\t * close him immediately, s-central.c has a\n+\t\t\t\t * timer that closes and removes sqlite3\n+\t\t\t\t * cache entries idle for longer than 60s\n+\t\t\t\t */\n+\t\t\t\tsc-\u003eidle_since \u003d lws_now_usecs();\n+\t\t\t}\n+\n+\t\t\treturn;\n+\t\t}\n+\n+\t} lws_end_foreach_dll(p);\n+\n+\tlws_struct_sq3_close(ppdb);\n+\t*ppdb \u003d NULL;\n+}\n+\n+int\n+sais_event_db_close_all_now(struct vhd *vhd)\n+{\n+\tsais_sqlite_cache_t *sc;\n+\n+\tlws_start_foreach_dll_safe(struct lws_dll2 *, p, p1,\n+\t\t\t\t vhd-\u003esqlite3_cache.head) {\n+\t\tsc \u003d lws_container_of(p, sais_sqlite_cache_t, list);\n+\n+\t\tlws_struct_sq3_close(\u0026sc-\u003epdb);\n+\t\tlws_dll2_remove(\u0026sc-\u003elist);\n+\t\tfree(sc);\n+\n+\t} lws_end_foreach_dll_safe(p, p1);\n+\n+\treturn 0;\n+}\n+\n+int\n+sais_event_db_delete_database(struct vhd *vhd, const char *event_uuid)\n+{\n+\tchar filepath[256], saf[33], r \u003d 0;\n+\n+\tlws_strncpy(saf, event_uuid, sizeof(saf));\n+\tlws_filename_purify_inplace(saf);\n+\n+\tlws_snprintf(filepath, sizeof(filepath), \u0022%s-event-%s.sqlite3\u0022,\n+\t\t vhd-\u003esqlite3_path_lhs, saf);\n+\n+\tr \u003d unlink(filepath);\n+\n+\tlws_snprintf(filepath, sizeof(filepath), \u0022%s-event-%s.sqlite3-wal\u0022,\n+\t\t vhd-\u003esqlite3_path_lhs, saf);\n+\n+\tr |\u003d unlink(filepath);\n+\n+\tlws_snprintf(filepath, sizeof(filepath), \u0022%s-event-%s.sqlite3-shm\u0022,\n+\t\t vhd-\u003esqlite3_path_lhs, saf);\n+\n+\treturn r | unlink(filepath);\n+}\n+\n+\n+#if 0\n+static void\n+sais_all_browser_on_writable(struct vhd *vhd)\n+{\n+\tlws_start_foreach_dll(struct lws_dll2 *, mp, vhd-\u003ebrowsers.head) {\n+\t\tstruct pss *pss \u003d lws_container_of(mp, struct pss, same);\n+\n+\t\tlws_callback_on_writable(pss-\u003ewsi);\n+\t} lws_end_foreach_dll(mp);\n+}\n+#endif\n+\n+static int\n+sai_destroy_builder(struct lws_dll2 *d, void *user)\n+{\n+//\tsaib_t *b \u003d lws_container_of(d, saib_t, c.builder_list);\n+\n+\tlws_dll2_remove(d);\n+\n+\treturn 0;\n+}\n+\n+static void\n+sais_server_destroy(struct vhd *vhd, sais_t *server)\n+{\n+\tlws_dll2_foreach_safe(\u0026server-\u003ebuilder_owner, NULL, sai_destroy_builder);\n+\n+\tsais_event_db_close_all_now(vhd);\n+\n+\tlws_struct_sq3_close(\u0026server-\u003epdb);\n+}\n+\n+typedef enum {\n+\tSHMUT_NONE \u003d -1,\n+\tSHMUT_HOOK,\n+\tSHMUT_BROWSE,\n+\tSHMUT_STATUS,\n+\tSHMUT_ARTIFACTS,\n+\tSHMUT_LOGIN\n+} sai_http_murl_t;\n+\n+static const char * const well_known[] \u003d {\n+\t\u0022/update-hook\u0022,\n+\t\u0022/sai/browse\u0022,\n+\t\u0022/status\u0022,\n+\t\u0022/artifacts/\u0022, /* HTTP api for accessing build artifacts */\n+\t\u0022/login\u0022\n+};\n+\n+static const char *hmac_names[] \u003d {\n+\t\u0022sai sha256\u003d\u0022,\n+\t\u0022sai sha384\u003d\u0022,\n+\t\u0022sai sha512\u003d\u0022\n+};\n+\n+int\n+sai_get_head_status(struct vhd *vhd, const char *projname)\n+{\n+\tstruct lwsac *ac \u003d NULL;\n+\tlws_dll2_owner_t o;\n+\tsai_event_t *e;\n+\tint state;\n+\n+\tif (lws_struct_sq3_deserialize(vhd-\u003eserver.pdb, NULL, \u0022created \u0022,\n+\t\t\tlsm_schema_sq3_map_event, \u0026o, \u0026ac, 0, -1))\n+\t\treturn -1;\n+\n+\tif (!o.head)\n+\t\treturn -1;\n+\n+\te \u003d lws_container_of(o.head, sai_event_t, list);\n+\tstate \u003d e-\u003estate;\n+\n+\tlwsac_free(\u0026ac);\n+\n+\treturn state;\n+}\n+\n+\n+static int\n+sai_login_cb(void *data, const char *name, const char *filename,\n+\t char *buf, int len, enum lws_spa_fileupload_states state)\n+{\n+\treturn 0;\n+}\n+\n+static const char * const auth_param_names[] \u003d {\n+\t\u0022lname\u0022,\n+\t\u0022lpass\u0022,\n+\t\u0022success_redir\u0022,\n+};\n+\n+enum enum_param_names {\n+\tEPN_LNAME,\n+\tEPN_LPASS,\n+\tEPN_SUCCESS_REDIR,\n+};\n+\n+static int\n+callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user,\n+\t void *in, size_t len)\n+{\n+\tstruct vhd *vhd \u003d (struct vhd *)lws_protocol_vh_priv_get(\n+\t\t\t\tlws_get_vhost(wsi), lws_get_protocol(wsi));\n+\tuint8_t buf[LWS_PRE + 8192], *start \u003d \u0026buf[LWS_PRE], *p \u003d start,\n+\t\t*end \u003d \u0026buf[sizeof(buf) - LWS_PRE - 1];\n+\tstruct pss *pss \u003d (struct pss *)user;\n+\tsai_http_murl_t mu \u003d SHMUT_NONE;\n+\tchar projname[64];\n+\tint n, resp, r;\n+\tconst char *cp;\n+\n+\t(void)end;\n+\t(void)p;\n+\n+\tswitch (reason) {\n+\tcase LWS_CALLBACK_PROTOCOL_INIT:\n+\t\tvhd \u003d lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),\n+\t\t\t\t\t\t lws_get_protocol(wsi),\n+\t\t\t\t\t\t sizeof(struct vhd));\n+\t\tif (!vhd)\n+\t\t\treturn -1;\n+\n+\t\tvhd-\u003econtext \u003d lws_get_context(wsi);\n+\t\tvhd-\u003evhost \u003d lws_get_vhost(wsi);\n+\n+\t\tif (lws_pvo_get_str(in, \u0022notification-key\u0022,\n+\t\t\t\t \u0026vhd-\u003enotification_key)) {\n+\t\t\tlwsl_err(\u0022%s: notification_key pvo required\u005cn\u0022, __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tif (lws_pvo_get_str(in, \u0022database\u0022, \u0026vhd-\u003esqlite3_path_lhs)) {\n+\t\t\tlwsl_err(\u0022%s: database pvo required\u005cn\u0022, __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tlws_snprintf((char *)buf, sizeof(buf), \u0022%s-events.sqlite3\u0022,\n+\t\t\t\tvhd-\u003esqlite3_path_lhs);\n+\n+\t\tif (lws_struct_sq3_open(vhd-\u003econtext, (char *)buf, 1,\n+\t\t\t\t\t\u0026vhd-\u003eserver.pdb)) {\n+\t\t\tlwsl_err(\u0022%s: Unable to open session db %s: %s\u005cn\u0022,\n+\t\t\t\t __func__, vhd-\u003esqlite3_path_lhs, sqlite3_errmsg(\n+\t\t\t\t\t\t vhd-\u003eserver.pdb));\n+\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tsai_sqlite3_statement(vhd-\u003eserver.pdb,\n+\t\t\t\t \u0022PRAGMA journal_mode\u003dWAL;\u0022, \u0022set WAL\u0022);\n+\n+\t\tif (lws_struct_sq3_create_table(vhd-\u003eserver.pdb,\n+\t\t\t\t\t\tlsm_schema_sq3_map_event)) {\n+\t\t\tlwsl_err(\u0022%s: unable to create event table\u005cn\u0022, __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tlwsl_notice(\u0022%s: creating server stream\u005cn\u0022, __func__);\n+\n+\t\tif (lws_ss_create(vhd-\u003econtext, 0, \u0026ssi_server, vhd,\n+\t\t\t\t \u0026vhd-\u003eh_ss_websrv, NULL, NULL)) {\n+\t\t\tlwsl_err(\u0022%s: failed to create secure stream\u005cn\u0022,\n+\t\t\t\t __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tlws_sul_schedule(vhd-\u003econtext, 0, \u0026vhd-\u003esul_central,\n+\t\t\t\t sais_central_cb, 500 * LWS_US_PER_MS);\n+\n+\t\tbreak;\n+\n+\tcase LWS_CALLBACK_PROTOCOL_DESTROY:\n+\t\tsais_server_destroy(vhd, \u0026vhd-\u003eserver);\n+\t\tgoto passthru;\n+\n+\t/*\n+\t * receive http hook notifications\n+\t */\n+\n+\tcase LWS_CALLBACK_HTTP:\n+\n+\t\tresp \u003d HTTP_STATUS_FORBIDDEN;\n+\t\tpss-\u003evhd \u003d vhd;\n+\n+\t\tfor (n \u003d 0; n \u003c (int)LWS_ARRAY_SIZE(well_known); n++)\n+\t\t\tif (!strncmp((const char *)in, well_known[n],\n+\t\t\t\t strlen(well_known[n]))) {\n+\t\t\t\tmu \u003d n;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\n+\t\tpss-\u003eour_form \u003d 0;\n+\n+\t\t// lwsl_notice(\u0022%s: HTTP: xmu \u003d %d\u005cn\u0022, __func__, n);\n+\n+\t\tswitch (mu) {\n+\n+\t\tcase SHMUT_NONE:\n+\t\t\tgoto passthru;\n+\n+\t\tcase SHMUT_HOOK:\n+\t\t\tpss-\u003eour_form \u003d 1;\n+\t\t\tlwsl_notice(\u0022LWS_CALLBACK_HTTP: sees hook\u005cn\u0022);\n+\t\t\treturn 0;\n+\n+\t\tcase SHMUT_STATUS:\n+\t\t\t/*\n+\t\t\t * in is a string like /libwebsockets/status.svg\n+\t\t\t */\n+\t\t\tcp \u003d ((const char *)in) + 7;\n+\t\t\twhile (*cp \u003d\u003d '/')\n+\t\t\t\tcp++;\n+\t\t\tn \u003d 0;\n+\t\t\twhile (*cp !\u003d '/' \u0026\u0026 *cp \u0026\u0026 (size_t)n \u003c sizeof(projname) - 1)\n+\t\t\t\tprojname[n++] \u003d *cp++;\n+\t\t\tprojname[n] \u003d '\u005c0';\n+\n+\t\t\t// lwsl_notice(\u0022%s: status %s\u005cn\u0022, __func__, projname);\n+\n+\t\t\tr \u003d sai_get_head_status(vhd, projname);\n+\t\t\tif (r \u003c 2)\n+\t\t\t\tr \u003d 2;\n+\t\t\tn \u003d lws_snprintf(projname, sizeof(projname),\n+\t\t\t\t \u0022../decal-%d.svg\u0022, r);\n+\n+\t\t\tif (lws_http_redirect(wsi, 307,\n+\t\t\t\t\t (unsigned char *)projname, n,\n+\t\t\t\t\t \u0026p, end) \u003c 0)\n+\t\t\t\treturn -1;\n+\n+\t\t\tgoto passthru;\n+\n+\t\tdefault:\n+\t\t\tlwsl_notice(\u0022%s: DEFAULT!!!\u005cn\u0022, __func__);\n+\t\t\treturn 0;\n+\t\t}\n+\n+\t\tresp \u003d HTTP_STATUS_OK;\n+\n+\t\tif (lws_add_http_header_status(wsi, resp, \u0026p, end))\n+\t\t\tgoto bail;\n+\t\tif (lws_add_http_header_content_length(wsi, 0, \u0026p, end))\n+\t\t\tgoto bail;\n+\t\tif (lws_finalize_write_http_header(wsi, start, \u0026p, end))\n+\t\t\tgoto bail;\n+\t\tgoto try_to_reuse;\n+\n+\n+\tcase LWS_CALLBACK_HTTP_WRITEABLE:\n+\n+\t\tlwsl_notice(\u0022%s: HTTP_WRITEABLE\u005cn\u0022, __func__);\n+\n+\t\tif (!pss || !pss-\u003eblob_artifact)\n+\t\t\tbreak;\n+\n+\t\tn \u003d lws_ptr_diff(end, start);\n+\t\tif ((int)(pss-\u003eartifact_length - pss-\u003eartifact_offset) \u003c n)\n+\t\t\tn \u003d (int)(pss-\u003eartifact_length - pss-\u003eartifact_offset);\n+\n+\t\tif (sqlite3_blob_read(pss-\u003eblob_artifact, start, n,\n+\t\t\t\t pss-\u003eartifact_offset)) {\n+\t\t\tlwsl_err(\u0022%s: blob read failed\u005cn\u0022, __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tpss-\u003eartifact_offset +\u003d n;\n+\n+\t\tif (lws_write(wsi, start, n,\n+\t\t\t\tpss-\u003eartifact_offset !\u003d pss-\u003eartifact_length ?\n+\t\t\t\t\tLWS_WRITE_HTTP : LWS_WRITE_HTTP_FINAL) !\u003d n)\n+\t\t\treturn -1;\n+\n+\t\tif (pss-\u003eartifact_offset !\u003d pss-\u003eartifact_length)\n+\t\t\tlws_callback_on_writable(wsi);\n+\n+\t\tbreak;\n+\n+\t/*\n+\t * Notifcation POSTs\n+\t */\n+\n+\tcase LWS_CALLBACK_HTTP_BODY:\n+\n+\t\tif (pss-\u003elogin_form) {\n+\n+\t\t\tif (!pss-\u003espa) {\n+\t\t\t\tpss-\u003espa \u003d lws_spa_create(wsi, auth_param_names,\n+\t\t\t\t\t\tLWS_ARRAY_SIZE(auth_param_names),\n+\t\t\t\t\t\t1024, sai_login_cb, pss);\n+\t\t\t\tif (!pss-\u003espa) {\n+\t\t\t\t\tlwsl_err(\u0022failed to create spa\u005cn\u0022);\n+\t\t\t\t\treturn -1;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tgoto spa_process;\n+\n+\t\t}\n+\n+\t\tif (!pss-\u003eour_form) {\n+\t\t\tlwsl_notice(\u0022%s: not our form\u005cn\u0022, __func__);\n+\t\t\tgoto passthru;\n+\t\t}\n+\n+\t\tlwsl_user(\u0022LWS_CALLBACK_HTTP_BODY: %d\u005cn\u0022, (int)len);\n+\t\t/* create the POST argument parser if not already existing */\n+\n+\t\tif (!pss-\u003espa) {\n+\t\t\tpss-\u003ewsi \u003d wsi;\n+\t\t\tif (lws_hdr_copy(wsi, pss-\u003enotification_sig,\n+\t\t\t\t\t sizeof(pss-\u003enotification_sig),\n+\t\t\t\t\t WSI_TOKEN_HTTP_AUTHORIZATION) \u003c 0) {\n+\t\t\t\tlwsl_err(\u0022%s: failed to get signature hdr\u005cn\u0022,\n+\t\t\t\t\t __func__);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\n+\t\t\tif (lws_hdr_copy(wsi, pss-\u003esn.e.source_ip,\n+\t\t\t\t\t sizeof(pss-\u003esn.e.source_ip),\n+\t\t\t\t\t WSI_TOKEN_X_FORWARDED_FOR) \u003c 0)\n+\t\t\t\tlws_get_peer_simple(wsi, pss-\u003esn.e.source_ip,\n+\t\t\t\t\t\tsizeof(pss-\u003esn.e.source_ip));\n+\n+\t\t\tpss-\u003espa \u003d lws_spa_create(wsi, NULL, 0, 1024,\n+\t\t\t\t\tsai_notification_file_upload_cb, pss);\n+\t\t\tif (!pss-\u003espa) {\n+\t\t\t\tlwsl_err(\u0022failed to create spa\u005cn\u0022);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\n+\t\t\t/* find out the hmac used to sign it */\n+\n+\t\t\tpss-\u003ehmac_type \u003d LWS_GENHMAC_TYPE_UNKNOWN;\n+\t\t\tfor (n \u003d 0; n \u003c (int)LWS_ARRAY_SIZE(hmac_names); n++)\n+\t\t\t\tif (!strncmp(pss-\u003enotification_sig,\n+\t\t\t\t\t hmac_names[n],\n+\t\t\t\t\t strlen(hmac_names[n]))) {\n+\t\t\t\t\tpss-\u003ehmac_type \u003d n + 1;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\n+\t\t\tif (pss-\u003ehmac_type \u003d\u003d LWS_GENHMAC_TYPE_UNKNOWN) {\n+\t\t\t\tlwsl_notice(\u0022%s: unknown sig hash type\u005cn\u0022,\n+\t\t\t\t\t\t__func__);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\n+\t\t\t/* convert it to binary */\n+\n+\t\t\tn \u003d lws_hex_to_byte_array(\n+\t\t\t\tpss-\u003enotification_sig + strlen(hmac_names[n]),\n+\t\t\t\t(uint8_t *)pss-\u003enotification_sig, 64);\n+\n+\t\t\tif (n !\u003d (int)lws_genhmac_size(pss-\u003ehmac_type)) {\n+\t\t\t\tlwsl_notice(\u0022%s: notifcation hash bad length\u005cn\u0022,\n+\t\t\t\t\t\t__func__);\n+\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\t\t}\n+\n+spa_process:\n+\n+\t\t/* let it parse the POST data */\n+\n+\t\tif (!pss-\u003espa_failed \u0026\u0026\n+\t\t lws_spa_process(pss-\u003espa, in, (int)len))\n+\t\t\t/*\n+\t\t\t * mark it as failed, and continue taking body until\n+\t\t\t * completion, and return error there\n+\t\t\t */\n+\t\t\tpss-\u003espa_failed \u003d 1;\n+\n+\t\tbreak;\n+\n+\tcase LWS_CALLBACK_HTTP_BODY_COMPLETION:\n+\t\tlwsl_user(\u0022%s: LWS_CALLBACK_HTTP_BODY_COMPLETION: %d\u005cn\u0022,\n+\t\t\t __func__, (int)len);\n+\n+\t\tif (!pss-\u003eour_form \u0026\u0026 !pss-\u003elogin_form) {\n+\t\t\tlwsl_user(\u0022%s: no sai form\u005cn\u0022, __func__);\n+\t\t\tgoto passthru;\n+\t\t}\n+\n+\t\tif (pss-\u003espa) {\n+\t\t\tlws_spa_finalize(pss-\u003espa);\n+\t\t\tlws_spa_destroy(pss-\u003espa);\n+\t\t\tpss-\u003espa \u003d NULL;\n+\t\t}\n+\n+\t\tif (pss-\u003espa_failed)\n+\t\t\tlwsl_notice(\u0022%s: notification failed\u005cn\u0022, __func__);\n+\t\telse {\n+\t\t\tlwsl_notice(\u0022%s: notification: %d %s %s %s\u005cn\u0022, __func__,\n+\t\t\t\t pss-\u003esn.action, pss-\u003esn.e.hash,\n+\t\t\t\t pss-\u003esn.e.ref, pss-\u003esn.e.repo_name);\n+\n+\t\t\t/*\n+\t\t\t * Inform sai-webs about notification processing, so\n+\t\t\t * they can update connected browsers to show the new\n+\t\t\t * event\n+\t\t\t */\n+\n+\t\t\tsais_websrv_broadcast(vhd-\u003eh_ss_websrv,\n+\t\t\t\t\t\u0022{\u005c\u0022schema\u005c\u0022:\u005c\u0022sai-overview\u005c\u0022}\u0022, 25);\n+\t\t}\n+\n+\t\tif (lws_return_http_status(wsi,\n+\t\t\t\tpss-\u003espa_failed ? HTTP_STATUS_FORBIDDEN :\n+\t\t\t\t\t\t HTTP_STATUS_OK,\n+\t\t\t\tNULL) \u003c 0)\n+\t\t\treturn -1;\n+\t\treturn 0;\n+\n+\t/*\n+\t * ws connections from builders and browsers\n+\t */\n+\n+\tcase LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:\n+\t\tn \u003d lws_hdr_copy(wsi, (char *)buf, sizeof(buf) - 1,\n+\t\t\t\t WSI_TOKEN_GET_URI);\n+\t\tif (!n)\n+\t\t\tbuf[0] \u003d '\u005c0';\n+\t\t//lwsl_notice(\u0022%s: checking with lwsgs for ws conn: %s\u005cn\u0022,\n+\t\t//\t __func__, (const char *)buf);\n+\n+\t\t/*\n+\t\t * Builders don't authenticate using sessions...\n+\t\t */\n+\n+\t\tif (n \u003e\u003d 8 \u0026\u0026 !strncmp((const char *)buf + n - 8,\n+\t\t\t\t\t\u0022/builder\u0022, 8))\n+\t\t\treturn 0;\n+\n+\t\treturn 0;\n+\n+\tcase LWS_CALLBACK_ESTABLISHED:\n+\t\tpss-\u003ewsi \u003d wsi;\n+\t\tpss-\u003evhd \u003d vhd;\n+\t\tif (!vhd)\n+\t\t\treturn -1;\n+\t\tpss-\u003ealang[0] \u003d '\u005c0';\n+\t\tlws_hdr_copy(wsi, pss-\u003ealang, sizeof(pss-\u003ealang),\n+\t\t\t WSI_TOKEN_HTTP_ACCEPT_LANGUAGE);\n+\n+\t\tif (lws_hdr_total_length(wsi, WSI_TOKEN_GET_URI)) {\n+\t\t\tif (lws_hdr_copy(wsi, (char *)start, 64,\n+\t\t\t\t\t WSI_TOKEN_GET_URI) \u003c 0)\n+\t\t\t\treturn -1;\n+\t\t}\n+#if defined(LWS_ROLE_H2)\n+\t\telse\n+\t\t\tif (lws_hdr_copy(wsi, (char *)start, 64,\n+\t\t\t\t\t WSI_TOKEN_HTTP_COLON_PATH) \u003c 0)\n+\t\t\t\treturn -1;\n+#endif\n+\n+\t\tif (!memcmp((char *)start, \u0022/sai\u0022, 4))\n+\t\t\tstart +\u003d 4;\n+\n+\t\tif (!strcmp((char *)start, \u0022/builder\u0022)) {\n+\t\t\tlwsl_info(\u0022%s: ESTABLISHED: builder\u005cn\u0022, __func__);\n+\t\t\tpss-\u003ewsi \u003d wsi;\n+\t\t\t/*\n+\t\t\t * this adds our pss part, but not the logical builder\n+\t\t\t * yet, until we get the ws rx\n+\t\t\t */\n+\t\t\tlws_dll2_add_head(\u0026pss-\u003esame, \u0026vhd-\u003ebuilders);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tlwsl_err(\u0022%s: unknown URL '%s'\u005cn\u0022, __func__, start);\n+\n+\t\treturn -1;\n+\n+\tcase LWS_CALLBACK_CLOSED:\n+\t\tlwsac_free(\u0026pss-\u003equery_ac);\n+\n+\t\tlwsl_user(\u0022%s: CLOSED builder conn\u005cn\u0022, __func__);\n+\t\t/* remove pss from vhd-\u003ebuilders */\n+\t\tlws_dll2_remove(\u0026pss-\u003esame);\n+\n+\t\t/*\n+\t\t * Destroy any the builder-tracking objects that\n+\t\t * were using this departing connection\n+\t\t */\n+\n+\t\tlws_start_foreach_dll_safe(struct lws_dll2 *, p, p1,\n+\t\t\t\t vhd-\u003eserver.builder_owner.head) {\n+\t\t\tsai_plat_t *cb \u003d lws_container_of(p, sai_plat_t,\n+\t\t\t\t\t\t\t sai_plat_list);\n+\n+\t\t\tif (cb-\u003ewsi \u003d\u003d wsi) {\n+\t\t\t\t/* remove builder object itself from server list */\n+\t\t\t\tcb-\u003ewsi \u003d NULL;\n+\t\t\t\tlws_dll2_remove(\u0026cb-\u003esai_plat_list);\n+\t\t\t\t/*\n+\t\t\t\t * free the deserialized builder object,\n+\t\t\t\t * everything he pointed to was overallocated\n+\t\t\t\t * when his deep copy was made\n+\t\t\t\t */\n+\t\t\t\tfree(cb);\n+\t\t\t}\n+\n+\t\t} lws_end_foreach_dll_safe(p, p1);\n+\n+\t\tif (pss-\u003eblob_artifact) {\n+\t\t\tsqlite3_blob_close(pss-\u003eblob_artifact);\n+\t\t\tpss-\u003eblob_artifact \u003d NULL;\n+\t\t}\n+\n+\t\tif (pss-\u003epdb_artifact) {\n+\t\t\tsais_event_db_close(pss-\u003evhd, \u0026pss-\u003epdb_artifact);\n+\t\t\tpss-\u003epdb_artifact \u003d NULL;\n+\t\t}\n+\n+\t\t/*\n+\t\t * Update the sai-webs about the builder removal, so they\n+\t\t * can update their connected browsers\n+\t\t */\n+\t\tsais_list_builders(vhd);\n+\t\tbreak;\n+\n+\tcase LWS_CALLBACK_RECEIVE:\n+\n+\t\tif (!pss-\u003evhd)\n+\t\t\tpss-\u003evhd \u003d vhd;\n+\n+\t\tlwsl_info(\u0022SWT_BUILDER RX: %d\u005cn\u0022, (int)len);\n+\t\t/*\n+\t\t * Builder sent us something on websockets\n+\t\t */\n+\t\tpss-\u003ewsi \u003d wsi;\n+\t\tif (sais_ws_json_rx_builder(vhd, pss, in, len))\n+\t\t\treturn -1;\n+\t\tif (!pss-\u003eannounced) {\n+\n+\t\t\t/*\n+\t\t\t * Update the sai-webs about the builder removal, so they\n+\t\t\t * can update their connected browsers\n+\t\t\t */\n+\t\t\tsais_list_builders(vhd);\n+\n+\t\t\tpss-\u003eannounced \u003d 1;\n+\t\t}\n+\t\tbreak;\n+\n+\tcase LWS_CALLBACK_SERVER_WRITEABLE:\n+\t\tif (!vhd) {\n+\t\t\tlwsl_notice(\u0022%s: no vhd\u005cn\u0022, __func__);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\treturn sais_ws_json_tx_builder(vhd, pss, buf, sizeof(buf));\n+\n+\tdefault:\n+passthru:\n+\t//\tif (!pss || !vhd)\n+\t\t\tbreak;\n+\n+\t//\treturn vhd-\u003egsp-\u003ecallback(wsi, reason, pss-\u003epss_gs, in, len);\n+\t}\n+\n+\treturn lws_callback_http_dummy(wsi, reason, user, in, len);\n+\n+bail:\n+\treturn 1;\n+\n+try_to_reuse:\n+\tif (lws_http_transaction_completed(wsi))\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+const struct lws_protocols protocol_ws \u003d\n+\t{ \u0022com-warmcat-sai\u0022, callback_ws, sizeof(struct pss), 0 };\ndiff --git a/src/server/s-conf.c b/src/server/s-conf.c\nnew file mode 100644\nindex 0000000..c03a236\n--- /dev/null\n+++ b/src/server/s-conf.c\n@@ -0,0 +1,86 @@\n+/*\n+ * Sai server src/server/conf.c\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ */\n+#include \u003clibwebsockets.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003ctime.h\u003e\n+\n+#define SAI_CONFIG_STRING_SIZE (16 * 1024)\n+\n+extern struct lws_context *context;\n+\n+struct lws_context *\n+sai_lws_context_from_json(const char *config_dir,\n+\t\t\t struct lws_context_creation_info *info,\n+\t\t\t const struct lws_protocols **pprotocols,\n+\t\t\t const char *jpol)\n+{\n+\tint cs_len \u003d SAI_CONFIG_STRING_SIZE - 1;\n+\tstruct lws_context *context;\n+\tchar *cs, *config_strings;\n+\n+\tcs \u003d config_strings \u003d malloc(SAI_CONFIG_STRING_SIZE);\n+\tif (!config_strings) {\n+\t\tlwsl_err(\u0022Unable to allocate config strings heap\u005cn\u0022);\n+\n+\t\treturn NULL;\n+\t}\n+\n+\tmemset(info, 0, sizeof(*info));\n+\n+\tinfo-\u003eexternal_baggage_free_on_destroy \u003d config_strings;\n+\tinfo-\u003ept_serv_buf_size \u003d 8192;\n+\tinfo-\u003eoptions \u003d LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT |\n+\t\t LWS_SERVER_OPTION_EXPLICIT_VHOSTS |\n+\t\t LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE |\n+\t\t LWS_SERVER_OPTION_VALIDATE_UTF8;\n+\tinfo-\u003epss_policies_json \u003d jpol;\n+\n+\tlwsl_notice(\u0022Using config dir: \u005c\u0022%s\u005c\u0022\u005cn\u0022, config_dir);\n+\n+\t/*\n+\t * first go through the config for creating the outer context\n+\t */\n+\tif (lwsws_get_config_globals(info, config_dir, \u0026cs, \u0026cs_len))\n+\t\tgoto init_failed;\n+\n+\tcontext \u003d lws_create_context(info);\n+\tif (context \u003d\u003d NULL) {\n+\t\t/* config_strings freed as 'external baggage' */\n+\t\treturn NULL;\n+\t}\n+\n+\tinfo-\u003epprotocols \u003d pprotocols;\n+\n+\tif (lwsws_get_config_vhosts(context, info, config_dir, \u0026cs, \u0026cs_len)) {\n+\t\tlwsl_err(\u0022%s: sai_lws_context_from_json failed\u005cn\u0022, __func__);\n+\t\tlws_context_destroy(context);\n+\n+\t\treturn NULL;\n+\t}\n+\n+\treturn context;\n+\n+init_failed:\n+\tfree(config_strings);\n+\n+\treturn NULL;\n+}\ndiff --git a/src/server/s-notification.c b/src/server/s-notification.c\nnew file mode 100644\nindex 0000000..315be97\n--- /dev/null\n+++ b/src/server/s-notification.c\n@@ -0,0 +1,988 @@\n+/*\n+ * Sai server - src/server/notification.c\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ *\n+ * The same ws interface is connected-to by builders (on path /builder), and\n+ * provides the query transport for browsers (on path /browse).\n+ */\n+\n+#include \u003clibwebsockets.h\u003e\n+#include \u0022s-private.h\u0022\n+\n+/* starts from +1 of sai_notification_action_t */\n+\n+static const char *notifaction_action_names[] \u003d {\n+\t\u0022repo-update\u0022\n+};\n+\n+static const char * const paths[] \u003d {\n+\t\u0022schema\u0022,\n+\t\u0022action\u0022,\n+\t\u0022repository.name\u0022,\n+\t\u0022repository.fetchurl\u0022,\n+\t\u0022ref\u0022,\n+\t\u0022hash\u0022,\n+\t\u0022nonce\u0022,\n+\t\u0022saifile_len\u0022,\n+\t\u0022saifile\u0022,\n+};\n+\n+enum enum_paths {\n+\tLEJPN_SCHEMA,\n+\tLEJPN_ACTION,\n+\tLEJPN_REPOSITORY_NAME,\n+\tLEJPN_REPOSITORY_FETCHURL,\n+\tLEJPN_REF,\n+\tLEJPN_HASH,\n+\tLEJPN_NONCE,\n+\tLEJPN_SAIFILE_LEN,\n+\tLEJPN_SAIFILE,\n+};\n+\n+/*\n+ * Saifile parser\n+ */\n+\n+static const char * const saifile_paths[] \u003d {\n+\t\u0022schema\u0022,\n+\t\u0022platforms.*.build\u0022,\n+\t\u0022platforms.*.default\u0022,\n+\t\u0022platforms.*\u0022,\n+\t\u0022configurations.*.prep\u0022,\n+\t\u0022configurations.*.cmake\u0022,\n+\t\u0022configurations.*.deps\u0022,\n+\t\u0022configurations.*.platforms\u0022,\n+\t\u0022configurations.*.artifacts\u0022,\n+\t\u0022configurations.*.cpack\u0022,\n+\t\u0022configurations.*\u0022,\n+};\n+\n+enum enum_saifile_paths {\n+\tLEJPNSAIF_SCHEMA,\n+\tLEJPNSAIF_PLAT_BUILD,\n+\tLEJPNSAIF_PLAT_DEFAULT,\n+\tLEJPNSAIF_PLAT_NAME,\n+\tLEJPNSAIF_CONFIGURATIONS_PREP,\n+\tLEJPNSAIF_CONFIGURATIONS_CMAKE,\n+\tLEJPNSAIF_CONFIGURATIONS_DEPS,\n+\tLEJPNSAIF_CONFIGURATIONS_PLATFORMS,\n+\tLEJPNSAIF_CONFIGURATIONS_ARTIFACTS,\n+\tLEJPNSAIF_CONFIGURATIONS_CPACK,\n+\tLEJPNSAIF_CONFIGURATIONS_NAME,\n+};\n+\n+/* do the string subst for ${cmake} etc */\n+\n+static int\n+exp_cmake(void *priv, const char *name, char *out, size_t *pos, size_t olen,\n+\t size_t *exp_ofs)\n+{\n+\tsai_notification_t *sn \u003d (sai_notification_t *)priv;\n+\tconst char *replace \u003d NULL;\n+\tsize_t replace_len, rem_out;\n+\n+\tif (!strcmp(name, \u0022prep\u0022)) {\n+\t\treplace \u003d sn-\u003et.prep;\n+\t\treplace_len \u003d strlen(sn-\u003et.prep);\n+\t\tgoto expand;\n+\t}\n+\n+\tif (!strcmp(name, \u0022cmake\u0022)) {\n+\t\treplace \u003d sn-\u003et.cmake;\n+\t\treplace_len \u003d strlen(sn-\u003et.cmake);\n+\t\tgoto expand;\n+\t}\n+\n+\tif (!strcmp(name, \u0022cpack\u0022)) {\n+\t\treplace \u003d sn-\u003et.cpack;\n+\t\treplace_len \u003d strlen(sn-\u003et.cpack);\n+\t\tgoto expand;\n+\t}\n+\n+\treturn LSTRX_FATAL_NAME_UNKNOWN;\n+\n+expand:\n+\trem_out \u003d olen - *pos;\t\t/* remaining rhs */\n+\treplace_len -\u003d *exp_ofs;\n+\tif (replace_len \u003c rem_out)\n+\t\trem_out \u003d replace_len;\n+\n+\tmemcpy(out + *pos, replace + (*exp_ofs), rem_out);\n+\t*exp_ofs +\u003d rem_out;\n+\t*pos +\u003d rem_out;\n+\n+\tif (rem_out \u003d\u003d replace_len)\n+\t\treturn LSTRX_DONE;\n+\n+\treturn LSTRX_FILLED_OUT;\n+}\n+\n+static int\n+arg_to_bool(const char *s)\n+{\n+\tstatic const char * const on[] \u003d { \u0022on\u0022, \u0022yes\u0022, \u0022true\u0022 };\n+\tint n \u003d atoi(s);\n+\n+\tif (n)\n+\t\treturn 1;\n+\n+\tfor (n \u003d 0; n \u003c (int)LWS_ARRAY_SIZE(on); n++)\n+\t\tif (!strcasecmp(s, on[n]))\n+\t\t\treturn 1;\n+\n+\treturn 0;\n+}\n+\n+static int\n+sai_tuple_element_compare(const char *e1, const char *e2)\n+{\n+\tchar *p;\n+\n+\t// lwsl_notice(\u0022%s: comp '%s' '%s'\u005cn\u0022, __func__, e1, e2);\n+\n+\tif (strlen(e1) \u003e strlen(e2))\n+\t\t/* can't match if req is longer than plat */\n+\t\treturn -1;\n+\n+\tp \u003d strchr(e2, '/');\n+\tif (p \u0026\u0026 lws_ptr_diff(p, e2) \u003c (int)strlen(e1))\n+\t\t/* e2 contains a / section terminal inside e1 scope */\n+\t\treturn 1;\n+\n+\treturn strncmp(e1, e2, strlen(e1));\n+}\n+\n+static int\n+sai_tuple_compare(const char *req, int req_len, const char *plat)\n+{\n+\tconst char *pos \u003d req;\n+\tchar e1[96];\n+\tint n;\n+\n+\tdo {\n+\t\tn \u003d 0;\n+\t\twhile (n \u003c (int)sizeof(e1) - 1 \u0026\u0026 req_len \u0026\u0026 *pos !\u003d '/') {\n+\t\t\te1[n++] \u003d *pos++;\n+\t\t\treq_len--;\n+\t\t}\n+\t\te1[n] \u003d '\u005c0';\n+\t\tif (*pos \u003d\u003d '/' \u0026\u0026 req_len) {\n+\t\t\treq_len--;\n+\t\t\tpos++;\n+\t\t}\n+\n+\t\tif (n \u003d\u003d sizeof(e1) - 1) {\n+\t\t\t// lwsl_notice(\u0022%s: NOMATCH 3 '%s' '%s'\u005cn\u0022, __func__, req, plat);\n+\t\t\t/* element too long */\n+\t\t\treturn 1;\n+\t\t}\n+\n+\t\tif (n \u0026\u0026 /* let it pass if empty match section, eg, linux//gcc */\n+\t\t sai_tuple_element_compare(e1, plat)) {\n+\t\t\t// lwsl_notice(\u0022%s: NOMATCH 2 '%s' '%s'\u005cn\u0022, __func__, req, plat);\n+\t\t\t/* section does not match */\n+\t\t\treturn 1;\n+\t\t}\n+\n+\t\twhile (*plat \u0026\u0026 *plat !\u003d '/')\n+\t\t\tplat++;\n+\t\tif (*plat \u003d\u003d '/')\n+\t\t\tplat++;\n+\n+\t\tif (!*plat \u0026\u0026 req_len) {\n+\t\t\t// lwsl_notice(\u0022%s: NOMATCH 1 '%s' '%s'\u005cn\u0022, __func__, req, plat);\n+\t\t\t/* FAIL: req had more than we had */\n+\t\t\treturn 1;\n+\t\t}\n+\n+\t\tif (!req_len) {\n+\t\t\t// lwsl_notice(\u0022%s: MATCH '%s' '%s'\u005cn\u0022, __func__, req, plat);\n+\t\t\t/* MATCH: we matched at least as far as we had */\n+\t\t\treturn 0;\n+\t\t}\n+\n+\t} while (1);\n+\n+\treturn 1;\n+}\n+\n+/*\n+ * We parse the saifile JSON\n+ */\n+\n+static signed char\n+sai_saifile_lejp_cb(struct lejp_ctx *ctx, char reason)\n+{\n+\tstruct pss *pss \u003d (struct pss *)ctx-\u003euser;\n+\tsai_notification_t *sn \u003d (sai_notification_t *)\u0026pss-\u003esn;\n+\tsqlite3 *pdb \u003d NULL;\n+\tsize_t n;\n+\n+\t// lwsl_notice(\u0022%s: reason %d, %s\u005cn\u0022, __func__, reason, ctx-\u003epath);\n+\n+\tif (reason \u003d\u003d LEJPCB_COMPLETE) {\n+\n+\t\tif (!pss-\u003edry)\n+\t\t\tlwsl_notice(\u0022%s: saifile decode completed\u005cn\u0022, __func__);\n+\n+\t\treturn 0;\n+\t}\n+\n+\tif (reason \u003d\u003d LEJPCB_OBJECT_START \u0026\u0026\n+\t ctx-\u003epath_match - 1 \u003d\u003d LEJPNSAIF_CONFIGURATIONS_NAME) {\n+\t\t/*\n+\t\t * We're at the testname part of \u0022testname\u0022 : { }\n+\t\t */\n+\t\tlws_strncpy(sn-\u003et.taskname, \u0026ctx-\u003epath[15],\n+\t\t\t sizeof(sn-\u003et.taskname));\n+\t\tsn-\u003et.prep[0] \u003d '\u005c0';\n+\t\tsn-\u003et.packages[0] \u003d '\u005c0';\n+\t\tsn-\u003et.cmake[0] \u003d '\u005c0';\n+\t\tsn-\u003et.artifacts[0] \u003d '\u005c0';\n+\t\tsn-\u003eexplicit_platforms[0] \u003d '\u005c0';\n+\t\treturn 0;\n+\t}\n+\n+\tif (reason \u003d\u003d LEJPCB_OBJECT_END \u0026\u0026\n+\t ctx-\u003epath_match - 1 \u003d\u003d LEJPNSAIF_CONFIGURATIONS_NAME \u0026\u0026\n+\t sn-\u003et.taskname[0]) {\n+\t\tlws_dll2_owner_t owner;\n+\t\tchar *err;\n+\n+\t\t/*\n+\t\t * We're at the testname part of \u0022testname\u0022 : { }\n+\t\t */\n+\t\tif (pss-\u003edry) {\n+\t\t\tsn-\u003et.taskname[0] \u003d '\u005c0';\n+\n+\t\t\treturn 0;\n+\t\t}\n+\n+\t\t/*\n+\t\t * Iterate through each platform creating task entries for this\n+\t\t * configuration customized for each platform... first time just\n+\t\t * check the string subst for all of them is OK...\n+\t\t */\n+\n+\t\tlwsl_notice(\u0022%s: Creating task entries for notification\u005cn\u0022, __func__);\n+\n+\t\tlws_start_foreach_dll(struct lws_dll2 *, p,\n+\t\t\t\t\t pss-\u003eplatform_owner.head) {\n+\t\t\tsai_platform_t *pl \u003d\n+\t\t\t\tlws_container_of(p, sai_platform_t, list);\n+\t\t\tsize_t used_in, used_out;\n+\t\t\tstruct lws_tokenize ts;\n+\t\t\tint n, match \u003d 0;\n+\t\t\tlws_strexp_t sx;\n+\n+\t\t\tif (!pl-\u003enondefault)\n+\t\t\t\t/*\n+\t\t\t\t * If it's a default platform, then match\n+\t\t\t\t * by default\n+\t\t\t\t */\n+\t\t\t\tmatch \u003d 1;\n+\n+\t\t\tif (sn-\u003eexplicit_platforms[0]) {\n+\t\t\t\tchar not \u003d 0;\n+\n+\t\t\t\t/*\n+\t\t\t\t * If the configuration gives explicit\n+\t\t\t\t * platforms, we have to filter the platform\n+\t\t\t\t * against the comma-separated list.\n+\t\t\t\t *\n+\t\t\t\t * \u0022none\u0022 - don't match any plats just because\n+\t\t\t\t *\t they are default\n+\t\t\t\t *\n+\t\t\t\t * \u0022not plat\u0022 - disallow specific plat \u0022plat\u0022,\n+\t\t\t\t *\t\timplies you're not using \u0022none\u0022\n+\t\t\t\t *\n+\t\t\t\t * \u0022plat\u0022 - allow specific plat \u0022plat\u0022\n+\t\t\t\t */\n+\t\t\t\tmemset(\u0026ts, 0, sizeof(ts));\n+\t\t\t\tts.start \u003d (char *)sn-\u003eexplicit_platforms;\n+\t\t\t\tts.len \u003d strlen(ts.start);\n+\t\t\t\tts.flags \u003d LWS_TOKENIZE_F_DOT_NONTERM |\n+\t\t\t\t\t LWS_TOKENIZE_F_SLASH_NONTERM |\n+\t\t\t\t\t LWS_TOKENIZE_F_MINUS_NONTERM;\n+\t\t\t\tdo {\n+\t\t\t\t\tts.e \u003d lws_tokenize(\u0026ts);\n+\t\t\t\t\tif (ts.e !\u003d LWS_TOKZE_TOKEN)\n+\t\t\t\t\t\tcontinue;\n+\n+\t\t\t//\t\tlwsl_notice(\u0022%s: check %.*s\u005cn\u0022, __func__,\n+\t\t\t//\t\t\t (int)ts.token_len, ts.token);\n+\n+\t\t\t\t\tif (!strncmp(ts.token, \u0022none\u0022,\n+\t\t\t\t\t\t ts.token_len)) {\n+\t\t\t\t\t\tnot \u003d match \u003d 0;\n+\t\t\t\t\t\tcontinue;\n+\t\t\t\t\t}\n+\t\t\t\t\tif (!strncmp(ts.token, \u0022not\u0022,\n+\t\t\t\t\t\t ts.token_len)) {\n+\t\t\t\t\t\tnot \u003d 1;\n+\t\t\t\t\t\tcontinue;\n+\t\t\t\t\t}\n+\t\t\t\t\t/*\n+\t\t\t\t\t * We need to check with per-slash\n+\t\t\t\t\t * minimal matching, ie, \u0022linux\u0022\n+\t\t\t\t\t * matches \u0022linux-ubuntu/x86_64/gcc\u0022\n+\t\t\t\t\t */\n+\t\t\t\t\tif (!sai_tuple_compare(ts.token,\n+\t\t\t\t\t\t ts.token_len, pl-\u003ename)) {\n+\t\t\t\t\t\tmatch \u003d !not;\n+\t\t\t\t\t\tif (match)\n+\t\t\t\t\t\t\tbreak;\n+\t\t\t\t\t}\n+\t\t\t\t\tnot \u003d 0;\n+\t\t\t\t} while (ts.e \u003e 0);\n+\t\t\t}\n+\n+\t\t\tif (match) {\n+\n+\t\t\t\t/*\n+\t\t\t\t * The platform build string (pl-\u003ebuild) is the\n+\t\t\t\t * base, with optional entries like ${cmake}\n+\t\t\t\t * that are filled in with the corresponding\n+\t\t\t\t * info from the specific configuration's\n+\t\t\t\t * \u0022cmake\u0022 entry (sn-\u003et.cmake)...\n+\t\t\t\t *\n+\t\t\t\t * sn-\u003et.build becomes the string-substituted\n+\t\t\t\t * copy that is serialized\n+\t\t\t\t */\n+\n+\t\t\t\tlws_strexp_init(\u0026sx, sn, exp_cmake, sn-\u003et.build,\n+\t\t\t\t\t\tsizeof(sn-\u003et.build));\n+\n+\t\t\t\tn \u003d lws_strexp_expand(\u0026sx, pl-\u003ebuild,\n+\t\t\t\t\t\t strlen(pl-\u003ebuild),\n+\t\t\t\t\t\t \u0026used_in, \u0026used_out);\n+\t\t\t\tif (n !\u003d LSTRX_DONE) {\n+\t\t\t\t\tlwsl_notice(\u0022%s: strsubst fail n\u003d%d %s %s\u005cn\u0022,\n+\t\t\t\t\t\t __func__, n, pl-\u003ebuild,\n+\t\t\t\t\t\t sn-\u003et.cmake);\n+\t\t\t\t\treturn -1;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t} lws_end_foreach_dll(p);\n+\n+\t\t/*\n+\t\t * ...now we know the string handling will go well, create the\n+\t\t * event database file and create task entries in there for this\n+\t\t * configuration's tasks for each platform\n+\t\t */\n+\n+\t\tif (sais_event_db_ensure_open(pss-\u003evhd, pss-\u003esn.e.uuid, 1, \u0026pdb)) {\n+\t\t\tlwsl_err(\u0022%s: unable to open event-specific db\u005cn\u0022, __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tsqlite3_exec(pdb, \u0022BEGIN TRANSACTION\u0022, NULL, NULL, \u0026err);\n+\t\tif (err)\n+\t\t\tsqlite3_free(err);\n+\n+\t\tlws_start_foreach_dll(struct lws_dll2 *, p,\n+\t\t\t\t\t pss-\u003eplatform_owner.head) {\n+\t\t\tsai_platform_t *pl \u003d\n+\t\t\t\tlws_container_of(p, sai_platform_t, list);\n+\t\t\tsize_t used_in, used_out;\n+\t\t\tstruct lws_tokenize ts;\n+\t\t\tint n, match \u003d 0;\n+\t\t\tlws_strexp_t sx;\n+\n+\t\t\tif (!pl-\u003enondefault)\n+\t\t\t\t/*\n+\t\t\t\t * If it's a nondefault platform, then match\n+\t\t\t\t * by default\n+\t\t\t\t */\n+\t\t\t\tmatch \u003d 1;\n+\n+\t\t\tif (sn-\u003eexplicit_platforms[0]) {\n+\t\t\t\tchar not \u003d 0;\n+\n+\t\t\t\t/*\n+\t\t\t\t * If the configuration gives explicit\n+\t\t\t\t * platforms, we have to filter the platform\n+\t\t\t\t * against the comma-separated list.\n+\t\t\t\t *\n+\t\t\t\t * \u0022none\u0022 - don't match any plats just because\n+\t\t\t\t *\t they are default\n+\t\t\t\t *\n+\t\t\t\t * \u0022not plat\u0022 - disallow specific plat \u0022plat\u0022,\n+\t\t\t\t *\t\timplies you're not using \u0022none\u0022\n+\t\t\t\t *\n+\t\t\t\t * \u0022plat\u0022 - allow specific plat \u0022plat\u0022\n+\t\t\t\t */\n+\n+\t\t\t\tmemset(\u0026ts, 0, sizeof(ts));\n+\t\t\t\tts.start \u003d (char *)sn-\u003eexplicit_platforms;\n+\t\t\t\tts.len \u003d strlen(ts.start);\n+\t\t\t\tts.flags \u003d LWS_TOKENIZE_F_DOT_NONTERM |\n+\t\t\t\t\t LWS_TOKENIZE_F_SLASH_NONTERM |\n+\t\t\t\t\t LWS_TOKENIZE_F_MINUS_NONTERM;\n+\n+\t\t\t\tdo {\n+\t\t\t\t\tts.e \u003d lws_tokenize(\u0026ts);\n+\t\t\t\t\tif (ts.e !\u003d LWS_TOKZE_TOKEN)\n+\t\t\t\t\t\tcontinue;\n+\n+\t\t\t//\t\tlwsl_notice(\u0022%s: check %.*s\u005cn\u0022, __func__,\n+\t\t\t//\t\t\t (int)ts.token_len, ts.token);\n+\n+\t\t\t\t\tif (!strncmp(ts.token, \u0022none\u0022,\n+\t\t\t\t\t\t ts.token_len)) {\n+\t\t\t\t\t\tnot \u003d match \u003d 0;\n+\t\t\t\t\t\tcontinue;\n+\t\t\t\t\t}\n+\t\t\t\t\tif (!strncmp(ts.token, \u0022not\u0022,\n+\t\t\t\t\t\t ts.token_len)) {\n+\t\t\t\t\t\tnot \u003d 1;\n+\t\t\t\t\t\tcontinue;\n+\t\t\t\t\t}\n+\t\t\t\t\tif (!sai_tuple_compare(ts.token,\n+\t\t\t\t\t\t ts.token_len, pl-\u003ename)) {\n+\t\t\t\t\t\tmatch \u003d !not;\n+\t\t\t\t\t\tif (match)\n+\t\t\t\t\t\t\tbreak;\n+\t\t\t\t\t}\n+\t\t\t\t\tnot \u003d 0;\n+\t\t\t\t} while (ts.e \u003e 0);\n+\t\t\t}\n+\n+\t\t\tif (match) {\n+\t\t\t\t/*\n+\t\t\t\t * For this platform, we want to create a task\n+\t\t\t\t * associated with this event. Tasks and logs\n+\t\t\t\t * associated with an event go in an event-\n+\t\t\t\t * specific database file for scalability.\n+\t\t\t\t */\n+\n+\t\t\t\tlws_strexp_init(\u0026sx, sn, exp_cmake, sn-\u003et.build,\n+\t\t\t\t\t\tsizeof(sn-\u003et.build));\n+\n+\t\t\t\tn \u003d lws_strexp_expand(\u0026sx, pl-\u003ebuild,\n+\t\t\t\t\t\t strlen(pl-\u003ebuild),\n+\t\t\t\t\t\t \u0026used_in, \u0026used_out);\n+\t\t\t\tif (n !\u003d LSTRX_DONE) {\n+\t\t\t\t\tlwsl_notice(\u0022%s: strsubst failed %s %s\u005cn\u0022,\n+\t\t\t\t\t\t __func__, pl-\u003ebuild,\n+\t\t\t\t\t\t sn-\u003et.cmake);\n+\t\t\t\t\tsqlite3_exec(pdb, \u0022END TRANSACTION\u0022, NULL, NULL, \u0026err);\n+\t\t\t\t\tif (err)\n+\t\t\t\t\t\tsqlite3_free(err);\n+\t\t\t\t\tsais_event_db_close(pss-\u003evhd, \u0026pdb);\n+\t\t\t\t\treturn -1;\n+\t\t\t\t}\n+\n+\t\t\t\t/*\n+\t\t\t\t * Prepare a struct of the task object...\n+\t\t\t\t * task uuid is the event uuid and another\n+\t\t\t\t * random 32 chars, so you can always recover\n+\t\t\t\t * the related event uuid from the task uuid\n+\t\t\t\t */\n+\n+\t\t\t\tmemcpy(pss-\u003esn.t.uuid, pss-\u003esn.e.uuid, 32);\n+\t\t\t\tsai_uuid16_create(lws_get_context(pss-\u003ewsi),\n+\t\t\t\t\t\t pss-\u003esn.t.uuid + 32);\n+\t\t\t\tstrcpy(pss-\u003esn.t.event_uuid, pss-\u003esn.e.uuid);\n+\t\t\t\tpss-\u003esn.t.uid \u003d pss-\u003esn.event_task_index++;\n+\n+\t\t\t\t/*\n+\t\t\t\t * This is basically a secret that anything\n+\t\t\t\t * trying to upload an artifact for the task\n+\t\t\t\t * must provide to authenticate.\n+\t\t\t\t */\n+\t\t\t\tsai_uuid16_create(lws_get_context(pss-\u003ewsi),\n+\t\t\t\t\t\t pss-\u003esn.t.art_up_nonce);\n+\t\t\t\t/*\n+\t\t\t\t * An unrelated secret that anything\n+\t\t\t\t * trying to download an artifact for the task\n+\t\t\t\t * must provide to identify it.\n+\t\t\t\t */\n+\t\t\t\tsai_uuid16_create(lws_get_context(pss-\u003ewsi),\n+\t\t\t\t\t\t pss-\u003esn.t.art_down_nonce);\n+\n+\t\t\t\tpss-\u003esn.t.git_repo_url \u003d\n+\t\t\t\t\t\tpss-\u003esn.e.repo_fetchurl;\n+\t\t\t\tpss-\u003esn.e.last_updated \u003d\n+\t\t\t\t\t(unsigned long long)lws_now_secs();\n+\t\t\t\tpss-\u003esn.e.state \u003d 0;\n+\t\t\t\tlws_strncpy(pss-\u003esn.t.platform, pl-\u003ename,\n+\t\t\t\t\t sizeof(pss-\u003esn.t.platform));\n+\n+\t\t\t\tmemset(\u0026pss-\u003esn.t.list, 0, sizeof(pss-\u003esn.t.list));\n+\t\t\t\tlws_dll2_owner_clear(\u0026owner);\n+\t\t\t\tlws_dll2_add_head(\u0026pss-\u003esn.t.list, \u0026owner);\n+\n+\t\t\t\t/*\n+\t\t\t\t * Create the task in event-specific database\n+\t\t\t\t */\n+\n+\t\t\t\tlws_struct_sq3_serialize(pdb,\n+\t\t\t\t\t\t\t lsm_schema_sq3_map_task,\n+\t\t\t\t\t\t\t \u0026owner, pss-\u003esn.t.uid);\n+\t\t\t}\n+\n+\t\t} lws_end_foreach_dll(p);\n+\n+\t\tsqlite3_exec(pdb, \u0022END TRANSACTION\u0022, NULL, NULL, \u0026err);\n+\t\tif (err)\n+\t\t\tsqlite3_free(err);\n+\n+\t\tsais_event_db_close(pss-\u003evhd, \u0026pdb);\n+\n+//\t\tlwsl_notice(\u0022%s: New test '%s', '%s', '%s'\u005cn\u0022, __func__,\n+//\t\t\t sn-\u003et.taskname, sn-\u003et.cmake, sn-\u003et.packages);\n+\n+\t\tsn-\u003et.taskname[0] \u003d '\u005c0';\n+\t\treturn 0;\n+\t}\n+\n+\tif (reason \u003d\u003d LEJPCB_OBJECT_START \u0026\u0026\n+\t ctx-\u003epath_match - 1 \u003d\u003d LEJPNSAIF_PLAT_NAME) {\n+\t\t/*\n+\t\t * We're at the platformname part of \u0022platformname\u0022 : { }\n+\t\t */\n+\t\tlws_strncpy(sn-\u003eplatname, \u0026ctx-\u003epath[10], sizeof(sn-\u003eplatname));\n+\t\tsn-\u003et.prep[0] \u003d '\u005c0';\n+\t\tsn-\u003et.cmake[0] \u003d '\u005c0';\n+\t\tsn-\u003et.cpack[0] \u003d '\u005c0';\n+\t\tsn-\u003eplatbuild[0] \u003d '\u005c0';\n+\t\tsn-\u003enondefault \u003d 0;\n+\t\treturn 0;\n+\t}\n+\n+\tif (reason \u003d\u003d LEJPCB_OBJECT_END \u0026\u0026\n+\t ctx-\u003epath_match - 1 \u003d\u003d LEJPNSAIF_PLAT_NAME \u0026\u0026\n+\t sn-\u003eplatname[0] \u0026\u0026 sn-\u003eplatbuild[0]) {\n+\t\tsai_platform_t *pl;\n+\t\tuint8_t *plb;\n+\t\tsize_t pnl;\n+\n+\t\tif (pss-\u003edry) {\n+\t\t\tsn-\u003et.platform[0] \u003d '\u005c0';\n+\t\t\treturn 0;\n+\t\t}\n+\n+\t\t/*\n+\t\t * We create a platform object with space for its strings after\n+\t\t */\n+\n+\t\tpnl \u003d strlen(sn-\u003eplatname);\n+\t\tpl \u003d (sai_platform_t *)malloc(sizeof(*pl) + pnl +\n+\t\t\t\t\t strlen(sn-\u003eplatbuild) + 2);\n+\t\tif (!pl)\n+\t\t\treturn -1;\n+\n+\t\tmemset(pl, 0, sizeof(*pl));\n+\t\tplb \u003d (uint8_t *)\u0026pl[1];\n+\n+\t\t/*\n+\t\t * Platforms are malloc'd up and added to pss\n+\t\t * .platform_owner\n+\t\t */\n+\n+\t\tpl-\u003ename \u003d (const char *)plb;\n+\t\tmemcpy(plb, sn-\u003eplatname, pnl + 1);\n+\t\tplb +\u003d pnl + 1;\n+\n+\t\tpl-\u003ebuild \u003d (const char *)plb;\n+\t\tmemcpy(plb, sn-\u003eplatbuild, strlen(sn-\u003eplatbuild) + 1);\n+\n+\t\tpl-\u003enondefault \u003d sn-\u003enondefault;\n+\n+\t\tlws_dll2_add_head(\u0026pl-\u003elist, \u0026pss-\u003eplatform_owner);\n+\n+//\t\tlwsl_notice(\u0022%s: New platform '%s', build '%s', notdefault %d\u005cn\u0022,\n+//\t\t\t __func__, pl-\u003ename, pl-\u003ebuild, pl-\u003enondefault);\n+\n+\t\tsn-\u003eplatbuild[0] \u003d '\u005c0';\n+\t\treturn 0;\n+\t}\n+\n+\t/* we only match on the prepared path strings */\n+\tif (!(reason \u0026 LEJP_FLAG_CB_IS_VALUE) || !ctx-\u003epath_match)\n+\t\treturn 0;\n+\n+//\tif (reason !\u003d LEJPCB_VAL_STR_END)\n+//\t\treturn 0;\n+\n+\t/* only the end part of the string, where we know the length */\n+\n+\tswitch (ctx-\u003epath_match - 1) {\n+\tcase LEJPNSAIF_SCHEMA:\n+\t\tsn-\u003eevent_task_index \u003d 0;\n+\t\treturn 0;\n+\n+\tcase LEJPNSAIF_CONFIGURATIONS_PREP:\n+\t\t/* the additional cmake options for this test configuration */\n+\t\tn \u003d strlen(sn-\u003et.prep);\n+\t\tif (n \u003c sizeof(sn-\u003et.prep) - 2)\n+\t\t\tlws_strnncpy(sn-\u003et.prep + n, ctx-\u003ebuf, ctx-\u003enpos,\n+\t\t\t\t sizeof(sn-\u003et.prep) - n);\n+\t\tbreak;\n+\n+\tcase LEJPNSAIF_CONFIGURATIONS_CMAKE:\n+\t\t/* the additional cmake options for this test configuration */\n+\t\tn \u003d strlen(sn-\u003et.cmake);\n+\t\tif (n \u003c sizeof(sn-\u003et.cmake) - 2)\n+\t\t\tlws_strnncpy(sn-\u003et.cmake + n, ctx-\u003ebuf, ctx-\u003enpos,\n+\t\t\t\t sizeof(sn-\u003et.cmake) - n);\n+\t\tbreak;\n+\n+\tcase LEJPNSAIF_CONFIGURATIONS_DEPS:\n+\t\t/* the necessary dependent package strings */\n+\t\tn \u003d strlen(sn-\u003et.packages);\n+\t\tif (n \u003c sizeof(sn-\u003et.packages) - 2) {\n+\t\t\tif (n)\n+\t\t\t\tsn-\u003et.packages[n++] \u003d ',';\n+\t\t\tlws_strncpy(sn-\u003et.packages + n, ctx-\u003ebuf,\n+\t\t\t\t sizeof(sn-\u003et.packages) - n);\n+\t\t}\n+\t\tbreak;\n+\n+\tcase LEJPNSAIF_CONFIGURATIONS_PLATFORMS:\n+\t\t/* the necessary dependent package strings */\n+\t\tlws_strncpy(sn-\u003eexplicit_platforms, ctx-\u003ebuf,\n+\t\t\t\t sizeof(sn-\u003eexplicit_platforms));\n+\t\tbreak;\n+\n+\tcase LEJPNSAIF_CONFIGURATIONS_ARTIFACTS:\n+\t\tlws_strncpy(sn-\u003et.artifacts, ctx-\u003ebuf, sizeof(sn-\u003et.artifacts));\n+\t\tbreak;\n+\n+\tcase LEJPNSAIF_CONFIGURATIONS_CPACK:\n+\t\tlws_strncpy(sn-\u003et.cpack, ctx-\u003ebuf, sizeof(sn-\u003et.cpack));\n+\t\tbreak;\n+\n+\tcase LEJPNSAIF_PLAT_BUILD:\n+\t\t/*\n+\t\t * The overall build script for this platform\n+\t\t * is appended into the temp sn.platbuild\n+\t\t */\n+\t\tn \u003d strlen(sn-\u003eplatbuild);\n+\t\tif (n \u003c sizeof(sn-\u003eplatbuild) - 2)\n+\t\t\tlws_strnncpy(sn-\u003eplatbuild + n, ctx-\u003ebuf, ctx-\u003enpos,\n+\t\t\t\t sizeof(sn-\u003eplatbuild) - n);\n+\t\tbreak;\n+\n+\tcase LEJPNSAIF_PLAT_DEFAULT:\n+\t\tsn-\u003enondefault \u003d !arg_to_bool(ctx-\u003ebuf);\n+\t\tbreak;\n+\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static signed char\n+sai_notification_lejp_cb(struct lejp_ctx *ctx, char reason)\n+{\n+\tstruct pss *pss \u003d (struct pss *)ctx-\u003euser;\n+\tsai_notification_t *sn \u003d (sai_notification_t *)\u0026pss-\u003esn;\n+\tsize_t ile, ole;\n+\tint n;\n+\n+\t/* we only match on the prepared path strings */\n+\tif (!(reason \u0026 LEJP_FLAG_CB_IS_VALUE) || !ctx-\u003epath_match)\n+\t\treturn 0;\n+\n+//\tif (reason !\u003d LEJPCB_VAL_STR_END)\n+//\t\treturn 0;\n+\n+\t/* only the end part of the string, where we know the length */\n+\n+\tswitch (ctx-\u003epath_match - 1) {\n+\tcase LEJPN_SCHEMA:\n+\t\tif (strcmp(ctx-\u003ebuf, \u0022com-warmcat-sai-notification\u0022)) {\n+\t\t\tlwsl_err(\u0022%s: unknown schema '%s'\u005cn\u0022, __func__, ctx-\u003ebuf);\n+\t\t\treturn -1;\n+\t\t}\n+\t\treturn 0;\n+\n+\tcase LEJPN_ACTION:\n+\t\tfor (n \u003d 0; n \u003c (int)LWS_ARRAY_SIZE(notifaction_action_names);n++)\n+\t\t\tif (!strcmp(ctx-\u003ebuf, notifaction_action_names[n])) {\n+\t\t\t\tsn-\u003eaction \u003d n + 1;\n+\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\t\tlwsl_notice(\u0022%s: unknown action '%s' ignored\u005cn\u0022,\n+\t\t\t __func__, ctx-\u003ebuf);\n+\t\treturn -1;\n+\n+\tcase LEJPN_REPOSITORY_NAME:\n+\t\tlws_strncpy(sn-\u003ee.repo_name, ctx-\u003ebuf, sizeof(sn-\u003ee.repo_name));\n+\t\tbreak;\n+\n+\tcase LEJPN_REPOSITORY_FETCHURL:\n+\t\tlws_strncpy(sn-\u003ee.repo_fetchurl, ctx-\u003ebuf, sizeof(sn-\u003ee.repo_fetchurl));\n+\t\tbreak;\n+\n+\tcase LEJPN_REF:\n+\t\tlws_strncpy(sn-\u003ee.ref, ctx-\u003ebuf, sizeof(sn-\u003ee.ref));\n+\t\tbreak;\n+\n+\tcase LEJPN_HASH:\n+\t\tlws_strncpy(sn-\u003ee.hash, ctx-\u003ebuf, sizeof(sn-\u003ee.hash));\n+\t\tbreak;\n+\n+\tcase LEJPN_NONCE:\n+\t\tbreak;\n+\n+\tcase LEJPN_SAIFILE_LEN:\n+\t\tsn-\u003esaifile_in_len \u003d atoi(ctx-\u003ebuf);\n+\t\t/* only accept sane base64 size */\n+\t\tif (sn-\u003esaifile_in_len \u003c 8 || sn-\u003esaifile_in_len \u003e 65536) {\n+\t\t\tlwsl_err(\u0022%s: bad saifile_len %u\u005cn\u0022, __func__,\n+\t\t\t\t\t(unsigned int)sn-\u003esaifile_in_len);\n+\n+\t\t\treturn -1;\n+\t\t}\n+\t\tsn-\u003esaifile_out_len \u003d (sn-\u003esaifile_in_len * 3) / 4 + 4;\n+\t\tsn-\u003esaifile \u003d malloc(sn-\u003esaifile_out_len);\n+\t\tif (!sn-\u003esaifile) {\n+\t\t\tlwsl_err(\u0022%s: OOM\u005cn\u0022, __func__);\n+\n+\t\t\treturn -1;\n+\t\t}\n+\t\t/*\n+\t\t * Caller must take responsibility for sn-\u003esaifile allocation\n+\t\t */\n+\t\tsn-\u003esaifile_in_seen \u003d sn-\u003esaifile_out_pos \u003d 0;\n+\t\tlws_b64_decode_state_init(\u0026sn-\u003eb64);\n+\t\tbreak;\n+\n+\tcase LEJPN_SAIFILE:\n+\t\t/*\n+\t\t * Base64 encoding of the commit's .sai.json file contents... we\n+\t\t * have to treat this with caution since it can be anything,\n+\t\t * including unparsable JSON.\n+\t\t *\n+\t\t * Let's base64-decode it and collect it into an buffer first,\n+\t\t * before sn test parse and then sn second parse to extract the\n+\t\t * tasks\n+\t\t */\n+\n+\t\tsn-\u003esaifile_in_seen +\u003d ctx-\u003enpos;\n+\t\tif (sn-\u003esaifile_in_seen \u003e sn-\u003esaifile_in_len) {\n+\t\t\tlwsl_err(\u0022%s: SAIFILE: too large in %u vs %u\u005cn\u0022, __func__,\n+\t\t\t\t (unsigned int)sn-\u003esaifile_in_seen,\n+\t\t\t\t (unsigned int)sn-\u003esaifile_in_len);\n+\t\t\treturn -1;\n+\t\t}\n+\t\tile \u003d ctx-\u003enpos;\n+\t\tole \u003d sn-\u003esaifile_out_len - sn-\u003esaifile_out_pos;\n+\t\tlws_b64_decode_stateful(\u0026sn-\u003eb64, ctx-\u003ebuf, \u0026ile,\n+\t\t\t\t(uint8_t *)sn-\u003esaifile + sn-\u003esaifile_out_pos, \u0026ole,\n+\t\t\t\t\tsn-\u003esaifile_in_seen \u003d\u003d sn-\u003esaifile_in_len);\n+\n+\t\tsn-\u003esaifile_out_pos +\u003d ole;\n+\t\tbreak;\n+\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+int\n+sai_notification_file_upload_cb(void *data, const char *name,\n+\t\t\t\tconst char *filename, char *buf, int len,\n+\t\t\t\tenum lws_spa_fileupload_states state)\n+{\n+\tstruct pss *pss \u003d (struct pss *)data;\n+\tstruct lejp_ctx saictx;\n+\tlws_dll2_owner_t owner;\n+\tuint8_t result[64];\n+\tint m;\n+\n+\tswitch (state) {\n+\tcase LWS_UFS_OPEN:\n+\t\tlwsl_notice(\u0022%s: LWS_UFS_OPEN\u005cn\u0022, __func__);\n+\t\tlejp_construct(\u0026pss-\u003ectx, sai_notification_lejp_cb,\n+\t\t\t pss, paths, LWS_ARRAY_SIZE(paths));\n+\n+\t\tif (lws_genhmac_init(\u0026pss-\u003ehmac, pss-\u003ehmac_type,\n+\t\t\t\t (uint8_t *)pss-\u003evhd-\u003enotification_key,\n+\t\t\t\t strlen(pss-\u003evhd-\u003enotification_key))) {\n+\t\t\tlwsl_err(\u0022%s: failed to init hmac\u005cn\u0022, __func__);\n+\n+\t\t\treturn -1;\n+\t\t}\n+\t\tbreak;\n+\tcase LWS_UFS_FINAL_CONTENT:\n+\tcase LWS_UFS_CONTENT:\n+\t\tlwsl_notice(\u0022%s: LWS_UFS_[]CONTENT: %p %p, \u005cn\u0022, __func__, pss, buf);\n+\t\tif (len \u0026\u0026 lws_genhmac_update(\u0026pss-\u003ehmac, buf, len))\n+\t\t\treturn -1;\n+\n+\t\tprintf(\u0022%.*s\u0022, (int)len, buf);\n+\n+\t\tm \u003d lejp_parse(\u0026pss-\u003ectx, (uint8_t *)buf, len);\n+\t\tif (m \u003c 0 \u0026\u0026 m !\u003d LEJP_CONTINUE) {\n+\t\t\tlwsl_notice(\u0022%s: notif JSON decode failed '%s' (%d)\u005cn\u0022,\n+\t\t\t\t\t__func__, lejp_error_to_string(m), m);\n+\t\t\treturn m;\n+\t\t}\n+\n+\t\tlwsl_notice(\u0022%s: m \u003d %d\u005cn\u0022, __func__, m);\n+\n+\t\tif (m !\u003d 1)\n+\t\t\tbreak;\n+\n+\t\tlws_genhmac_destroy(\u0026pss-\u003ehmac, result);\n+\n+\t\tif (memcmp(result, pss-\u003enotification_sig,\n+\t\t\t lws_genhmac_size(pss-\u003ehmac_type))) {\n+\t\t\tlwsl_err(\u0022%s: hmac mismatch\u005cn\u0022, __func__);\n+\n+\t\t\treturn -1;\n+\t\t}\n+\t\tlwsl_notice(\u0022%s: hmac OK\u005cn\u0022, __func__);\n+\n+\t\t/*\n+\t\t * We have the notification metadata JSON parsed into pss-\u003esn.e,\n+\t\t * eg, pss-\u003esn-\u003ee.hash ... since it's common to, eg, push a tree\n+\t\t * in a branch and then later tag the same commit, we don't want\n+\t\t * to pointlessly repeat CI for the same tree multiple times,\n+\t\t * and need to basically dedupe.\n+\t\t */\n+\n+\t\t{\n+\t\t\tuint64_t rid \u003d 0;\n+\t\t\tchar qu[192];\n+\n+\t\t\tlws_snprintf(qu, sizeof(qu), \u0022select rowid from events \u0022\n+\t\t\t\t\t\t \u0022where hash\u003d'%s'\u0022,\n+\t\t\t\t\t\t pss-\u003esn.e.hash);\n+\n+\t\t\tif (sqlite3_exec(pss-\u003evhd-\u003eserver.pdb, qu,\n+\t\t\t\t\t sai_sql3_get_uint64_cb,\n+\t\t\t\t\t \u0026rid, NULL) \u003d\u003d SQLITE_OK \u0026\u0026 rid) {\n+\t\t\t\t/* it already exists */\n+\t\t\t\tlwsl_notice(\u0022%s: ignoring notification as \u0022\n+\t\t\t\t\t \u0022tree hash event exists\u005cn\u0022,\n+\t\t\t\t\t __func__);\n+\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (!pss-\u003esn.saifile)\n+\t\t\treturn -1;\n+\n+\t\t/*\n+\t\t * We processed the notification JSON, but we only decoded the\n+\t\t * base64'd copy of the .sai.json so far... now's the time we\n+\t\t * want to process that and break it down into tasks.\n+\t\t *\n+\t\t * We don't trust it since it's controlled by the guy who pushed\n+\t\t * the commit, there can be anything at all in there. We made\n+\t\t * sure he can't attack us until now by base64-ing it at the\n+\t\t * server hook, so he's just dumb payload.\n+\t\t *\n+\t\t * Let's try to parse it in two passes, first without acting on\n+\t\t * the content to confirm it's going to succeed...\n+\t\t */\n+\n+\t\tsai_uuid16_create(lws_get_context(pss-\u003ewsi), pss-\u003esn.e.uuid);\n+\t\tif (sais_event_db_ensure_open(pss-\u003evhd, pss-\u003esn.e.uuid, 1,\n+\t\t\t\t\t (sqlite3 **)\u0026pss-\u003esn.e.pdb)) {\n+\t\t\tlwsl_err(\u0022%s: unable to open event-specific database\u005cn\u0022,\n+\t\t\t\t\t__func__);\n+\n+\t\t\tgoto saifile_bail;\n+\t\t}\n+\n+\t\tpss-\u003edry \u003d 1;\n+\t\tlejp_construct(\u0026saictx, sai_saifile_lejp_cb, pss, saifile_paths,\n+\t\t\t LWS_ARRAY_SIZE(saifile_paths));\n+\t\tm \u003d lejp_parse(\u0026saictx, (uint8_t *)pss-\u003esn.saifile,\n+\t\t\t pss-\u003esn.saifile_out_pos);\n+\t\tsais_event_db_close(pss-\u003evhd, (sqlite3 **)\u0026pss-\u003esn.e.pdb);\n+\t\tif (m \u003c 0) {\n+\t\t\tlwsl_notice(\u0022%s: saifile JSON decode failed '%s' (%d)\u005cn\u0022,\n+\t\t\t\t __func__, lejp_error_to_string(m), m);\n+\t\t\tgoto saifile_bail;\n+\t\t}\n+\n+\t\t/* ... then add the 32-char event object in the database ... */\n+\n+\t\tpss-\u003esn.e.created \u003d (unsigned long long)lws_now_secs();\n+\t\tpss-\u003esn.e.state \u003d SAIES_WAITING;\n+\n+\t\tmemset(\u0026pss-\u003esn.e.list, 0, sizeof(pss-\u003esn.e.list));\n+\t\tlws_dll2_owner_clear(\u0026owner);\n+\t\tlws_dll2_add_head(\u0026pss-\u003esn.e.list, \u0026owner);\n+\n+\t\t/*\n+\t\t * This is our new event going into the event database...\n+\t\t */\n+\n+\t\tlws_struct_sq3_serialize(pss-\u003evhd-\u003eserver.pdb,\n+\t\t\t\t\t lsm_schema_sq3_map_event, \u0026owner, 0);\n+\n+\t\t/*\n+\t\t * ... process the saifile JSON again creating tasks for each\n+\t\t * entry in the saifile, for each platform, against that event\n+\t\t * object...\n+\t\t */\n+\n+\t\tpss-\u003edry \u003d 0;\n+\t\tlejp_construct(\u0026saictx, sai_saifile_lejp_cb, pss, saifile_paths,\n+\t\t\t LWS_ARRAY_SIZE(saifile_paths));\n+\t\tm \u003d lejp_parse(\u0026saictx, (uint8_t *)pss-\u003esn.saifile,\n+\t\t\t pss-\u003esn.saifile_out_pos);\n+\t\tfree(pss-\u003esn.saifile);\n+\t\tpss-\u003esn.saifile \u003d NULL;\n+\t\tif (m \u003c 0) {\n+\t\t\tlwsl_notice(\u0022%s: saifile JSON decode failed '%s' (%d)\u005cn\u0022,\n+\t\t\t\t __func__, lejp_error_to_string(m), m);\n+\t\t\treturn m;\n+\t\t}\n+\n+\t\tlwsl_notice(\u0022%s: notification inserted into db\u005cn\u0022, __func__);\n+\n+\t\t/*\n+\t\t * Reassess now if there's a builder we can match to a pending task\n+\t\t */\n+\n+\t\tlws_sul_schedule(pss-\u003evhd-\u003econtext, 0, \u0026pss-\u003evhd-\u003esul_central,\n+\t\t\t\t sais_central_cb, 1);\n+\n+\t\treturn 0;\n+\n+saifile_bail:\n+\t\tfree(pss-\u003esn.saifile);\n+\t\tpss-\u003esn.saifile \u003d NULL;\n+\n+\t\treturn -1;\n+\n+\tcase LWS_UFS_CLOSE:\n+\t\t// lwsl_info(\u0022%s: LWS_UFS_CLOSE\u005cn\u0022, __func__);\n+\t\tlejp_destruct(\u0026pss-\u003ectx);\n+\t\tbreak;\n+\t}\n+\n+\treturn 0;\n+}\n+\ndiff --git a/src/server/s-private.h b/src/server/s-private.h\nnew file mode 100644\nindex 0000000..2337450\n--- /dev/null\n+++ b/src/server/s-private.h\n@@ -0,0 +1,285 @@\n+/*\n+ * Sai server definitions src/server/private.h\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ */\n+\n+#include \u0022../common/include/private.h\u0022\n+#include \u003csqlite3.h\u003e\n+#include \u003csys/stat.h\u003e\n+\n+#define SAI_EVENTID_LEN 32\n+#define SAI_TASKID_LEN 64\n+\n+struct sai_plat;\n+\n+typedef struct sai_platm {\n+\tstruct lws_dll2_owner builder_owner;\n+\tstruct lws_dll2_owner subs_owner;\n+\n+\tsqlite3 *pdb;\n+\tsqlite3 *pdb_auth;\n+} sais_t;\n+\n+typedef struct sai_platform {\n+\tstruct lws_dll2\t\tlist;\n+\n+\tconst char\t\t*name;\n+\tconst char\t\t*build;\n+\n+\tuint8_t\t\t\tnondefault;\n+\n+\t/* build and name over-allocated here */\n+} sai_platform_t;\n+\n+typedef enum {\n+\tSAIN_ACTION_INVALID,\n+\tSAIN_ACTION_REPO_UPDATED\n+} sai_notification_action_t;\n+\n+typedef struct {\n+\n+\tsai_event_t\t\t\te;\n+\tsai_task_t\t\t\tt;\n+\n+\tchar\t\t\t\tplatbuild[4096];\n+\tchar\t\t\t\tplatname[96];\n+\tchar\t\t\t\texplicit_platforms[2048];\n+\n+\tint\t\t\t\tevent_task_index;\n+\n+\tstruct lws_b64state\t\tb64;\n+\tchar\t\t\t\t*saifile;\n+\tuint64_t\t\t\twhen;\n+\tsize_t\t\t\t\tsaifile_in_len;\n+\tsize_t\t\t\t\tsaifile_out_len;\n+\tsize_t\t\t\t\tsaifile_out_pos;\n+\tsize_t\t\t\t\tsaifile_in_seen;\n+\tsai_notification_action_t\taction;\n+\n+\tuint8_t\t\t\t\tnondefault;\n+} sai_notification_t;\n+\n+typedef enum {\n+\tWSS_IDLE,\n+\tWSS_PREPARE_OVERVIEW,\n+\tWSS_SEND_OVERVIEW,\n+\tWSS_PREPARE_BUILDER_SUMMARY,\n+\tWSS_SEND_BUILDER_SUMMARY,\n+\n+\tWSS_PREPARE_TASKINFO,\n+\tWSS_SEND_ARTIFACT_INFO,\n+\n+\tWSS_PREPARE_EVENTINFO,\n+\tWSS_SEND_EVENTINFO,\n+} ws_state;\n+\n+typedef struct sai_builder {\n+\tsais_t c;\n+} saib_t;\n+\n+struct vhd;\n+\n+struct pss {\n+\tstruct vhd\t\t*vhd;\n+\tstruct lws\t\t*wsi;\n+\n+\tstruct lws_spa\t\t*spa;\n+\tstruct lejp_ctx\t\tctx;\n+\tsai_notification_t\tsn;\n+\tstruct lws_dll2\t\tsame; /* owner: vhd.builders */\n+\n+\tstruct lws_dll2\t\tsubs_list;\n+\n+\tuint64_t\t\tsub_timestamp;\n+\tchar\t\t\tsub_task_uuid[65];\n+\tchar\t\t\tspecific[65];\n+\tchar\t\t\tspecific_project[96];\n+\tchar\t\t\tauth_user[33];\n+\n+\tsqlite3\t\t\t*pdb_artifact;\n+\tsqlite3_blob\t\t*blob_artifact;\n+\n+\tlws_dll2_owner_t\tplatform_owner; /* sai_platform_t builder offers */\n+\tlws_dll2_owner_t\ttask_cancel_owner; /* sai_platform_t builder offers */\n+\tlws_dll2_owner_t\taft_owner; /* for statefully spooling artifact info */\n+\tlws_struct_args_t\ta;\n+\n+\tunion {\n+\t\tsai_plat_t\t*b;\n+\t\tsai_plat_owner_t *o;\n+\t} u;\n+\tconst char\t\t*server_name;\n+\n+\tstruct lwsac\t\t*query_ac;\n+\tstruct lwsac\t\t*logs_ac;\n+\tlws_dll2_owner_t\tissue_task_owner; /* list of sai_task_t */\n+\tconst sai_task_t\t*one_task; /* only for browser */\n+\tconst sai_event_t\t*one_event;\n+\tlws_dll2_owner_t\tquery_owner;\n+\tlws_dll2_t\t\t*walk;\n+\n+\tint\t\t\ttask_index;\n+\tint\t\t\tlog_cache_index;\n+\tint\t\t\tlog_cache_size;\n+\tint\t\t\tauthorized;\n+\tint\t\t\tspecificity;\n+\tunsigned long\t\texpiry_unix_time;\n+\n+\t/* notification hmac information */\n+\tchar\t\t\tnotification_sig[128];\n+\tchar\t\t\talang[128];\n+\tstruct lws_genhmac_ctx\thmac;\n+\tenum lws_genhmac_types\thmac_type;\n+\tchar\t\t\tour_form;\n+\tchar\t\t\tlogin_form;\n+\n+\tuint64_t\t\tfirst_log_timestamp;\n+\tuint64_t\t\tartifact_offset;\n+\tuint64_t\t\tartifact_length;\n+\n+\tws_state\t\tsend_state;\n+\n+\tunsigned int\t\tspa_failed:1;\n+\tunsigned int\t\tsubsequent:1; /* for individual JSON */\n+\tunsigned int\t\tdry:1;\n+\tunsigned int\t\tfrag:1;\n+\tunsigned int\t\tmark_started:1;\n+\tunsigned int\t\twants_event_updates:1;\n+\tunsigned int\t\tannounced:1;\n+\tunsigned int\t\tbulk_binary_data:1;\n+\n+\tuint8_t\t\t\tovstate; /* SOS_ substate when doing overview */\n+};\n+\n+typedef struct sais_sqlite_cache {\n+\tlws_dll2_t\tlist;\n+\tchar\t\tuuid[65];\n+\tsqlite3\t\t*pdb;\n+\tlws_usec_t\tidle_since;\n+\tint\t\trefcount;\n+} sais_sqlite_cache_t;\n+\n+struct vhd {\n+\tstruct lws_context *context;\n+\tstruct lws_vhost *vhost;\n+\n+\tstruct lws_ss_handle\t\t*h_ss_websrv; /* server */\n+\n+\tchar\t\t\t\tjson_builders[8192];\n+\n+\t/* pss lists */\n+\tstruct lws_dll2_owner builders;\n+\n+\tconst char *sqlite3_path_lhs;\n+\n+\tlws_dll2_owner_t sqlite3_cache; /* sais_sqlite_cache_t */\n+\tlws_dll2_owner_t tasklog_cache;\n+\tlws_sorted_usec_list_t sul_logcache;\n+\tlws_sorted_usec_list_t sul_central; /* background task allocation sul */\n+\n+\tlws_usec_t\tlast_check_abandoned_tasks;\n+\n+\tconst char *notification_key;\n+\n+\tsais_t server;\n+};\n+\n+extern struct lws_context *\n+sai_lws_context_from_json(const char *config_dir,\n+\t\t\t struct lws_context_creation_info *info,\n+\t\t\t const struct lws_protocols **pprotocols,\n+\t\t\t const char *jpol);\n+extern const struct lws_protocols protocol_ws;\n+\n+int\n+sai_notification_file_upload_cb(void *data, const char *name,\n+\t\t\t\tconst char *filename, char *buf, int len,\n+\t\t\t\tenum lws_spa_fileupload_states state);\n+\n+int\n+sai_sqlite3_statement(sqlite3 *pdb, const char *cmd, const char *desc);\n+\n+int\n+sai_uuid16_create(struct lws_context *context, char *dest33);\n+\n+int\n+sais_event_db_ensure_open(struct vhd *vhd, const char *event_uuid, char can_create, sqlite3 **ppdb);\n+\n+void\n+sais_event_db_close(struct vhd *vhd, sqlite3 **ppdb);\n+\n+int\n+sais_event_db_delete_database(struct vhd *vhd, const char *event_uuid);\n+\n+int\n+sais_event_db_close_all_now(struct vhd *vhd);\n+\n+int\n+sai_sq3_event_lookup(sqlite3 *pdb, uint64_t start, lws_struct_args_cb cb, void *ca);\n+\n+int\n+sai_sql3_get_uint64_cb(void *user, int cols, char **values, char **name);\n+\n+int\n+saiw_ws_json_tx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t bl);\n+\n+int\n+sais_ws_json_rx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t bl);\n+\n+int\n+sais_list_builders(struct vhd *vhd);\n+\n+void\n+sais_eventchange(struct lws_ss_handle *h, const char *event_uuid, int state);\n+\n+void\n+sais_taskchange(struct lws_ss_handle *h, const char *task_uuid, int state);\n+\n+int\n+lws_struct_map_set(const lws_struct_map_t *map, char *u);\n+\n+void\n+sai_task_uuid_to_event_uuid(char *event_uuid33, const char *task_uuid65);\n+\n+int\n+sais_ws_json_tx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t bl);\n+\n+int\n+sais_subs_request_writeable(struct vhd *vhd, const char *task_uuid);\n+\n+void\n+sais_central_cb(lws_sorted_usec_list_t *sul);\n+\n+int\n+sais_task_reset(struct vhd *vhd, const char *task_uuid);\n+\n+int\n+sais_task_cancel(struct vhd *vhd, const char *task_uuid);\n+\n+int\n+sais_allocate_task(struct vhd *vhd, struct pss *pss, sai_plat_t *cb,\n+\t\t const char *cns_name);\n+\n+int\n+sais_set_task_state(struct vhd *vhd, const char *builder_name,\n+\t\t const char *builder_uuid, const char *task_uuid, int state,\n+\t\t uint64_t started, uint64_t duration);\n+\n+void\n+sais_websrv_broadcast(struct lws_ss_handle *hsrv, const char *str, size_t len);\ndiff --git a/src/server/s-sai.c b/src/server/s-sai.c\nnew file mode 100644\nindex 0000000..df6ca96\n--- /dev/null\n+++ b/src/server/s-sai.c\n@@ -0,0 +1,93 @@\n+/*\n+ * Sai server\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ */\n+\n+#include \u003clibwebsockets.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003ctime.h\u003e\n+\n+#include \u0022s-private.h\u0022\n+\n+static int interrupted;\n+struct lws_context *context;\n+\n+/*\n+ * sai-server also has a unix domain socket serving ws, that the sai-web part(s)\n+ * connect to as client(s).\n+ *\n+ * The pass up authenticated browser task and event redo requests, and receive\n+ * information about updates to tasks and events that they might have clients\n+ * that are watching.\n+ */\n+\n+\n+static const char * const default_ss_policy \u003d\n+\t\u0022{\u0022\n+\t \u0022\u005c\u0022s\u005c\u0022: [\u0022\n+\t\t/* uds link between web and server pieces */\n+\t\t\u0022{\u005c\u0022websrv\u005c\u0022: {\u0022\n+\t\t\t\u0022\u005c\u0022server\u005c\u0022:\u0022\t\t\u0022true,\u0022\n+\t\t\t\u0022\u005c\u0022endpoint\u005c\u0022:\u0022\t\t\u0022\u005c\u0022+@com.warmcat.sai-websrv\u005c\u0022,\u0022\n+\t\t\t\u0022\u005c\u0022protocol\u005c\u0022:\u0022\t\t\u0022\u005c\u0022ws\u005c\u0022\u0022\n+\t\t\u0022}}\u0022\n+\t \u0022]\u0022\n+\t\u0022}\u0022\n+;\n+\n+static const struct lws_protocols\n+\t*pprotocols[] \u003d { \u0026protocol_ws, NULL };\n+\n+static void sigint_handler(int sig)\n+{\n+\tinterrupted \u003d 1;\n+}\n+\n+int main(int argc, const char **argv)\n+{\n+\tint logs \u003d LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE;\n+\tconst char *p, *conf \u003d \u0022/etc/sai/server\u0022;\n+\tstruct lws_context_creation_info info;\n+\n+\tsignal(SIGINT, sigint_handler);\n+\n+\tif ((p \u003d lws_cmdline_option(argc, argv, \u0022-d\u0022)))\n+\t\tlogs \u003d atoi(p);\n+\n+\tlws_set_log_level(logs, NULL);\n+\tlwsl_user(\u0022Sai Server - Copyright (C) 2019-2020 Andy Green \u003candy@warmcat.com\u003e\u005cn\u0022);\n+\n+\tif ((p \u003d lws_cmdline_option(argc, argv, \u0022-c\u0022)))\n+\t\tconf \u003d p;\n+\n+\tcontext \u003d sai_lws_context_from_json(conf, \u0026info, pprotocols,\n+\t\t\t\t\t default_ss_policy);\n+\tif (!context) {\n+\t\tlwsl_err(\u0022lws init failed\u005cn\u0022);\n+\t\treturn 1;\n+\t}\n+\n+\twhile (!lws_service(context, 0) \u0026\u0026 !interrupted)\n+\t\t;\n+\n+\tlws_context_destroy(context);\n+\n+\treturn 0;\n+}\ndiff --git a/src/server/s-task.c b/src/server/s-task.c\nnew file mode 100644\nindex 0000000..66e26d3\n--- /dev/null\n+++ b/src/server/s-task.c\n@@ -0,0 +1,494 @@\n+/*\n+ * Sai server\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ */\n+\n+#include \u003clibwebsockets.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003ctime.h\u003e\n+#include \u003cassert.h\u003e\n+\n+#include \u0022s-private.h\u0022\n+\n+\n+static int\n+sql3_get_integer_cb(void *user, int cols, char **values, char **name)\n+{\n+\tunsigned int *pui \u003d (unsigned int *)user;\n+\n+\t// lwsl_warn(\u0022%s: values[0] '%s'\u005cn\u0022, __func__, values[0]);\n+\t*pui \u003d atoi(values[0]);\n+\n+\treturn 0;\n+}\n+\n+void\n+sai_task_uuid_to_event_uuid(char *event_uuid33, const char *task_uuid65)\n+{\n+\tmemcpy(event_uuid33, task_uuid65, 32);\n+\tevent_uuid33[32] \u003d '\u005c0';\n+}\n+\n+int\n+sais_set_task_state(struct vhd *vhd, const char *builder_name,\n+\t\t const char *builder_uuid, const char *task_uuid, int state,\n+\t\t uint64_t started, uint64_t duration)\n+{\n+\tchar update[384], esc[96], esc1[96], esc2[96], esc3[32], esc4[32],\n+\t\tevent_uuid[33];\n+\tunsigned int count \u003d 0, count_good \u003d 0, count_bad \u003d 0;\n+\tsai_event_state_t oes, sta;\n+\tstruct lwsac *ac \u003d NULL;\n+\tsai_event_t *e \u003d NULL;\n+\tlws_dll2_owner_t o;\n+\tint n, task_ostate;\n+\n+\t/*\n+\t * Extract the event uuid from the task uuid\n+\t */\n+\n+\tsai_task_uuid_to_event_uuid(event_uuid, task_uuid);\n+\n+\t/*\n+\t * Look up the task's event in the event database...\n+\t */\n+\n+\tlws_dll2_owner_clear(\u0026o);\n+\tlws_sql_purify(esc1, event_uuid, sizeof(esc1));\n+\tlws_snprintf(esc2, sizeof(esc2), \u0022 and uuid\u003d'%s'\u0022, esc1);\n+\tn \u003d lws_struct_sq3_deserialize(vhd-\u003eserver.pdb, esc2, NULL,\n+\t\t\t\t lsm_schema_sq3_map_event, \u0026o, \u0026ac, 0, 1);\n+\tif (n \u003c 0 || !o.head) {\n+\t\tlwsl_err(\u0022%s: failed to get task_uuid %s\u005cn\u0022, __func__, esc1);\n+\t\tgoto bail;\n+\t}\n+\n+\te \u003d lws_container_of(o.head, sai_event_t, list);\n+\toes \u003d e-\u003estate;\n+\n+\t/*\n+\t * Open the event-specific database on the temporary event object\n+\t */\n+\n+\tif (sais_event_db_ensure_open(vhd, event_uuid, 0, (sqlite3 **)\u0026e-\u003epdb)) {\n+\t\tlwsl_err(\u0022%s: unable to open event-specific database\u005cn\u0022,\n+\t\t\t\t__func__);\n+\n+\t\treturn -1;\n+\t}\n+\n+\tif (builder_name)\n+\t\tlws_sql_purify(esc, builder_name, sizeof(esc));\n+\telse\n+\t\tesc[0] \u003d '\u005c0';\n+\n+\tif (builder_uuid)\n+\t\tlws_sql_purify(esc1, builder_uuid, sizeof(esc1));\n+\telse\n+\t\tesc1[0] \u003d '\u005c0';\n+\tlws_sql_purify(esc2, task_uuid, sizeof(esc2));\n+\n+\tesc3[0] \u003d esc4[0] \u003d '\u005c0';\n+\n+\t/*\n+\t * grab the current state of it for seeing if it changed\n+\t */\n+\tlws_snprintf(update, sizeof(update),\n+\t\t \u0022select state from tasks where uuid\u003d'%s'\u0022, esc2);\n+\tif (sqlite3_exec((sqlite3 *)e-\u003epdb, update,\n+\t\t\t sql3_get_integer_cb, \u0026task_ostate, NULL) !\u003d SQLITE_OK) {\n+\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, update,\n+\t\t\t sqlite3_errmsg(vhd-\u003eserver.pdb));\n+\t\tgoto bail;\n+\t}\n+\n+\tif (started) {\n+\t\tif (started \u003d\u003d 1)\n+\t\t\tstarted \u003d 0;\n+\t\tlws_snprintf(esc3, sizeof(esc3), \u0022,started\u003d%llu\u0022,\n+\t\t\t (unsigned long long)started);\n+\t}\n+\tif (duration) {\n+\t\tif (duration \u003d\u003d 1)\n+\t\t\tduration \u003d 0;\n+\t\tlws_snprintf(esc4, sizeof(esc4), \u0022,duration\u003d%llu\u0022,\n+\t\t\t (unsigned long long)duration);\n+\t}\n+\n+\t/*\n+\t * Update the task by uuid, in the event-specific database\n+\t */\n+\n+\tlws_snprintf(update, sizeof(update),\n+\t\t\u0022update tasks set state\u003d%d%s%s%s%s%s%s%s%s where uuid\u003d'%s'\u0022,\n+\t\t state, builder_uuid ? \u0022,builder\u003d'\u0022: \u0022\u0022,\n+\t\t\tbuilder_uuid ? esc1 : \u0022\u0022,\n+\t\t\tbuilder_uuid ? \u0022'\u0022 : \u0022\u0022,\n+\t\t\tbuilder_name ? \u0022,builder_name\u003d'\u0022 : \u0022\u0022,\n+\t\t\tbuilder_name ? esc : \u0022\u0022,\n+\t\t\tbuilder_name ? \u0022'\u0022 : \u0022\u0022,\n+\t\t\tesc3, esc4, esc2);\n+\n+\tif (sqlite3_exec((sqlite3 *)e-\u003epdb, update, NULL, NULL, NULL) !\u003d SQLITE_OK) {\n+\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, update,\n+\t\t\t sqlite3_errmsg(vhd-\u003eserver.pdb));\n+\t\tgoto bail;\n+\t}\n+\n+\t/*\n+\t * We tell interested parties about logs separately. So there's only\n+\t * something to tell about change to task state if he literally changed\n+\t * the state\n+\t */\n+\n+\tif (state !\u003d task_ostate) {\n+\n+\t\tlwsl_notice(\u0022%s: seen task %s %d -\u003e %d\u005cn\u0022, __func__,\n+\t\t\t\ttask_uuid, task_ostate, state);\n+\n+\t\tsais_taskchange(vhd-\u003eh_ss_websrv, task_uuid, state);\n+\n+\t\t/*\n+\t\t * So, how many tasks for this event?\n+\t\t */\n+\n+\t\tif (sqlite3_exec((sqlite3 *)e-\u003epdb, \u0022select count(state) from tasks\u0022,\n+\t\t\t\t sql3_get_integer_cb, \u0026count, NULL) !\u003d SQLITE_OK) {\n+\t\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, update,\n+\t\t\t\t sqlite3_errmsg(vhd-\u003eserver.pdb));\n+\t\t\tgoto bail;\n+\t\t}\n+\n+\t\t/*\n+\t\t * ... how many completed well?\n+\t\t */\n+\n+\t\tif (sqlite3_exec((sqlite3 *)e-\u003epdb, \u0022select count(state) from tasks where state \u003d\u003d 3\u0022,\n+\t\t\t\t sql3_get_integer_cb, \u0026count_good, NULL) !\u003d SQLITE_OK) {\n+\t\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, update,\n+\t\t\t\t sqlite3_errmsg(vhd-\u003eserver.pdb));\n+\t\t\tgoto bail;\n+\t\t}\n+\n+\t\t/*\n+\t\t * ... how many failed?\n+\t\t */\n+\n+\t\tif (sqlite3_exec((sqlite3 *)e-\u003epdb, \u0022select count(state) from tasks where state \u003d\u003d 4\u0022,\n+\t\t\t\t sql3_get_integer_cb, \u0026count_bad, NULL) !\u003d SQLITE_OK) {\n+\t\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, update,\n+\t\t\t\t sqlite3_errmsg(vhd-\u003eserver.pdb));\n+\t\t\tgoto bail;\n+\t\t}\n+\n+\t\t/*\n+\t\t * Decide how to set the event state based on that\n+\t\t */\n+\n+\t\tlwsl_notice(\u0022%s: ev %s, count %u, count_good %u, count_bad %u\u005cn\u0022,\n+\t\t\t __func__, event_uuid, count, count_good, count_bad);\n+\n+\t\tsta \u003d SAIES_BEING_BUILT;\n+\n+\t\tif (count) {\n+\t\t\tif (count \u003d\u003d count_good)\n+\t\t\t\tsta \u003d SAIES_SUCCESS;\n+\t\t\telse\n+\t\t\t\tif (count \u003d\u003d count_bad)\n+\t\t\t\t\tsta \u003d SAIES_FAIL;\n+\t\t\t\telse\n+\t\t\t\t\tif (count_bad)\n+\t\t\t\t\t\tsta \u003d SAIES_BEING_BUILT_HAS_FAILURES;\n+\t\t}\n+\n+\t\tif (sta !\u003d oes) {\n+\t\t\tlwsl_notice(\u0022%s: event state changed\u005cn\u0022, __func__);\n+\n+\t\t\t/*\n+\t\t\t * Update the event\n+\t\t\t */\n+\n+\t\t\tlws_sql_purify(esc1, event_uuid, sizeof(esc1));\n+\t\t\tlws_snprintf(update, sizeof(update),\n+\t\t\t\t\u0022update events set state\u003d%d where uuid\u003d'%s'\u0022, sta, esc1);\n+\n+\t\t\tif (sqlite3_exec(vhd-\u003eserver.pdb, update, NULL, NULL, NULL) !\u003d SQLITE_OK) {\n+\t\t\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, update,\n+\t\t\t\t\t sqlite3_errmsg(vhd-\u003eserver.pdb));\n+\t\t\t\tgoto bail;\n+\t\t\t}\n+\n+\t\t\tsais_eventchange(vhd-\u003eh_ss_websrv, event_uuid, sta);\n+\t\t}\n+\t}\n+\n+\tsais_event_db_close(vhd, (sqlite3 **)\u0026e-\u003epdb);\n+\tlwsac_free(\u0026ac);\n+\n+\treturn 0;\n+\n+bail:\n+\tif (e)\n+\t\tsais_event_db_close(vhd, (sqlite3 **)\u0026e-\u003epdb);\n+\tlwsac_free(\u0026ac);\n+\n+\treturn 1;\n+}\n+\n+/*\n+ * Find the most recent task that still needs doing for platform, on any event\n+ *\n+ * If any, the task pointed-to lives inside *pac, along with its strings etc\n+ * and is the responsibility of the caller to free\n+ */\n+\n+static const sai_task_t *\n+sais_task_pending(struct vhd *vhd, struct lwsac **pac, const char *platform)\n+{\n+\tstruct lwsac *ac \u003d NULL;\n+\tchar esc[96], pf[96];\n+\tlws_dll2_owner_t o;\n+\tint n;\n+\n+\tlws_sql_purify(esc, platform, sizeof(esc));\n+\n+\tassert(platform);\n+\tassert(pac);\n+\n+\t/*\n+\t * Collect a list of events that still have any open tasks\n+\t *\n+\t * We don't put this list in the pac since we can dispose of it in this\n+\t * scope whether we find something or not\n+\t */\n+\n+\tn \u003d lws_struct_sq3_deserialize(vhd-\u003eserver.pdb,\n+\t\t\t\t \u0022 and (state !\u003d 3 and state !\u003d 4 and state !\u003d 5)\u0022,\n+\t\t\t\t \u0022created desc \u0022, lsm_schema_sq3_map_event, \u0026o,\n+\t\t\t\t \u0026ac, 0, 10);\n+\tif (n \u003c 0 || !o.head) {\n+\t//\tlwsl_notice(\u0022%s: all events complete\u005cn\u0022, __func__);\n+\t\t/* error, or there are no events that aren't complete */\n+\t\tgoto bail;\n+\t}\n+\n+\t/*\n+\t * Iterate through the events looking at his event-specific database\n+\t * for tasks on the specified platform...\n+\t */\n+\n+\tlws_start_foreach_dll(struct lws_dll2 *, p, o.head) {\n+\t\tsai_event_t *e \u003d lws_container_of(p, sai_event_t, list);\n+\t\tlws_dll2_owner_t ot;\n+\t\tsqlite3 *pdb \u003d NULL;\n+\n+\t\tif (!sais_event_db_ensure_open(vhd, e-\u003euuid, 0, \u0026pdb)) {\n+\t\t\tlws_snprintf(pf, sizeof(pf),\n+\t\t\t\t \u0022 and state\u003d0 and platform\u003d'%s'\u0022, esc);\n+\t\t\tn \u003d lws_struct_sq3_deserialize(pdb, pf, NULL,\n+\t\t\t\t\t\t lsm_schema_sq3_map_task,\n+\t\t\t\t\t\t \u0026ot, pac, 0, 1);\n+\t\t\tsais_event_db_close(vhd, \u0026pdb);\n+\t\t\tif (ot.count) {\n+\n+\t\t\t\tlwsl_notice(\u0022%s: found task for %s\u005cn\u0022,\n+\t\t\t\t\t\t__func__, esc);\n+\n+\t\t\t\tlwsac_free(\u0026ac);\n+\n+\t\t\t\treturn lws_container_of(ot.head,\n+\t\t\t\t\t\tsai_task_t, list);\n+\t\t\t}\n+\t\t}\n+\n+\t} lws_end_foreach_dll(p);\n+\n+\tlwsl_debug(\u0022%s: no free tasks matching '%s'\u005cn\u0022, __func__, esc);\n+\n+bail:\n+\tlwsac_free(\u0026ac);\n+\n+\treturn NULL;\n+}\n+\n+int\n+sais_task_cancel(struct vhd *vhd, const char *task_uuid)\n+{\n+\tsai_cancel_t *can;\n+\n+\t/*\n+\t * For every pss that we have from builders...\n+\t */\n+\tlws_start_foreach_dll(struct lws_dll2 *, p, vhd-\u003ebuilders.head) {\n+\t\tstruct pss *pss \u003d lws_container_of(p, struct pss, same);\n+\n+\n+\t\t/*\n+\t\t * ... queue the task cancel message\n+\t\t */\n+\t\tcan \u003d malloc(sizeof *can);\n+\t\tif (!can)\n+\t\t\treturn -1;\n+\t\tmemset(can, 0, sizeof(*can));\n+\n+\t\tstrncpy(can-\u003etask_uuid, task_uuid, sizeof(can-\u003etask_uuid));\n+\n+\t\tlws_dll2_add_tail(\u0026can-\u003elist, \u0026pss-\u003etask_cancel_owner);\n+\n+\t\tlws_callback_on_writable(pss-\u003ewsi);\n+\n+\t} lws_end_foreach_dll(p);\n+\n+\tsais_taskchange(vhd-\u003eh_ss_websrv, task_uuid, SAIES_CANCELLED);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Keep the task record itself, but remove all logs and artifacts related to\n+ * it and reset the task state back to WAITING.\n+ */\n+\n+int\n+sais_task_reset(struct vhd *vhd, const char *task_uuid)\n+{\n+\tchar esc[96], cmd[256], event_uuid[33];\n+\tsqlite3 *pdb \u003d NULL;\n+\n+\tif (!task_uuid[0])\n+\t\treturn 0;\n+\n+\tlwsl_notice(\u0022%s: received request to reset task %s\u005cn\u0022, __func__, task_uuid);\n+\n+\tsai_task_uuid_to_event_uuid(event_uuid, task_uuid);\n+\n+\tif (sais_event_db_ensure_open(vhd, event_uuid, 0, \u0026pdb)) {\n+\t\tlwsl_err(\u0022%s: unable to open event-specific database\u005cn\u0022,\n+\t\t\t\t__func__);\n+\n+\t\treturn -1;\n+\t}\n+\n+\tlws_sql_purify(esc, task_uuid, sizeof(esc));\n+\tlws_snprintf(cmd, sizeof(cmd), \u0022delete from logs where task_uuid\u003d'%s'\u0022,\n+\t\t esc);\n+\n+\tif (sqlite3_exec(pdb, cmd, NULL, NULL, NULL) !\u003d SQLITE_OK) {\n+\t\tsais_event_db_close(vhd, \u0026pdb);\n+\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, cmd,\n+\t\t\t sqlite3_errmsg(vhd-\u003eserver.pdb));\n+\t\treturn 1;\n+\t}\n+\tlws_snprintf(cmd, sizeof(cmd), \u0022delete from artifacts where task_uuid\u003d'%s'\u0022,\n+\t\t esc);\n+\n+\tsqlite3_exec(pdb, cmd, NULL, NULL, NULL);\n+\tsais_event_db_close(vhd, \u0026pdb);\n+\n+\tsais_set_task_state(vhd, NULL, NULL, task_uuid, SAIES_WAITING, 1, 1);\n+\n+\tsais_task_cancel(vhd, task_uuid);\n+\n+\t/*\n+\t * Reassess now if there's a builder we can match to a pending task\n+\t */\n+\n+\tlws_sul_schedule(vhd-\u003econtext, 0, \u0026vhd-\u003esul_central, sais_central_cb, 1);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Look for any task on any event that needs building on platform_name, if found\n+ * the caller must take responsibility to free pss-\u003ea.ac\n+ */\n+\n+int\n+sais_allocate_task(struct vhd *vhd, struct pss *pss, sai_plat_t *cb,\n+\t\t const char *platform_name)\n+{\n+\tchar esc1[96], esc2[96];\n+\tlws_dll2_owner_t o;\n+\tsai_task_t *task;\n+\tint n;\n+\n+\tif (cb-\u003eongoing \u003e\u003d cb-\u003einstances)\n+\t\treturn 1;\n+\n+\t/*\n+\t * Look for a task for this platform, on any event that needs building\n+\t */\n+\n+\ttask \u003d (sai_task_t *)sais_task_pending(vhd, \u0026pss-\u003ea.ac, platform_name);\n+\tif (!task)\n+\t\treturn 1;\n+\n+\tlwsl_notice(\u0022%s: %s: task found %s\u005cn\u0022, __func__, platform_name, cb-\u003ename);\n+\n+\t/* yes, we will offer it to him */\n+\n+\tif (sais_set_task_state(vhd, cb-\u003ename, cb-\u003ename, task-\u003euuid,\n+\t\t\t\tSAIES_PASSED_TO_BUILDER, lws_now_secs(), 0))\n+\t\tgoto bail;\n+\n+\t/* advance the task state first time we get logs */\n+\tpss-\u003emark_started \u003d 1;\n+\n+\t/* let's get ahold of his event as well */\n+\n+\tlws_sql_purify(esc1, task-\u003eevent_uuid, sizeof(esc1));\n+\tlws_snprintf(esc2, sizeof(esc2), \u0022 and uuid\u003d'%s'\u0022, esc1);\n+\tn \u003d lws_struct_sq3_deserialize(vhd-\u003eserver.pdb, esc2, NULL,\n+\t\t\t\t lsm_schema_sq3_map_event, \u0026o,\n+\t\t\t\t \u0026pss-\u003ea.ac, 0, 1);\n+\tif (n \u003c 0 || !o.head)\n+\t\tgoto bail;\n+\n+\ttask-\u003eone_event \u003d lws_container_of(o.head, sai_event_t, list);\n+\n+\ttask-\u003eserver_name\t\u003d pss-\u003eserver_name;\n+\ttask-\u003erepo_name\t\t\u003d task-\u003eone_event-\u003erepo_name;\n+\ttask-\u003egit_ref\t\t\u003d task-\u003eone_event-\u003eref;\n+\ttask-\u003egit_hash\t\t\u003d task-\u003eone_event-\u003ehash;\n+\ttask-\u003eac_task_container \u003d pss-\u003ea.ac;\n+\n+\t/*\n+\t * add to server's estimate of builder's ongoing tasks...\n+\t */\n+\tcb-\u003eongoing++;\n+\n+\tlws_dll2_add_tail(\u0026task-\u003epending_assign_list, \u0026pss-\u003eissue_task_owner);\n+\tlws_callback_on_writable(pss-\u003ewsi);\n+\n+\tpss-\u003ea.ac \u003d NULL;\n+\n+\t/*\n+\t * We are going to leave here with a live pss-\u003ea.ac (pointed into by\n+\t * task-\u003eone_event) that the caller has to take responsibility to\n+\t * clean up pss-\u003ea.ac\n+\t */\n+\n+\treturn 0;\n+\n+bail:\n+\tlwsac_free(\u0026pss-\u003ea.ac);\n+\n+\treturn -1;\n+}\ndiff --git a/src/server/s-websrv.c b/src/server/s-websrv.c\nnew file mode 100644\nindex 0000000..1425648\n--- /dev/null\n+++ b/src/server/s-websrv.c\n@@ -0,0 +1,514 @@\n+/*\n+ * Sai server\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ *\n+ *\n+ * This is a ws server over a unix domain socket made available by sai-server\n+ * and connected to by sai-web instances running on the same box.\n+ *\n+ * The server notifies the sai-web instances of event and task changes (just\n+ * that a particular event or task changed) and builder list updates (the\n+ * whole current builder list JSON each time).\n+ *\n+ * Sai-web instances can send requests to restart or delete tasks and whole\n+ * events made by authenticated clients.\n+ *\n+ * Since this is on a local UDS protected by user:group, there's no tls or auth\n+ * on this link itself.\n+ */\n+\n+#include \u003clibwebsockets.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003ctime.h\u003e\n+\n+#include \u0022s-private.h\u0022\n+\n+typedef struct websrvss_srv {\n+\tstruct lws_ss_handle \t\t*ss;\n+\tstruct vhd\t\t\t*vhd;\n+\t/* ... application specific state ... */\n+\n+\tstruct lejp_ctx\t\t\tctx;\n+\tstruct lws_buflist\t\t*bltx;\n+\n+} websrvss_srv_t;\n+\n+static lws_struct_map_t lsm_browser_taskreset[] \u003d {\n+\tLSM_CARRAY\t(sai_browse_rx_evinfo_t, event_hash,\t\u0022uuid\u0022),\n+};\n+\n+static const lws_struct_map_t lsm_schema_json_map[] \u003d {\n+\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_browser_taskreset,\n+\t\t\t/* shares struct */ \u0022com.warmcat.sai.taskreset\u0022),\n+\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_browser_taskreset,\n+\t\t\t/* shares struct */ \u0022com.warmcat.sai.eventreset\u0022),\n+\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_browser_taskreset,\n+\t\t\t/* shares struct */ \u0022com.warmcat.sai.eventdelete\u0022),\n+\tLSM_SCHEMA\t(sai_cancel_t,\t\t NULL, lsm_task_cancel,\n+\t\t\t\t\t \u0022com.warmcat.sai.taskcan\u0022),\n+};\n+\n+enum {\n+\tSAIS_WS_WEBSRV_RX_TASKRESET,\n+\tSAIS_WS_WEBSRV_RX_EVENTRESET,\n+\tSAIS_WS_WEBSRV_RX_EVENTDELETE,\n+\tSAIS_WS_WEBSRV_RX_TASKCANCEL\n+};\n+\n+int\n+sais_validate_id(const char *id, int reqlen)\n+{\n+\tconst char *idin \u003d id;\n+\tint n \u003d reqlen;\n+\n+\twhile (*id \u0026\u0026 n--) {\n+\t\tif (!((*id \u003e\u003d '0' \u0026\u0026 *id \u003c\u003d '9') ||\n+\t\t (*id \u003e\u003d 'a' \u0026\u0026 *id \u003c\u003d 'z') ||\n+\t\t (*id \u003e\u003d 'A' \u0026\u0026 *id \u003c\u003d 'Z')))\n+\t\t\tgoto reject;\n+\t\tid++;\n+\t}\n+\n+\tif (!n \u0026\u0026 !*id)\n+\t\treturn 0;\n+reject:\n+\n+\tlwsl_notice(\u0022%s: Invalid ID (%d) '%s'\u005cn\u0022, __func__, reqlen, idin);\n+\n+\treturn 1;\n+}\n+\n+int\n+sais_websrv_queue_tx(struct lws_ss_handle *h, void *buf, size_t len)\n+{\n+\twebsrvss_srv_t *m \u003d (websrvss_srv_t *)lws_ss_to_user_object(h);\n+\tint n;\n+\n+\tn \u003d lws_buflist_append_segment(\u0026m-\u003ebltx, buf, len);\n+\n+\tlwsl_notice(\u0022%s: appened h %p: %d\u005cn\u0022, __func__, h, n);\n+\tlws_ss_request_tx(h);\n+\n+\treturn n \u003c 0;\n+}\n+\n+typedef struct {\n+\tconst uint8_t *buf;\n+\tsize_t len;\n+} sais_websrv_broadcast_t;\n+\n+static void\n+_sais_websrv_broadcast(struct lws_ss_handle *h, void *arg)\n+{\n+\twebsrvss_srv_t *m \u003d (websrvss_srv_t *)lws_ss_to_user_object(h);\n+\tsais_websrv_broadcast_t *a \u003d (sais_websrv_broadcast_t *)arg;\n+\n+\tif (lws_buflist_append_segment(\u0026m-\u003ebltx, a-\u003ebuf, a-\u003elen) \u003e\u003d 0)\n+\t\tlws_ss_request_tx(h);\n+\telse\n+\t\tlwsl_warn(\u0022%s: buflist append fail\u005cn\u0022, __func__);\n+}\n+\n+void\n+sais_websrv_broadcast(struct lws_ss_handle *hsrv, const char *str, size_t len)\n+{\n+\tsais_websrv_broadcast_t a;\n+\n+\ta.buf \u003d (const uint8_t *)str;\n+\ta.len \u003d len;\n+\n+\tlws_ss_server_foreach_client(hsrv, _sais_websrv_broadcast, \u0026a);\n+}\n+\n+\n+int\n+sais_list_builders(struct vhd *vhd)\n+{\n+\tlws_dll2_t *walk \u003d lws_dll2_get_head(\u0026vhd-\u003eserver.builder_owner);\n+\tchar *p \u003d vhd-\u003ejson_builders, *end \u003d p + sizeof(vhd-\u003ejson_builders),\n+\t subsequent \u003d 0;\n+\tlws_struct_serialize_t *js;\n+\tsai_plat_t *b;\n+\tsize_t w;\n+\tint n;\n+\n+\tp +\u003d lws_snprintf((char *)p, end - p, \u0022{\u005c\u0022schema\u005c\u0022:\u005c\u0022sai-builders\u005c\u0022,\u0022\n+\t\t\t\t\t \u0022\u005c\u0022platforms\u005c\u0022:[\u0022);\n+\n+\twhile (end - p \u003e 512 \u0026\u0026 walk) {\n+\n+\t\tb \u003d lws_container_of(walk, sai_plat_t, sai_plat_list);\n+\n+\t\tjs \u003d lws_struct_json_serialize_create(\n+\t\t\tlsm_schema_map_plat_simple,\n+\t\t\tLWS_ARRAY_SIZE(lsm_schema_map_plat_simple),\n+\t\t\t0, b);\n+\t\tif (!js)\n+\t\t\treturn 1;\n+\t\tif (subsequent)\n+\t\t\t*p++ \u003d ',';\n+\t\tsubsequent \u003d 1;\n+\n+\t\tn \u003d lws_struct_json_serialize(js, (unsigned char *)p,\n+\t\t\t\t\t lws_ptr_diff(end, p), \u0026w);\n+\t\tp +\u003d w;\n+\t\tlws_struct_json_serialize_destroy(\u0026js);\n+\n+\t\tif (n \u003d\u003d LSJS_RESULT_ERROR)\n+\t\t\treturn 1;\n+\n+\t\twalk \u003d walk-\u003enext;\n+\t\tif (!walk) {\n+\t\t\tp +\u003d lws_snprintf((char *)p, end - p, \u0022]}\u0022);\n+\n+\t\t\tsais_websrv_broadcast(vhd-\u003eh_ss_websrv,\n+\t\t\t\t\t vhd-\u003ejson_builders,\n+\t\t\t\t\t lws_ptr_diff(p, vhd-\u003ejson_builders));\n+\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\n+\treturn 1;\n+}\n+\n+struct sais_arg {\n+\tconst char *uid;\n+\tint state;\n+};\n+\n+static void\n+_sais_taskchange(struct lws_ss_handle *h, void *_arg)\n+{\n+\twebsrvss_srv_t *m \u003d (websrvss_srv_t *)lws_ss_to_user_object(h);\n+\tstruct sais_arg *arg \u003d (struct sais_arg *)_arg;\n+\tchar tc[128];\n+\tint n;\n+\n+\tn \u003d lws_snprintf(tc, sizeof(tc), \u0022{\u005c\u0022schema\u005c\u0022:\u005c\u0022sai-taskchange\u005c\u0022, \u0022\n+\t\t\t\t\t \u0022\u005c\u0022event_hash\u005c\u0022:\u005c\u0022%s\u005c\u0022, \u005c\u0022state\u005c\u0022:%d}\u0022,\n+\t\t\t\t\t arg-\u003euid, arg-\u003estate);\n+\n+\tif (lws_buflist_append_segment(\u0026m-\u003ebltx, (uint8_t *)tc, n) \u003e\u003d 0)\n+\t\tlws_ss_request_tx(h);\n+\telse\n+\t\tlwsl_warn(\u0022%s: buflist append failed\u005cn\u0022, __func__);\n+}\n+\n+void\n+sais_taskchange(struct lws_ss_handle *hsrv, const char *task_uuid, int state)\n+{\n+\tstruct sais_arg arg \u003d { task_uuid, state };\n+\n+\tlws_ss_server_foreach_client(hsrv, _sais_taskchange, (void *)\u0026arg);\n+}\n+\n+void\n+_sais_eventchange(struct lws_ss_handle *h, void *_arg)\n+{\n+\twebsrvss_srv_t *m \u003d (websrvss_srv_t *)lws_ss_to_user_object(h);\n+\tstruct sais_arg *arg \u003d (struct sais_arg *)_arg;\n+\tchar tc[128];\n+\tint n;\n+\n+\tn \u003d lws_snprintf(tc, sizeof(tc), \u0022{\u005c\u0022schema\u005c\u0022:\u005c\u0022sai-eventchange\u005c\u0022, \u0022\n+\t\t\t\t\t \u0022\u005c\u0022event_hash\u005c\u0022:\u005c\u0022%s\u005c\u0022, \u005c\u0022state\u005c\u0022:%d}\u0022,\n+\t\t\t\t\t arg-\u003euid, arg-\u003estate);\n+\n+\tif (lws_buflist_append_segment(\u0026m-\u003ebltx, (uint8_t *)tc, n) \u003e\u003d 0)\n+\t\tlws_ss_request_tx(h);\n+\telse\n+\t\tlwsl_warn(\u0022%s: buflist append failed\u005cn\u0022, __func__);\n+}\n+\n+void\n+sais_eventchange(struct lws_ss_handle *hsrv, const char *event_uuid, int state)\n+{\n+\tstruct sais_arg arg \u003d { event_uuid, state };\n+\n+\tlws_ss_server_foreach_client(hsrv, _sais_eventchange, (void *)\u0026arg);\n+}\n+\n+\n+static lws_ss_state_return_t\n+websrvss_ws_rx(void *userobj, const uint8_t *buf, size_t len, int flags)\n+{\n+\twebsrvss_srv_t *m \u003d (websrvss_srv_t *)userobj;\n+\tchar qu[128], esc[96], *err \u003d NULL;\n+\tsai_browse_rx_evinfo_t *ei;\n+\tsqlite3 *pdb \u003d NULL;\n+\tlws_struct_args_t a;\n+\tlws_dll2_owner_t o;\n+\tint n;\n+\n+\tlwsl_user(\u0022%s: len %d, flags: %d\u005cn\u0022, __func__, (int)len, flags);\n+\tlwsl_hexdump_info(buf, len);\n+\n+\tmemset(\u0026a, 0, sizeof(a));\n+\ta.map_st[0] \u003d lsm_schema_json_map;\n+\ta.map_entries_st[0] \u003d LWS_ARRAY_SIZE(lsm_schema_json_map);\n+\ta.map_entries_st[1] \u003d LWS_ARRAY_SIZE(lsm_schema_json_map);\n+\ta.ac_block_size \u003d 128;\n+\n+\tlws_struct_json_init_parse(\u0026m-\u003ectx, NULL, \u0026a);\n+\tn \u003d lejp_parse(\u0026m-\u003ectx, (uint8_t *)buf, len);\n+\tif (n \u003c 0 || !a.dest) {\n+\t\tlwsl_hexdump_notice(buf, len);\n+\t\tlwsl_notice(\u0022%s: notification JSON decode failed '%s'\u005cn\u0022,\n+\t\t\t\t__func__, lejp_error_to_string(n));\n+\t\treturn LWSSSSRET_DISCONNECT_ME;\n+\t}\n+\n+\tlwsl_notice(\u0022%s: schema idx %d\u005cn\u0022, __func__, a.top_schema_index);\n+\n+\tswitch (a.top_schema_index) {\n+\tcase SAIS_WS_WEBSRV_RX_TASKRESET:\n+\n+\t\tei \u003d (sai_browse_rx_evinfo_t *)a.dest;\n+\t\tif (sais_validate_id(ei-\u003eevent_hash, SAI_TASKID_LEN))\n+\t\t\tgoto soft_error;\n+\n+\t\tsais_task_reset(m-\u003evhd, ei-\u003eevent_hash);\n+\t\tbreak;\n+\n+\tcase SAIS_WS_WEBSRV_RX_EVENTRESET:\n+\n+\t\tei \u003d (sai_browse_rx_evinfo_t *)a.dest;\n+\t\tif (sais_validate_id(ei-\u003eevent_hash, SAI_EVENTID_LEN))\n+\t\t\tgoto soft_error;\n+\n+\t\t/* open the event-specific database object */\n+\n+\t\tif (sais_event_db_ensure_open(m-\u003evhd, ei-\u003eevent_hash, 0, \u0026pdb)) {\n+\t\t\tlwsl_err(\u0022%s: unable to open event-specific database\u005cn\u0022,\n+\t\t\t\t\t__func__);\n+\t\t\t/*\n+\t\t\t * hanging up isn't a good way to deal with browser\n+\t\t\t * tabs left open with a live connection to a\n+\t\t\t * now-deleted task... the page will reconnect endlessly\n+\t\t\t */\n+\t\t\tgoto soft_error;\n+\t\t}\n+\n+\t\t/*\n+\t\t * Retreive all the related structs into a dll2 list\n+\t\t */\n+\n+\t\tlws_sql_purify(esc, ei-\u003eevent_hash, sizeof(esc));\n+\n+\t\tif (lws_struct_sq3_deserialize(pdb, NULL, NULL,\n+\t\t\t\t\t lsm_schema_sq3_map_task,\n+\t\t\t\t\t \u0026o, \u0026a.ac, 0, 999) \u003e\u003d 0) {\n+\n+\t\t\tsqlite3_exec(pdb, \u0022BEGIN TRANSACTION\u0022,\n+\t\t\t\t NULL, NULL, \u0026err);\n+\n+\t\t\t/*\n+\t\t\t * Walk the results list resetting all the tasks\n+\t\t\t */\n+\n+\t\t\tlws_start_foreach_dll(struct lws_dll2 *, p, o.head) {\n+\t\t\t\tsai_task_t *t \u003d lws_container_of(p, sai_task_t,\n+\t\t\t\t\t\t\t\t list);\n+\n+\t\t\t\tsais_task_reset(m-\u003evhd, t-\u003euuid);\n+\n+\t\t\t} lws_end_foreach_dll(p);\n+\n+\t\t\tsqlite3_exec(pdb, \u0022END TRANSACTION\u0022,\n+\t\t\t\t NULL, NULL, \u0026err);\n+\t\t}\n+\n+\t\tsais_event_db_close(m-\u003evhd, \u0026pdb);\n+\t\tlwsac_free(\u0026a.ac);\n+\n+\t\treturn 0;\n+\n+\tcase SAIS_WS_WEBSRV_RX_EVENTDELETE:\n+\n+\t\tei \u003d (sai_browse_rx_evinfo_t *)a.dest;\n+\t\tif (sais_validate_id(ei-\u003eevent_hash, SAI_EVENTID_LEN))\n+\t\t\tgoto soft_error;\n+\n+\t\tlwsl_notice(\u0022%s: eventdelete %s\u005cn\u0022, __func__, ei-\u003eevent_hash);\n+\n+\t\t/* open the event-specific database object */\n+\n+\t\tif (sais_event_db_ensure_open(m-\u003evhd, ei-\u003eevent_hash, 0, \u0026pdb)) {\n+\t\t\tlwsl_notice(\u0022%s: unable to open event-specific database\u005cn\u0022,\n+\t\t\t\t\t__func__);\n+\t\t\t/*\n+\t\t\t * hanging up isn't a good way to deal with browser\n+\t\t\t * tabs left open with a live connection to a\n+\t\t\t * now-deleted task... the page will reconnect endlessly\n+\t\t\t */\n+\n+\t\t\t// goto soft_error;\n+\t\t} else {\n+\n+\t\t\t/*\n+\t\t\t * Retreive all the related structs into a dll2 list\n+\t\t\t */\n+\n+\t\t\tlws_sql_purify(esc, ei-\u003eevent_hash, sizeof(esc));\n+\n+\t\t\tif (lws_struct_sq3_deserialize(pdb, NULL, NULL,\n+\t\t\t\t\t\t lsm_schema_sq3_map_task,\n+\t\t\t\t\t\t \u0026o, \u0026a.ac, 0, 999) \u003e\u003d 0) {\n+\n+\t\t\t\tsqlite3_exec(pdb, \u0022BEGIN TRANSACTION\u0022,\n+\t\t\t\t\t NULL, NULL, \u0026err);\n+\n+\t\t\t\t/*\n+\t\t\t\t * Walk the results list cancelling all the tasks\n+\t\t\t\t * that look like they might be ongoing\n+\t\t\t\t */\n+\n+\t\t\t\tlws_start_foreach_dll(struct lws_dll2 *, p, o.head) {\n+\t\t\t\t\tsai_task_t *t \u003d lws_container_of(p, sai_task_t,\n+\t\t\t\t\t\t\t\t\t list);\n+\n+\t\t\t\t\tif (t-\u003estate !\u003d SAIES_WAITING \u0026\u0026\n+\t\t\t\t\t t-\u003estate !\u003d SAIES_SUCCESS \u0026\u0026\n+\t\t\t\t\t t-\u003estate !\u003d SAIES_FAIL \u0026\u0026\n+\t\t\t\t\t t-\u003estate !\u003d SAIES_CANCELLED)\n+\t\t\t\t\t\tsais_task_cancel(m-\u003evhd, t-\u003euuid);\n+\n+\t\t\t\t} lws_end_foreach_dll(p);\n+\n+\t\t\t\tsqlite3_exec(pdb, \u0022END TRANSACTION\u0022,\n+\t\t\t\t\t NULL, NULL, \u0026err);\n+\t\t\t}\n+\n+\t\t\tsais_event_db_close(m-\u003evhd, \u0026pdb);\n+\t\t}\n+\n+\t\t/* delete the event iself */\n+\n+\t\tlws_sql_purify(esc, ei-\u003eevent_hash, sizeof(esc));\n+\t\tlws_snprintf(qu, sizeof(qu), \u0022delete from events where uuid\u003d'%s'\u0022,\n+\t\t\t\tesc);\n+\t\tsqlite3_exec(m-\u003evhd-\u003eserver.pdb, qu, NULL, NULL, \u0026err);\n+\t\tif (err) {\n+\t\t\tlwsl_err(\u0022%s: evdel uuid %s, sq3 err %s\u005cn\u0022, __func__,\n+\t\t\t\t\tesc, err);\n+\t\t\tsqlite3_free(err);\n+\t\t}\n+\n+\t\t/* remove the event-specific database */\n+\n+\t\tsais_event_db_delete_database(m-\u003evhd, ei-\u003eevent_hash);\n+\n+\t\tlwsac_free(\u0026a.ac);\n+\t\tsais_eventchange(m-\u003evhd-\u003eh_ss_websrv, ei-\u003eevent_hash,\n+\t\t\t\t SAIES_DELETED);\n+\t\tsais_websrv_broadcast(m-\u003evhd-\u003eh_ss_websrv,\n+\t\t\t\t \u0022{\u005c\u0022schema\u005c\u0022:\u005c\u0022sai-overview\u005c\u0022}\u0022, 25);\n+\n+\t\treturn 0;\n+\n+\tcase SAIS_WS_WEBSRV_RX_TASKCANCEL:\n+\n+\n+\t\tei \u003d (sai_browse_rx_evinfo_t *)a.dest;\n+\t\tif (sais_validate_id(ei-\u003eevent_hash, SAI_TASKID_LEN))\n+\t\t\tgoto soft_error;\n+\n+\t\tsais_task_cancel(m-\u003evhd, ei-\u003eevent_hash);\n+\n+\t\tbreak;\n+\t}\n+\n+\treturn 0;\n+\n+soft_error:\n+\tlwsl_warn(\u0022%s: soft error\u005cn\u0022, __func__);\n+\n+\treturn 0;\n+}\n+\n+static lws_ss_state_return_t\n+websrvss_ws_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf,\n+\t size_t *len, int *flags)\n+{\n+\twebsrvss_srv_t *m \u003d (websrvss_srv_t *)userobj;\n+\tchar som, eom;\n+\tint used;\n+\n+\tif (!m-\u003ebltx)\n+\t\treturn LWSSSSRET_TX_DONT_SEND;\n+\n+\tused \u003d lws_buflist_fragment_use(\u0026m-\u003ebltx, buf, *len, \u0026som, \u0026eom);\n+\tif (!used)\n+\t\treturn LWSSSSRET_TX_DONT_SEND;\n+\n+\t*flags \u003d (som ? LWSSS_FLAG_SOM : 0) | (eom ? LWSSS_FLAG_EOM : 0);\n+\t*len \u003d (size_t)used;\n+\n+\tif (m-\u003ebltx)\n+\t\tlws_ss_request_tx(m-\u003ess);\n+\n+\treturn 0;\n+}\n+\n+static lws_ss_state_return_t\n+websrvss_srv_state(void *userobj, void *sh, lws_ss_constate_t state,\n+\t lws_ss_tx_ordinal_t ack)\n+{\n+\twebsrvss_srv_t *m \u003d (websrvss_srv_t *)userobj;\n+\n+\tlwsl_user(\u0022%s: %p %s, ord 0x%x\u005cn\u0022, __func__, m-\u003ess,\n+\t\t lws_ss_state_name(state), (unsigned int)ack);\n+\n+\tswitch (state) {\n+\tcase LWSSSCS_DISCONNECTED:\n+\t\tlws_buflist_destroy_all_segments(\u0026m-\u003ebltx);\n+\t\tbreak;\n+\tcase LWSSSCS_CREATING:\n+\t\tlws_ss_request_tx(m-\u003ess);\n+\t\tbreak;\n+\tcase LWSSSCS_CONNECTED:\n+\t\tsais_list_builders(m-\u003evhd);\n+\t\tbreak;\n+\tcase LWSSSCS_ALL_RETRIES_FAILED:\n+\t\tbreak;\n+\n+\tcase LWSSSCS_SERVER_TXN:\n+\t\tbreak;\n+\n+\tcase LWSSSCS_SERVER_UPGRADE:\n+\t\tbreak;\n+\n+\tdefault:\n+\t\tbreak;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+const lws_ss_info_t ssi_server \u003d {\n+\t.handle_offset\t\t\t\u003d offsetof(websrvss_srv_t, ss),\n+\t.opaque_user_data_offset\t\u003d offsetof(websrvss_srv_t, vhd),\n+\t.streamtype\t\t\t\u003d \u0022websrv\u0022,\n+\t.rx\t\t\t\t\u003d websrvss_ws_rx,\n+\t.tx\t\t\t\t\u003d websrvss_ws_tx,\n+\t.state\t\t\t\t\u003d websrvss_srv_state,\n+\t.user_alloc\t\t\t\u003d sizeof(websrvss_srv_t),\n+};\ndiff --git a/src/server/s-ws-builder.c b/src/server/s-ws-builder.c\nnew file mode 100644\nindex 0000000..44936db\n--- /dev/null\n+++ b/src/server/s-ws-builder.c\n@@ -0,0 +1,750 @@\n+/*\n+ * Sai server - ./src/server/ws-json-rx.c\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ *\n+ * These are ws rx and tx handlers related to builder ws connections, on\n+ * /builder\n+ */\n+\n+#include \u003clibwebsockets.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003ctime.h\u003e\n+\n+#include \u0022s-private.h\u0022\n+\n+enum sai_overview_state {\n+\tSOS_EVENT,\n+\tSOS_TASKS,\n+};\n+\n+typedef struct sais_logcache_pertask {\n+\tlws_dll2_t\t\tlist; /* vhd-\u003etasklog_cache is the owner */\n+\tchar\t\t\tuuid[65];\n+\tlws_dll2_owner_t\tcache; /* sai_log_t */\n+} sais_logcache_pertask_t;\n+\n+/*\n+ * The Schema that may be sent to us by a builder\n+ *\n+ * Artifacts are sent on secondary SS connections so they don't block ongoing\n+ * log delivery etc. The JSON is immediately followed by binary data to the\n+ * length told in the JSON.\n+ */\n+\n+static const lws_struct_map_t lsm_schema_map_ba[] \u003d {\n+\tLSM_SCHEMA_DLL2\t(sai_plat_owner_t, plat_owner, NULL, lsm_plat_list,\n+\t\t\t\t\t\t\u0022com-warmcat-sai-ba\u0022),\n+\tLSM_SCHEMA (sai_log_t,\t NULL, lsm_log,\n+\t\t\t\t\t\t\u0022com-warmcat-sai-logs\u0022),\n+\tLSM_SCHEMA (sai_event_t,\t NULL, lsm_task_rej,\n+\t\t\t\t\t\t\u0022com.warmcat.sai.taskrej\u0022),\n+\tLSM_SCHEMA (sai_artifact_t, NULL, lsm_artifact,\n+\t\t\t\t\t\t\u0022com-warmcat-sai-artifact\u0022),\n+};\n+\n+enum {\n+\tSAIM_WSSCH_BUILDER_PLATS,\n+\tSAIM_WSSCH_BUILDER_LOGS,\n+\tSAIM_WSSCH_BUILDER_TASKREJ,\n+\tSAIM_WSSCH_BUILDER_ARTIFACT\n+};\n+\n+static void\n+sais_dump_logs_to_db(lws_sorted_usec_list_t *sul)\n+{\n+\tstruct vhd *vhd \u003d lws_container_of(sul, struct vhd, sul_logcache);\n+\tsais_logcache_pertask_t *lcpt;\n+\tchar event_uuid[33], sw[192];\n+\tsqlite3 *pdb \u003d NULL;\n+\tsai_log_t *hlog;\n+\tchar *err;\n+\tint n;\n+\n+\t/*\n+\t * for each task that acquired logs in the interval\n+\t */\n+\n+\tlws_start_foreach_dll_safe(struct lws_dll2 *, p, p1,\n+\t\t\t\t vhd-\u003etasklog_cache.head) {\n+\t\tlcpt \u003d lws_container_of(p, sais_logcache_pertask_t, list);\n+\n+\t\tsai_task_uuid_to_event_uuid(event_uuid, lcpt-\u003euuid);\n+\n+\t\tif (!sais_event_db_ensure_open(vhd, event_uuid, 0, \u0026pdb)) {\n+\n+\t\t\t/*\n+\t\t\t * Empty the task-specific log cache into the event-\n+\t\t\t * specific db for the task in one go, this is much\n+\t\t\t * more efficient\n+\t\t\t */\n+\n+\t\t\tsqlite3_exec(pdb, \u0022BEGIN TRANSACTION\u0022, NULL, NULL, \u0026err);\n+\t\t\tif (err)\n+\t\t\t\tsqlite3_free(err);\n+\n+\t\t\tlws_struct_sq3_serialize(pdb, lsm_schema_sq3_map_log,\n+\t\t\t\t\t \u0026lcpt-\u003ecache, 0);\n+\n+\t\t\tsqlite3_exec(pdb, \u0022END TRANSACTION\u0022, NULL, NULL, \u0026err);\n+\t\t\tif (err)\n+\t\t\t\tsqlite3_free(err);\n+\t\t\tsais_event_db_close(vhd, \u0026pdb);\n+\n+\t\t} else\n+\t\t\tlwsl_err(\u0022%s: unable to open event-specific database\u005cn\u0022,\n+\t\t\t\t\t__func__);\n+\n+\t\t/*\n+\t\t * Destroy the logs in the task cache and the task cache\n+\t\t */\n+\n+\t\tlws_start_foreach_dll_safe(struct lws_dll2 *, pq, pq1,\n+\t\t\t\t\t lcpt-\u003ecache.head) {\n+\t\t\thlog \u003d lws_container_of(pq, sai_log_t, list);\n+\t\t\tlws_dll2_remove(\u0026hlog-\u003elist);\n+\t\t\tfree(hlog);\n+\t\t} lws_end_foreach_dll_safe(pq, pq1);\n+\n+\t\t/*\n+\t\t * Inform anybody who's looking at this task's logs that\n+\t\t * something changed (event_hash is actually the task hash)\n+\t\t */\n+\n+\t\tn \u003d lws_snprintf(sw, sizeof(sw), \u0022{\u005c\u0022schema\u005c\u0022:\u005c\u0022sai-tasklogs\u005c\u0022,\u0022\n+\t\t\t\t \u0022\u005c\u0022event_hash\u005c\u0022:\u005c\u0022%s\u005c\u0022}\u0022, lcpt-\u003euuid);\n+\t\tsais_websrv_broadcast(vhd-\u003eh_ss_websrv, sw, n);\n+\n+\t\t/*\n+\t\t * Destroy the whole task-specific cache, it will regenerate\n+\t\t * if more logs come for it\n+\t\t */\n+\n+\t\tlws_dll2_remove(\u0026lcpt-\u003elist);\n+\t\tfree(lcpt);\n+\n+\t} lws_end_foreach_dll_safe(p, p1);\n+\n+}\n+\n+/*\n+ * We're going to stash these logs on a per-task list, and deal with them\n+ * inside a single trasaction per task efficiently on a timer.\n+ */\n+\n+static void\n+sais_log_to_db(struct vhd *vhd, sai_log_t *log)\n+{\n+\tsais_logcache_pertask_t *lcpt \u003d NULL;\n+\tsai_log_t *hlog;\n+\n+\t/*\n+\t * find the pertask if one exists\n+\t */\n+\n+\tlws_start_foreach_dll(struct lws_dll2 *, p, vhd-\u003etasklog_cache.head) {\n+\t\tlcpt \u003d lws_container_of(p, sais_logcache_pertask_t, list);\n+\n+\t\tif (!strcmp(lcpt-\u003euuid, log-\u003etask_uuid))\n+\t\t\tbreak;\n+\t\tlcpt \u003d NULL;\n+\n+\t} lws_end_foreach_dll(p);\n+\n+\tif (!lcpt) {\n+\t\t/*\n+\t\t * Create a pertask and add it to the vhd list of them\n+\t\t */\n+\t\tlcpt \u003d malloc(sizeof(*lcpt));\n+\t\tmemset(lcpt, 0, sizeof(*lcpt));\n+\t\tlws_strncpy(lcpt-\u003euuid, log-\u003etask_uuid, sizeof(lcpt-\u003euuid));\n+\t\tlws_dll2_add_tail(\u0026lcpt-\u003elist, \u0026vhd-\u003etasklog_cache);\n+\t}\n+\n+\thlog \u003d malloc(sizeof(*hlog) + log-\u003elen + strlen(log-\u003elog) + 1);\n+\tif (!hlog)\n+\t\treturn;\n+\n+\t*hlog \u003d *log;\n+\tmemset(\u0026hlog-\u003elist, 0, sizeof(hlog-\u003elist));\n+\tmemcpy(\u0026hlog[1], log-\u003elog, strlen(log-\u003elog) + 1);\n+\thlog-\u003elog \u003d (char *)\u0026hlog[1];\n+\n+\t/*\n+\t * add our log copy to the task-specific cache\n+\t */\n+\n+\tlws_dll2_add_tail(\u0026hlog-\u003elist, \u0026lcpt-\u003ecache);\n+\n+\tif (!vhd-\u003esul_logcache.list.owner)\n+\t\t/* if not already scheduled, schedule it for 250ms */\n+\n+\t\tlws_sul_schedule(vhd-\u003econtext, 0, \u0026vhd-\u003esul_logcache,\n+\t\t\t\t sais_dump_logs_to_db, 250 * LWS_US_PER_MS);\n+}\n+\n+static sai_plat_t *\n+sais_builder_from_uuid(struct vhd *vhd, const char *hostname)\n+{\n+\tlws_start_foreach_dll(struct lws_dll2 *, p,\n+\t\t\t vhd-\u003eserver.builder_owner.head) {\n+\t\tsai_plat_t *cb \u003d lws_container_of(p, sai_plat_t,\n+\t\t\t\tsai_plat_list);\n+\n+\t\tif (!strcmp(hostname, cb-\u003ename))\n+\t\t\treturn cb;\n+\n+\t} lws_end_foreach_dll(p);\n+\n+\treturn NULL;\n+}\n+\n+int\n+sai_sql3_get_uint64_cb(void *user, int cols, char **values, char **name)\n+{\n+\tuint64_t *pui \u003d (uint64_t *)user;\n+\n+\t*pui \u003d (uint64_t)atoll(values[0]);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Master received a communication from a builder\n+ */\n+\n+int\n+sais_ws_json_rx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t bl)\n+{\n+\tchar event_uuid[33], s[128], esc[96];\n+\tstruct lwsac *ac \u003d NULL;\n+\tsai_plat_t *build, *cb;\n+\tsai_rejection_t *rej;\n+\tlws_dll2_owner_t o;\n+\tsai_artifact_t *ap;\n+\tsai_task_t *task;\n+\tsai_log_t *log;\n+\tuint64_t rid;\n+\tint n, m;\n+\n+\t/*\n+\t * use the schema name on the incoming JSON to decide what kind of\n+\t * structure to instantiate\n+\t *\n+\t * We may have:\n+\t *\n+\t * - just received a fragment of the whole JSON\n+\t *\n+\t * - received the JSON and be handling appeneded blob data\n+\t */\n+\n+\tif (pss-\u003ebulk_binary_data)\n+\t\tgoto handle;\n+\n+\tif (!pss-\u003efrag) {\n+\t\tmemset(\u0026pss-\u003ea, 0, sizeof(pss-\u003ea));\n+\t\tpss-\u003ea.map_st[0] \u003d lsm_schema_map_ba;\n+\t\tpss-\u003ea.map_entries_st[0] \u003d LWS_ARRAY_SIZE(lsm_schema_map_ba);\n+\t\tpss-\u003ea.ac_block_size \u003d 4096;\n+\n+\t\tlws_struct_json_init_parse(\u0026pss-\u003ectx, NULL, \u0026pss-\u003ea);\n+\t} else\n+\t\tpss-\u003efrag \u003d 0;\n+\n+\tm \u003d lejp_parse(\u0026pss-\u003ectx, (uint8_t *)buf, bl);\n+\n+\t/*\n+\t * returns negative, or unused amount... for us, we either had a\n+\t * (negative) error, had LEJP_CONTINUE, or if 0/positive, finished\n+\t */\n+\tif (m \u003c 0 \u0026\u0026 m !\u003d LEJP_CONTINUE) {\n+\t\t/* an explicit error */\n+\t\tlwsl_hexdump_err(buf, bl);\n+\t\tlwsl_err(\u0022%s: rx JSON decode failed '%s', %d, %s, %s, %d\u005cn\u0022,\n+\t\t\t __func__, lejp_error_to_string(m), m,\n+\t\t\t pss-\u003ectx.path, pss-\u003ectx.buf, pss-\u003ectx.npos);\n+\t\tlwsac_free(\u0026pss-\u003ea.ac);\n+\t\treturn 1;\n+\t}\n+\n+\tif (m \u003d\u003d LEJP_CONTINUE) {\n+\t\tpss-\u003efrag \u003d 1;\n+\t\treturn 0;\n+\t}\n+\n+\tif (!pss-\u003ea.dest) {\n+\t\tlwsac_free(\u0026pss-\u003ea.ac);\n+\t\tlwsl_err(\u0022%s: json decode didn't make an object\u005cn\u0022, __func__);\n+\t\treturn 1;\n+\t}\n+\n+handle:\n+\tswitch (pss-\u003ea.top_schema_index) {\n+\tcase SAIM_WSSCH_BUILDER_PLATS:\n+\n+\t\t// lwsl_hexdump_notice(buf, bl);\n+\n+\t\t/*\n+\t\t * builder is sending us an array of platforms it provides us\n+\t\t */\n+\n+\t\tpss-\u003eu.o \u003d (sai_plat_owner_t *)pss-\u003ea.dest;\n+\n+\t\tlwsl_notice(\u0022%s: seen platform list: count %d\u005cn\u0022, __func__,\n+\t\t\t\tpss-\u003eu.o-\u003eplat_owner.count);\n+\n+\t\tlws_start_foreach_dll(struct lws_dll2 *, pb,\n+\t\t\t\t pss-\u003eu.o-\u003eplat_owner.head) {\n+\t\t\tbuild \u003d lws_container_of(pb, sai_plat_t, sai_plat_list);\n+\n+\t\t\tlwsl_notice(\u0022%s: seeing plat %s\u005cn\u0022, __func__, build-\u003ename);\n+\n+\t\t\t/*\n+\t\t\t * ... so is this one a new guy?\n+\t\t\t */\n+\n+\t\t\tcb \u003d sais_builder_from_uuid(vhd, build-\u003ename);\n+\t\t\tif (!cb) {\n+\t\t\t\tchar *cp;\n+\n+\t\t\t\t/*\n+\t\t\t\t * We need to make a persistent, deep, copy of\n+\t\t\t\t * the (from JSON) builder object representing\n+\t\t\t\t * this client.\n+\t\t\t\t *\n+\t\t\t\t * \u0022platform\u0022 is eg \u0022linux-ubuntu-bionic-arm64\u0022\n+\t\t\t\t * and \u0022name\u0022 is \u0022hostname.\u003cplatform\u003e\u0022.\n+\t\t\t\t */\n+\n+\t\t\t\tif (!build-\u003ename || !build-\u003eplatform) {\n+\t\t\t\t\tlwsl_err(\u0022%s: missing build '%s'/hostname '%s'\u005cn\u0022,\n+\t\t\t\t\t\t__func__,\n+\t\t\t\t\t\tbuild-\u003ename ? build-\u003ename : \u0022null\u0022,\n+\t\t\t\t\t\tbuild-\u003eplatform ? build-\u003eplatform : \u0022null\u0022);\n+\t\t\t\t\treturn -1;\n+\t\t\t\t}\n+\n+\t\t\t\tcb \u003d malloc(sizeof(*cb) +\n+\t\t\t\t\t strlen(build-\u003ename) + 1 +\n+\t\t\t\t\t strlen(build-\u003eplatform) + 1);\n+\n+\t\t\t\tmemset(cb, 0, sizeof(*cb));\n+\t\t\t\tcp \u003d (char *)\u0026cb[1];\n+\n+\t\t\t\tmemcpy(cp, build-\u003ename, strlen(build-\u003ename) + 1);\n+\t\t\t\tcb-\u003ename \u003d cp;\n+\t\t\t\tcp +\u003d strlen(build-\u003ename) + 1;\n+\n+\t\t\t\tmemcpy(cp, build-\u003eplatform, strlen(build-\u003eplatform) + 1);\n+\t\t\t\tcb-\u003eplatform \u003d cp;\n+\t\t\t\tcp +\u003d strlen(build-\u003eplatform) + 1;\n+\n+\t\t\t\tcb-\u003eongoing \u003d build-\u003eongoing;\n+\t\t\t\tcb-\u003einstances \u003d build-\u003einstances;\n+\n+\t\t\t\tcb-\u003ewsi \u003d pss-\u003ewsi;\n+\n+\t\t\t\t/* Then attach the copy to the server in the vhd\n+\t\t\t\t */\n+\t\t\t\tlws_dll2_add_tail(\u0026cb-\u003esai_plat_list,\n+\t\t\t\t\t\t \u0026vhd-\u003eserver.builder_owner);\n+\t\t\t}\n+\n+\t\t\t/*\n+\t\t\t * Even if he's not new, we should use his updated info about\n+\t\t\t * builder load\n+\t\t\t */\n+\t\t\tcb-\u003eongoing \u003d build-\u003eongoing;\n+\t\t\tcb-\u003einstances \u003d build-\u003einstances;\n+\n+\t\t\tlwsl_notice(\u0022%s: builder %s reports load %d/%d\u005cn\u0022,\n+\t\t\t\t __func__, cb-\u003ename, cb-\u003eongoing,\n+\t\t\t\t cb-\u003einstances);\n+\n+\t\t} lws_end_foreach_dll(pb);\n+\n+\t\tlwsac_free(\u0026pss-\u003ea.ac);\n+\n+\n+\t\t/*\n+\t\t * look if we should offer the builder a task, given the\n+\t\t * platforms he's offering\n+\t\t */\n+\n+\t\tif (sais_allocate_task(vhd, pss, cb, cb-\u003eplatform) \u003c 0)\n+\t\t\tgoto bail;\n+\n+\t\t/*\n+\t\t * If we did allocate a task in pss-\u003ea.ac, responsibility of\n+\t\t * callback_on_writable handler to empty it\n+\t\t */\n+\n+\t\tbreak;\n+\n+bail:\n+\t\tlwsac_free(\u0026pss-\u003ea.ac);\n+\t\treturn -1;\n+\n+\tcase SAIM_WSSCH_BUILDER_LOGS:\n+\t\t/*\n+\t\t * builder is sending us info about task logs\n+\t\t */\n+\n+\t\tlog \u003d (sai_log_t *)pss-\u003ea.dest;\n+\t\tsais_log_to_db(vhd, log);\n+\n+\t\tif (pss-\u003emark_started) {\n+\t\t\tpss-\u003emark_started \u003d 0;\n+\t\t\tpss-\u003efirst_log_timestamp \u003d log-\u003etimestamp;\n+\t\t\tif (sais_set_task_state(vhd, NULL, NULL, log-\u003etask_uuid,\n+\t\t\t\t\t\tSAIES_BEING_BUILT, 0, 0))\n+\t\t\t\tgoto bail;\n+\t\t}\n+\n+\t\tif (log-\u003efinished) {\n+\t\t\t/*\n+\t\t\t * We have reached the end of the logs for this task\n+\t\t\t */\n+\t\t\tlwsl_info(\u0022%s: log-\u003efinished says 0x%x, dur %lluus\u005cn\u0022,\n+\t\t\t\t __func__, log-\u003efinished, (unsigned long long)(\n+\t\t\t\t log-\u003etimestamp - pss-\u003efirst_log_timestamp));\n+\t\t\tif (log-\u003efinished \u0026 SAISPRF_EXIT) {\n+\t\t\t\tif ((log-\u003efinished \u0026 0xff) \u003d\u003d 0)\n+\t\t\t\t\tn \u003d SAIES_SUCCESS;\n+\t\t\t\telse\n+\t\t\t\t\tn \u003d SAIES_FAIL;\n+\t\t\t} else\n+\t\t\t\tif (log-\u003efinished \u0026 8192)\n+\t\t\t\t\tn \u003d SAIES_CANCELLED;\n+\t\t\t\telse\n+\t\t\t\t\tn \u003d SAIES_FAIL;\n+\n+\t\t\tif (sais_set_task_state(vhd, NULL, NULL, log-\u003etask_uuid,\n+\t\t\t\t\t\tn, 0, log-\u003etimestamp -\n+\t\t\t\t\t\t pss-\u003efirst_log_timestamp))\n+\t\t\t\tgoto bail;\n+\t\t}\n+\n+\t\tlwsac_free(\u0026pss-\u003ea.ac);\n+\n+\t\tbreak;\n+\n+\tcase SAIM_WSSCH_BUILDER_TASKREJ:\n+\n+\t\t/*\n+\t\t * builder is updating us about his status, and may be\n+\t\t * rejecting a task we tried to give him\n+\t\t */\n+\n+\t\trej \u003d (sai_rejection_t *)pss-\u003ea.dest;\n+\n+\t\tcb \u003d sais_builder_from_uuid(vhd, rej-\u003ehost_platform);\n+\t\tif (!cb) {\n+\t\t\tlwsl_info(\u0022%s: unknown builder %s rejecting\u005cn\u0022,\n+\t\t\t\t __func__, rej-\u003ehost_platform);\n+\t\t\tlwsac_free(\u0026pss-\u003ea.ac);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\t/* update our info about builder state with reality */\n+\n+\t\tcb-\u003eongoing \u003d rej-\u003eongoing;\n+\t\tcb-\u003einstances \u003d rej-\u003elimit;\n+\n+\t\tlwsl_notice(\u0022%s: builder %s reports load %d/%d (rej %s)\u005cn\u0022,\n+\t\t\t __func__, cb-\u003ename, cb-\u003eongoing, cb-\u003einstances,\n+\t\t\t rej-\u003etask_uuid[0] ? rej-\u003etask_uuid : \u0022none\u0022);\n+\n+\t\tif (rej-\u003etask_uuid[0])\n+\t\t\tsais_task_reset(vhd, rej-\u003etask_uuid);\n+\n+\t\tlwsac_free(\u0026pss-\u003ea.ac);\n+\t\tbreak;\n+\n+\tcase SAIM_WSSCH_BUILDER_ARTIFACT:\n+\t\t/*\n+\t\t * We get sent a JSON object immediately followed by binary\n+\t\t * data for the artifact.\n+\t\t *\n+\t\t * We place the binary data as a blob in the sql record in the\n+\t\t * artifact table.\n+\t\t */\n+\n+\t\tlwsl_debug(\u0022%s: SAIM_WSSCH_BUILDER_ARTIFACT\u005cn\u0022, __func__);\n+\n+\t\tif (!pss-\u003ebulk_binary_data) {\n+\n+\t\t\tap \u003d (sai_artifact_t *)pss-\u003ea.dest;\n+\n+\t\t\tsai_task_uuid_to_event_uuid(event_uuid, ap-\u003etask_uuid);\n+\n+\t\t\t/*\n+\t\t\t * Open the event-specific database object... the\n+\t\t\t * handle is closed when the stream closes for whatever\n+\t\t\t * reason.\n+\t\t\t */\n+\n+\t\t\tif (sais_event_db_ensure_open(pss-\u003evhd, event_uuid, 0,\n+\t\t\t\t\t\t \u0026pss-\u003epdb_artifact)) {\n+\t\t\t\tlwsl_err(\u0022%s: unable to open event-specific \u0022\n+\t\t\t\t\t \u0022database\u005cn\u0022, __func__);\n+\n+\t\t\t\tlwsac_free(\u0026pss-\u003ea.ac);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\n+\t\t\t/*\n+\t\t\t * Retreive the task object\n+\t\t\t */\n+\n+\t\t\tlws_sql_purify(esc, ap-\u003etask_uuid, sizeof(esc));\n+\t\t\tlws_snprintf(s, sizeof(s),\u0022 and uuid \u003d\u003d \u005c\u0022%s\u005c\u0022\u0022, esc);\n+\t\t\tn \u003d lws_struct_sq3_deserialize(pss-\u003epdb_artifact, s,\n+\t\t\t\t\t\t NULL, lsm_schema_sq3_map_task,\n+\t\t\t\t\t\t \u0026o, \u0026ac, 0, 1);\n+\t\t\tif (n \u003c 0 || !o.head) {\n+\t\t\t\tsais_event_db_close(vhd, \u0026pss-\u003epdb_artifact);\n+\t\t\t\tlwsl_notice(\u0022%s: no task of that id\u005cn\u0022, __func__);\n+\t\t\t\tlwsac_free(\u0026pss-\u003ea.ac);\n+\t\t\t\treturn -1;\n+\t\t\t}\n+\n+\t\t\ttask \u003d (sai_task_t *)o.head;\n+\t\t\tn \u003d strcmp(task-\u003eart_up_nonce, ap-\u003eartifact_up_nonce);\n+\n+\t\t\tif (n) {\n+\t\t\t\tlwsl_err(\u0022%s: artifact nonce mismatch\u005cn\u0022,\n+\t\t\t\t\t __func__);\n+\t\t\t\tgoto afail;\n+\t\t\t}\n+\n+\t\t\t/*\n+\t\t\t * The task the sender is sending us an artifact for\n+\t\t\t * exists. The sender knows the random upload nonce\n+\t\t\t * for that task's artifacts.\n+\t\t\t *\n+\t\t\t * Create a random download nonce unrelated to the\n+\t\t\t * random upload nonce (so knowing the download one\n+\t\t\t * won't let you upload anything).\n+\t\t\t *\n+\t\t\t * Create the artifact's entry in the event-specific\n+\t\t\t * database\n+\t\t\t */\n+\n+\t\t\tsai_uuid16_create(pss-\u003evhd-\u003econtext,\n+\t\t\t\t\t ap-\u003eartifact_down_nonce);\n+\n+\t\t\tlws_dll2_owner_clear(\u0026o);\n+\t\t\tlws_dll2_add_head(\u0026ap-\u003elist, \u0026o);\n+\n+\t\t\t/*\n+\t\t\t * Create the task in event-specific database\n+\t\t\t */\n+\n+\t\t\tif (lws_struct_sq3_serialize(pss-\u003epdb_artifact,\n+\t\t\t\t\t\t lsm_schema_sq3_map_artifact,\n+\t\t\t\t\t\t \u0026o, ap-\u003euid)) {\n+\t\t\t\tlwsl_err(\u0022%s: failed artifact struct insert\u005cn\u0022,\n+\t\t\t\t\t\t__func__);\n+\n+\t\t\t\tgoto afail;\n+\t\t\t}\n+\n+\t\t\t/*\n+\t\t\t * recover the rowid\n+\t\t\t */\n+\n+\t\t\tlws_snprintf(s, sizeof(s),\n+\t\t\t\t \u0022select rowid from artifacts \u0022\n+\t\t\t\t\t\u0022where timestamp\u003d%llu\u0022,\n+\t\t\t\t (unsigned long long)ap-\u003etimestamp);\n+\n+\t\t\tif (sqlite3_exec((sqlite3 *)pss-\u003epdb_artifact, s,\n+\t\t\t\t\tsai_sql3_get_uint64_cb, \u0026rid, NULL) !\u003d\n+\t\t\t\t\t\t\t\t SQLITE_OK) {\n+\t\t\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, s,\n+\t\t\t\t\t sqlite3_errmsg(pss-\u003epdb_artifact));\n+\t\t\t\tgoto afail;\n+\t\t\t}\n+\n+\t\t\t/*\n+\t\t\t * Set the blob size on associated row\n+\t\t\t */\n+\n+\t\t\tlws_snprintf(s, sizeof(s),\n+\t\t\t\t \u0022update artifacts set blob\u003dzeroblob(%llu) \u0022\n+\t\t\t\t\t\u0022where rowid\u003d%llu\u0022,\n+\t\t\t\t (unsigned long long)ap-\u003elen,\n+\t\t\t\t (unsigned long long)rid);\n+\n+\t\t\tif (sqlite3_exec((sqlite3 *)pss-\u003epdb_artifact, s,\n+\t\t\t\t\t NULL, NULL, NULL) !\u003d SQLITE_OK) {\n+\t\t\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, s,\n+\t\t\t\t\t sqlite3_errmsg(pss-\u003epdb_artifact));\n+\t\t\t\tgoto afail;\n+\t\t\t}\n+\n+\t\t\t/*\n+\t\t\t * Open a blob on the associated row... the blob handle\n+\t\t\t * is closed when this stream closes for whatever\n+\t\t\t * reason.\n+\t\t\t */\n+\n+\t\t\tif (sqlite3_blob_open(pss-\u003epdb_artifact, \u0022main\u0022,\n+\t\t\t\t\t \u0022artifacts\u0022, \u0022blob\u0022, rid, 1,\n+\t\t\t\t\t \u0026pss-\u003eblob_artifact) !\u003d SQLITE_OK) {\n+\t\t\t\tlwsl_err(\u0022%s: unable to open blob\u005cn\u0022, __func__);\n+\t\t\t\tgoto afail;\n+\t\t\t}\n+\n+\t\t\t/*\n+\t\t\t * First time around, m \u003d\u003d number of bytes let in buf\n+\t\t\t * after JSON, (bl - m) offset\n+\t\t\t */\n+\t\t\tpss-\u003ebulk_binary_data \u003d 1;\n+\t\t\tpss-\u003eartifact_length \u003d ap-\u003elen;\n+\t\t} else\n+\t\t\tm \u003d bl;\n+\n+\t\tif (m) {\n+\t\t\tlwsl_notice(\u0022%s: blob write +%d, ofs %llu / %llu, len %d\u005cn\u0022,\n+\t\t\t\t __func__, (int)(bl - m),\n+\t\t\t\t (unsigned long long)pss-\u003eartifact_offset,\n+\t\t\t\t (unsigned long long)pss-\u003eartifact_length, m);\n+\t\t\tif (sqlite3_blob_write(pss-\u003eblob_artifact,\n+\t\t\t\t\t (uint8_t *)buf + (bl - m), (int)m,\n+\t\t\t\t\t pss-\u003eartifact_offset)) {\n+\t\t\t\tlwsl_err(\u0022%s: writing blob failed\u005cn\u0022, __func__);\n+\t\t\t\tgoto afail;\n+\t\t\t}\n+\n+\t\t\tlws_set_timeout(pss-\u003ewsi, PENDING_TIMEOUT_HTTP_CONTENT, 5);\n+\t\t\tpss-\u003eartifact_offset +\u003d (int)m;\n+\t\t}\n+\n+\t\tif (pss-\u003eartifact_offset \u003d\u003d pss-\u003eartifact_length) {\n+\t\t\tlwsl_notice(\u0022%s: blob upload finished\u005cn\u0022, __func__);\n+\n+\t\t\tgoto afail;\n+\t\t}\n+\n+\t}\n+\n+\treturn 0;\n+\n+afail:\n+\tlwsac_free(\u0026ac);\n+\tlwsac_free(\u0026pss-\u003ea.ac);\n+\tsais_event_db_close(vhd, \u0026pss-\u003epdb_artifact);\n+\n+\treturn -1;\n+}\n+\n+/*\n+ * We're sending something on a builder ws connection\n+ */\n+\n+int\n+sais_ws_json_tx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf,\n+\t\t\tsize_t bl)\n+{\n+\tuint8_t *start \u003d buf + LWS_PRE, *p \u003d start, *end \u003d p + bl - LWS_PRE - 1;\n+\tint n, flags \u003d LWS_WRITE_TEXT, first \u003d 0;\n+\tlws_struct_serialize_t *js;\n+\tsai_task_t *task;\n+\tsize_t w;\n+\n+\tif (pss-\u003etask_cancel_owner.head) {\n+\t\t/*\n+\t\t * Pending cancel message to send\n+\t\t */\n+\t\tsai_cancel_t *c \u003d lws_container_of(pss-\u003etask_cancel_owner.head,\n+\t\t\t\t\t\t sai_cancel_t, list);\n+\n+\t\tjs \u003d lws_struct_json_serialize_create(lsm_schema_json_map_can,\n+\t\t\t\tLWS_ARRAY_SIZE(lsm_schema_json_map_can), 0, c);\n+\t\tif (!js)\n+\t\t\treturn 1;\n+\n+\t\tn \u003d lws_struct_json_serialize(js, p, end - p, \u0026w);\n+\t\tlws_struct_json_serialize_destroy(\u0026js);\n+\n+\t\tlws_dll2_remove(\u0026c-\u003elist);\n+\t\tfree(c);\n+\n+\t\tfirst \u003d 1;\n+\t\tpss-\u003ewalk \u003d NULL;\n+\n+\t\tgoto send_json;\n+\t}\n+\n+\tif (!pss-\u003eissue_task_owner.count)\n+\t\treturn 0; /* nothing to send */\n+\n+\t/*\n+\t * We're sending a builder specific task info that has been bound to the\n+\t * builder.\n+\t *\n+\t * We already got the task struct out of the db in .one_event\n+\t * (all in .ac)\n+\t */\n+\n+\ttask \u003d lws_container_of(pss-\u003eissue_task_owner.head, sai_task_t,\n+\t\t\t\tpending_assign_list);\n+\tlws_dll2_remove(\u0026task-\u003epending_assign_list);\n+\n+\tjs \u003d lws_struct_json_serialize_create(lsm_schema_map_ta,\n+\t\t\t\t\t LWS_ARRAY_SIZE(lsm_schema_map_ta),\n+\t\t\t\t\t 0, task);\n+\tif (!js)\n+\t\treturn 1;\n+\n+\tn \u003d lws_struct_json_serialize(js, p, end - p, \u0026w);\n+\tlws_struct_json_serialize_destroy(\u0026js);\n+\tpss-\u003eone_event \u003d NULL;\n+\tlwsac_free(\u0026task-\u003eac_task_container);\n+\n+\tfirst \u003d 1;\n+\tpss-\u003ewalk \u003d NULL;\n+\n+\t//lwsac_free(\u0026pss-\u003equery_ac);\n+\n+send_json:\n+\tp +\u003d w;\n+\tif (n \u003d\u003d LSJS_RESULT_ERROR) {\n+\t\tlwsl_notice(\u0022%s: taskinfo: error generating json\u005cn\u0022,\n+\t\t\t __func__);\n+\t\treturn 1;\n+\t}\n+\tif (!lws_ptr_diff(p, start)) {\n+\t\tlwsl_notice(\u0022%s: taskinfo: empty json\u005cn\u0022, __func__);\n+\t\treturn 0;\n+\t}\n+\n+\tflags \u003d lws_write_ws_flags(LWS_WRITE_TEXT, first, !pss-\u003ewalk);\n+\n+\t// lwsl_hexdump_notice(start, p - start);\n+\n+\tif (lws_write(pss-\u003ewsi, start, p - start, flags) \u003c 0)\n+\t\treturn -1;\n+\n+\tlws_callback_on_writable(pss-\u003ewsi);\n+\n+\treturn 0;\n+}\ndiff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt\nnew file mode 100644\nindex 0000000..ac66c81\n--- /dev/null\n+++ b/src/web/CMakeLists.txt\n@@ -0,0 +1,136 @@\n+set(SUB \u0022sai-web\u0022)\n+\n+set(CPACK_DEBIAN_SERVER_PACKAGE_NAME ${SUB})\n+\n+set(SRCS\n+\tw-sai.c\n+\tw-conf.c\n+\tw-comms.c\n+\tw-central.c\n+\tw-artifact.c\n+\tw-ws-browser.c\n+\tw-websrv.c\n+)\n+\n+set(requirements 1)\n+require_lws_config(LWS_WITH_STRUCT_SQLITE3\t1 requirements)\n+require_lws_config(LWS_WITH_SERVER\t\t1 requirements)\n+require_lws_config(LWS_WITH_GENCRYPTO\t\t1 requirements)\n+require_lws_config(LWS_WITH_UNIX_SOCK\t\t1 requirements)\n+\n+if (requirements)\n+\tadd_executable(${SUB} ${SRCS})\n+\tif (APPLE)\n+\t\tset_property(TARGET sai-web PROPERTY MACOSX_RPATH YES)\n+\tendif()\n+\n+\t#\n+\t# sqlite3 paths (web)\n+\t#\n+\tfind_path( SQLITE3_INC_PATH NAMES \u0022sqlite3.h\u0022)\n+\tfind_library(SQLITE3_LIB_PATH NAMES \u0022sqlite3\u0022)\n+\t\n+\tif (SQLITE3_INC_PATH AND SQLITE3_LIB_PATH)\n+\t\tinclude_directories(BEFORE \u0022${SQLITE3_INC_PATH}\u0022)\n+\telse()\n+\t\tmessage(FATAL_ERROR \u0022 Unable to find sqlite3\u0022)\n+\tendif()\n+\n+\ttarget_link_libraries(${SUB} ${SQLITE3_LIB_PATH})\n+\n+ \tinclude_directories(BEFORE \u0022${SAI_LWS_INC_PATH}\u0022)\n+\n+\tCHECK_C_SOURCE_COMPILES(\u0022#include \u003clibwebsockets.h\u003e\u005cnint\n+\tmain(void) {\u005cn#if defined(LWS_HAVE_LIBCAP)\u005cn return\n+\t\t0;\u005cn#else\u005cn fail;\u005cn#endif\u005cn return 0;\u005cn}\u005cn\u0022 HAS_LIBCAP)\n+\tif (HAS_LIBCAP)\n+\t\tfind_library(CAP_LIB_PATH NAMES \u0022cap\u0022)\n+\tendif()\n+\n+\ttarget_link_libraries(${SUB} websockets ${SAI_LWS_LIB_PATH})\n+\t\n+\tif (LWS_OPENSSL_LIBRARIES)\n+\t\ttarget_link_libraries(${SUB} ${LWS_OPENSSL_LIBRARIES})\n+\tendif()\n+\t\n+\tif (SAI_EXT_PTHREAD_LIBRARIES)\n+\t\ttarget_link_libraries(${SUB} ${SAI_EXT_PTHREAD_LIBRARIES})\n+\tendif()\n+\n+\tif (HAS_LIBCAP)\n+\t\ttarget_link_libraries(${SUB} ${CAP_LIB_PATH})\n+\tendif()\n+\n+\tif (MSVC)\n+\t\ttarget_link_libraries(${SUB} ws2_32.lib userenv.lib psapi.lib iphlpapi.lib)\n+\tendif()\n+\t\n+\tinstall(TARGETS ${SUB}\n+\t\tRUNTIME DESTINATION \u0022${BIN_DIR}\u0022 COMPONENT web)\n+\tinstall(\n+\t\tFILES ../../assets/index.html\n+\t\t ../../assets/OpenSans-Light.ttf\n+\t\t ../../assets/rebuild.png\n+\t\t ../../assets/delete.png\n+\t\t ../../assets/passed.svg\n+\t\t ../../assets/failed.svg\n+\t\t ../../assets/sai.css\n+\t\t ../../assets/sai.js\n+\t\t ../../assets/sai.svg\n+\t\t ../../assets/sai-icon.svg\n+\t\t ../../assets/builder.png\n+\t\t ../../assets/favicon.ico\n+\t\t ../../assets/strict-csp.svg\n+\t\t ../../assets/lws-common.js\n+\t\t ../../assets/gs/lwsgs.js\n+\t\t ../../assets/gs/lwsgs.css\n+\t\t\t../../assets/arch-aarch64-a72-bcm2711-rpi4.svg\n+\t\t\t../../assets/arch-aarch64.svg\n+\t\t\t../../assets/arch-arm32-m4-mt7697-usi.svg\n+\t\t\t../../assets/arch-arm32.svg\n+\t\t\t../../assets/arch-freertos.svg\n+\t\t\t../../assets/arch-riscv64-virt.svg\n+\t\t\t../../assets/arch-riscv.svg\n+\t\t\t../../assets/arch-x86_64-amd.svg\n+\t\t\t../../assets/arch-x86_64-intel-i3.svg\n+\t\t\t../../assets/arch-x86_64-intel.svg\n+\t\t\t../../assets/arch-xl6-esp32.svg\n+\t\t\t../../assets/artifact.svg\n+\t\t\t../../assets/builder.svg\n+\t\t\t../../assets/decal-2.svg\n+\t\t\t../../assets/decal-3.svg\n+\t\t\t../../assets/decal-4.svg\n+\t\t\t../../assets/decal-5.svg\n+\t\t\t../../assets/decal-6.svg\n+\t\t\t../../assets/freertos-espidf.svg\n+\t\t\t../../assets/freertos-linkit.svg\n+\t\t\t../../assets/git.svg\n+\t\t\t../../assets/jsplease.svg\n+\t\t\t../../assets/linux-android.svg\n+\t\t\t../../assets/linux-centos-8.svg\n+\t\t\t../../assets/linux-fedora-32-riscv.svg\n+\t\t\t../../assets/linux-fedora-32.svg\n+\t\t\t../../assets/linux-gentoo.svg\n+\t\t\t../../assets/linux-ubuntu-1804.svg\n+\t\t\t../../assets/linux-ubuntu-2004.svg\n+\t\t\t../../assets/linux-ubuntu-xenial-amd64.svg\n+\t\t\t../../assets/netbsd-iOS.svg\n+\t\t\t../../assets/netbsd-OSX-catalina.svg\n+\t\t\t../../assets/sai-event.svg\n+\t\t\t../../assets/sai-icon.svg\n+\t\t\t../../assets/sai.svg\n+\t\t\t../../assets/stop.svg\n+\t\t\t../../assets/strict-csp.svg\n+\t\t\t../../assets/tc-gcc.svg\n+\t\t\t../../assets/tc-llvm.svg\n+\t\t\t../../assets/tc-mingw32.svg\n+\t\t\t../../assets/tc-mingw64.svg\n+\t\t\t../../assets/tc-msvc.svg\n+\t\t\t../../assets/ubuntu-focal-aarch64.svg\n+\t\t\t../../assets/virt-qemu.svg\n+\t\t\t../../assets/windows-10.svg\n+\n+\t\tDESTINATION \u0022${DATA_DIR}/sai/assets\u0022\n+\t\tCOMPONENT web)\n+\n+endif(requirements)\ndiff --git a/src/web/w-artifact.c b/src/web/w-artifact.c\nnew file mode 100644\nindex 0000000..2b87270\n--- /dev/null\n+++ b/src/web/w-artifact.c\n@@ -0,0 +1,151 @@\n+/*\n+ * Sai server\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ */\n+\n+#include \u003clibwebsockets.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003ctime.h\u003e\n+\n+#include \u0022w-private.h\u0022\n+\n+void\n+sai_task_uuid_to_event_uuid(char *event_uuid33, const char *task_uuid65)\n+{\n+\tmemcpy(event_uuid33, task_uuid65, 32);\n+\tevent_uuid33[32] \u003d '\u005c0';\n+}\n+\n+int\n+saiw_get_blob(struct vhd *vhd, const char *url, sqlite3 **pdb,\n+\t sqlite3_blob **blob, uint64_t *length)\n+{\n+\tchar task_uuid[66], event_uuid[34], nonce[34], qu[200],\n+\t esc[66], esc1[34];\n+\tstruct lwsac *ac \u003d NULL;\n+\tlws_dll2_owner_t o;\n+\tsai_artifact_t *a;\n+\tuint64_t rid \u003d 0;\n+\tconst char *p;\n+\tint n;\n+\n+\t/*\n+\t * We get passed the RHS of a URL string like\n+\t *\n+\t * \u003ctask_uuid\u003e/\u003cdown_nonce\u003e/filename\n+\t *\n+\t * filename is not used for matching, but make sure the client saves it\n+\t * using the name generated along with the link.\n+\t *\n+\t * Extract the pieces from the URL\n+\t */\n+\n+\tn \u003d 0;\n+\tp \u003d url;\n+\twhile (*p \u0026\u0026 *p !\u003d '/' \u0026\u0026 n \u003c (int)sizeof(task_uuid) - 1)\n+\t\ttask_uuid[n++] \u003d *p++;\n+\n+\tif (n !\u003d sizeof(task_uuid) - 2 || *p !\u003d '/') {\n+\t\tlwsl_notice(\u0022%s: url layout 1: %d %s\u005cn\u0022, __func__, n, url);\n+\t\treturn -1;\n+\t}\n+\n+\ttask_uuid[n] \u003d '\u005c0';\n+\tp++; /* skip the / */\n+\n+\tn \u003d 0;\n+\twhile (*p \u0026\u0026 *p !\u003d '/' \u0026\u0026 n \u003c (int)sizeof(nonce) - 1)\n+\t\tnonce[n++] \u003d *p++;\n+\n+\tif (n !\u003d sizeof(nonce) - 2 || *p !\u003d '/') {\n+\t\tlwsl_notice(\u0022%s: url layout 2\u005cn\u0022, __func__);\n+\t\treturn -1;\n+\t}\n+\n+\tsai_task_uuid_to_event_uuid(event_uuid, task_uuid);\n+\n+\t/* open the event-specific database object */\n+\n+\tif (sais_event_db_ensure_open(vhd, event_uuid, 0, pdb)) {\n+\t\tlwsl_info(\u0022%s: unable to open event-specific database\u005cn\u0022,\n+\t\t\t\t__func__);\n+\n+\t\treturn -1;\n+\t}\n+\n+\t/*\n+\t * Query the artifact he's asking for\n+\t */\n+\n+\tlws_sql_purify(esc, task_uuid, sizeof(esc));\n+\tlws_sql_purify(esc1, nonce, sizeof(esc1));\n+\tlws_snprintf(qu, sizeof(qu),\n+\t\t \u0022 and task_uuid\u003d'%s' and artifact_down_nonce\u003d'%s'\u0022,\n+\t\t esc, esc1);\n+\tn \u003d lws_struct_sq3_deserialize(*pdb, qu, NULL,\n+\t\t\t\t lsm_schema_sq3_map_artifact,\n+\t\t\t\t \u0026o, \u0026ac, 0, 1);\n+\tif (n \u003c 0 || !o.head) {\n+\t\tlwsl_err(\u0022%s: no result from %s\u005cn\u0022, __func__, qu);\n+\t\tgoto fail;\n+\t}\n+\n+\ta \u003d (sai_artifact_t *)o.head;\n+\n+\t*length \u003d a-\u003elen;\n+\n+\t/*\n+\t * recover the rowid the blob api requires\n+\t */\n+\n+\tlws_snprintf(qu, sizeof(qu), \u0022select rowid from artifacts \u0022\n+\t\t\t\t \u0022where artifact_down_nonce\u003d'%s'\u0022, esc1);\n+\n+\tif (sqlite3_exec(*pdb, qu, sai_sql3_get_uint64_cb, \u0026rid, NULL) !\u003d SQLITE_OK) {\n+\t\tlwsl_err(\u0022%s: %s: %s: fail\u005cn\u0022, __func__, qu, sqlite3_errmsg(*pdb));\n+\t\tgoto fail;\n+\t}\n+\n+\t/*\n+\t * Get a read-only handle on the blob\n+\t */\n+\n+\tif (sqlite3_blob_open(*pdb, \u0022main\u0022, \u0022artifacts\u0022, \u0022blob\u0022, rid, 0, blob) !\u003d SQLITE_OK) {\n+\t\tlwsl_err(\u0022%s: unable to open blob, rid %d\u005cn\u0022, __func__, (int)rid);\n+\t\tgoto fail;\n+\t}\n+\n+\tlwsac_free(\u0026ac); /* drop the lwsac holding that result */\n+\n+\t/*\n+\t * It was successful, *length was set, *pdb and *blob are live handles\n+\t * to the db and the blob itself.\n+\t */\n+\n+\treturn 0;\n+\n+fail:\n+\tlwsac_free(\u0026ac);\n+\tsais_event_db_close(vhd, pdb);\n+\n+\tlwsl_notice(\u0022%s: couldn't find blob %s\u005cn\u0022, __func__, url);\n+\n+\treturn -1;\n+}\ndiff --git a/src/web/w-central.c b/src/web/w-central.c\nnew file mode 100644\nindex 0000000..c94634d\n--- /dev/null\n+++ b/src/web/w-central.c\n@@ -0,0 +1,45 @@\n+/*\n+ * Sai server\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ *\n+ *\n+ * Central dispatcher for jobs from events that made it into the database. This\n+ * is done in an event-driven way in m-task.c, but management of it also has to\n+ * be done in the background for when there are no events coming,\n+ */\n+\n+#include \u003clibwebsockets.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003ctime.h\u003e\n+\n+#include \u0022w-private.h\u0022\n+\n+extern struct lws_context *context;\n+\n+void\n+saiw_central_cb(lws_sorted_usec_list_t *sul)\n+{\n+\tstruct vhd *vhd \u003d lws_container_of(sul, struct vhd, sul_central);\n+\n+\t/* check again in 1s */\n+\n+\tlws_sul_schedule(context, 0, \u0026vhd-\u003esul_central, saiw_central_cb,\n+\t\t\t 1 * LWS_US_PER_SEC);\n+}\ndiff --git a/src/web/w-comms.c b/src/web/w-comms.c\nnew file mode 100644\nindex 0000000..49f6d38\n--- /dev/null\n+++ b/src/web/w-comms.c\n@@ -0,0 +1,1123 @@\n+/*\n+ * Sai server\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ *\n+ * The same ws interface is connected-to by builders (on path /builder), and\n+ * provides the query transport for browsers (on path /browse).\n+ *\n+ * There's a single server slite3 database containing events, and a separate\n+ * sqlite3 database file for each event, it only contains tasks and logs for\n+ * the event and can be deleted when the event record associated with it is\n+ * deleted. This is to keep is scalable when there may be thousands of events\n+ * and related tasks and logs stored.\n+ */\n+\n+#include \u003clibwebsockets.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003ctime.h\u003e\n+#include \u003cstdio.h\u003e\n+#include \u003cfcntl.h\u003e\n+\n+#include \u0022w-private.h\u0022\n+\n+#include \u0022../common/struct-metadata.c\u0022\n+\n+typedef enum {\n+\tSJS_CLONING,\n+\tSJS_ASSIGNING,\n+\tSJS_WAITING,\n+\tSJS_DONE\n+} sai_job_state_t;\n+\n+typedef struct sai_job {\n+\tstruct lws_dll2 jobs_list;\n+\tchar reponame[64];\n+\tchar ref[64];\n+\tchar head[64];\n+\n+\ttime_t requested;\n+\n+\tsai_job_state_t state;\n+\n+} sai_job_t;\n+\n+const lws_struct_map_t lsm_schema_map_ta[] \u003d {\n+\tLSM_SCHEMA (sai_task_t,\t NULL, lsm_task, \u0022com-warmcat-sai-ta\u0022),\n+};\n+\n+typedef struct sai_auth {\n+\tlws_dll2_t\t\tlist;\n+\tchar\t\t\tname[33];\n+\tchar\t\t\tpassphrase[65];\n+\tunsigned long\t\tsince;\n+\tunsigned long\t\tlast_updated;\n+} sai_auth_t;\n+\n+const lws_struct_map_t lsm_auth[] \u003d {\n+\tLSM_CARRAY\t(sai_auth_t, name,\t\t\u0022name\u0022),\n+\tLSM_CARRAY\t(sai_auth_t, passphrase,\t\u0022passphrase\u0022),\n+\tLSM_UNSIGNED\t(sai_auth_t, since,\t\t\u0022since\u0022),\n+\tLSM_UNSIGNED\t(sai_auth_t, last_updated,\t\u0022last_updated\u0022),\n+};\n+\n+const lws_struct_map_t lsm_schema_sq3_map_auth[] \u003d {\n+\tLSM_SCHEMA_DLL2\t(sai_auth_t, list, NULL, lsm_auth,\t\u0022auth\u0022),\n+};\n+\n+extern const lws_struct_map_t lsm_schema_sq3_map_event[];\n+\n+\n+/* len is typically 16 (event uuid is 32 chars + NUL)\n+ * But eg, task uuid is concatenated 32-char eventid and 32-char taskid\n+ */\n+\n+int\n+sai_uuid16_create(struct lws_context *context, char *dest33)\n+{\n+\treturn lws_hex_random(context, dest33, 33);\n+}\n+\n+int\n+sai_sqlite3_statement(sqlite3 *pdb, const char *cmd, const char *desc)\n+{\n+\tsqlite3_stmt *sm;\n+\tint n;\n+\n+\tif (sqlite3_prepare_v2(pdb, cmd, -1, \u0026sm, NULL) !\u003d SQLITE_OK) {\n+\t\tlwsl_err(\u0022%s: Unable to %s: %s\u005cn\u0022,\n+\t\t\t __func__, desc, sqlite3_errmsg(pdb));\n+\n+\t\treturn 1;\n+\t}\n+\n+\tn \u003d sqlite3_step(sm);\n+\tsqlite3_reset(sm);\n+\tsqlite3_finalize(sm);\n+\tif (n !\u003d SQLITE_DONE) {\n+\t\tn \u003d sqlite3_extended_errcode(pdb);\n+\t\tif (!n) {\n+\t\t\tlwsl_info(\u0022%s: failed '%s'\u005cn\u0022, __func__, cmd);\n+\t\t\treturn 0;\n+\t\t}\n+\n+\t\tlwsl_err(\u0022%s: %d: Unable to perform \u005c\u0022%s\u005c\u0022: %s\u005cn\u0022, __func__,\n+\t\t\t n, desc, sqlite3_errmsg(pdb));\n+\t\tputs(cmd);\n+\n+\t\treturn 1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+int\n+sais_event_db_ensure_open(struct vhd *vhd, const char *event_uuid,\n+\t\t\t char create_if_needed, sqlite3 **ppdb)\n+{\n+\tchar filepath[256], saf[33];\n+\tsais_sqlite_cache_t *sc;\n+\n+\tif (*ppdb)\n+\t\treturn 0;\n+\n+\t/* do we have this guy cached? */\n+\n+\tlws_start_foreach_dll(struct lws_dll2 *, p, vhd-\u003esqlite3_cache.head) {\n+\t\tsc \u003d lws_container_of(p, sais_sqlite_cache_t, list);\n+\n+\t\tif (!strcmp(event_uuid, sc-\u003euuid)) {\n+\t\t\tsc-\u003erefcount++;\n+\t\t\t*ppdb \u003d sc-\u003epdb;\n+\t\t\treturn 0;\n+\t\t}\n+\n+\t} lws_end_foreach_dll(p);\n+\n+\t/* ... nope, well, let's open and cache him then... */\n+\n+\tlws_strncpy(saf, event_uuid, sizeof(saf));\n+\tlws_filename_purify_inplace(saf);\n+\n+\tlws_snprintf(filepath, sizeof(filepath), \u0022%s-event-%s.sqlite3\u0022,\n+\t\t vhd-\u003esqlite3_path_lhs, saf);\n+\n+\tif (lws_struct_sq3_open(vhd-\u003econtext, filepath, create_if_needed, ppdb)) {\n+\t\tlwsl_err(\u0022%s: Unable to open db %s: %s\u005cn\u0022, __func__,\n+\t\t\t filepath, sqlite3_errmsg(*ppdb));\n+\n+\t\treturn 1;\n+\t}\n+\n+\t/* create / add to the schema for the tables we will have in here */\n+\n+\tif (lws_struct_sq3_create_table(*ppdb, lsm_schema_sq3_map_task))\n+\t\treturn 1;\n+\n+\tsai_sqlite3_statement(*ppdb, \u0022PRAGMA journal_mode\u003dWAL;\u0022, \u0022set WAL\u0022);\n+\n+\tif (lws_struct_sq3_create_table(*ppdb, lsm_schema_sq3_map_log))\n+\t\treturn 1;\n+\n+\tif (lws_struct_sq3_create_table(*ppdb, lsm_schema_sq3_map_artifact))\n+\t\treturn 1;\n+\n+\tsc \u003d malloc(sizeof(*sc));\n+\tmemset(sc, 0, sizeof(*sc));\n+\tif (!sc) {\n+\t\tlws_struct_sq3_close(ppdb);\n+\t\t*ppdb \u003d NULL;\n+\t\treturn 1;\n+\t}\n+\n+\tlws_strncpy(sc-\u003euuid, event_uuid, sizeof(sc-\u003euuid));\n+\tsc-\u003erefcount \u003d 1;\n+\tsc-\u003epdb \u003d *ppdb;\n+\tlws_dll2_add_tail(\u0026sc-\u003elist, \u0026vhd-\u003esqlite3_cache);\n+\n+\treturn 0;\n+}\n+\n+void\n+sais_event_db_close(struct vhd *vhd, sqlite3 **ppdb)\n+{\n+\tsais_sqlite_cache_t *sc;\n+\n+\tif (!*ppdb)\n+\t\treturn;\n+\n+\t/* look for him in the cache */\n+\n+\tlws_start_foreach_dll(struct lws_dll2 *, p, vhd-\u003esqlite3_cache.head) {\n+\t\tsc \u003d lws_container_of(p, sais_sqlite_cache_t, list);\n+\n+\t\tif (sc-\u003epdb \u003d\u003d *ppdb) {\n+\t\t\t*ppdb \u003d NULL;\n+\t\t\tif (--sc-\u003erefcount) {\n+\t\t\t\tlwsl_notice(\u0022%s: zero refcount to idle\u005cn\u0022, __func__);\n+\t\t\t\t/*\n+\t\t\t\t * He's not currently in use then... don't\n+\t\t\t\t * close him immediately, m-central.c has a\n+\t\t\t\t * timer that closes and removes sqlite3\n+\t\t\t\t * cache entries idle for longer than 60s\n+\t\t\t\t */\n+\t\t\t\tsc-\u003eidle_since \u003d lws_now_usecs();\n+\t\t\t}\n+\n+\t\t\treturn;\n+\t\t}\n+\n+\t} lws_end_foreach_dll(p);\n+\n+\tlws_struct_sq3_close(ppdb);\n+\t*ppdb \u003d NULL;\n+}\n+\n+int\n+sais_event_db_delete_database(struct vhd *vhd, const char *event_uuid)\n+{\n+\tchar filepath[256], saf[33];\n+\n+\tlws_strncpy(saf, event_uuid, sizeof(saf));\n+\tlws_filename_purify_inplace(saf);\n+\n+\tlws_snprintf(filepath, sizeof(filepath), \u0022%s-event-%s.sqlite3\u0022,\n+\t\t vhd-\u003esqlite3_path_lhs, saf);\n+\n+\treturn unlink(filepath);\n+}\n+\n+\n+#if 0\n+static void\n+sais_all_browser_on_writable(struct vhd *vhd)\n+{\n+\tlws_start_foreach_dll(struct lws_dll2 *, mp, vhd-\u003ebrowsers.head) {\n+\t\tstruct pss *pss \u003d lws_container_of(mp, struct pss, same);\n+\n+\t\tlws_callback_on_writable(pss-\u003ewsi);\n+\t} lws_end_foreach_dll(mp);\n+}\n+#endif\n+\n+typedef enum {\n+\tSHMUT_NONE \u003d -1,\n+\tSHMUT_HOOK,\n+\tSHMUT_BROWSE,\n+\tSHMUT_STATUS,\n+\tSHMUT_ARTIFACTS,\n+\tSHMUT_LOGIN\n+} sai_http_murl_t;\n+\n+static const char * const well_known[] \u003d {\n+\t\u0022/update-hook\u0022,\n+\t\u0022/sai/browse\u0022,\n+\t\u0022/status\u0022,\n+\t\u0022/artifacts/\u0022, /* HTTP api for accessing build artifacts */\n+\t\u0022/login\u0022\n+};\n+\n+int\n+saiw_task_cancel(struct vhd *vhd, const char *task_uuid)\n+{\n+\tsai_cancel_t *can \u003d malloc(sizeof(*can));\n+\n+\tmemset(can, 0, sizeof(*can));\n+\n+\tlws_strncpy(can-\u003etask_uuid, task_uuid, sizeof(can-\u003etask_uuid));\n+\n+\tlws_dll2_add_tail(\u0026can-\u003elist, \u0026vhd-\u003eweb_to_srv_owner);\n+\n+\n+\treturn 0;\n+}\n+\n+int\n+saiw_sched_destroy(struct lws_dll2 *d, void *user)\n+{\n+\tsaiw_scheduled_t *sch \u003d lws_container_of(d, saiw_scheduled_t, list);\n+\n+\tsaiw_dealloc_sched(sch);\n+\n+\treturn 0;\n+}\n+\n+int\n+sai_get_head_status(struct vhd *vhd, const char *projname)\n+{\n+\tstruct lwsac *ac \u003d NULL;\n+\tlws_dll2_owner_t o;\n+\tsai_event_t *e;\n+\tint state;\n+\n+\tif (lws_struct_sq3_deserialize(vhd-\u003epdb, NULL, \u0022created \u0022,\n+\t\t\t\t lsm_schema_sq3_map_event,\n+\t\t\t\t \u0026o, \u0026ac, 0, -1))\n+\t\treturn -1;\n+\n+\tif (!o.head)\n+\t\treturn -1;\n+\n+\te \u003d lws_container_of(o.head, sai_event_t, list);\n+\tstate \u003d e-\u003estate;\n+\n+\tlwsac_free(\u0026ac);\n+\n+\treturn state;\n+}\n+\n+\n+static int\n+sai_login_cb(void *data, const char *name, const char *filename,\n+\t char *buf, int len, enum lws_spa_fileupload_states state)\n+{\n+\tlwsl_notice(\u0022%s: name '%s'\u005cn\u0022, __func__, name);\n+\treturn 0;\n+}\n+\n+static const char * const auth_param_names[] \u003d {\n+\t\u0022lname\u0022,\n+\t\u0022lpass\u0022,\n+\t\u0022success_redir\u0022,\n+};\n+\n+enum enum_param_names {\n+\tEPN_LNAME,\n+\tEPN_LPASS,\n+\tEPN_SUCCESS_REDIR,\n+};\n+\n+static int\n+saiw_event_db_close_all_now(struct vhd *vhd)\n+{\n+\tsais_sqlite_cache_t *sc;\n+\n+\tlws_start_foreach_dll_safe(struct lws_dll2 *, p, p1,\n+\t\t\t\t vhd-\u003esqlite3_cache.head) {\n+\t\tsc \u003d lws_container_of(p, sais_sqlite_cache_t, list);\n+\n+\t\tlws_struct_sq3_close(\u0026sc-\u003epdb);\n+\t\tlws_dll2_remove(\u0026sc-\u003elist);\n+\t\tfree(sc);\n+\n+\t} lws_end_foreach_dll_safe(p, p1);\n+\n+\treturn 0;\n+}\n+\n+static int\n+callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user,\n+\t void *in, size_t len)\n+{\n+\tstruct vhd *vhd \u003d (struct vhd *)lws_protocol_vh_priv_get(\n+\t\t\t\tlws_get_vhost(wsi), lws_get_protocol(wsi));\n+\tuint8_t buf[LWS_PRE + 8192], *start \u003d \u0026buf[LWS_PRE], *p \u003d start,\n+\t\t*end \u003d \u0026buf[sizeof(buf) - LWS_PRE - 1];\n+\tstruct pss *pss \u003d (struct pss *)user;\n+\tstruct lws_jwt_sign_set_cookie ck;\n+\tsai_http_murl_t mu \u003d SHMUT_NONE;\n+\tchar projname[64];\n+\tint n, resp, r;\n+\tconst char *cp;\n+\tsize_t cml;\n+\n+\t(void)end;\n+\t(void)p;\n+\n+\tswitch (reason) {\n+\tcase LWS_CALLBACK_PROTOCOL_INIT:\n+\t\tvhd \u003d lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),\n+\t\t\t\t\t\t lws_get_protocol(wsi),\n+\t\t\t\t\t\t sizeof(struct vhd));\n+\t\tif (!vhd)\n+\t\t\treturn -1;\n+\n+\t\tvhd-\u003econtext \u003d lws_get_context(wsi);\n+\t\tvhd-\u003evhost \u003d lws_get_vhost(wsi);\n+\n+\t\tif (lws_pvo_get_str(in, \u0022database\u0022, \u0026vhd-\u003esqlite3_path_lhs)) {\n+\t\t\tlwsl_err(\u0022%s: database pvo required\u005cn\u0022, __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tlws_snprintf((char *)buf, sizeof(buf), \u0022%s-events.sqlite3\u0022,\n+\t\t\t\tvhd-\u003esqlite3_path_lhs);\n+\n+\t\tif (lws_struct_sq3_open(vhd-\u003econtext, (char *)buf, 1, \u0026vhd-\u003epdb)) {\n+\t\t\tlwsl_err(\u0022%s: Unable to open session db %s: %s\u005cn\u0022,\n+\t\t\t\t __func__, vhd-\u003esqlite3_path_lhs, sqlite3_errmsg(\n+\t\t\t\t\t\t vhd-\u003epdb));\n+\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tsai_sqlite3_statement(vhd-\u003epdb,\n+\t\t\t\t \u0022PRAGMA journal_mode\u003dWAL;\u0022, \u0022set WAL\u0022);\n+\n+\t\tif (lws_struct_sq3_create_table(vhd-\u003epdb,\n+\t\t\t\t\t\tlsm_schema_sq3_map_event)) {\n+\t\t\tlwsl_err(\u0022%s: unable to create event table\u005cn\u0022, __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\t/* auth database */\n+\n+\t\tlws_snprintf((char *)buf, sizeof(buf), \u0022%s-auth.sqlite3\u0022,\n+\t\t\t\tvhd-\u003esqlite3_path_lhs);\n+\n+\t\tif (lws_struct_sq3_open(vhd-\u003econtext, (char *)buf, 1,\n+\t\t\t\t\t\u0026vhd-\u003epdb_auth)) {\n+\t\t\tlwsl_err(\u0022%s: Unable to open auth db %s: %s\u005cn\u0022,\n+\t\t\t\t __func__, vhd-\u003esqlite3_path_lhs, sqlite3_errmsg(\n+\t\t\t\t\t\t vhd-\u003epdb_auth));\n+\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tif (lws_struct_sq3_create_table(vhd-\u003epdb_auth,\n+\t\t\t\t\t\tlsm_schema_sq3_map_auth)) {\n+\t\t\tlwsl_err(\u0022%s: unable to create auth table\u005cn\u0022, __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\t/*\n+\t\t * jwt-iss\n+\t\t */\n+\n+\t\tif (lws_pvo_get_str(in, \u0022jwt-iss\u0022, \u0026vhd-\u003ejwt_issuer)) {\n+\t\t\tlwsl_err(\u0022%s: jwt-iss required\u005cn\u0022, __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\t/*\n+\t\t * jwt-aud\n+\t\t */\n+\n+\t\tif (lws_pvo_get_str(in, \u0022jwt-aud\u0022, \u0026vhd-\u003ejwt_audience)) {\n+\t\t\tlwsl_err(\u0022%s: jwt-aud required\u005cn\u0022, __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\t/*\n+\t\t * auth-alg\n+\t\t */\n+\n+\t\tif (lws_pvo_get_str(in, \u0022jwt-auth-alg\u0022, \u0026cp)) {\n+\t\t\tlwsl_err(\u0022%s: jwt-auth-alg required\u005cn\u0022, __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tlws_strncpy(vhd-\u003ejwt_auth_alg, cp, sizeof(vhd-\u003ejwt_auth_alg));\n+\n+\t\t/*\n+\t\t * auth-jwk-path\n+\t\t */\n+\n+\t\tif (lws_pvo_get_str(in, \u0022jwt-auth-jwk-path\u0022, \u0026cp)) {\n+\t\t\tlwsl_err(\u0022%s: jwt-auth-jwk-path required\u005cn\u0022, __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tn \u003d open(cp, LWS_O_RDONLY);\n+\t\tif (!n) {\n+\t\t\tlwsl_err(\u0022%s: can't open auth JWK %s\u005cn\u0022, __func__, cp);\n+\t\t\treturn -1;\n+\t\t}\n+\t\tr \u003d read(n, buf, sizeof(buf));\n+\t\tclose(n);\n+\t\tif (r \u003c 0) {\n+\t\t\tlwsl_err(\u0022%s: can't read auth JWK %s\u005cn\u0022, __func__, cp);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tif (lws_jwk_import(\u0026vhd-\u003ejwt_jwk_auth, NULL, NULL,\n+\t\t\t\t (const char *)buf, r)) {\n+\t\t\tlwsl_notice(\u0022%s: Failed to parse JWK key\u005cn\u0022, __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tlwsl_notice(\u0022%s: Auth JWK type %d\u005cn\u0022, __func__,\n+\t\t\t\t\t\tvhd-\u003ejwt_jwk_auth.kty);\n+\n+\t\tlws_sul_schedule(vhd-\u003econtext, 0, \u0026vhd-\u003esul_central,\n+\t\t\t\t saiw_central_cb, 500 * LWS_US_PER_MS);\n+\n+\t\t/*\n+\t\t * Reach out to the sai-server part over the SS ws websrv link\n+\t\t */\n+\n+\t\tif (lws_ss_create(lws_get_context(wsi), 0, \u0026ssi_saiw_websrv, vhd,\n+\t\t\t\t \u0026vhd-\u003eh_ss_websrv, NULL, NULL)) {\n+\t\t\tlwsl_err(\u0022%s: failed to create SS for websrv\u005cn\u0022,\n+\t\t\t\t\t__func__);\n+\n+\t\t\treturn 1;\n+\t\t}\n+\n+\t\tlws_ss_set_metadata(vhd-\u003eh_ss_websrv, \u0022sockpath\u0022,\n+\t\t\t\t \u0022@com.warmcat.sai-websrv\u0022, 23);\n+\t\tlws_ss_client_connect(vhd-\u003eh_ss_websrv);\n+\n+\t\tbreak;\n+\n+\tcase LWS_CALLBACK_PROTOCOL_DESTROY:\n+\t\tsaiw_event_db_close_all_now(vhd);\n+\t\tlws_struct_sq3_close(\u0026vhd-\u003epdb);\n+\t\tlws_struct_sq3_close(\u0026vhd-\u003epdb_auth);\n+\t\tlws_jwk_destroy(\u0026vhd-\u003ejwt_jwk_auth);\n+\t\tgoto passthru;\n+\n+\t/*\n+\t * receive http hook notifications\n+\t */\n+\n+\tcase LWS_CALLBACK_HTTP:\n+\n+\t\t// lwsl_notice(\u0022%s: HTTP\u005cn\u0022, __func__);\n+\n+\t\tresp \u003d HTTP_STATUS_FORBIDDEN;\n+\t\tpss-\u003evhd \u003d vhd;\n+\n+\t\t/*\n+\t\t * What's the situation with a JWT cookie? Normal users won't\n+\t\t * have any, but privileged users will have one, and we should\n+\t\t * try to confirm it and set the pss auth level accordingly\n+\t\t */\n+\n+\t\tmemset(\u0026ck, 0, sizeof(ck));\n+\t\tck.jwk \u003d \u0026vhd-\u003ejwt_jwk_auth;\n+\t\tck.alg \u003d vhd-\u003ejwt_auth_alg;\n+\t\tck.iss \u003d vhd-\u003ejwt_issuer;\n+\t\tck.aud \u003d vhd-\u003ejwt_audience;\n+\t\tck.cookie_name \u003d \u0022__Host-sai_jwt\u0022;\n+\n+\t\tcml \u003d sizeof(buf);\n+\t\tif (!lws_jwt_get_http_cookie_validate_jwt(wsi, \u0026ck,\n+\t\t\t\t\t\t\t (char *)buf, \u0026cml) \u0026\u0026\n+\t\t ck.extra_json \u0026\u0026\n+\t\t !lws_json_simple_strcmp(ck.extra_json, ck.extra_json_len,\n+\t\t\t\t\t \u0022\u005c\u0022authorized\u005c\u0022:\u0022, \u00221\u0022)) {\n+\t\t\t\t/* the token allows him to manage us */\n+\t\t\t\tpss-\u003eauthorized \u003d 1;\n+\t\t\t\tpss-\u003eexpiry_unix_time \u003d ck.expiry_unix_time;\n+\t\t\t\tlws_strncpy(pss-\u003eauth_user, ck.sub,\n+\t\t\t\t\t sizeof(pss-\u003eauth_user));\n+\t\t} else\n+\t\t\tlwsl_info(\u0022%s: cookie rejected\u005cn\u0022, __func__);\n+\n+\t\tfor (n \u003d 0; n \u003c (int)LWS_ARRAY_SIZE(well_known); n++)\n+\t\t\tif (!strncmp((const char *)in, well_known[n],\n+\t\t\t\t strlen(well_known[n]))) {\n+\t\t\t\tmu \u003d n;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\n+\t\tpss-\u003eour_form \u003d 0;\n+\n+\t\t// lwsl_notice(\u0022%s: HTTP: '%s' mu \u003d %d\u005cn\u0022, __func__, (const char *)in, n);\n+\n+\t\tswitch (mu) {\n+\n+\t\tcase SHMUT_NONE:\n+\t\t\tgoto passthru;\n+\n+\t\tcase SHMUT_HOOK:\n+\t\t\tpss-\u003eour_form \u003d 1;\n+\t\t\tlwsl_notice(\u0022LWS_CALLBACK_HTTP: sees hook\u005cn\u0022);\n+\t\t\treturn 0;\n+\n+\t\tcase SHMUT_STATUS:\n+\t\t\t/*\n+\t\t\t * in is a string like /libwebsockets/status.svg\n+\t\t\t */\n+\t\t\tcp \u003d ((const char *)in) + 7;\n+\t\t\twhile (*cp \u003d\u003d '/')\n+\t\t\t\tcp++;\n+\t\t\tn \u003d 0;\n+\t\t\twhile (*cp !\u003d '/' \u0026\u0026 *cp \u0026\u0026 (size_t)n \u003c sizeof(projname) - 1)\n+\t\t\t\tprojname[n++] \u003d *cp++;\n+\t\t\tprojname[n] \u003d '\u005c0';\n+\n+\t\t\t// lwsl_notice(\u0022%s: status %s\u005cn\u0022, __func__, projname);\n+\n+\t\t\tr \u003d sai_get_head_status(vhd, projname);\n+\t\t\tif (r \u003c 2)\n+\t\t\t\tr \u003d 2;\n+\t\t\tn \u003d lws_snprintf(projname, sizeof(projname),\n+\t\t\t\t \u0022../decal-%d.svg\u0022, r);\n+\n+\t\t\tif (lws_http_redirect(wsi, 307,\n+\t\t\t\t\t (unsigned char *)projname, n,\n+\t\t\t\t\t \u0026p, end) \u003c 0)\n+\t\t\t\treturn -1;\n+\n+\t\t\tgoto passthru;\n+\n+\t\tcase SHMUT_LOGIN:\n+\t\t\tpss-\u003elogin_form \u003d 1;\n+\t\t\tlwsl_notice(\u0022LWS_CALLBACK_HTTP: sees login\u005cn\u0022);\n+\t\t\treturn 0;\n+\n+\t\tcase SHMUT_ARTIFACTS:\n+\t\t\t/*\n+\t\t\t * HTTP Bulk GET interface for artifact download\n+\t\t\t *\n+\t\t\t * /artifacts/\u003ctaskhash\u003e/\u003cdown_nonce\u003e/filename\n+\t\t\t */\n+\t\t\tlwsl_notice(\u0022%s: SHMUT_ARTIFACTS\u005cn\u0022, __func__);\n+\t\t\tpss-\u003eartifact_offset \u003d 0;\n+\t\t\tif (saiw_get_blob(vhd, (const char *)in + 11,\n+\t\t\t\t\t \u0026pss-\u003epdb_artifact,\n+\t\t\t\t\t \u0026pss-\u003eblob_artifact,\n+\t\t\t\t\t \u0026pss-\u003eartifact_length)) {\n+\t\t\t\tlwsl_notice(\u0022%s: get_blob failed\u005cn\u0022, __func__);\n+\t\t\t\tresp \u003d 404;\n+\t\t\t\tgoto http_resp;\n+\t\t\t}\n+\n+\t\t\t/*\n+\t\t\t * Well, it seems what he wanted exists..\n+\t\t\t */\n+\n+\t\t\tif (lws_add_http_header_status(wsi, 200, \u0026p, end))\n+\t\t\t\tgoto bail;\n+\t\t\tif (lws_add_http_header_content_length(wsi,\n+\t\t\t\t\t(unsigned long)pss-\u003eartifact_length,\n+\t\t\t\t\t\u0026p, end))\n+\t\t\t\tgoto bail;\n+\n+\t\t\tif (lws_add_http_header_by_token(wsi,\n+\t\t\t\t\tWSI_TOKEN_HTTP_CONTENT_TYPE,\n+\t\t\t\t\t(uint8_t *)\u0022application/octet-stream\u0022,\n+\t\t\t\t\t24, \u0026p, end))\n+\t\t\t\tgoto bail;\n+\t\t\tif (lws_finalize_write_http_header(wsi, start, \u0026p, end))\n+\t\t\t\tgoto bail;\n+\n+\t\t\tlwsl_notice(\u0022%s: started artifact transaction %d\u005cn\u0022, __func__,\n+\t\t\t\t\t(int)pss-\u003eartifact_length);\n+\n+\t\t\tlws_callback_on_writable(wsi);\n+\t\t\treturn 0;\n+\n+\t\tdefault:\n+\t\t\tlwsl_notice(\u0022%s: DEFAULT!!!\u005cn\u0022, __func__);\n+\t\t\treturn 0;\n+\t\t}\n+\n+\t\tresp \u003d HTTP_STATUS_OK;\n+\n+\t\t/* faillthru */\n+\n+http_resp:\n+\t\tif (lws_add_http_header_status(wsi, resp, \u0026p, end))\n+\t\t\tgoto bail;\n+\t\tif (lws_add_http_header_content_length(wsi, 0, \u0026p, end))\n+\t\t\tgoto bail;\n+\t\tif (lws_finalize_write_http_header(wsi, start, \u0026p, end))\n+\t\t\tgoto bail;\n+\t\tgoto try_to_reuse;\n+\n+\n+\tcase LWS_CALLBACK_HTTP_WRITEABLE:\n+\n+\t\t// lwsl_notice(\u0022%s: HTTP_WRITEABLE\u005cn\u0022, __func__);\n+\n+\t\tif (!pss || !pss-\u003eblob_artifact)\n+\t\t\tbreak;\n+\n+\t\tn \u003d lws_ptr_diff(end, start);\n+\t\tif ((int)(pss-\u003eartifact_length - pss-\u003eartifact_offset) \u003c n)\n+\t\t\tn \u003d (int)(pss-\u003eartifact_length - pss-\u003eartifact_offset);\n+\n+\t\tif (sqlite3_blob_read(pss-\u003eblob_artifact, start, n,\n+\t\t\t\t pss-\u003eartifact_offset)) {\n+\t\t\tlwsl_err(\u0022%s: blob read failed\u005cn\u0022, __func__);\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tpss-\u003eartifact_offset +\u003d n;\n+\n+\t\tif (lws_write(wsi, start, n,\n+\t\t\t\tpss-\u003eartifact_offset !\u003d pss-\u003eartifact_length ?\n+\t\t\t\t\tLWS_WRITE_HTTP : LWS_WRITE_HTTP_FINAL) !\u003d n)\n+\t\t\treturn -1;\n+\n+\t\tif (pss-\u003eartifact_offset !\u003d pss-\u003eartifact_length)\n+\t\t\tlws_callback_on_writable(wsi);\n+\n+\t\tbreak;\n+\n+\t/*\n+\t * Notifcation POSTs\n+\t */\n+\n+\tcase LWS_CALLBACK_HTTP_BODY:\n+\n+\t\t// lwsl_notice(\u0022%s: HTTP_BODY\u005cn\u0022, __func__);\n+\n+\t\tif (pss-\u003elogin_form) {\n+\n+\t\t\tif (!pss-\u003espa) {\n+\t\t\t\tpss-\u003espa \u003d lws_spa_create(wsi, auth_param_names,\n+\t\t\t\t\t\tLWS_ARRAY_SIZE(auth_param_names),\n+\t\t\t\t\t\t1024, sai_login_cb, pss);\n+\t\t\t\tif (!pss-\u003espa) {\n+\t\t\t\t\tlwsl_err(\u0022failed to create spa\u005cn\u0022);\n+\t\t\t\t\treturn -1;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\t/* let it parse the POST data */\n+\n+\t\t\tlwsl_hexdump_notice(in, len);\n+\n+\t\t\tif (!pss-\u003espa_failed \u0026\u0026\n+\t\t\t lws_spa_process(pss-\u003espa, in, (int)len)) {\n+\n+\t\t\t\tlwsl_notice(\u0022%s: spa failed\u005cn\u0022, __func__);\n+\n+\t\t\t\t/*\n+\t\t\t\t * mark it as failed, and continue taking body until\n+\t\t\t\t * completion, and return error there\n+\t\t\t\t */\n+\t\t\t\tpss-\u003espa_failed \u003d 1;\n+\t\t\t}\n+\t\t}\n+\n+\t\tbreak;\n+\n+\tcase LWS_CALLBACK_HTTP_BODY_COMPLETION:\n+\t\tlwsl_user(\u0022%s: LWS_CALLBACK_HTTP_BODY_COMPLETION: %d\u005cn\u0022,\n+\t\t\t __func__, (int)len);\n+\n+\t\tif (!pss-\u003eour_form \u0026\u0026 !pss-\u003elogin_form) {\n+\t\t\tlwsl_user(\u0022%s: no sai form\u005cn\u0022, __func__);\n+\t\t\tgoto passthru;\n+\t\t}\n+\n+\t\t/* inform the spa no more payload data coming */\n+\t\tif (pss-\u003espa)\n+\t\t\tlws_spa_finalize(pss-\u003espa);\n+\n+\t\tif (pss-\u003elogin_form) {\n+\t\t\tconst char *un, *pw, *sr;\n+\t\t\tlws_dll2_owner_t o;\n+\t\t\tstruct lwsac *ac \u003d NULL;\n+\n+\t\t\tif (lws_add_http_header_status(wsi,\n+\t\t\t\t\t\tHTTP_STATUS_SEE_OTHER, \u0026p, end))\n+\t\t\t\tgoto clean_spa;\n+\t\t\tif (lws_add_http_header_content_length(wsi, 0, \u0026p, end))\n+\t\t\t\tgoto clean_spa;\n+\n+\t\t\tif (pss-\u003espa_failed)\n+\t\t\t\tgoto final;\n+\n+\t\t\tif (pss-\u003eauthorized) {\n+\n+\t\t\t\tchar temp[128];\n+\t\t\t\t/*\n+\t\t\t\t * It means, logout then\n+\t\t\t\t */\n+\n+\t\t\t\tn \u003d lws_snprintf(temp, sizeof(temp), \u0022__Host-sai_jwt\u003ddeleted;\u0022\n+\t\t\t\t\t\t \u0022HttpOnly;\u0022\n+\t\t\t\t\t\t \u0022Secure;\u0022\n+\t\t\t\t\t\t \u0022SameSite\u003dstrict;\u0022\n+\t\t\t\t\t\t \u0022Path\u003d/;\u0022\n+\t\t\t\t\t\t \u0022expires\u003dSun, 06 Nov 1994 08:49:37 GMT;\u0022);\n+\n+\t\t\t\tsr \u003d \u0022x/..\u0022;\n+\n+\t\t\t\tif (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_SET_COOKIE,\n+\t\t\t\t\t\t\t\t (uint8_t *)temp, n, \u0026p, end)) {\n+\t\t\t\t\tlwsl_err(\u0022%s: failed to add JWT cookie header\u005cn\u0022, __func__);\n+\t\t\t\t\treturn 1;\n+\t\t\t\t}\n+\n+\t\t\t\tgoto back;\n+\t\t\t}\n+\n+\t\t\tun \u003d lws_spa_get_string(pss-\u003espa, EPN_LNAME);\n+\t\t\tpw \u003d lws_spa_get_string(pss-\u003espa, EPN_LPASS);\n+\t\t\tsr \u003d lws_spa_get_string(pss-\u003espa, EPN_SUCCESS_REDIR);\n+\n+\t\t\tif (!un || !pw || !sr) {\n+\t\t\t\tlwsl_notice(\u0022%s: missing form args %p %p %p\u005cn\u0022,__func__, un, pw, sr);\n+\t\t\t\tpss-\u003espa_failed \u003d 1;\n+\t\t\t\tgoto final;\n+\t\t\t}\n+\n+\t\t\t// lwsl_notice(\u0022%s: login attempt %s %s %s\u005cn\u0022, __func__,\n+\t\t\t//\t un, pw, sr);\n+\n+\t\t\t/*\n+\t\t\t * Try to look up his credentials\n+\t\t\t */\n+\n+\t\t\tlws_sql_purify((char *)buf + 512, un, 34);\n+\t\t\tlws_sql_purify((char *)buf + 768, pw, 66);\n+\t\t\tlws_snprintf((char *)buf + 256, 256,\n+\t\t\t\t\t\u0022 and name\u003d'%s' and passphrase\u003d'%s'\u0022,\n+\t\t\t\t\t(const char *)buf + 512,\n+\t\t\t\t\t(const char *)buf + 768);\n+\t\t\tlws_dll2_owner_clear(\u0026o);\n+\t\t\tn \u003d lws_struct_sq3_deserialize(pss-\u003evhd-\u003epdb_auth,\n+\t\t\t\t\t\t (const char *)buf + 256,\n+\t\t\t\t\t\t NULL,\n+\t\t\t\t\t\t lsm_schema_sq3_map_auth,\n+\t\t\t\t\t\t \u0026o, \u0026ac, 0, 1);\n+\t\t\tif (n \u003c 0 || !o.head) {\n+\t\t\t\t/* no results, failed */\n+\t\t\t\t// lwsl_notice(\u0022%s: login attempt %s failed %d\u005cn\u0022,\n+\t\t\t\t//\t\t__func__, (const char *)buf, n);\n+\t\t\t\tlwsac_free(\u0026ac);\n+\t\t\t\tpss-\u003espa_failed \u003d 1;\n+\t\t\t\tgoto final;\n+\t\t\t}\n+\n+\t\t\t/* any result in o means a successful match */\n+\n+\t\t\tlwsac_free(\u0026ac);\n+\n+\t\t\t/*\n+\t\t\t * Produce a signed JWT allowing managing this Sai\n+\t\t\t * instance for a short time, and redirect ourselves\n+\t\t\t * back to the page we were on\n+\t\t\t */\n+\n+\n+\n+\t\t\tlwsl_notice(\u0022%s: setting cookie\u005cn\u0022, __func__);\n+\t\t\t/* un is invalidated by destroying the spa */\n+\t\t\tmemset(\u0026ck, 0, sizeof(ck));\n+\t\t\tlws_strncpy(ck.sub, un, sizeof(ck.sub));\n+\t\t\tck.jwk \u003d \u0026vhd-\u003ejwt_jwk_auth;\n+\t\t\tck.alg \u003d vhd-\u003ejwt_auth_alg;\n+\t\t\tck.iss \u003d vhd-\u003ejwt_issuer;\n+\t\t\tck.aud \u003d vhd-\u003ejwt_audience;\n+\t\t\tck.cookie_name \u003d \u0022sai_jwt\u0022;\n+\t\t\tck.extra_json \u003d \u0022\u005c\u0022authorized\u005c\u0022: 1\u0022;\n+\t\t\tck.expiry_unix_time \u003d 2 * 60 * 60; /* 2 days */\n+\n+\t\t\tif (lws_jwt_sign_token_set_http_cookie(wsi, \u0026ck, \u0026p, end))\n+\t\t\t\tgoto clean_spa;\n+\n+back:\n+\t\t\t/*\n+\t\t\t * Auth succeeded, go to the page the form was on\n+\t\t\t */\n+\n+\t\t\tif (lws_add_http_header_by_token(wsi,\n+\t\t\t\t\t\tWSI_TOKEN_HTTP_LOCATION,\n+\t\t\t\t\t\t(unsigned char *)sr,\n+\t\t\t\t\t\tstrlen((const char *)sr),\n+\t\t\t\t\t\t\u0026p, end)) {\n+\t\t\t\tgoto clean_spa;\n+\t\t\t}\n+\n+\t\t\tif (pss-\u003espa) {\n+\t\t\t\tlws_spa_destroy(pss-\u003espa);\n+\t\t\t\tpss-\u003espa \u003d NULL;\n+\t\t\t}\n+\n+\t\t\tif (lws_finalize_write_http_header(wsi, start, \u0026p, end))\n+\t\t\t\tgoto bail;\n+\n+\t\t\tlwsl_notice(\u0022%s: set / delete cookie OK\u005cn\u0022, __func__);\n+\t\t\t// lwsl_hexdump_notice(start, lws_ptr_diff(p, start));\n+\t\t\treturn 0;\n+\n+final:\n+\t\t\tlwsl_notice(\u0022%s: auth failed, login_form %d\u005cn\u0022, __func__, pss-\u003elogin_form);\n+\t\t\t/*\n+\t\t\t * Auth failed, go back to /\n+\t\t\t */\n+\t\t\tif (lws_add_http_header_by_token(wsi,\n+\t\t\t\t\t\tWSI_TOKEN_HTTP_LOCATION,\n+\t\t\t\t\t\t(unsigned char *)\u0022/\u0022, 1,\n+\t\t\t\t\t\t\u0026p, end)) {\n+\t\t\t\tgoto clean_spa;\n+\t\t\t}\n+\t\t\tif (lws_finalize_write_http_header(wsi, start, \u0026p, end))\n+\t\t\t\tgoto bail;\n+\t\t\treturn 0;\n+\t\t}\n+\n+\t\tif (pss-\u003espa) {\n+\t\t\tlws_spa_destroy(pss-\u003espa);\n+\t\t\tpss-\u003espa \u003d NULL;\n+\t\t}\n+\n+\t\tif (pss-\u003espa_failed)\n+\t\t\tlwsl_notice(\u0022%s: POST failed\u005cn\u0022, __func__);\n+\n+\t\tif (lws_return_http_status(wsi,\n+\t\t\t\tpss-\u003espa_failed ? HTTP_STATUS_FORBIDDEN :\n+\t\t\t\t\t\t HTTP_STATUS_OK,\n+\t\t\t\tNULL) \u003c 0)\n+\t\t\treturn -1;\n+\t\tbreak;\n+\n+clean_spa:\n+\t\tif (pss-\u003espa) {\n+\t\t\tlws_spa_destroy(pss-\u003espa);\n+\t\t\tpss-\u003espa \u003d NULL;\n+\t\t}\n+\t\tpss-\u003espa_failed \u003d 1;\n+\t\tgoto final;\n+\n+\t/*\n+\t * ws connections from builders and browsers\n+\t */\n+\n+\tcase LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:\n+\t\tn \u003d lws_hdr_copy(wsi, (char *)buf, sizeof(buf) - 1,\n+\t\t\t\t WSI_TOKEN_GET_URI);\n+\t\tif (!n)\n+\t\t\tbuf[0] \u003d '\u005c0';\n+\t\t//lwsl_notice(\u0022%s: checking with lwsgs for ws conn: %s\u005cn\u0022,\n+\t\t//\t __func__, (const char *)buf);\n+\n+\t\t/*\n+\t\t * Builders don't authenticate using sessions...\n+\t\t */\n+\n+\t\tif (n \u003e\u003d 8 \u0026\u0026 !strncmp((const char *)buf + n - 8,\n+\t\t\t\t\t\u0022/builder\u0022, 8))\n+\t\t\treturn 0;\n+\n+\t\treturn 0;\n+#if 0\n+\t\t/* but everything else does */\n+\n+\t\tcar_args.max_len \u003d LWSGS_AUTH_LOGGED_IN | LWSGS_AUTH_VERIFIED;\n+\t\tcar_args.final \u003d 0;\n+\t\tcar_args.chunked \u003d 1; /* ie, we are ws */\n+\n+\t\tin \u003d \u0026car_args;\n+\t\tgoto passthru;\n+#endif\n+\n+\tcase LWS_CALLBACK_ESTABLISHED:\n+\n+\t\tif (!vhd)\n+\t\t\treturn -1;\n+\n+\t\t/*\n+\t\t * What's the situation with a JWT cookie? Normal users won't\n+\t\t * have any, but privileged users will have one, and we should\n+\t\t * try to confirm it and set the pss auth level accordingly\n+\t\t */\n+\n+\t\tmemset(\u0026ck, 0, sizeof(ck));\n+\t\tck.jwk \u003d \u0026vhd-\u003ejwt_jwk_auth;\n+\t\tck.alg \u003d vhd-\u003ejwt_auth_alg;\n+\t\tck.iss \u003d vhd-\u003ejwt_issuer;\n+\t\tck.aud \u003d vhd-\u003ejwt_audience;\n+\t\tck.cookie_name \u003d \u0022__Host-sai_jwt\u0022;\n+\n+\t\tlws_dll2_add_head(\u0026pss-\u003esame, \u0026vhd-\u003ebrowsers);\n+\n+\t\tcml \u003d sizeof(buf);\n+\t\tif (!lws_jwt_get_http_cookie_validate_jwt(wsi, \u0026ck,\n+\t\t\t\t\t\t\t (char *)buf, \u0026cml) \u0026\u0026\n+\t\t ck.extra_json \u0026\u0026\n+\t\t !lws_json_simple_strcmp(ck.extra_json, ck.extra_json_len,\n+\t\t\t\t\t \u0022\u005c\u0022authorized\u005c\u0022:\u0022, \u00221\u0022)) {\n+\t\t\t/* the token allows him to manage us */\n+\t\t\tpss-\u003eauthorized \u003d 1;\n+\t\t\tpss-\u003eexpiry_unix_time \u003d ck.expiry_unix_time;\n+\t\t\tlws_strncpy(pss-\u003eauth_user, ck.sub,\n+\t\t\t\t sizeof(pss-\u003eauth_user));\n+\t\t} else\n+\t\t\tlwsl_info(\u0022%s: cookie rejected\u005cn\u0022, __func__);\n+\t\tpss-\u003ewsi \u003d wsi;\n+\t\tpss-\u003evhd \u003d vhd;\n+\t\tpss-\u003ealang[0] \u003d '\u005c0';\n+\t\tlws_hdr_copy(wsi, pss-\u003ealang, sizeof(pss-\u003ealang),\n+\t\t\t WSI_TOKEN_HTTP_ACCEPT_LANGUAGE);\n+\n+\t\tif (lws_hdr_total_length(wsi, WSI_TOKEN_GET_URI)) {\n+\t\t\tif (lws_hdr_copy(wsi, (char *)start, 64,\n+\t\t\t\t\t WSI_TOKEN_GET_URI) \u003c 0)\n+\t\t\t\treturn -1;\n+\t\t}\n+#if defined(LWS_ROLE_H2)\n+\t\telse\n+\t\t\tif (lws_hdr_copy(wsi, (char *)start, 64,\n+\t\t\t\t\t WSI_TOKEN_HTTP_COLON_PATH) \u003c 0)\n+\t\t\t\treturn -1;\n+#endif\n+\n+\t\tif (!memcmp((char *)start, \u0022/sai\u0022, 4))\n+\t\t\tstart +\u003d 4;\n+\n+\t\tif (!strncmp((char *)start, \u0022/browse/specific\u0022, 16)) {\n+\t\t\tconst char *spe;\n+\n+\t\t\tlwsl_info(\u0022%s: ESTABLISHED: browser (specific)\u005cn\u0022, __func__);\n+\t\t\tpss-\u003ewsi \u003d wsi;\n+\t\t\tpss-\u003especific_project[0] \u003d '\u005c0';\n+\t\t\tspe \u003d (const char *)start + 16;\n+\t\t\twhile (*spe \u003d\u003d '/')\n+\t\t\t\tspe++;\n+\t\t\tn \u003d 0;\n+\t\t\twhile(*spe \u0026\u0026 *spe !\u003d '/' \u0026\u0026\n+\t\t\t (size_t)n \u003c sizeof(pss-\u003especific_project) - 2)\n+\t\t\t\tpss-\u003especific_project[n++] \u003d *spe++;\n+\n+\t\t\tpss-\u003especific_project[n] \u003d '\u005c0';\n+\n+\t\t\tpss-\u003especific_task[0] \u003d '\u005c0';\n+\t\t\tpss-\u003especific_ref[0] \u003d '\u005c0';\n+\t\t\tif (lws_get_urlarg_by_name(wsi, \u0022h\u0022, pss-\u003especific_ref + 9,\n+\t\t\t\t\t\t sizeof(pss-\u003especific_ref) - 9)) {\n+\t\t\t\tmemcpy(pss-\u003especific_ref, \u0022refs/heads/\u0022, 11);\n+\t\t\t\tpss-\u003especificity \u003d SAIM_SPECIFIC_H;\n+\t\t\t} else\n+\t\t\t\tif (lws_get_urlarg_by_name(wsi, \u0022id\u0022, (char *)buf,\n+\t\t\t\t\t\t\t sizeof(buf))) {\n+\t\t\t\t\tlws_strncpy(pss-\u003especific_ref, (const char *)buf + 3,\n+\t\t\t\t\t\t\tsizeof(pss-\u003especific_ref));\n+\t\t\t\t\tpss-\u003especificity \u003d SAIM_SPECIFIC_ID;\n+\t\t\t\t} else {\n+\t\t\t\t\tif (lws_get_urlarg_by_name(wsi, \u0022task\u0022,\n+\t\t\t\t\t\t(char *)pss-\u003especific_task,\n+\t\t\t\t\t\tsizeof(pss-\u003especific_task))) {\n+\t\t\t\t\t\tpss-\u003especificity \u003d SAIM_SPECIFIC_TASK;\n+\t\t\t\t\t} else {\n+\t\t\t\t\t\tpss-\u003especificity \u003d SAIM_SPECIFIC_H;\n+\t\t\t\t\t\tlws_strncpy(pss-\u003especific_ref,\n+\t\t\t\t\t\t\t\u0022refs/heads/master\u0022,\n+\t\t\t\t\t\t\tsizeof(pss-\u003especific_ref));\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\n+\t\t\t\t//lwsl_notice(\u0022%s: spec %d, '%s'\u005cn\u0022, __func__,\n+\t\t\t\t//\tpss-\u003especificity, pss-\u003especific_ref);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tif (!strcmp((char *)start, \u0022/browse\u0022)) {\n+\t\t\tlwsl_info(\u0022%s: ESTABLISHED: browser\u005cn\u0022, __func__);\n+\t\t\tpss-\u003ewsi \u003d wsi;\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tlwsl_err(\u0022%s: unknown URL '%s'\u005cn\u0022, __func__, start);\n+\n+\t\treturn -1;\n+\n+\tcase LWS_CALLBACK_CLOSED:\n+\n+\t\tlwsl_info(\u0022%s: CLOSED browse conn\u005cn\u0022, __func__);\n+\t\tlws_dll2_remove(\u0026pss-\u003esame);\n+\t\tlws_dll2_remove(\u0026pss-\u003esubs_list);\n+\n+\t\tlws_dll2_foreach_safe(\u0026pss-\u003esched, NULL, saiw_sched_destroy);\n+\t\tlwsac_free(\u0026pss-\u003elogs_ac);\n+\n+\t\tbreak;\n+\n+\tcase LWS_CALLBACK_RECEIVE:\n+\n+\t\tif (!pss-\u003evhd)\n+\t\t\tpss-\u003evhd \u003d vhd;\n+\n+\t\t// lwsl_user(\u0022SWT_BROWSE RX: %d\u005cn\u0022, (int)len);\n+\t\t/*\n+\t\t * Browser UI sent us something on websockets\n+\t\t */\n+\t\tif (saiw_ws_json_rx_browser(vhd, pss, in, len))\n+\t\t\treturn -1;\n+\n+\t\tbreak;\n+\n+\tcase LWS_CALLBACK_SERVER_WRITEABLE:\n+\t\tif (!vhd) {\n+\t\t\tlwsl_notice(\u0022%s: no vhd\u005cn\u0022, __func__);\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\treturn saiw_ws_json_tx_browser(vhd, pss, buf, sizeof(buf));\n+\n+\tdefault:\n+passthru:\n+\t//\tif (!pss || !vhd)\n+\t\t\tbreak;\n+\n+\t//\treturn vhd-\u003egsp-\u003ecallback(wsi, reason, pss-\u003epss_gs, in, len);\n+\t}\n+\n+\treturn lws_callback_http_dummy(wsi, reason, user, in, len);\n+\n+bail:\n+\treturn 1;\n+\n+try_to_reuse:\n+\tif (lws_http_transaction_completed(wsi))\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+const struct lws_protocols protocol_ws \u003d\n+\t{ \u0022com-warmcat-sai\u0022, callback_ws, sizeof(struct pss), 0 };\ndiff --git a/src/web/w-conf.c b/src/web/w-conf.c\nnew file mode 100644\nindex 0000000..b229ace\n--- /dev/null\n+++ b/src/web/w-conf.c\n@@ -0,0 +1,84 @@\n+/*\n+ * Sai server src/server/conf.c\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ */\n+#include \u003clibwebsockets.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003ctime.h\u003e\n+\n+#define SAI_CONFIG_STRING_SIZE (16 * 1024)\n+\n+struct lws_context *\n+sai_lws_context_from_json(const char *config_dir,\n+\t\t\t struct lws_context_creation_info *info,\n+\t\t\t const struct lws_protocols **pprotocols,\n+\t\t\t const char *pol)\n+{\n+\tint cs_len \u003d SAI_CONFIG_STRING_SIZE - 1;\n+\tstruct lws_context *context;\n+\tchar *cs, *config_strings;\n+\n+\tcs \u003d config_strings \u003d malloc(SAI_CONFIG_STRING_SIZE);\n+\tif (!config_strings) {\n+\t\tlwsl_err(\u0022Unable to allocate config strings heap\u005cn\u0022);\n+\n+\t\treturn NULL;\n+\t}\n+\n+\tmemset(info, 0, sizeof(*info));\n+\n+\tinfo-\u003eexternal_baggage_free_on_destroy \u003d config_strings;\n+\tinfo-\u003ept_serv_buf_size \u003d 8192;\n+\tinfo-\u003eoptions \u003d LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT |\n+\t\t LWS_SERVER_OPTION_EXPLICIT_VHOSTS |\n+\t\t LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE |\n+\t\t LWS_SERVER_OPTION_VALIDATE_UTF8;\n+\tinfo-\u003epss_policies_json \u003d pol;\n+\n+\tlwsl_notice(\u0022Using config dir: \u005c\u0022%s\u005c\u0022\u005cn\u0022, config_dir);\n+\n+\t/*\n+\t * first go through the config for creating the outer context\n+\t */\n+\tif (lwsws_get_config_globals(info, config_dir, \u0026cs, \u0026cs_len))\n+\t\tgoto init_failed;\n+\n+\tcontext \u003d lws_create_context(info);\n+\tif (context \u003d\u003d NULL) {\n+\t\t/* config_strings freed as 'external baggage' */\n+\t\treturn NULL;\n+\t}\n+\n+\tinfo-\u003epprotocols \u003d pprotocols;\n+\n+\tif (lwsws_get_config_vhosts(context, info, config_dir, \u0026cs, \u0026cs_len)) {\n+\t\tlwsl_err(\u0022%s: sai_lws_context_from_json failed\u005cn\u0022, __func__);\n+\t\tlws_context_destroy(context);\n+\n+\t\treturn NULL;\n+\t}\n+\n+\treturn context;\n+\n+init_failed:\n+\tfree(config_strings);\n+\n+\treturn NULL;\n+}\ndiff --git a/src/web/w-private.h b/src/web/w-private.h\nnew file mode 100644\nindex 0000000..8e3b27d\n--- /dev/null\n+++ b/src/web/w-private.h\n@@ -0,0 +1,316 @@\n+/*\n+ * Sai server definitions src/server/private.h\n+ *\n+ * Copyright (C) 2019 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ */\n+\n+#include \u0022../common/include/private.h\u0022\n+#include \u003csqlite3.h\u003e\n+#include \u003csys/stat.h\u003e\n+\n+struct sai_plat;\n+\n+typedef struct sai_platm {\n+\tstruct lws_dll2_owner builder_owner;\n+\tstruct lws_dll2_owner subs_owner;\n+\n+\tsqlite3 *pdb;\n+\tsqlite3 *pdb_auth;\n+} sais_t;\n+\n+typedef struct sai_platform {\n+\tstruct lws_dll2\t\tlist;\n+\n+\tconst char\t\t*name;\n+\tconst char\t\t*build;\n+\n+\tuint8_t\t\t\tnondefault;\n+\n+\t/* build and name over-allocated here */\n+} sai_platform_t;\n+\n+typedef enum {\n+\tSAIN_ACTION_INVALID,\n+\tSAIN_ACTION_REPO_UPDATED\n+} sai_notification_action_t;\n+\n+typedef struct {\n+\n+\tsai_event_t\t\t\te;\n+\tsai_task_t\t\t\tt;\n+\n+\tchar\t\t\t\tplatbuild[4096];\n+\tchar\t\t\t\tplatname[96];\n+\tchar\t\t\t\texplicit_platforms[2048];\n+\n+\tint\t\t\t\tevent_task_index;\n+\n+\tstruct lws_b64state\t\tb64;\n+\tchar\t\t\t\t*saifile;\n+\tuint64_t\t\t\twhen;\n+\tsize_t\t\t\t\tsaifile_in_len;\n+\tsize_t\t\t\t\tsaifile_out_len;\n+\tsize_t\t\t\t\tsaifile_out_pos;\n+\tsize_t\t\t\t\tsaifile_in_seen;\n+\tsai_notification_action_t\taction;\n+\n+\tuint8_t\t\t\t\tnondefault;\n+} sai_notification_t;\n+\n+typedef enum {\n+\tWSS_IDLE,\n+\tWSS_PREPARE_OVERVIEW,\n+\tWSS_SEND_OVERVIEW,\n+\tWSS_PREPARE_BUILDER_SUMMARY,\n+\tWSS_SEND_BUILDER_SUMMARY,\n+\n+\tWSS_PREPARE_TASKINFO,\n+\tWSS_SEND_ARTIFACT_INFO,\n+\n+\tWSS_PREPARE_EVENTINFO,\n+\tWSS_SEND_EVENTINFO,\n+} ws_state;\n+\n+typedef struct sai_builder {\n+\tsais_t c;\n+} saib_t;\n+\n+struct vhd;\n+\n+enum {\n+\tSAIM_NOT_SPECIFIC,\n+\tSAIM_SPECIFIC_H,\n+\tSAIM_SPECIFIC_ID,\n+\tSAIM_SPECIFIC_TASK,\n+};\n+\n+typedef struct saiw_scheduled {\n+\tlws_dll2_t\t\tlist;\n+\n+\tchar\t\t\ttask_uuid[65];\n+\n+\tconst sai_task_t\t*one_task; /* only for browser */\n+\tconst sai_event_t\t*one_event;\n+\n+\tlws_dll2_t\t\t*walk;\n+\n+\tlws_dll2_owner_t\towner;\n+\n+\tstruct lwsac\t\t*ac;\n+\tstruct lwsac\t\t*query_ac; /* taskinfo event only */\n+\n+\tws_state\t\taction;\n+\tint\t\t\ttask_index;\n+\n+\tuint8_t\t\t\tovstate; /* SOS_ substate when doing overview */\n+\n+\tuint8_t\t\t\tsubsequent:1; /* for individual JSON */\n+\tuint8_t\t\t\tov_db_done:1; /* for individual JSON */\n+\n+} saiw_scheduled_t;\n+\n+struct pss {\n+\tstruct vhd\t\t*vhd;\n+\tstruct lws\t\t*wsi;\n+\n+\tstruct lws_spa\t\t*spa;\n+\tstruct lejp_ctx\t\tctx;\n+\tsai_notification_t\tsn;\n+\tstruct lws_dll2\t\tsame; /* owner: vhd.browsers */\n+\n+\tstruct lws_dll2\t\tsubs_list;\n+\n+\tuint64_t\t\tsub_timestamp;\n+\tchar\t\t\tsub_task_uuid[65];\n+\tchar\t\t\tspecific_ref[65];\n+\tchar\t\t\tspecific_task[65];\n+\tchar\t\t\tspecific_project[96];\n+\tchar\t\t\tauth_user[33];\n+\n+\tsqlite3\t\t\t*pdb_artifact;\n+\tsqlite3_blob\t\t*blob_artifact;\n+\n+\tlws_dll2_owner_t\tplatform_owner; /* sai_platform_t builder offers */\n+\tlws_dll2_owner_t\ttask_cancel_owner; /* sai_platform_t builder offers */\n+\tlws_dll2_owner_t\tlogs_owner;\n+\tlws_struct_args_t\ta;\n+\n+\tunion {\n+\t\tsai_plat_t\t*b;\n+\t\tsai_plat_owner_t *o;\n+\t} u;\n+\tconst char\t\t*server_name;\n+\n+\tlws_dll2_owner_t\tsched;\t/* scheduled messages */\n+\n+\tstruct lwsac\t\t*logs_ac;\n+\tlws_dll2_owner_t\tissue_task_owner; /* list of sai_task_t */\n+\n+\tint\t\t\tlog_cache_index;\n+\tint\t\t\tlog_cache_size;\n+\tint\t\t\tauthorized;\n+\tint\t\t\tspecificity;\n+\tunsigned long\t\texpiry_unix_time;\n+\n+\t/* notification hmac information */\n+\tchar\t\t\tnotification_sig[128];\n+\tchar\t\t\talang[128];\n+\tstruct lws_genhmac_ctx\thmac;\n+\tenum lws_genhmac_types\thmac_type;\n+\tchar\t\t\tour_form;\n+\tchar\t\t\tlogin_form;\n+\n+\tuint64_t\t\tfirst_log_timestamp;\n+\tuint64_t\t\tartifact_offset;\n+\tuint64_t\t\tartifact_length;\n+\n+\tws_state\t\tsend_state;\n+\n+\tunsigned int\t\tspa_failed:1;\n+\tunsigned int\t\tdry:1;\n+\tunsigned int\t\tfrag:1;\n+\tunsigned int\t\tmark_started:1;\n+\tunsigned int\t\twants_event_updates:1;\n+\tunsigned int\t\tannounced:1;\n+\tunsigned int\t\tbulk_binary_data:1;\n+\tunsigned int\t\ttoggle_favour_sch:1;\n+};\n+\n+typedef struct sais_sqlite_cache {\n+\tlws_dll2_t\tlist;\n+\tchar\t\tuuid[65];\n+\tsqlite3\t\t*pdb;\n+\tlws_usec_t\tidle_since;\n+\tint\t\trefcount;\n+} sais_sqlite_cache_t;\n+\n+struct vhd {\n+\tstruct lws_context *context;\n+\tstruct lws_vhost *vhost;\n+\n+\t/* pss lists */\n+\tstruct lws_dll2_owner browsers;\n+\n+\tstruct lws_dll2_owner\t\t*builders_owner;\n+\tstruct lwsac\t\t\t*builders;\n+\n+\t/* our keys */\n+\tstruct lws_jwk\t\t\tjwt_jwk_auth;\n+\tchar\t\t\t\tjwt_auth_alg[16];\n+\tconst char\t\t\t*jwt_issuer;\n+\tconst char\t\t\t*jwt_audience;\n+\n+\tlws_dll2_owner_t\t\tweb_to_srv_owner;\n+\tlws_dll2_owner_t\t\tsubs_owner;\n+\tsqlite3\t\t\t\t*pdb;\n+\tsqlite3\t\t\t\t*pdb_auth;\n+\n+\tstruct lws_ss_handle\t\t*h_ss_websrv; /* client */\n+\n+\tconst char *sqlite3_path_lhs;\n+\n+\tlws_dll2_owner_t sqlite3_cache; /* sais_sqlite_cache_t */\n+\tlws_dll2_owner_t tasklog_cache;\n+\tlws_sorted_usec_list_t sul_logcache;\n+\tlws_sorted_usec_list_t sul_central; /* background task allocation sul */\n+};\n+\n+extern struct lws_context *\n+sai_lws_context_from_json(const char *config_dir,\n+\t\t\t struct lws_context_creation_info *info,\n+\t\t\t const struct lws_protocols **pprotocols,\n+\t\t\t const char *pol);\n+extern const struct lws_protocols protocol_ws;\n+extern const lws_ss_info_t ssi_saiw_websrv;\n+\n+int\n+sai_notification_file_upload_cb(void *data, const char *name,\n+\t\t\t\tconst char *filename, char *buf, int len,\n+\t\t\t\tenum lws_spa_fileupload_states state);\n+\n+int\n+sai_sqlite3_statement(sqlite3 *pdb, const char *cmd, const char *desc);\n+\n+int\n+sai_uuid16_create(struct lws_context *context, char *dest33);\n+\n+int\n+sais_event_db_ensure_open(struct vhd *vhd, const char *event_uuid, char can_create, sqlite3 **ppdb);\n+\n+void\n+sais_event_db_close(struct vhd *vhd, sqlite3 **ppdb);\n+\n+int\n+sais_event_db_delete_database(struct vhd *vhd, const char *event_uuid);\n+\n+int\n+sai_sq3_event_lookup(sqlite3 *pdb, uint64_t start, lws_struct_args_cb cb, void *ca);\n+\n+int\n+sai_sql3_get_uint64_cb(void *user, int cols, char **values, char **name);\n+\n+int\n+saiw_ws_json_tx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t bl);\n+\n+int\n+lws_struct_map_set(const lws_struct_map_t *map, char *u);\n+\n+int\n+saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss,\n+\t\t\t uint8_t *buf, size_t bl);\n+\n+void\n+sai_task_uuid_to_event_uuid(char *event_uuid33, const char *task_uuid65);\n+\n+int\n+sais_ws_json_tx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t bl);\n+\n+int\n+saiw_subs_request_writeable(struct vhd *vhd, const char *task_uuid);\n+\n+int\n+saiw_event_state_change(struct vhd *vhd, const char *event_uuid);\n+\n+int\n+saiw_subs_task_state_change(struct vhd *vhd, const char *task_uuid);\n+\n+void\n+saiw_central_cb(lws_sorted_usec_list_t *sul);\n+\n+int\n+saiw_task_cancel(struct vhd *vhd, const char *task_uuid);\n+\n+int\n+saiw_websrv_queue_tx(struct lws_ss_handle *h, void *buf, size_t len);\n+\n+int\n+saiw_get_blob(struct vhd *vhd, const char *url, sqlite3 **pdb,\n+\t sqlite3_blob **blob, uint64_t *length);\n+\n+int\n+saiw_browsers_task_state_change(struct vhd *vhd, const char *task_uuid);\n+\n+saiw_scheduled_t *\n+saiw_alloc_sched(struct pss *pss, ws_state action);\n+\n+void\n+saiw_dealloc_sched(saiw_scheduled_t *sch);\n+\n+int\n+saiw_sched_destroy(struct lws_dll2 *d, void *user);\n+\ndiff --git a/src/web/w-sai.c b/src/web/w-sai.c\nnew file mode 100644\nindex 0000000..cdcfddb\n--- /dev/null\n+++ b/src/web/w-sai.c\n@@ -0,0 +1,97 @@\n+/*\n+ * Sai web\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ */\n+\n+#include \u003clibwebsockets.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003ctime.h\u003e\n+\n+#include \u0022w-private.h\u0022\n+\n+static int interrupted;\n+struct lws_context *context;\n+\n+static const char * const default_ss_policy \u003d\n+\t\u0022{\u0022\n+\t\t\u0022\u005c\u0022retry\u005c\u0022: [{\u0022\n+\t\t\t\u0022\u005c\u0022default\u005c\u0022: {\u0022\n+\t\t\t\t\u0022\u005c\u0022backoff\u005c\u0022: [1000, 2000, 3000],\u0022\n+\t\t\t\t\u0022\u005c\u0022conceal\u005c\u0022: 5,\u0022\n+\t\t\t\t\u0022\u005c\u0022jitterpc\u005c\u0022: 20,\u0022\n+\t\t\t\t\u0022\u005c\u0022svalidping\u005c\u0022: 30,\u0022\n+\t\t\t\t\u0022\u005c\u0022svalidhup\u005c\u0022: 35\u0022\n+\t\t\t\u0022}\u0022\n+\t\t\u0022}],\u0022\n+\t \u0022\u005c\u0022s\u005c\u0022: [\u0022\n+\t\t/*\n+\t\t * Unix Domain Socket connections to sai-server webevents\n+\t\t */\n+\t\t\u0022{\u005c\u0022websrv\u005c\u0022: {\u0022\n+\t\t\t\u0022\u005c\u0022endpoint\u005c\u0022:\u0022\t\t\u0022\u005c\u0022+${sockpath}\u005c\u0022,\u0022\n+\t\t\t\u0022\u005c\u0022protocol\u005c\u0022:\u0022\t\t\u0022\u005c\u0022ws\u005c\u0022,\u0022\n+\t\t\t\u0022\u005c\u0022metadata\u005c\u0022: [\u0022\n+\t\t\t\t\u0022{\u005c\u0022sockpath\u005c\u0022: \u005c\u0022\u005c\u0022}\u0022\n+\t\t\t\u0022],\u0022\n+\t\t\t\u0022\u005c\u0022retry\u005c\u0022: \u005c\u0022default\u005c\u0022,\u0022\n+\t\t\t\u0022\u005c\u0022nailed_up\u005c\u0022:\u0022\t\t\u0022true\u0022\n+\t\t\u0022}}\u0022\n+\t\u0022]}\u0022\n+;\n+\n+static const struct lws_protocols\n+\t*pprotocols[] \u003d { \u0026protocol_ws, NULL };\n+\n+static void sigint_handler(int sig)\n+{\n+\tinterrupted \u003d 1;\n+}\n+\n+int main(int argc, const char **argv)\n+{\n+\tint logs \u003d LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE;\n+\tconst char *p, *conf \u003d \u0022/etc/sai/web\u0022;\n+\tstruct lws_context_creation_info info;\n+\n+\tsignal(SIGINT, sigint_handler);\n+\n+\tif ((p \u003d lws_cmdline_option(argc, argv, \u0022-d\u0022)))\n+\t\tlogs \u003d atoi(p);\n+\n+\tlws_set_log_level(logs, NULL);\n+\tlwsl_user(\u0022Sai Web - Copyright (C) 2019-2020 Andy Green \u003candy@warmcat.com\u003e\u005cn\u0022);\n+\n+\tif ((p \u003d lws_cmdline_option(argc, argv, \u0022-c\u0022)))\n+\t\tconf \u003d p;\n+\n+\tcontext \u003d sai_lws_context_from_json(conf, \u0026info, pprotocols,\n+\t\t\t\t\t\tdefault_ss_policy);\n+\tif (!context) {\n+\t\tlwsl_err(\u0022lws init failed\u005cn\u0022);\n+\t\treturn 1;\n+\t}\n+\n+\twhile (!lws_service(context, 0) \u0026\u0026 !interrupted)\n+\t\t;\n+\n+\tlws_context_destroy(context);\n+\n+\treturn 0;\n+}\ndiff --git a/src/web/w-websrv.c b/src/web/w-websrv.c\nnew file mode 100644\nindex 0000000..ab7bf88\n--- /dev/null\n+++ b/src/web/w-websrv.c\n@@ -0,0 +1,253 @@\n+/*\n+ * Sai web websrv - saiw SS client private UDS link to sais SS server\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ *\n+ * We copy JSON to heap and forward it in order to sais side.\n+ */\n+\n+#include \u003clibwebsockets.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003ctime.h\u003e\n+\n+#include \u0022w-private.h\u0022\n+\n+typedef struct saiw_websrv {\n+\tstruct lws_ss_handle\t\t*ss;\n+\tvoid\t\t\t\t*opaque_data;\n+\n+\tlws_struct_args_t\t\ta;\n+\tstruct lejp_ctx\t\t\tctx;\n+\t//lws_dll2_t\n+\tstruct lws_buflist\t\t*bltx;\n+\n+} saiw_websrv_t;\n+\n+static lws_struct_map_t lsm_websrv_evinfo[] \u003d {\n+\tLSM_CARRAY\t(sai_browse_rx_evinfo_t, event_hash,\t\u0022event_hash\u0022),\n+};\n+\n+static const lws_struct_map_t lsm_schema_json_map[] \u003d {\n+\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_websrv_evinfo,\n+\t\t\t/* shares struct */ \u0022sai-taskchange\u0022),\n+\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_websrv_evinfo,\n+\t\t\t/* shares struct */ \u0022sai-eventchange\u0022),\n+\tLSM_SCHEMA\t(sai_plat_owner_t, NULL, lsm_plat_list, \u0022sai-builders\u0022),\n+\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_websrv_evinfo,\n+\t\t\t/* shares struct */ \u0022sai-overview\u0022),\n+\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_websrv_evinfo,\n+\t\t\t/* shares struct */ \u0022sai-tasklogs\u0022),\n+};\n+\n+enum {\n+\tSAIS_WS_WEBSRV_RX_TASKCHANGE,\n+\tSAIS_WS_WEBSRV_RX_EVENTCHANGE,\n+\tSAIS_WS_WEBSRV_RX_SAI_BUILDERS,\n+\tSAIS_WS_WEBSRV_RX_OVERVIEW, /* deleted or added event */\n+\tSAIS_WS_WEBSRV_RX_TASKLOGS /* new logs for task (ratelimited) */\n+};\n+\n+\n+static int\n+saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags)\n+{\n+\tsaiw_websrv_t *m \u003d (saiw_websrv_t *)userobj;\n+\tstruct vhd *vhd \u003d (struct vhd *)m-\u003eopaque_data;\n+\tsai_browse_rx_evinfo_t *ei;\n+\tint n;\n+\n+\t// lwsl_user(\u0022%s: len %d, flags: %d\u005cn\u0022, __func__, (int)len, flags);\n+\t// lwsl_hexdump_info(buf, len);\n+\n+\tif (flags \u0026 LWSSS_FLAG_SOM) {\n+\t\tmemset(\u0026m-\u003ea, 0, sizeof(m-\u003ea));\n+\t\tm-\u003ea.map_st[0] \u003d lsm_schema_json_map;\n+\t\tm-\u003ea.map_entries_st[0] \u003d LWS_ARRAY_SIZE(lsm_schema_json_map);\n+\t\tm-\u003ea.map_entries_st[1] \u003d LWS_ARRAY_SIZE(lsm_schema_json_map);\n+\t\tm-\u003ea.ac_block_size \u003d 128;\n+\n+\t\tlws_struct_json_init_parse(\u0026m-\u003ectx, NULL, \u0026m-\u003ea);\n+\t}\n+\n+\tn \u003d lejp_parse(\u0026m-\u003ectx, (uint8_t *)buf, len);\n+\tif (n \u003c LEJP_CONTINUE || (n \u003e\u003d 0 \u0026\u0026 !m-\u003ea.dest)) {\n+\t\tlwsac_free(\u0026m-\u003ea.ac);\n+\t\tlwsl_hexdump_notice(buf, len);\n+\t\tlwsl_notice(\u0022%s: srv-\u003eweb JSON decode failed '%s'\u005cn\u0022,\n+\t\t\t\t__func__, lejp_error_to_string(n));\n+\t\treturn LWSSSSRET_DISCONNECT_ME;\n+\t}\n+\n+\tif (!(flags \u0026 LWSSS_FLAG_EOM))\n+\t\treturn 0;\n+\n+//\tlwsl_notice(\u0022%s: schema idx %d\u005cn\u0022, __func__, m-\u003ea.top_schema_index);\n+\n+\tswitch (m-\u003ea.top_schema_index) {\n+\n+\tcase SAIS_WS_WEBSRV_RX_TASKCHANGE:\n+\t\tei \u003d (sai_browse_rx_evinfo_t *)m-\u003ea.dest;\n+\t\t/* server has told us of a task change */\n+\t\tlwsl_notice(\u0022%s: TASKCHANGE %s\u005cn\u0022, __func__, ei-\u003eevent_hash);\n+\t\tsaiw_browsers_task_state_change(vhd, ei-\u003eevent_hash);\n+\t\tbreak;\n+\n+\tcase SAIS_WS_WEBSRV_RX_EVENTCHANGE:\n+\t\tei \u003d (sai_browse_rx_evinfo_t *)m-\u003ea.dest;\n+\t\tlwsl_notice(\u0022%s: EVENTCHANGE %s\u005cn\u0022, __func__, ei-\u003eevent_hash);\n+\t\tsaiw_event_state_change(vhd, ei-\u003eevent_hash);\n+\t\tbreak;\n+\n+\tcase SAIS_WS_WEBSRV_RX_SAI_BUILDERS:\n+\t\tlwsl_notice(\u0022%s: updated sai builder list\u005cn\u0022, __func__);\n+\t\tif (vhd-\u003ebuilders)\n+\t\t\tlwsac_detach(\u0026vhd-\u003ebuilders);\n+\t\tvhd-\u003ebuilders \u003d m-\u003ea.ac;\n+\t\tm-\u003ea.ac \u003d NULL;\n+\t\tvhd-\u003ebuilders_owner \u003d\n+\t\t\t\t\u0026((sai_plat_owner_t *)m-\u003ea.dest)-\u003eplat_owner;\n+\t\tlws_start_foreach_dll(struct lws_dll2 *, p, vhd-\u003ebrowsers.head) {\n+\t\t\tstruct pss *pss \u003d lws_container_of(p, struct pss, same);\n+\n+\t\t\tsaiw_alloc_sched(pss, WSS_PREPARE_BUILDER_SUMMARY);\n+\n+\t\t} lws_end_foreach_dll(p);\n+\t\tbreak;\n+\n+\tcase SAIS_WS_WEBSRV_RX_OVERVIEW:\n+\t\tlwsl_notice(\u0022%s: force overview\u005cn\u0022, __func__);\n+\t\tlws_start_foreach_dll(struct lws_dll2 *, p, vhd-\u003ebrowsers.head) {\n+\t\t\tstruct pss *pss \u003d lws_container_of(p, struct pss, same);\n+\n+\t\t\tsaiw_alloc_sched(pss, WSS_PREPARE_OVERVIEW);\n+\t\t} lws_end_foreach_dll(p);\n+\t\tbreak;\n+\n+\tcase SAIS_WS_WEBSRV_RX_TASKLOGS:\n+\t\tei \u003d (sai_browse_rx_evinfo_t *)m-\u003ea.dest;\n+\t\t/*\n+\t\t * ratelimited indication that logs for a particular task\n+\t\t * changed... for each connected browser subscribed to logs for\n+\t\t * that task, let them know\n+\t\t */\n+\t\tlws_start_foreach_dll(struct lws_dll2 *, p,\n+\t\t\t\t vhd-\u003esubs_owner.head) {\n+\t\t\tstruct pss *pss \u003d lws_container_of(p, struct pss,\n+\t\t\t\t\t\t\t subs_list);\n+\n+\t\t\tif (!strcmp(pss-\u003esub_task_uuid, ei-\u003eevent_hash))\n+\t\t\t\tlws_callback_on_writable(pss-\u003ewsi);\n+\t\t} lws_end_foreach_dll(p);\n+\t\tbreak;\n+\t}\n+\n+\tif (flags \u0026 LWSSS_FLAG_EOM)\n+\t\tlwsac_free(\u0026m-\u003ea.ac);\n+\n+\treturn 0;\n+}\n+\n+static int\n+saiw_lp_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, size_t *len,\n+\t int *flags)\n+{\n+\tsaiw_websrv_t *m \u003d (saiw_websrv_t *)userobj;\n+\tchar som, eom;\n+\tint used;\n+\n+\tif (!m-\u003ebltx)\n+\t\treturn LWSSSSRET_TX_DONT_SEND;\n+\n+\tused \u003d lws_buflist_fragment_use(\u0026m-\u003ebltx, buf, *len, \u0026som, \u0026eom);\n+\tif (!used)\n+\t\treturn LWSSSSRET_TX_DONT_SEND;\n+\n+\t*flags \u003d (som ? LWSSS_FLAG_SOM : 0) | (eom ? LWSSS_FLAG_EOM : 0);\n+\t*len \u003d (size_t)used;\n+\n+\tif (m-\u003ebltx)\n+\t\tlws_ss_request_tx(m-\u003ess);\n+\n+\treturn 0;\n+}\n+\n+static int\n+saiw_lp_state(void *userobj, void *sh, lws_ss_constate_t state,\n+\t lws_ss_tx_ordinal_t ack)\n+{\n+\tsaiw_websrv_t *m \u003d (saiw_websrv_t *)userobj;\n+\tstruct vhd *vhd \u003d (struct vhd *)m-\u003eopaque_data;\n+\n+\tlwsl_user(\u0022%s: %s, ord 0x%x\u005cn\u0022, __func__, lws_ss_state_name(state),\n+\t\t (unsigned int)ack);\n+\n+\tswitch (state) {\n+\tcase LWSSSCS_DESTROYING:\n+\t\tbreak;\n+\n+\tcase LWSSSCS_CONNECTED:\n+\t\tlwsl_notice(\u0022%s: connected to websrv uds\u005cn\u0022, __func__);\n+\t\tlws_ss_request_tx(m-\u003ess);\n+\t\tbreak;\n+\n+\tcase LWSSSCS_DISCONNECTED:\n+\t\tlws_buflist_destroy_all_segments(\u0026m-\u003ebltx);\n+\t\tlwsac_detach(\u0026vhd-\u003ebuilders);\n+\t\tbreak;\n+\n+\tcase LWSSSCS_ALL_RETRIES_FAILED:\n+\t\tlws_ss_client_connect(m-\u003ess);\n+\t\tbreak;\n+\n+\tcase LWSSSCS_QOS_ACK_REMOTE:\n+\t\tbreak;\n+\n+\tdefault:\n+\t\tbreak;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+const lws_ss_info_t ssi_saiw_websrv \u003d {\n+\t.handle_offset\t\t \u003d offsetof(saiw_websrv_t, ss),\n+\t.opaque_user_data_offset \u003d offsetof(saiw_websrv_t, opaque_data),\n+\t.rx\t\t\t \u003d saiw_lp_rx,\n+\t.tx\t\t\t \u003d saiw_lp_tx,\n+\t.state\t\t\t \u003d saiw_lp_state,\n+\t.user_alloc\t\t \u003d sizeof(saiw_websrv_t),\n+\t.streamtype\t\t \u003d \u0022websrv\u0022\n+};\n+\n+/*\n+ * send to server\n+ */\n+\n+int\n+saiw_websrv_queue_tx(struct lws_ss_handle *h, void *buf, size_t len)\n+{\n+\tsaiw_websrv_t *m \u003d (saiw_websrv_t *)lws_ss_to_user_object(h);\n+\n+\tif (lws_buflist_append_segment(\u0026m-\u003ebltx, buf, len) \u003c 0)\n+\t\treturn 1;\n+\n+\tlws_ss_request_tx(h);\n+\n+\treturn 0;\n+}\ndiff --git a/src/web/w-ws-browser.c b/src/web/w-ws-browser.c\nnew file mode 100644\nindex 0000000..1db984a\n--- /dev/null\n+++ b/src/web/w-ws-browser.c\n@@ -0,0 +1,1164 @@\n+/*\n+ * Sai server - ./src/server/m-ws-browser.c\n+ *\n+ * Copyright (C) 2019 - 2020 Andy Green \u003candy@warmcat.com\u003e\n+ *\n+ * This library is free software; you can redistribute it and/or\n+ * modify it under the terms of the GNU Lesser General Public\n+ * License as published by the Free Software Foundation:\n+ * version 2.1 of the License.\n+ *\n+ * This library is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n+ * Lesser General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Lesser General Public\n+ * License along with this library; if not, write to the Free Software\n+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,\n+ * MA 02110-1301 USA\n+ *\n+ * These are ws rx and tx handlers related to browser ws connections, on\n+ * /broswe URLs.\n+ */\n+\n+#include \u003clibwebsockets.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003cassert.h\u003e\n+#include \u003ctime.h\u003e\n+\n+#include \u0022w-private.h\u0022\n+\n+/*\n+ * For decoding specific event data request from browser\n+ */\n+\n+static lws_struct_map_t lsm_browser_evinfo[] \u003d {\n+\tLSM_CARRAY\t(sai_browse_rx_evinfo_t, event_hash,\t\u0022event_hash\u0022),\n+};\n+\n+static lws_struct_map_t lsm_browser_taskreset[] \u003d {\n+\tLSM_CARRAY\t(sai_browse_rx_evinfo_t, event_hash,\t\u0022uuid\u0022),\n+};\n+\n+static lws_struct_map_t lsm_browser_taskinfo[] \u003d {\n+\tLSM_CARRAY\t(sai_browse_rx_taskinfo_t, task_hash,\t\u0022task_hash\u0022),\n+\tLSM_UNSIGNED\t(sai_browse_rx_taskinfo_t, logs,\t\u0022logs\u0022),\n+};\n+\n+\n+/*\n+ * Schema list so lws_struct can pick the right object to create based on the\n+ * incoming schema name\n+ */\n+\n+static const lws_struct_map_t lsm_schema_json_map_bwsrx[] \u003d {\n+\tLSM_SCHEMA\t(sai_browse_rx_taskinfo_t, NULL, lsm_browser_taskinfo,\n+\t\t\t\t\t \u0022com.warmcat.sai.taskinfo\u0022),\n+\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_browser_evinfo,\n+\t\t\t\t\t \u0022com.warmcat.sai.eventinfo\u0022),\n+\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_browser_taskreset,\n+\t\t\t/* shares struct */ \u0022com.warmcat.sai.taskreset\u0022),\n+\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_browser_taskreset,\n+\t\t\t/* shares struct */ \u0022com.warmcat.sai.eventreset\u0022),\n+\tLSM_SCHEMA\t(sai_browse_rx_evinfo_t, NULL, lsm_browser_taskreset,\n+\t\t\t/* shares struct */ \u0022com.warmcat.sai.eventdelete\u0022),\n+\tLSM_SCHEMA\t(sai_cancel_t,\t\t NULL, lsm_task_cancel,\n+\t\t\t\t\t \u0022com.warmcat.sai.taskcan\u0022),\n+};\n+\n+enum {\n+\tSAIM_WS_BROWSER_RX_TASKINFO,\n+\tSAIM_WS_BROWSER_RX_EVENTINFO,\n+\tSAIM_WS_BROWSER_RX_TASKRESET,\n+\tSAIM_WS_BROWSER_RX_EVENTRESET,\n+\tSAIM_WS_BROWSER_RX_EVENTDELETE,\n+\tSAIM_WS_BROWSER_RX_TASKCANCEL\n+};\n+\n+\n+/*\n+ * For issuing combined task and event data back to browser\n+ */\n+\n+typedef struct sai_browse_taskreply {\n+\tconst sai_event_t\t*event;\n+\tconst sai_task_t\t*task;\n+\tchar\t\t\tauth_user[33];\n+\tint\t\t\tauthorized;\n+\tint\t\t\tauth_secs;\n+} sai_browse_taskreply_t;\n+\n+static lws_struct_map_t lsm_taskreply[] \u003d {\n+\tLSM_CHILD_PTR\t(sai_browse_taskreply_t, event,\tsai_event_t, NULL,\n+\t\t\t lsm_event, \u0022e\u0022),\n+\tLSM_CHILD_PTR\t(sai_browse_taskreply_t, task,\tsai_task_t, NULL,\n+\t\t\t lsm_task, \u0022t\u0022),\n+\tLSM_CARRAY\t(sai_browse_taskreply_t, auth_user,\t\u0022auth_user\u0022),\n+\tLSM_UNSIGNED\t(sai_browse_taskreply_t, authorized,\t\u0022authorized\u0022),\n+\tLSM_UNSIGNED\t(sai_browse_taskreply_t, auth_secs,\t\u0022auth_secs\u0022),\n+};\n+\n+const lws_struct_map_t lsm_schema_json_map_taskreply[] \u003d {\n+\tLSM_SCHEMA\t(sai_browse_taskreply_t, NULL, lsm_taskreply,\n+\t\t\t \u0022com.warmcat.sai.taskinfo\u0022),\n+};\n+\n+enum sai_overview_state {\n+\tSOS_EVENT,\n+\tSOS_TASKS,\n+};\n+\n+int\n+sai_sql3_get_uint64_cb(void *user, int cols, char **values, char **name)\n+{\n+\tuint64_t *pui \u003d (uint64_t *)user;\n+\n+\t*pui \u003d (uint64_t)atoll(values[0]);\n+\n+\treturn 0;\n+}\n+\n+/* 1 \u003d\u003d authorized */\n+\n+static int\n+sais_conn_auth(struct pss *pss)\n+{\n+\tif (!pss-\u003eauthorized)\n+\t\treturn 0;\n+\tif (pss-\u003eexpiry_unix_time \u003c (unsigned long)lws_now_secs())\n+\t\treturn 0;\n+\n+\treturn 1;\n+}\n+\n+/*\n+ * Ask for writeable cb on all browser connections subscribed to a particular\n+ * task (so we can send them some more logs)\n+ */\n+\n+int\n+saiw_subs_request_writeable(struct vhd *vhd, const char *task_uuid)\n+{\n+\tlws_start_foreach_dll(struct lws_dll2 *, p,\n+\t\t\t vhd-\u003esubs_owner.head) {\n+\t\tstruct pss *pss \u003d lws_container_of(p, struct pss, subs_list);\n+\n+\t\tif (!strcmp(pss-\u003esub_task_uuid, task_uuid))\n+\t\t\tlws_callback_on_writable(pss-\u003ewsi);\n+\n+\t} lws_end_foreach_dll(p);\n+\n+\treturn 0;\n+}\n+\n+saiw_scheduled_t *\n+saiw_alloc_sched(struct pss *pss, ws_state action)\n+{\n+\tsaiw_scheduled_t *sch \u003d malloc(sizeof(*sch));\n+\n+\tif (sch) {\n+\t\tmemset(sch, 0, sizeof(*sch));\n+\t\tsch-\u003eaction \u003d action;\n+\t\tlws_dll2_add_tail(\u0026sch-\u003elist, \u0026pss-\u003esched);\n+\t\tlws_callback_on_writable(pss-\u003ewsi);\n+\t}\n+\n+\treturn sch;\n+}\n+\n+void\n+saiw_dealloc_sched(saiw_scheduled_t *sch)\n+{\n+\tif (!sch)\n+\t\treturn;\n+\n+\tlws_dll2_remove(\u0026sch-\u003elist);\n+\n+\tlwsac_free(\u0026sch-\u003eac);\n+\tlwsac_free(\u0026sch-\u003equery_ac);\n+\n+\tfree(sch);\n+}\n+\n+static int\n+saiw_pss_schedule_eventinfo(struct pss *pss, const char *event_uuid)\n+{\n+\tsaiw_scheduled_t *sch \u003d saiw_alloc_sched(pss, WSS_PREPARE_OVERVIEW);\n+\tchar qu[80], esc[66], esc2[96];\n+\tint n;\n+\n+\tif (!sch)\n+\t\treturn -1;\n+\n+\t/*\n+\t * This pss may be locked to a specific event\n+\t */\n+\n+\tif (pss-\u003especific_task[0] \u0026\u0026 memcmp(pss-\u003especific_task, event_uuid, 32))\n+\t\tgoto bail;\n+\n+\t/*\n+\t * This pss may be locked to a specific project, qualify the db lookup\n+\t * vs any project name specificity.\n+\t *\n+\t * Just collect the event struct into pss-\u003equery_owner to dump\n+\t */\n+\n+\tlws_sql_purify(esc, event_uuid, sizeof(esc));\n+\n+\tif (pss-\u003especific_project[0]) {\n+\t\tlws_sql_purify(esc2, pss-\u003especific_project, sizeof(esc2));\n+\t\tlws_snprintf(qu, sizeof(qu), \u0022 and uuid\u003d'%s' and repo_name\u003d'%s'\u0022, esc, esc2);\n+\t} else\n+\t\tlws_snprintf(qu, sizeof(qu), \u0022 and uuid\u003d'%s'\u0022, esc);\n+\tn \u003d lws_struct_sq3_deserialize(pss-\u003evhd-\u003epdb, qu, NULL,\n+\t\t\t\t lsm_schema_sq3_map_event,\n+\t\t\t\t \u0026sch-\u003eowner, \u0026sch-\u003eac, 0, 1);\n+\tif (n \u003c 0 || !sch-\u003eowner.head)\n+\t\tgoto bail;\n+\n+\tsch-\u003eov_db_done \u003d 1;\n+\tsaiw_alloc_sched(pss, WSS_PREPARE_BUILDER_SUMMARY);\n+\n+\treturn 0;\n+\n+bail:\n+\tsaiw_dealloc_sched(sch);\n+\treturn 1;\n+}\n+\n+static int\n+saiw_pss_schedule_taskinfo(struct pss *pss, const char *task_uuid, int logsub)\n+{\n+\tsaiw_scheduled_t *sch \u003d saiw_alloc_sched(pss, WSS_PREPARE_TASKINFO);\n+\tchar qu[192], esc[66], event_uuid[33], esc2[96];\n+\tsqlite3 *pdb \u003d NULL;\n+\tlws_dll2_owner_t o;\n+\tint n, m;\n+\n+\tif (!sch)\n+\t\treturn -1;\n+\n+\tsai_task_uuid_to_event_uuid(event_uuid, task_uuid);\n+\n+\t/*\n+\t * This pss may be locked to a specific event and not want to hear\n+\t * anything unrelated to that event... lock to task is same deal but\n+\t * we will also send it non-log info about other tasks, so it can\n+\t * keep its event summary alive\n+\t */\n+\n+\tif (pss-\u003especific_task[0] \u0026\u0026\n+\t memcmp(pss-\u003especific_task, event_uuid, 32)) {\n+\t\tlwsl_info(\u0022%s: specific_task '%s' vs event_uuid '%s\u005cn\u0022,\n+\t\t\t __func__, pss-\u003especific_task, event_uuid);\n+\t\tgoto bail;\n+\t}\n+\n+\t/* open the event-specific database object */\n+\n+\tif (sais_event_db_ensure_open(pss-\u003evhd, event_uuid, 0, \u0026pdb)) {\n+\t\t/* no longer exists, nothing to do */\n+\t\tsaiw_dealloc_sched(sch);\n+\t\treturn 0;\n+\t}\n+\n+\t/*\n+\t * get the related task object into its own ac... there might\n+\t * be a lot of related data, so we hold the ac in the sch for\n+\t * as long as needed to send it out\n+\t */\n+\n+\tlws_sql_purify(esc, task_uuid, sizeof(esc));\n+\tlws_snprintf(qu, sizeof(qu), \u0022 and uuid\u003d'%s'\u0022, esc);\n+\tn \u003d lws_struct_sq3_deserialize(pdb, qu, NULL, lsm_schema_sq3_map_task,\n+\t\t\t\t \u0026o, \u0026sch-\u003equery_ac, 0, 1);\n+\tsais_event_db_close(pss-\u003evhd, \u0026pdb);\n+\t// lwsl_notice(\u0022%s: n %d, o.head %p\u005cn\u0022, __func__, n, o.head);\n+\tif (n \u003c 0 || !o.head)\n+\t\tgoto bail;\n+\n+\tsch-\u003eone_task \u003d lws_container_of(o.head, sai_task_t, list);\n+\n+\tlwsl_info(\u0022%s: browser ws asked for task hash: %s, plat %s\u005cn\u0022,\n+\t\t __func__, task_uuid, sch-\u003eone_task-\u003eplatform);\n+\n+\t/* let the pss take over the task info ac and schedule sending */\n+\n+\tlws_dll2_remove((struct lws_dll2 *)\u0026sch-\u003eone_task-\u003elist);\n+\n+\t/*\n+\t * let's also get the event object the task relates to into\n+\t * its own event struct, additionally qualify this task against any\n+\t * pss reponame-specific constraint and bail if doesn't match\n+\t */\n+\n+\tlws_sql_purify(esc, event_uuid, sizeof(esc));\n+\tm \u003d lws_snprintf(qu, sizeof(qu), \u0022 and uuid\u003d'%s'\u0022, esc);\n+\tif (pss-\u003especific_project[0]) {\n+\t\tlws_sql_purify(esc2, pss-\u003especific_project, sizeof(esc2));\n+\t\tm +\u003d lws_snprintf(qu + m, sizeof(qu) - m, \u0022 and repo_name\u003d'%s'\u0022, esc2);\n+\t}\n+\n+\tif (pss-\u003especific_ref[0]) {\n+\t\tlws_sql_purify(esc2, pss-\u003especific_ref, sizeof(esc2));\n+\t\tif (pss-\u003especific_ref[0] \u003d\u003d 'r') {\n+\t\t\t/* check event ref against, eg, ref/heads/xxx */\n+\t\t\tif (!strcmp(pss-\u003especific_ref, \u0022refs/heads/master\u0022))\n+\t\t\t\tm +\u003d lws_snprintf(qu + m, sizeof(qu) - m,\n+\t\t\t\t\t\u0022 and (ref\u003d'refs/heads/master' or ref\u003d'refs/heads/main')\u0022);\n+\t\t\telse\n+\t\t\t\tm +\u003d lws_snprintf(qu + m, sizeof(qu) - m, \u0022 and ref\u003d'%s'\u0022, esc2);\n+\t\t} else\n+\t\t\t/* check event hash against, eg, 12341234abcd... */\n+\t\t\tm +\u003d lws_snprintf(qu + m, sizeof(qu) - m, \u0022 and hash\u003d'%s'\u0022, esc2);\n+\t}\n+\n+\tn \u003d lws_struct_sq3_deserialize(pss-\u003evhd-\u003epdb, qu, NULL,\n+\t\t\t\t lsm_schema_sq3_map_event, \u0026o,\n+\t\t\t\t \u0026sch-\u003equery_ac, 0, 1);\n+\tif (n \u003c 0 || !o.head) {\n+\t\tlwsl_notice(\u0022%s: no result\u005cn\u0022, __func__);\n+\t\tgoto bail;\n+\t}\n+\n+\t/* does he want to subscribe to logs? */\n+\tif (logsub) {\n+\t\tstrcpy(pss-\u003esub_task_uuid, sch-\u003eone_task-\u003euuid);\n+\t\tlws_dll2_add_head(\u0026pss-\u003esubs_list, \u0026pss-\u003evhd-\u003esubs_owner);\n+\t\tpss-\u003esub_timestamp \u003d 0; /* where we got up to */\n+\t\tlws_callback_on_writable(pss-\u003ewsi);\n+\n+\t\tlwsl_notice(\u0022%s: subscribed to logs for %s\u005cn\u0022, __func__,\n+\t\t\t pss-\u003esub_task_uuid);\n+\t}\n+\n+\tsch-\u003eone_event \u003d lws_container_of(o.head, sai_event_t, list);\n+\n+\tsaiw_alloc_sched(pss, WSS_PREPARE_BUILDER_SUMMARY);\n+\n+\treturn 0;\n+\n+bail:\n+\tsaiw_dealloc_sched(sch);\n+\treturn 1;\n+}\n+\n+/*\n+ * We need to schedule re-sending out task and event state to anyone subscribed\n+ * to the task that changed or its associated event\n+ */\n+\n+int\n+saiw_subs_task_state_change(struct vhd *vhd, const char *task_uuid)\n+{\n+\tlws_start_foreach_dll(struct lws_dll2 *, p,\n+\t\t\t vhd-\u003esubs_owner.head) {\n+\t\tstruct pss *pss \u003d lws_container_of(p, struct pss, subs_list);\n+\n+\t\tif (!strcmp(pss-\u003esub_task_uuid, task_uuid))\n+\t\t\tsaiw_pss_schedule_taskinfo(pss, task_uuid, 0);\n+\n+\t} lws_end_foreach_dll(p);\n+\n+\treturn 0;\n+}\n+\n+\n+int\n+saiw_browsers_task_state_change(struct vhd *vhd, const char *task_uuid)\n+{\n+\tlws_start_foreach_dll(struct lws_dll2 *, p, vhd-\u003ebrowsers.head) {\n+\t\tstruct pss *pss \u003d lws_container_of(p, struct pss, same);\n+\n+\t\tsaiw_pss_schedule_taskinfo(pss, task_uuid, 0);\n+\t} lws_end_foreach_dll(p);\n+\n+\treturn 0;\n+}\n+\n+\n+int\n+saiw_event_state_change(struct vhd *vhd, const char *event_uuid)\n+{\n+\tlws_start_foreach_dll(struct lws_dll2 *, p, vhd-\u003ebrowsers.head) {\n+\t\tstruct pss *pss \u003d lws_container_of(p, struct pss, same);\n+\n+\t\tsaiw_pss_schedule_eventinfo(pss, event_uuid);\n+\t} lws_end_foreach_dll(p);\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * browser has sent us a request for either overview, or data on a specific\n+ * task\n+ */\n+\n+int\n+saiw_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf,\n+\t\t\tsize_t bl)\n+{\n+\tsai_browse_rx_taskinfo_t *ti;\n+\tsai_browse_rx_evinfo_t *ei;\n+\tlws_struct_args_t a;\n+\tsai_cancel_t *can;\n+\tint m, ret \u003d -1;\n+\n+\tmemset(\u0026a, 0, sizeof(a));\n+\ta.map_st[0] \u003d lsm_schema_json_map_bwsrx;\n+\ta.map_entries_st[0] \u003d LWS_ARRAY_SIZE(lsm_schema_json_map_bwsrx);\n+\ta.map_entries_st[1] \u003d LWS_ARRAY_SIZE(lsm_schema_json_map_bwsrx);\n+\ta.ac_block_size \u003d 128;\n+\n+\tlws_struct_json_init_parse(\u0026pss-\u003ectx, NULL, \u0026a);\n+\tm \u003d lejp_parse(\u0026pss-\u003ectx, (uint8_t *)buf, bl);\n+\tif (m \u003c 0 || !a.dest) {\n+\t\tlwsl_hexdump_notice(buf, bl);\n+\t\tlwsl_notice(\u0022%s: browser-\u003eweb JSON decode failed '%s'\u005cn\u0022,\n+\t\t\t\t__func__, lejp_error_to_string(m));\n+\t\treturn m;\n+\t}\n+\n+\t/*\n+\t * Which object we ended up with depends on the schema that came in...\n+\t * a.top_schema_index is the index in lsm_schema_json_map_bwsrx it\n+\t * matched on\n+\t */\n+\n+\tswitch (a.top_schema_index) {\n+\n+\tcase SAIM_WS_BROWSER_RX_TASKINFO:\n+\t\tti \u003d (sai_browse_rx_taskinfo_t *)a.dest;\n+\n+\t\tlwsl_info(\u0022%s: schema index %d, task hash %s\u005cn\u0022, __func__,\n+\t\t\t\ta.top_schema_index, ti-\u003etask_hash);\n+\n+\t\tif (!ti-\u003etask_hash[0]) {\n+\t\t\t/*\n+\t\t\t * he's asking for the overview schema\n+\t\t\t */\n+\n+\t\t\tsaiw_alloc_sched(pss, WSS_PREPARE_OVERVIEW);\n+\t\t\tsaiw_alloc_sched(pss, WSS_PREPARE_BUILDER_SUMMARY);\n+\t\t\tret \u003d 0;\n+\t\t\tgoto bail;\n+\t\t}\n+\n+\t\t/*\n+\t\t * get the related task object into its own ac... there might\n+\t\t * be a lot of related data, so we hold the ac in the pss for\n+\t\t * as long as needed to send it out\n+\t\t */\n+\n+\t\tif (saiw_pss_schedule_taskinfo(pss, ti-\u003etask_hash, !!ti-\u003elogs))\n+\t\t\tgoto soft_error;\n+\n+\t\treturn 0;\n+\n+\tcase SAIM_WS_BROWSER_RX_EVENTINFO:\n+\n+\t\tei \u003d (sai_browse_rx_evinfo_t *)a.dest;\n+\n+\t\tif (saiw_pss_schedule_eventinfo(pss, ei-\u003eevent_hash))\n+\t\t\tgoto soft_error;\n+\n+\t\tlwsac_free(\u0026a.ac);\n+\n+\t\treturn 0;\n+\n+\tcase SAIM_WS_BROWSER_RX_TASKRESET:\n+\n+\t\tif (!sais_conn_auth(pss))\n+\t\t\tgoto soft_error;\n+\n+\t\t/*\n+\t\t * User is asking us to reset / rebuild this task\n+\t\t */\n+\n+\t\tei \u003d (sai_browse_rx_evinfo_t *)a.dest;\n+\n+\t\tsaiw_websrv_queue_tx(vhd-\u003eh_ss_websrv, buf, bl);\n+\t\tlwsac_free(\u0026a.ac);\n+\n+\t\treturn 0;\n+\n+\tcase SAIM_WS_BROWSER_RX_EVENTRESET:\n+\n+\t\tif (!sais_conn_auth(pss))\n+\t\t\tgoto soft_error;\n+\n+\t\t/*\n+\t\t * User is asking us to reset / rebuild every task in the event\n+\t\t */\n+\n+\t\tei \u003d (sai_browse_rx_evinfo_t *)a.dest;\n+\n+\t\tlwsl_notice(\u0022%s: received request to reset event %s\u005cn\u0022,\n+\t\t\t __func__, ei-\u003eevent_hash);\n+\n+\t\tsaiw_websrv_queue_tx(vhd-\u003eh_ss_websrv, buf, bl);\n+\t\tlwsac_free(\u0026a.ac);\n+\n+\t\treturn 0;\n+\n+\tcase SAIM_WS_BROWSER_RX_EVENTDELETE:\n+\t\t/*\n+\t\t * User is asking us to delete the whole event\n+\t\t */\n+\n+\t\tif (!sais_conn_auth(pss))\n+\t\t\tgoto soft_error;\n+\n+\t\tei \u003d (sai_browse_rx_evinfo_t *)a.dest;\n+\n+\t\tlwsl_notice(\u0022%s: received request to delete event %s\u005cn\u0022,\n+\t\t\t __func__, ei-\u003eevent_hash);\n+\n+\t\tsaiw_websrv_queue_tx(vhd-\u003eh_ss_websrv, buf, bl);\n+\t\tlwsac_free(\u0026a.ac);\n+\n+\t\tgoto bail;\n+\n+\tcase SAIM_WS_BROWSER_RX_TASKCANCEL:\n+\n+\t\tif (!sais_conn_auth(pss))\n+\t\t\tgoto soft_error;\n+\n+\t\t/*\n+\t\t * Browser is informing us of task's STOP button clicked, we\n+\t\t * need to inform any builder that might be building it\n+\t\t */\n+\t\tcan \u003d (sai_cancel_t *)a.dest;\n+\n+\t\tlwsl_notice(\u0022%s: received request to cancel task %s\u005cn\u0022,\n+\t\t\t __func__, can-\u003etask_uuid);\n+\n+\t\tsaiw_task_cancel(vhd, can-\u003etask_uuid);\n+\t\tbreak;\n+\n+\tdefault:\n+\t\tassert(0);\n+\t\tbreak;\n+\t}\n+\n+\tret \u003d 0;\n+\n+bail:\n+\tlwsac_free(\u0026a.ac);\n+\n+\treturn ret;\n+\n+soft_error:\n+\tlwsac_free(\u0026a.ac);\n+\n+\treturn 0;\n+}\n+\n+\n+/*\n+ * We're sending something on a browser ws connection. Returning nonzero from\n+ * here drops the connection, necessary if we fail partway through a message\n+ * but undesirable if a browser tab will keep reconnecting and asking for the\n+ * same, no-longer-existant thing.\n+ */\n+\n+int\n+saiw_ws_json_tx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t bl)\n+{\n+\tuint8_t *start \u003d buf + LWS_PRE, *p \u003d start, *end \u003d p + bl - LWS_PRE - 1;\n+\tint n, flags \u003d LWS_WRITE_TEXT, first \u003d 0, iu, endo;\n+\tchar esc[256], esc1[33], filt[128];\n+\tsai_browse_taskreply_t task_reply;\n+\tstruct lwsac *task_ac \u003d NULL;\n+\tlws_dll2_owner_t task_owner;\n+\tlws_struct_serialize_t *js;\n+\tsaiw_scheduled_t *sch;\n+\tchar event_uuid[33];\n+\tsqlite3 *pdb \u003d NULL;\n+\tsai_event_t *e;\n+\tsai_task_t *t;\n+\tchar any, lg;\n+\tsize_t w;\n+\n+again:\n+\n+\tstart \u003d buf + LWS_PRE;\n+\tp \u003d start;\n+\tend \u003d p + bl - LWS_PRE - 1;\n+\tflags \u003d LWS_WRITE_TEXT;\n+\tfirst \u003d 0;\n+\tlg \u003d 0;\n+\tendo \u003d 0;\n+\n+\t// lwsl_notice(\u0022%s: send_state %d, pss %p, wsi %p\u005cn\u0022, __func__,\n+\t// pss-\u003esend_state, pss, pss-\u003ewsi);\n+\n+\tif (pss-\u003esched.count)\n+\t\tsch \u003d lws_container_of(pss-\u003esched.head, saiw_scheduled_t, list);\n+\telse\n+\t\tsch \u003d NULL;\n+\n+\tswitch (pss-\u003esend_state) {\n+\tcase WSS_IDLE:\n+\n+\t\t/*\n+\t\t * Anything from a task log he's subscribed to?\n+\t\t *\n+\t\t * If so, let's prioritize that first...\n+\t\t */\n+\n+\t\tif ((!pss-\u003esched.count || !pss-\u003etoggle_favour_sch) \u0026\u0026\n+\t\t\t\tpss-\u003esubs_list.owner) {\n+\n+\t\t\tsch \u003d NULL;\n+\n+\t\t\tlws_snprintf(esc, sizeof(esc),\n+\t\t\t\t \u0022and task_uuid\u003d'%s' and timestamp \u003e %llu\u0022,\n+\t\t\t\t pss-\u003esub_task_uuid,\n+\t\t\t\t (unsigned long long)pss-\u003esub_timestamp);\n+\n+\t\t\t/*\n+\t\t\t * For efficiency, let's try to grab the next 100 at\n+\t\t\t * once from sqlite and work our way through sending\n+\t\t\t * them\n+\t\t\t */\n+\n+\t\t\tif (pss-\u003elog_cache_index \u003d\u003d pss-\u003elog_cache_size) {\n+\t\t\t\tint sr;\n+\n+\t\t\t\tsai_task_uuid_to_event_uuid(event_uuid,\n+\t\t\t\t\t\t\t pss-\u003esub_task_uuid);\n+\n+\t\t\t\tlws_dll2_owner_clear(\u0026task_owner);\n+\t\t\t\tlwsac_free(\u0026pss-\u003elogs_ac);\n+\n+\t\t\t\tlwsl_info(\u0022%s: collecting logs %s\u005cn\u0022,\n+\t\t\t\t\t __func__, esc);\n+\n+\t\t\t\tif (sais_event_db_ensure_open(vhd, event_uuid, 0,\n+\t\t\t\t\t\t\t \u0026pdb)) {\n+\t\t\t\t\tlwsl_notice(\u0022%s: unable to open event-specific database\u005cn\u0022,\n+\t\t\t\t\t\t\t__func__);\n+\n+\t\t\t\t\treturn 0;\n+\t\t\t\t}\n+\n+\t\t\t\tsr \u003d lws_struct_sq3_deserialize(pdb, esc,\n+\t\t\t\t\t\t\t\t\u0022uid,timestamp \u0022,\n+\t\t\t\t\t\t\t\tlsm_schema_sq3_map_log,\n+\t\t\t\t\t\t\t\t\u0026pss-\u003elogs_owner,\n+\t\t\t\t\t\t\t\t\u0026pss-\u003elogs_ac, 0, 100);\n+\n+\t\t\t\tsais_event_db_close(vhd, \u0026pdb);\n+\n+\t\t\t\tif (sr) {\n+\n+\t\t\t\t\tlwsl_err(\u0022%s: subs failed\u005cn\u0022, __func__);\n+\n+\t\t\t\t\treturn 0;\n+\t\t\t\t}\n+\n+\t\t\t\tpss-\u003elog_cache_index \u003d 0;\n+\t\t\t\tpss-\u003elog_cache_size \u003d pss-\u003elogs_owner.count;\n+\t\t\t}\n+\n+\t\t\tif (pss-\u003elog_cache_index \u003c pss-\u003elog_cache_size) {\n+\t\t\t\tsai_log_t *log \u003d lws_container_of(\n+\t\t\t\t\t\tpss-\u003elogs_owner.head,\n+\t\t\t\t\t\tsai_log_t, list);\n+\n+\t\t\t\tlws_dll2_remove(\u0026log-\u003elist);\n+\t\t\t\tpss-\u003elog_cache_index++;\n+\n+\t\t\t\t/*\n+\t\t\t\t * Turn it back into JSON so we can give it to\n+\t\t\t\t * the browser\n+\t\t\t\t */\n+\n+\t\t\t\tjs \u003d lws_struct_json_serialize_create(\n+\t\t\t\t\tlsm_schema_json_map_log, 1, 0, log);\n+\t\t\t\tif (!js) {\n+\t\t\t\t\tlwsl_notice(\u0022%s: json ser fail\u005cn\u0022, __func__);\n+\t\t\t\t\treturn 0;\n+\t\t\t\t}\n+\n+\t\t\t\tn \u003d lws_struct_json_serialize(js, p, end - p, \u0026w);\n+\t\t\t\tlws_struct_json_serialize_destroy(\u0026js);\n+\t\t\t\tif (n \u003d\u003d LSJS_RESULT_ERROR) {\n+\t\t\t\t\tlwsl_notice(\u0022%s: json ser error\u005cn\u0022, __func__);\n+\t\t\t\t\treturn 0;\n+\t\t\t\t}\n+\n+\t\t\t\tp +\u003d w;\n+\t\t\t\tfirst \u003d 1;\n+\t\t\t\tlg \u003d 1;\n+\t\t\t\tpss-\u003etoggle_favour_sch \u003d 1;\n+\n+\t\t\t\t/*\n+\t\t\t\t * Record that this was the most recent log we\n+\t\t\t\t * saw so far\n+\t\t\t\t */\n+\t\t\t\tpss-\u003esub_timestamp \u003d log-\u003etimestamp;\n+\t\t\t\tgoto send_it;\n+\t\t\t}\n+\t\t}\n+\n+\t\t/*\n+\t\t * ...then do we have anything on the scheduled ll for this pss?\n+\t\t */\n+\n+\t\tif (!sch)\n+\t\t\t/* ... nope... */\n+\t\t\treturn 0;\n+\n+\t\tpss-\u003etoggle_favour_sch \u003d 0;\n+\t\tpss-\u003esend_state \u003d sch-\u003eaction;\n+\t\tgoto again;\n+\n+\tcase WSS_PREPARE_OVERVIEW:\n+\n+\t\tfilt[0] \u003d '\u005c0';\n+\t\tif (pss-\u003especific_project[0]) {\n+\t\t\tlws_sql_purify(esc, pss-\u003especific_project, sizeof(esc) - 1);\n+\t\t\tlws_snprintf(filt, sizeof(filt), \u0022 and repo_name\u003d\u005c\u0022%s\u005c\u0022\u0022, esc);\n+\t\t}\n+\t\tpss-\u003ewants_event_updates \u003d 1;\n+\t\tif (!sch-\u003eov_db_done \u0026\u0026 lws_struct_sq3_deserialize(vhd-\u003epdb,\n+\t\t\t\t filt[0] ? filt : NULL, \u0022created \u0022,\n+\t\t\t\tlsm_schema_sq3_map_event, \u0026sch-\u003eowner,\n+\t\t\t\t\u0026sch-\u003eac, 0, -8)) {\n+\t\t\tlwsl_notice(\u0022%s: OVERVIEW 2 failed\u005cn\u0022, __func__);\n+\n+\t\t\tpss-\u003esend_state \u003d WSS_IDLE;\n+\t\t\tsaiw_dealloc_sched(sch);\n+\n+\t\t\treturn 0;\n+\t\t}\n+\n+\t\t/*\n+\t\t * we get zero or more sai_event_t laid out in pss-\u003equery_ac,\n+\t\t * and listed in pss-\u003equery_owner\n+\t\t */\n+\n+\t\tlwsl_debug(\u0022%s: WSS_PREPARE_OVERVIEW: %d results %p\u005cn\u0022,\n+\t\t\t __func__, sch-\u003eowner.count, sch-\u003eac);\n+\n+\t\tp +\u003d lws_snprintf((char *)p, end - p,\n+\t\t\t\u0022{\u005c\u0022schema\u005c\u0022:\u005c\u0022sai.warmcat.com.overview\u005c\u0022,\u0022\n+\t\t\t\u0022 \u005c\u0022alang\u005c\u0022:\u005c\u0022%s\u005c\u0022,\u0022\n+\t\t\t\u0022 \u005c\u0022authorized\u005c\u0022: %d,\u0022\n+\t\t\t\u0022 \u005c\u0022auth_secs\u005c\u0022: %ld,\u0022\n+\t\t\t\u0022 \u005c\u0022auth_user\u005c\u0022: \u005c\u0022%s\u005c\u0022,\u0022\n+\t\t\t\u0022\u005c\u0022overview\u005c\u0022:[\u0022,\n+\t\t\tlws_json_purify(esc, pss-\u003ealang, sizeof(esc) - 1, \u0026iu),\n+\t\t\tpss-\u003eauthorized, pss-\u003eauthorized ? pss-\u003eexpiry_unix_time - lws_now_secs() : 0,\n+\t\t\tlws_json_purify(esc1, pss-\u003eauth_user, sizeof(esc1) - 1, \u0026iu)\n+\t\t\t);\n+\n+\t\t/*\n+\t\t * \u0022authorized\u0022 here is used to decide whether to render the\n+\t\t * additional controls clientside. The events the controls\n+\t\t * cause if used are separately checked for coming from an\n+\t\t * authorized pss when they are received.\n+\t\t */\n+\n+\t\tif (pss-\u003especificity)\n+\t\t\tsch-\u003ewalk \u003d lws_dll2_get_head(\u0026sch-\u003eowner);\n+\t\telse\n+\t\t\tsch-\u003ewalk \u003d lws_dll2_get_tail(\u0026sch-\u003eowner);\n+\t\tsch-\u003esubsequent \u003d 0;\n+\t\tfirst \u003d 1;\n+\n+\t\tpss-\u003esend_state \u003d WSS_SEND_OVERVIEW;\n+\n+\t\tif (!sch-\u003eowner.count)\n+\t\t\tgoto so_finish;\n+\n+\t\t/* fallthru */\n+\n+\tcase WSS_SEND_OVERVIEW:\n+\n+\t\tif (sch-\u003eovstate \u003d\u003d SOS_TASKS)\n+\t\t\tgoto enum_tasks;\n+\n+\t\tany \u003d 0;\n+\t\twhile (end - p \u003e 2048 \u0026\u0026 sch-\u003ewalk \u0026\u0026\n+\t\t pss-\u003esend_state \u003d\u003d WSS_SEND_OVERVIEW) {\n+\n+\t\t\te \u003d lws_container_of(sch-\u003ewalk, sai_event_t, list);\n+\n+\t\t\tif (pss-\u003especificity) {\n+\t\t\t\tlwsl_debug(\u0022%s: Specificity: e-\u003ehash: %s, \u0022\n+\t\t\t\t\t \u0022e-\u003eref: '%s', pss-\u003especific_ref: '%s'\u005cn\u0022,\n+\t\t\t\t\t __func__, e-\u003ehash, e-\u003eref,\n+\t\t\t\t\t pss-\u003especific_ref);\n+\n+\t\t\t\tif (!strcmp(pss-\u003especific_ref, \u0022refs/heads/master\u0022) \u0026\u0026\n+\t\t\t\t !strcmp(e-\u003eref, \u0022refs/heads/main\u0022)) {\n+\t\t\t\t\t// lwsl_notice(\u0022master-\u003emain\u005cn\u0022);\n+\t\t\t\t\tany \u003d 1;\n+\t\t\t\t} else {\n+\n+\t\t\t\t\tif (strcmp(e-\u003ehash, pss-\u003especific_ref) \u0026\u0026\n+\t\t\t\t\t strcmp(e-\u003eref, pss-\u003especific_ref)) {\n+\t\t\t\t\t\tsch-\u003ewalk \u003d sch-\u003ewalk-\u003enext;\n+\t\t\t\t\t\tcontinue;\n+\t\t\t\t\t}\n+\t\t\t\t\t//lwsl_notice(\u0022%s: match\u005cn\u0022, __func__);\n+\t\t\t\t\tany \u003d 1;\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\tjs \u003d lws_struct_json_serialize_create(\n+\t\t\t\tlsm_schema_json_map_event,\n+\t\t\t\tLWS_ARRAY_SIZE(lsm_schema_json_map_event), 0, e);\n+\t\t\tif (!js) {\n+\t\t\t\tlwsl_err(\u0022%s: json ser fail\u005cn\u0022, __func__);\n+\t\t\t\treturn 1;\n+\t\t\t}\n+\t\t\tif (sch-\u003esubsequent)\n+\t\t\t\t*p++ \u003d ',';\n+\t\t\tsch-\u003esubsequent \u003d 1;\n+\n+\t\t\tp +\u003d lws_snprintf((char *)p, end - p, \u0022{\u005c\u0022e\u005c\u0022:\u0022);\n+\n+\t\t\tn \u003d lws_struct_json_serialize(js, p, end - p, \u0026w);\n+\t\t\tlws_struct_json_serialize_destroy(\u0026js);\n+\t\t\tswitch (n) {\n+\t\t\tcase LSJS_RESULT_ERROR:\n+\t\t\t\tpss-\u003esend_state \u003d WSS_IDLE;\n+\t\t\t\tsaiw_dealloc_sched(sch);\n+\t\t\t\tlwsl_err(\u0022%s: json ser error\u005cn\u0022, __func__);\n+\t\t\t\treturn 1;\n+\n+\t\t\tcase LSJS_RESULT_FINISH:\n+\t\t\tcase LSJS_RESULT_CONTINUE:\n+\t\t\t\tp +\u003d w;\n+\t\t\t\tsch-\u003eovstate \u003d SOS_TASKS;\n+\t\t\t\tsch-\u003etask_index \u003d 0;\n+\t\t\t\tp +\u003d lws_snprintf((char *)p, end - p, \u0022, \u005c\u0022t\u005c\u0022:[\u0022);\n+\t\t\t\tgoto enum_tasks;\n+\t\t\t}\n+\t\t}\n+\t\tif (!any) {\n+\t\t\tpss-\u003esend_state \u003d WSS_IDLE;\n+\t\t\tsaiw_dealloc_sched(sch);\n+\n+\t\t\treturn 0;\n+\t\t}\n+\t\tbreak;\n+\n+enum_tasks:\n+\t\t/*\n+\t\t * Enumerate the tasks associated with this event... we will\n+\t\t * come back here as often as needed to dump all the tasks\n+\t\t */\n+\n+\t\te \u003d lws_container_of(sch-\u003ewalk, sai_event_t, list);\n+\n+\t\tdo {\n+\t\t\ttask_ac \u003d NULL;\n+\n+\t\t\tif (sais_event_db_ensure_open(vhd, e-\u003euuid, 0, \u0026pdb)) {\n+\t\t\t\tlwsl_err(\u0022%s: unable to open event-specific database\u005cn\u0022,\n+\t\t\t\t\t\t__func__);\n+\n+\t\t\t\tbreak;\n+\t\t\t}\n+\n+\t\t\tlws_dll2_owner_clear(\u0026task_owner);\n+\t\t\tif (lws_struct_sq3_deserialize(pdb, NULL, NULL,\n+\t\t\t\t\tlsm_schema_sq3_map_task, \u0026task_owner,\n+\t\t\t\t\t\u0026task_ac, sch-\u003etask_index, 1)) {\n+\t\t\t\tlwsl_err(\u0022%s: OVERVIEW 1 failed\u005cn\u0022, __func__);\n+\t\t\t\tsais_event_db_close(vhd, \u0026pdb);\n+\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tsais_event_db_close(vhd, \u0026pdb);\n+\n+\t\t\tif (!task_owner.count)\n+\t\t\t\tbreak;\n+\n+\t\t\tif (sch-\u003etask_index)\n+\t\t\t\t*p++ \u003d ',';\n+\n+\t\t\t/*\n+\t\t\t * We don't want to send everyone the artifact nonces...\n+\t\t\t * the up nonce is a key for uploading artifacts on to\n+\t\t\t * this task, it should only be stored in the server db\n+\t\t\t * and sent to the builder to use.\n+\t\t\t *\n+\t\t\t * The down nonce is used in generated links, but still\n+\t\t\t * you should have to acquire such a link via whatever\n+\t\t\t * auth rather than be able to cook them up yourself\n+\t\t\t * from knowing the task uuid.\n+\t\t\t */\n+\n+\t\t\tt \u003d (sai_task_t *)task_owner.head;\n+\t\t\tt-\u003eart_up_nonce[0] \u003d '\u005c0';\n+\t\t\tt-\u003eart_down_nonce[0] \u003d '\u005c0';\n+\n+\t\t\t/* only one in it at a time */\n+\t\t\tt \u003d lws_container_of(task_owner.head, sai_task_t, list);\n+\n+\t\t\tjs \u003d lws_struct_json_serialize_create(\n+\t\t\t\tlsm_schema_json_map_task,\n+\t\t\t\tLWS_ARRAY_SIZE(lsm_schema_json_map_task), 0, t);\n+\n+\t\t\tn \u003d lws_struct_json_serialize(js, p, end - p, \u0026w);\n+\t\t\tlws_struct_json_serialize_destroy(\u0026js);\n+\t\t\tlwsac_free(\u0026task_ac);\n+\t\t\tp +\u003d w;\n+\n+\t\t\tsch-\u003etask_index++;\n+\t\t} while ((end - p \u003e 2048) \u0026\u0026 task_owner.count);\n+\n+\t\tif (task_owner.count)\n+\t\t\t/* may be more left to do */\n+\t\t\tbreak;\n+\n+\t\t/* none left to do, go back up a level */\n+\n+\t\tp +\u003d lws_snprintf((char *)p, end - p, \u0022]}\u0022);\n+\n+\t\tsch-\u003eovstate \u003d SOS_EVENT;\n+\t\tif (pss-\u003especificity)\n+\t\t\tsch-\u003ewalk \u003d sch-\u003ewalk-\u003enext;\n+\t\telse\n+\t\t\tsch-\u003ewalk \u003d sch-\u003ewalk-\u003eprev;\n+\t\tif (!sch-\u003ewalk || pss-\u003especificity) {\n+\t\t\twhile (sch-\u003ewalk)\n+\t\t\t\tsch-\u003ewalk \u003d sch-\u003ewalk-\u003enext;\n+\t\t\tgoto so_finish;\n+\t\t}\n+\t\tbreak;\n+\n+so_finish:\n+\t\tp +\u003d lws_snprintf((char *)p, end - p, \u0022]}\u0022);\n+\t\tpss-\u003esend_state \u003d WSS_IDLE;\n+\t\tendo \u003d 1;\n+\t\tbreak;\n+\n+\tcase WSS_PREPARE_BUILDER_SUMMARY:\n+\t\tp +\u003d lws_snprintf((char *)p, end - p,\n+\t\t\t\u0022{\u005c\u0022schema\u005c\u0022:\u005c\u0022com.warmcat.sai.builders\u005c\u0022,\u0022\n+\t\t\t\u0022 \u005c\u0022alang\u005c\u0022:\u005c\u0022%s\u005c\u0022,\u0022\n+\t\t\t\u0022 \u005c\u0022authorized\u005c\u0022:%d,\u0022\n+\t\t\t\u0022 \u005c\u0022auth_secs\u005c\u0022:%ld,\u0022\n+\t\t\t\u0022 \u005c\u0022auth_user\u005c\u0022: \u005c\u0022%s\u005c\u0022,\u0022\n+\t\t\t\u0022 \u005c\u0022builders\u005c\u0022:[\u0022,\n+\t\t\tlws_sql_purify(esc, pss-\u003ealang, sizeof(esc) - 1),\n+\t\t\tpss-\u003eauthorized, pss-\u003eauthorized ? pss-\u003eexpiry_unix_time - lws_now_secs() : 0,\n+\t\t\tlws_json_purify(esc1, pss-\u003eauth_user, sizeof(esc1) - 1, \u0026iu));\n+\n+\t\tif (vhd \u0026\u0026 vhd-\u003ebuilders) {\n+\t\t\tlwsac_reference(vhd-\u003ebuilders);\n+\t\t\tsch-\u003ewalk \u003d lws_dll2_get_head(vhd-\u003ebuilders_owner);\n+\t\t}\n+\t\tsch-\u003esubsequent \u003d 0;\n+\t\tpss-\u003esend_state \u003d WSS_SEND_BUILDER_SUMMARY;\n+\t\tfirst \u003d 1;\n+\n+\t\t/* fallthru */\n+\n+\tcase WSS_SEND_BUILDER_SUMMARY:\n+\t\tif (!sch-\u003ewalk)\n+\t\t\tgoto b_finish;\n+\n+\t\t/*\n+\t\t * We're going to send the browser some JSON about all the\n+\t\t * builders / platforms we feel are connected to us\n+\t\t */\n+\n+\t\twhile (end - p \u003e 512 \u0026\u0026 sch-\u003ewalk \u0026\u0026\n+\t\t pss-\u003esend_state \u003d\u003d WSS_SEND_BUILDER_SUMMARY) {\n+\n+\t\t\tsai_plat_t *b \u003d lws_container_of(sch-\u003ewalk, sai_plat_t,\n+\t\t\t\t\t\t sai_plat_list);\n+\n+\t\t\tjs \u003d lws_struct_json_serialize_create(\n+\t\t\t\tlsm_schema_map_plat_simple,\n+\t\t\t\tLWS_ARRAY_SIZE(lsm_schema_map_plat_simple),\n+\t\t\t\t0, b);\n+\t\t\tif (!js) {\n+\t\t\t\tlwsl_err(\u0022a\u005cn\u0022);\n+\t\t\t\tlwsac_unreference(\u0026vhd-\u003ebuilders);\n+\t\t\t\treturn 1;\n+\t\t\t}\n+\t\t\tif (sch-\u003esubsequent)\n+\t\t\t\t*p++ \u003d ',';\n+\t\t\tsch-\u003esubsequent \u003d 1;\n+\n+\t\t\tswitch (lws_struct_json_serialize(js, p, end - p, \u0026w)) {\n+\t\t\tcase LSJS_RESULT_ERROR:\n+\t\t\t\tlws_struct_json_serialize_destroy(\u0026js);\n+\t\t\t\tpss-\u003esend_state \u003d WSS_IDLE;\n+\t\t\t\tsaiw_dealloc_sched(sch);\n+\t\t\t\treturn 1;\n+\t\t\tcase LSJS_RESULT_FINISH:\n+\t\t\t\tp +\u003d w;\n+\t\t\t\tlws_struct_json_serialize_destroy(\u0026js);\n+\t\t\t\tsch-\u003ewalk \u003d sch-\u003ewalk-\u003enext;\n+\t\t\t\tif (!sch-\u003ewalk)\n+\t\t\t\t\tgoto b_finish;\n+\t\t\t\tbreak;\n+\n+\t\t\tcase LSJS_RESULT_CONTINUE:\n+\t\t\t\tp +\u003d w;\n+\t\t\t\tlws_struct_json_serialize_destroy(\u0026js);\n+\t\t\t\tsch-\u003ewalk \u003d sch-\u003ewalk-\u003enext;\n+\t\t\t\tif (!sch-\u003ewalk)\n+\t\t\t\t\tgoto b_finish;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t\tbreak;\n+b_finish:\n+\t\tp +\u003d lws_snprintf((char *)p, end - p, \u0022]}\u0022);\n+\t\tlwsac_unreference(\u0026vhd-\u003ebuilders);\n+\t\tendo \u003d 1;\n+\t\tbreak;\n+\n+\n+\tcase WSS_PREPARE_TASKINFO:\n+\t\t/*\n+\t\t * We're sending a browser the specific task info that he\n+\t\t * asked for.\n+\t\t *\n+\t\t * We already got the task struct out of the db in .one_task\n+\t\t * (all in .query_ac)\n+\t\t */\n+\n+\t\ttask_reply.event \u003d sch-\u003eone_event;\n+\t\ttask_reply.task \u003d sch-\u003eone_task;\n+\t\ttask_reply.auth_secs \u003d pss-\u003eauthorized ? pss-\u003eexpiry_unix_time - lws_now_secs() : 0;\n+\t\ttask_reply.authorized \u003d pss-\u003eauthorized;\n+\t\tlws_strncpy(task_reply.auth_user, pss-\u003eauth_user,\n+\t\t\t sizeof(task_reply.auth_user));\n+\n+\t\tjs \u003d lws_struct_json_serialize_create(lsm_schema_json_map_taskreply,\n+\t\t\t\tLWS_ARRAY_SIZE(lsm_schema_json_map_taskreply),\n+\t\t\t\t0, \u0026task_reply);\n+\t\tif (!js) {\n+\t\t\tlwsl_warn(\u0022%s: couldn't create\u005cn\u0022, __func__);\n+\t\t\treturn 1;\n+\t\t}\n+\n+\t\tn \u003d lws_struct_json_serialize(js, p, end - p, \u0026w);\n+\t\tlws_struct_json_serialize_destroy(\u0026js);\n+\n+\t\t/*\n+\t\t * Let's also try to fetch any artifacts into pss-\u003eaft_owner...\n+\t\t * no db or no artifacts can also be a normal situation...\n+\t\t */\n+\n+\t\tif (sch-\u003eone_task) {\n+\n+\t\t\tsai_task_uuid_to_event_uuid(event_uuid,\n+\t\t\t\t\t\t sch-\u003eone_task-\u003euuid);\n+\n+\t\t\tlwsl_debug(\u0022%s: ---------------- event uuid '%s'\u005cn\u0022,\n+\t\t\t\t\t__func__, event_uuid);\n+\n+\t\t\tlws_dll2_owner_clear(\u0026sch-\u003eowner);\n+\t\t\tif (!sais_event_db_ensure_open(vhd, event_uuid, 0, \u0026pdb)) {\n+\n+\t\t\t\tlws_snprintf(filt, sizeof(filt), \u0022 and (task_uuid \u003d\u003d '%s')\u0022,\n+\t\t\t\t\t sch-\u003eone_task-\u003euuid);\n+\n+\t\t\t\tlwsl_debug(\u0022%s: ---------------- %s\u005cn\u0022, __func__, filt);\n+\n+\t\t\t\tif (lws_struct_sq3_deserialize(pdb, filt, NULL,\n+\t\t\t\t\t\t\tlsm_schema_sq3_map_artifact,\n+\t\t\t\t\t\t\t\u0026sch-\u003eowner,\n+\t\t\t\t\t\t\t\u0026sch-\u003eac, 0, 10)) {\n+\t\t\t\t\tlwsl_err(\u0022%s: get afcts failed\u005cn\u0022, __func__);\n+\t\t\t\t}\n+\t\t\t\tsais_event_db_close(vhd, \u0026pdb);\n+\t\t\t}\n+\t\t}\n+\n+\t\tfirst \u003d 1;\n+\t\tsch-\u003ewalk \u003d NULL;\n+\t\tpss-\u003esend_state \u003d WSS_SEND_ARTIFACT_INFO;\n+\t\tif (!sch-\u003eowner.head) {\n+\t\t\tlwsl_debug(\u0022%s: ---------------- no artifacts\u005cn\u0022, __func__);\n+\t\t\t/* there's no artifact stuff to do */\n+\t\t\tendo \u003d 1;\n+\t\t}\n+\t\tsch-\u003eone_task \u003d NULL;\n+\t\tif (n \u003d\u003d LSJS_RESULT_ERROR) {\n+\t\t\tlwsl_notice(\u0022%s: taskinfo: error generating json\u005cn\u0022, __func__);\n+\t\t\treturn 1;\n+\t\t}\n+\t\tp +\u003d w;\n+\t\tif (!lws_ptr_diff(p, start)) {\n+\t\t\tlwsl_notice(\u0022%s: taskinfo: empty json\u005cn\u0022, __func__);\n+\t\t\treturn 0;\n+\t\t}\n+\t\tbreak;\n+\n+\tcase WSS_SEND_ARTIFACT_INFO:\n+\t\tif (sch-\u003eowner.head) {\n+\t\t\tsai_artifact_t *aft \u003d (sai_artifact_t *)sch-\u003eowner.head;\n+\n+\t\t\tlws_dll2_remove(\u0026aft-\u003elist);\n+\n+\t\t\t/* we don't want to disclose this to browsers */\n+\t\t\taft-\u003eartifact_up_nonce[0] \u003d '\u005c0';\n+\n+\t\t\tjs \u003d lws_struct_json_serialize_create(lsm_schema_json_map_artifact,\n+\t\t\t\t\tLWS_ARRAY_SIZE(lsm_schema_json_map_artifact),\n+\t\t\t\t\t0, aft);\n+\t\t\tif (!js) {\n+\t\t\t\tlwsl_err(\u0022%s ----------------- failed to render artifact json\u005cn\u0022, __func__);\n+\t\t\t\treturn 1;\n+\t\t\t}\n+\n+\t\t\tn \u003d lws_struct_json_serialize(js, p, end - p, \u0026w);\n+\t\t\tlws_struct_json_serialize_destroy(\u0026js);\n+\t\t\tif (n \u003d\u003d LSJS_RESULT_ERROR) {\n+\t\t\t\tlwsl_notice(\u0022%s: taskinfo: ---------- error generating json\u005cn\u0022, __func__);\n+\t\t\t\treturn 1;\n+\t\t\t}\n+\t\t\tfirst \u003d 1;\n+\t\t\tp +\u003d w;\n+\t\t\tlwsl_warn(\u0022%s: --------------------- %.*s\u005cn\u0022, __func__, (int)w, start);\n+\t\t}\n+\n+\t\tif (!sch-\u003eowner.head)\n+\t\t\tendo \u003d 1;\n+\t\tbreak;\n+\n+\tdefault:\n+\t\tlwsl_err(\u0022%s: pss state %d\u005cn\u0022, __func__, pss-\u003esend_state);\n+\t\treturn 0;\n+\t}\n+\n+send_it:\n+\tflags \u003d lws_write_ws_flags(LWS_WRITE_TEXT, first, endo || lg || !sch-\u003ewalk);\n+\n+\tif (lg || !sch-\u003ewalk || endo) {\n+\t\tpss-\u003esend_state \u003d WSS_IDLE;\n+\t\tsaiw_dealloc_sched(sch);\n+\t}\n+\n+\tif (lws_write(pss-\u003ewsi, start, p - start, flags) \u003c 0)\n+\t\treturn -1;\n+\n+\t/*\n+\t * We get a bad ratio of reads to write when the builder spams us\n+\t * with rx... we have to try to clear as much as we can in one go.\n+\t */\n+\n+//\tif (!lws_send_pipe_choked(pss-\u003ewsi))\n+//\t\tgoto again;\n+\n+\tlws_callback_on_writable(pss-\u003ewsi);\n+\n+\treturn 0;\n+}\n","s":{"c":1749527399,"u": 31238}} ],"g": 218916,"chitpc": 0,"ehitpc": 0,"indexed":0 , "ab": 0, "si": 0, "db":0, "di":0, "sat":0, "lfc": "0000"}