diff --git a/CMakeLists.txt b/CMakeLists.txt
index fe0b18f..dbd61e5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -155,6 +155,8 @@ if (requirements)
if (SAI_BUILDER)
add_subdirectory(src/builder)
add_subdirectory(src/resource)
+ add_subdirectory(src/power)
+
if (NOT MSVC AND NOT WIN32 AND NOT (${CMAKE_SYSTEM_NAME} MATCHES "SunOS"))
add_subdirectory(src/device)
add_subdirectory(src/expect)
diff --git a/src/builder/b-conf.c b/src/builder/b-conf.c
index ef87972..a15b864 100644
--- a/src/builder/b-conf.c
+++ b/src/builder/b-conf.c
@@ -36,7 +36,12 @@ static const char * const paths_global[] = {
"metrics_uri",
"metrics_path",
"metrics_secret",
- "power-off"
+ "sai-power",
+ "power-on.type",
+ "power-on.url",
+ "power-on.mac",
+ "power-off.type",
+ "power-off.url"
};
enum enum_paths_global {
@@ -46,7 +51,12 @@ enum enum_paths_global {
LEJPM_METRICS_URI,
LEJPM_METRICS_PATH,
LEJPM_METRICS_SECRET,
- LEJPM_POWER_OFF,
+ LEJPM_SAI_POWER,
+ LEJPM_POWER_ON_TYPE,
+ LEJPM_POWER_ON_URL,
+ LEJPM_POWER_ON_MAC,
+ LEJPM_POWER_OFF_TYPE,
+ LEJPM_POWER_OFF_URL
};
/* platform-related part */
@@ -287,8 +297,20 @@ saib_conf_global_cb(struct lejp_ctx *ctx, char reason)
pp = &a->builder->metrics_secret;
break;
- case LEJPM_POWER_OFF:
- pp = &a->builder->power_off;
+ case LEJPM_POWER_ON_TYPE:
+ pp = &a->builder->power_on_type;
+ break;
+ case LEJPM_POWER_ON_URL:
+ pp = &a->builder->power_on_url;
+ break;
+ case LEJPM_POWER_ON_MAC:
+ pp = &a->builder->power_on_mac;
+ break;
+ case LEJPM_POWER_OFF_TYPE:
+ pp = &a->builder->power_off_type;
+ break;
+ case LEJPM_POWER_OFF_URL:
+ pp = &a->builder->power_off_url;
break;
default:
diff --git a/src/builder/b-private.h b/src/builder/b-private.h
index c481c15..f2fee9e 100644
--- a/src/builder/b-private.h
+++ b/src/builder/b-private.h
@@ -170,7 +170,12 @@ struct sai_builder {
const char *metrics_path;
const char *metrics_secret;
- const char *power_off;
+
+ const char *power_off_type;
+ const char *power_off_url;
+ const char *power_on_type;
+ const char *power_on_url;
+ const char *power_on_mac;
const char *home; /* home dir, usually /sai/home */
const char *perms; /* user:group */
diff --git a/src/builder/b-sai.c b/src/builder/b-sai.c
index 1287bfe..8da2cc4 100644
--- a/src/builder/b-sai.c
+++ b/src/builder/b-sai.c
@@ -28,6 +28,8 @@
#include <libwebsockets.h>
#include <string.h>
#include <signal.h>
+#include <limits.h>
+#include <stdlib.h>
#if defined(__linux__)
#include <unistd.h>
@@ -581,6 +583,13 @@ int main(int argc, const char **argv)
"Copyright (C) 2019-2020 Andy Green <andy@warmcat.com>\n");
lwsl_user(" sai-builder [-c <config-file>]\n");
+ lwsl_notice("%s: sai-power: %s %s %s %s %s\n",
+ __func__, builder.power_on_type,
+ builder.power_on_url,
+ builder.power_on_mac,
+ builder.power_off_type,
+ builder.power_off_url);
+
memset(&info, 0, sizeof info);
info.port = CONTEXT_PORT_NO_LISTEN;
info.pprotocols = pprotocols;
@@ -631,9 +640,12 @@ int main(int argc, const char **argv)
goto bail;
}
- if (!strcmp(builder.power_off, "suspend")) {
+ if (!strcmp(builder.power_off_type, "suspend")) {
struct lws_spawn_piped_info info;
- const char * const ea[] = { argv[0], "-s", NULL };
+ char rpath[PATH_MAX];
+ const char * const ea[] = { rpath, "-s", NULL };
+
+ realpath(argv[0], rpath);
memset(&info, 0, sizeof(info));
memset(&builder.suspend_nspawn, 0, sizeof(builder.suspend_nspawn));
@@ -683,7 +695,7 @@ int main(int argc, const char **argv)
bail:
- if (!strcmp(builder.power_off, "suspend") && lsp_suspender) {
+ if (!strcmp(builder.power_off_type, "suspend") && lsp_suspender) {
uint8_t te = 2;
/*
@@ -725,7 +737,7 @@ bail:
saib_config_destroy(&builder);
- if (!strcmp(builder.power_off, "suspend"))
+ if (!strcmp(builder.power_off_type, "suspend"))
lws_sul_cancel(&builder.sul_idle);
/*
diff --git a/src/builder/b-task.c b/src/builder/b-task.c
index 3b42437..aa9c659 100644
--- a/src/builder/b-task.c
+++ b/src/builder/b-task.c
@@ -165,7 +165,7 @@ saib_task_destroy(struct sai_nspawn *ns)
ns->sp->ongoing, ns->sp->ongoing - 1);
ns->sp->ongoing--;
- if (!strcmp(builder.power_off, "suspend") && !ns->sp->ongoing) {
+ if (!strcmp(builder.power_off_type, "suspend") && !ns->sp->ongoing) {
int m = 0;
/*
@@ -718,7 +718,7 @@ saib_ws_json_rx_builder(struct sai_plat_server *spm, const void *in, size_t len)
sp->ongoing++;
ns->task->told_ongoing = 1;
- if (!strcmp(builder.power_off, "suspend")) {
+ if (!strcmp(builder.power_off_type, "suspend")) {
/* we're busy, we're not in the mood for suspending */
lwsl_notice("%s: cancelling suspend grace time\n", __func__);
lws_sul_cancel(&ns->builder->sul_idle);
diff --git a/src/power/CMakeLists.txt b/src/power/CMakeLists.txt
new file mode 100644
index 0000000..176494d
--- /dev/null
+++ b/src/power/CMakeLists.txt
@@ -0,0 +1,53 @@
+set(SUB "sai-power")
+set(CPACK_DEBIAN_BUILDER_PACKAGE_NAME "sai-power")
+
+set(SRCS
+ p-sai.c
+ p-conf.c
+ p-comms.c
+)
+
+set(requirements 1)
+require_lws_config(LWS_WITH_CLIENT 1 requirements)
+require_lws_config(LWS_WITH_SPAWN 1 requirements)
+require_lws_config(LWS_WITH_STRUCT_JSON 1 requirements)
+require_lws_config(LWS_WITH_SECURE_STREAMS 1 requirements)
+require_lws_config(LWS_WITH_DIR 1 requirements)
+
+if (requirements)
+ add_executable(${SUB} ${SRCS})
+ if (APPLE)
+ set_property(TARGET ${SUB} PROPERTY MACOSX_RPATH YES)
+ endif()
+
+ if (SAI_LWS_INC_PATH)
+ target_include_directories(${SUB} PRIVATE ${SAI_LWS_INC_PATH})
+ endif()
+
+ if (HAS_LIBCAP)
+ find_library(CAP_LIB_PATH NAMES "cap")
+ endif()
+
+ target_link_libraries(${SUB} websockets ${SAI_LWS_LIB_PATH})
+
+ message("LWS_OPENSSL_LIBRARIES ${SUB} '${LWS_OPENSSL_LIBRARIES}'")
+ if (LWS_OPENSSL_LIBRARIES)
+ target_link_libraries(${SUB} ${LWS_OPENSSL_LIBRARIES})
+ endif()
+
+ if (SAI_EXT_PTHREAD_LIBRARIES)
+ target_link_libraries(${SUB} ${SAI_EXT_PTHREAD_LIBRARIES})
+ endif()
+ if (HAS_LIBCAP)
+ target_link_libraries(${SUB} ${CAP_LIB_PATH})
+ endif()
+
+ if (MSVC OR WIN32)
+ target_link_libraries(${SUB} ws2_32.lib userenv.lib psapi.lib iphlpapi.lib)
+ endif()
+
+ install(TARGETS "sai-power"
+ RUNTIME DESTINATION "${BIN_DIR}" COMPONENT power)
+
+endif(requirements)
+include(CPack)
diff --git a/src/power/p-comms.c b/src/power/p-comms.c
new file mode 100644
index 0000000..ee72344
--- /dev/null
+++ b/src/power/p-comms.c
@@ -0,0 +1,165 @@
+/*
+ * sai-power com-warmcat-sai client protocol implementation
+ *
+ * Copyright (C) 2019 - 2025 Andy Green <andy@warmcat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation:
+ * version 2.1 of the License.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#include <libwebsockets.h>
+#include <string.h>
+#include <signal.h>
+
+#include "p-private.h"
+
+#include "../common/struct-metadata.c"
+
+LWS_SS_USER_TYPEDEF
+ char payload[200];
+ size_t size;
+ size_t pos;
+} saip_server_link_t;
+
+static lws_ss_state_return_t
+saip_m_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
+{
+ saip_server_link_t *pss = (saip_server_link_t *)userobj;
+ saip_server_t *sps = (saip_server_t *)lws_ss_opaque_from_user(pss);
+
+ lwsl_notice("%s: len %d, flags: %d\n", __func__, (int)len, flags);
+ lwsl_hexdump_notice(buf, len);
+
+ (void)sps;
+
+// if (saip_ws_json_rx_power(sps, buf, len))
+// return 1;
+
+ return 0;
+}
+
+
+static int
+cleanup_on_ss_destroy(struct lws_dll2 *d, void *user)
+{
+ saip_server_link_t *pss = (saip_server_link_t *)user;
+ saip_server_t *sps = (saip_server_t *)lws_ss_opaque_from_user(pss);
+
+ (void)sps;
+
+
+ return 0;
+}
+
+static int
+cleanup_on_ss_disconnect(struct lws_dll2 *d, void *user)
+{
+ return 0;
+}
+
+static lws_ss_state_return_t
+saip_m_state(void *userobj, void *sh, lws_ss_constate_t state,
+ lws_ss_tx_ordinal_t ack)
+{
+ saip_server_link_t *pss = (saip_server_link_t *)userobj;
+ saip_server_t *sps = (saip_server_t *)lws_ss_opaque_from_user(pss);
+ const char *pq;
+ int n;
+
+ lwsl_user("%s: %s, ord 0x%x\n", __func__, lws_ss_state_name((int)state),
+ (unsigned int)ack);
+
+ switch (state) {
+
+ case LWSSSCS_CREATING:
+
+ lwsl_notice("%s: binding ss to %p %s\n", __func__, sps, sps->url);
+
+ if (lws_ss_set_metadata(sps->ss, "url", sps->url, strlen(sps->url)))
+ lwsl_warn("%s: unable to set metadata\n", __func__);
+
+ pq = sps->url;
+ while (*pq && (pq[0] != '/' || pq[1] != '/'))
+ pq++;
+
+ if (*pq) {
+ n = 0;
+ pq += 2;
+ while (pq[n] && pq[n] != '/')
+ n++;
+ } else {
+ pq = sps->url;
+ n = (int)strlen(pq);
+ }
+
+ sps->name = sps->url + strlen(sps->url) + 1;
+ memcpy((char *)sps->name, pq, (unsigned int)n);
+ ((char *)sps->name)[n] = '\0';
+
+ while (strchr(sps->name, '.'))
+ *strchr(sps->name, '.') = '_';
+ while (strchr(sps->name, '/'))
+ *strchr(sps->name, '/') = '_';
+
+ break;
+
+ case LWSSSCS_DESTROYING:
+
+ /*
+ * If the logical SS itself is going down, every platform that
+ * used us to connect to their server and has nspawns are also
+ * going down
+ */
+ lws_dll2_foreach_safe(&power.sai_server_owner, sps,
+ cleanup_on_ss_destroy);
+
+ break;
+
+ case LWSSSCS_CONNECTED:
+ lwsl_user("%s: CONNECTED: %p\n", __func__, sps->ss);
+ return lws_ss_request_tx(sps->ss);
+
+ case LWSSSCS_DISCONNECTED:
+ /*
+ * clean up any ongoing spawns related to this connection
+ */
+
+ lwsl_user("%s: DISCONNECTED\n", __func__);
+ lws_dll2_foreach_safe(&power.sai_server_owner, sps,
+ cleanup_on_ss_disconnect);
+ break;
+
+ case LWSSSCS_ALL_RETRIES_FAILED:
+ lwsl_user("%s: LWSSSCS_ALL_RETRIES_FAILED\n", __func__);
+ return lws_ss_request_tx(sps->ss);
+
+ case LWSSSCS_QOS_ACK_REMOTE:
+ lwsl_notice("%s: LWSSSCS_QOS_ACK_REMOTE\n", __func__);
+ break;
+
+ default:
+ break;
+ }
+
+ return LWSSSSRET_OK;
+}
+
+LWS_SS_INFO("power_server_link", saip_server_link_t)
+ .rx = saip_m_rx,
+// .tx = saip_m_tx,
+ .state = saip_m_state,
+ .user_alloc = sizeof(saip_server_link_t),
+ .streamtype = "sai_power"
+};
diff --git a/src/power/p-conf.c b/src/power/p-conf.c
new file mode 100644
index 0000000..6411158
--- /dev/null
+++ b/src/power/p-conf.c
@@ -0,0 +1,220 @@
+/*
+ * sai-power conf.c
+ *
+ * Copyright (C) 2019 - 2025 Andy Green <andy@warmcat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation:
+ * version 2.1 of the License.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#include <libwebsockets.h>
+#include <string.h>
+#include <signal.h>
+#include <time.h>
+#include <fcntl.h>
+
+#include "p-private.h"
+
+/* global part */
+
+static const char * const paths_global[] = {
+ "perms",
+ "servers[].url",
+ "servers[].platforms[].name",
+ "servers[].platforms[].power-on.type",
+ "servers[].platforms[].power-on.mac",
+ "servers[].platforms[].power-on.url",
+ "servers[].platforms[].power-on",
+ "servers[].platforms[].power-off.type",
+ "servers[].platforms[].power-off.url",
+ "servers[].platforms[].power-off",
+ "servers[].platforms[]",
+ "servers[]"
+};
+
+enum enum_paths_global {
+ LEJPM_PERMS,
+ LEJPM_SERVERS_URL,
+ LEJPM_SERVERS_PLATFORMS_NAME,
+ LEJPM_SERVERS_PLATFORMS_POWER_ON_TYPE,
+ LEJPM_SERVERS_PLATFORMS_POWER_ON_MAC,
+ LEJPM_SERVERS_PLATFORMS_POWER_ON_URL,
+ LEJPM_SERVERS_PLATFORMS_POWER_ON,
+ LEJPM_SERVERS_PLATFORMS_POWER_OFF_TYPE,
+ LEJPM_SERVERS_PLATFORMS_POWER_OFF_URL,
+ LEJPM_SERVERS_PLATFORMS_POWER_OFF,
+ LEJPM_SERVERS_PLATFORMS,
+ LEJPM_SERVERS
+};
+
+static signed char
+saip_conf_global_cb(struct lejp_ctx *ctx, char reason)
+{
+ struct jpargs *a = (struct jpargs *)ctx->user;
+
+ const char **pp = NULL;
+#if 0
+// int n;
+
+ lwsl_notice("%s: reason: %d, path: %s, match %d\n", __func__,
+ reason, ctx->path, ctx->path_match);
+// for (n = 0; n < ctx->wildcount; n++)
+// lwsl_notice(" %d\n", ctx->wild[n]);
+#endif
+
+
+ if (reason == LEJPCB_OBJECT_START) {
+ switch (ctx->path_match - 1) {
+
+ case LEJPM_SERVERS:
+ /*
+ * Create the saip_server object
+ */
+
+ a->sai_server = lwsac_use_zero(&a->power->ac_conf_head,
+ sizeof(*a->sai_server), 4096);
+ if (!a->sai_server)
+ return -1;
+
+ lwsl_notice("%s: adding server %p\n", __func__, a->sai_server);
+
+ lws_dll2_add_tail(&a->sai_server->list,
+ &a->power->sai_server_owner);
+ break;
+
+
+ case LEJPM_SERVERS_PLATFORMS:
+ /*
+ * Create the saip_platform object and bind to the server
+ */
+ a->sai_server_plat = lwsac_use_zero(&a->power->ac_conf_head,
+ sizeof(*a->sai_server_plat), 4096);
+ if (!a->sai_server_plat)
+ return -1;
+
+ lws_dll2_add_tail(&a->sai_server_plat->list,
+ &a->sai_server->sai_plat_owner);
+ break;
+
+ default:
+ return 0;
+ }
+ }
+
+ /* we only match on the prepared path strings */
+ if (!(reason & LEJP_FLAG_CB_IS_VALUE) || !ctx->path_match)
+ return 0;
+
+ if (reason != LEJPCB_VAL_STR_END)
+ return 0;
+
+ /* only the end part of the string, where we know the length */
+
+ switch (ctx->path_match - 1) {
+
+ case LEJPM_PERMS:
+ pp = &a->power->perms;
+ break;
+
+ case LEJPM_SERVERS_URL:
+ pp = &a->sai_server->url;
+ lwsl_user("%s: server url %.*s\n", __func__, ctx->npos, ctx->buf);
+ break;
+
+ case LEJPM_SERVERS_PLATFORMS_NAME:
+ pp = &a->sai_server_plat->name;
+ break;
+
+ case LEJPM_SERVERS_PLATFORMS_POWER_ON_TYPE:
+ pp = &a->sai_server_plat->power_on_type;
+ break;
+
+ case LEJPM_SERVERS_PLATFORMS_POWER_ON_MAC:
+ pp = &a->sai_server_plat->power_on_mac;
+ break;
+
+ case LEJPM_SERVERS_PLATFORMS_POWER_ON_URL:
+ pp = &a->sai_server_plat->power_on_url;
+ break;
+
+ case LEJPM_SERVERS_PLATFORMS_POWER_OFF_TYPE:
+ pp = &a->sai_server_plat->power_off_type;
+ break;
+
+ case LEJPM_SERVERS_PLATFORMS_POWER_OFF_URL:
+ pp = &a->sai_server_plat->power_off_url;
+ break;
+
+ default:
+ return 0;
+ }
+
+ *pp = lwsac_use(&a->power->ac_conf_head, ctx->npos + 1u, 512);
+ if (!*pp)
+ return 1;
+
+ memcpy((char *)(*pp), ctx->buf, ctx->npos);
+ ((char *)(*pp))[ctx->npos] = '\0';
+
+ return 0;
+}
+
+int
+saip_config_global(struct sai_power *power, const char *d)
+{
+ unsigned char buf[128];
+ struct lejp_ctx ctx;
+ int n, m, fd;
+ struct jpargs a;
+
+ memset(&a, 0, sizeof(a));
+ a.power = power;
+
+#if defined(WIN32)
+ lws_snprintf((char *)buf, sizeof(buf) - 1, "%s\\conf", d);
+#else
+ lws_snprintf((char *)buf, sizeof(buf) - 1, "%s/conf", d);
+#endif
+
+ fd = lws_open((char *)buf, O_RDONLY);
+ if (fd < 0) {
+ lwsl_err("Cannot open %s\n", (char *)buf);
+ return 2;
+ }
+ lwsl_info("%s: %s\n", __func__, (char *)buf);
+ lejp_construct(&ctx, saip_conf_global_cb, &a,
+ paths_global, LWS_ARRAY_SIZE(paths_global));
+
+ do {
+ n = (int)read(fd, buf, sizeof(buf));
+ if (!n)
+ break;
+
+ m = lejp_parse(&ctx, buf, n);
+ } while (m == LEJP_CONTINUE);
+
+ close(fd);
+ n = (int)ctx.line;
+ lejp_destruct(&ctx);
+
+ return 0;
+}
+
+
+void
+saip_config_destroy(struct sai_power *power)
+{
+ lwsac_free(&power->ac_conf_head);
+}
diff --git a/src/power/p-intake.c b/src/power/p-intake.c
new file mode 100644
index 0000000..f59a52e
--- /dev/null
+++ b/src/power/p-intake.c
@@ -0,0 +1,170 @@
+/*
+ * Sai server
+ *
+ * Copyright (C) 2019 - 2020 Andy Green <andy@warmcat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation:
+ * version 2.1 of the License.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ *
+ * This is a ws server run by sai-power, which accepts JSON config from builders
+ * that want to use it as a helper for managing their power state.
+ */
+
+#include <libwebsockets.h>
+#include <string.h>
+#include <signal.h>
+#include <time.h>
+#include <stdio.h>
+#include <fcntl.h>
+
+#include "p-private.h"
+
+#include "../common/struct-metadata.c"
+
+
+static int
+callback_ws_power(struct lws *wsi, enum lws_callback_reasons reason, void *user,
+ void *in, size_t len)
+{
+ struct vhd *vhd = (struct vhd *)lws_protocol_vh_priv_get(
+ lws_get_vhost(wsi), lws_get_protocol(wsi));
+ uint8_t buf[LWS_PRE + 8192], *start = &buf[LWS_PRE], *p = start,
+ *end = &buf[sizeof(buf) - LWS_PRE - 1];
+ struct pss *pss = (struct pss *)user;
+ const char *pvo_resources;
+ int n;
+
+ (void)end;
+ (void)p;
+
+ switch (reason) {
+ case LWS_CALLBACK_PROTOCOL_INIT:
+
+ break;
+
+ /*
+ * ws connections from builders
+ */
+
+ case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:
+ return 0;
+
+ case LWS_CALLBACK_ESTABLISHED:
+ pss->wsi = wsi;
+ pss->vhd = vhd;
+ if (!vhd)
+ return -1;
+
+ if (lws_hdr_total_length(wsi, WSI_TOKEN_GET_URI)) {
+ if (lws_hdr_copy(wsi, (char *)start, 64,
+ WSI_TOKEN_GET_URI) < 0)
+ return -1;
+ }
+#if defined(LWS_ROLE_H2)
+ else
+ if (lws_hdr_copy(wsi, (char *)start, 64,
+ WSI_TOKEN_HTTP_COLON_PATH) < 0)
+ return -1;
+#endif
+
+ if (!memcmp((char *)start, "/sai", 4))
+ start += 4;
+
+ if (!strcmp((char *)start, "/builder")) {
+ lwsl_info("%s: ESTABLISHED: builder\n", __func__);
+ pss->wsi = wsi;
+ /*
+ * this adds our pss part, but not the logical builder
+ * yet, until we get the ws rx
+ */
+ lws_dll2_add_head(&pss->same, &vhd->builders);
+ break;
+ }
+
+ lwsl_err("%s: unknown URL '%s'\n", __func__, start);
+
+ return -1;
+
+ case LWS_CALLBACK_CLOSED:
+ lwsac_free(&pss->query_ac);
+
+ lwsl_user("%s: CLOSED builder conn\n", __func__);
+ /* remove pss from vhd->builders */
+ lws_dll2_remove(&pss->same);
+
+ /*
+ * Destroy any the builder-tracking objects that
+ * were using this departing connection
+ */
+
+ lws_start_foreach_dll_safe(struct lws_dll2 *, p, p1,
+ vhd->server.builder_owner.head) {
+ sai_plat_t *cb = lws_container_of(p, sai_plat_t,
+ sai_plat_list);
+
+ if (cb->wsi == wsi) {
+ /* remove builder object itself from server list */
+ cb->wsi = NULL;
+ lws_dll2_remove(&cb->sai_plat_list);
+ /*
+ * free the deserialized builder object,
+ * everything he pointed to was overallocated
+ * when his deep copy was made
+ */
+ free(cb);
+ }
+
+ } lws_end_foreach_dll_safe(p, p1);
+
+ sais_resource_wellknown_remove_pss(&pss->vhd->server, pss);
+
+ if (pss->blob_artifact) {
+ sqlite3_blob_close(pss->blob_artifact);
+ pss->blob_artifact = NULL;
+ }
+
+ if (pss->pdb_artifact) {
+ sais_event_db_close(pss->vhd, &pss->pdb_artifact);
+ pss->pdb_artifact = NULL;
+ }
+
+ /*
+ * Update the sai-webs about the builder removal, so they
+ * can update their connected browsers
+ */
+ sais_list_builders(vhd);
+ break;
+
+ case LWS_CALLBACK_RECEIVE:
+ break;
+
+ case LWS_CALLBACK_SERVER_WRITEABLE:
+ if (!vhd) {
+ lwsl_notice("%s: no vhd\n", __func__);
+ break;
+ }
+
+ return sais_ws_json_tx_builder(vhd, pss, buf, sizeof(buf));
+
+ default:
+passthru:
+ break;
+ }
+
+ return lws_callback_http_dummy(wsi, reason, user, in, len);
+}
+
+const struct lws_protocols protocol_ws_power =
+ { "com-warmcat-sai-power", callback_ws_power, sizeof(struct pss), 0 };
diff --git a/src/power/p-private.h b/src/power/p-private.h
new file mode 100644
index 0000000..247f1d7
--- /dev/null
+++ b/src/power/p-private.h
@@ -0,0 +1,124 @@
+/*
+ * Sai power definitions src/power/b-private.h
+ *
+ * Copyright (C) 2019 - 2025 Andy Green <andy@warmcat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation:
+ * version 2.1 of the License.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#include "../common/include/private.h"
+
+#include <sys/stat.h>
+#if defined(WIN32)
+#include <direct.h>
+#define read _read
+#define open _open
+#define close _close
+#define write _write
+#define mkdir(x,y) _mkdir(x)
+#define rmdir _rmdir
+#define unlink _unlink
+#define HAVE_STRUCT_TIMESPEC
+#if defined(pid_t)
+#undef pid_t
+#endif
+#endif
+#include <pthread.h>
+#include <git2.h>
+
+#define SAI_IDLE_GRACE_US (20 * LWS_US_PER_SEC)
+
+typedef enum {
+ PHASE_IDLE,
+
+ PFL_FIRST = 128,
+
+ PHASE_START_ATTACH = PFL_FIRST | 1,
+ PHASE_SUMM_PLATFORMS = 2,
+
+ PHASE_BUILDING
+
+} cursor_phase_t;
+
+
+
+struct saip_ws_pss;
+
+typedef struct saip_server_plat {
+ struct lws_dll2 list;
+
+ const char *name;
+ const char *power_on_type;
+ const char *power_on_url;
+ const char *power_on_mac;
+ const char *power_off_type;
+ const char *power_off_url;
+
+} saip_server_plat_t;
+
+typedef struct saip_server {
+ struct lws_dll2 list;
+
+ lws_dll2_owner_t sai_plat_owner; /* list of platforms we offer */
+
+ struct lws_ss_handle *ss;
+
+ const char *url;
+ const char *name;
+} saip_server_t;
+
+/*
+ * This represents this power process as a whole
+ */
+
+struct sai_power {
+ lws_dll2_owner_t sai_server_owner; /* servers we connect to */
+
+ struct lwsac *ac_conf_head;
+ struct lws_context *context;
+ struct lws_vhost *vhost;
+
+ lws_sorted_usec_list_t sul_idle;
+
+ const char *power_off;
+
+ const char *bind; /* listen socket binding */
+ const char *perms; /* user:group */
+
+ const char *port; /* port we listen on */
+};
+
+struct jpargs {
+ struct sai_power *power;
+
+ saip_server_t *sai_server;
+ saip_server_plat_t *sai_server_plat;
+
+ sai_plat_server_ref_t *mref;
+
+ int next_server_index;
+ int next_plat_index;
+};
+
+extern struct sai_power power;
+extern const lws_ss_info_t ssi_saip_server_link_t;
+extern const struct lws_protocols protocol_com_warmcat_sai, protocol_ws_power;
+int
+saip_config_global(struct sai_power *power, const char *d);
+extern int saip_config(struct sai_power *power, const char *d);
+extern void saip_config_destroy(struct sai_power *power);
+
+
diff --git a/src/power/p-sai.c b/src/power/p-sai.c
new file mode 100644
index 0000000..472d665
--- /dev/null
+++ b/src/power/p-sai.c
@@ -0,0 +1,417 @@
+/*
+ * sai-power
+ *
+ * Copyright (C) 2019 - 2025 Andy Green <andy@warmcat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation:
+ * version 2.1 of the License.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ *
+ * /----------------------|---
+ * b1 --\ <---WOL--\ | \
+ * --- [sai-power] --|- sai-server
+ * b2 --/ plug <---/ | /
+ * \----------------------|---
+ *
+ * Sai-power is a daemon that runs typically on a machine on the local subnet of
+ * the bulders that it is used by. When idle, laptop-type builders may suspend
+ * themselves, but while suspended, they need a helper to watch the sai-server
+ * for them to see if any tasks appeared for their platform, and to restart the
+ * builder when that is seen, eg, by sending a WOL magic packet. After that,
+ * the builder will reconnect to sai-server and deal with the situation that it
+ * finds at sai-server itself, going back to sleep if nothing to do (eg, because
+ * another builder for the same platform took the task first).
+ *
+ * The same situation exists for the case the builder can't suspend (like many
+ * SBC) and instead powers off using a smartplug, they also need a helper to
+ * talk to the smartplug for powerdown after builder shutdown; to watch the
+ * sai-server on the builder's behalf while it is down; and to power the builder
+ * back up by switching the builder's smartplug on when tasks for the powered-
+ * down builder's platform are seen at sai-server.
+ *
+ * If there are builders at different sites / subnets (if using WOL) it's no
+ * problem to have sai-power helpers for each subnet / site pointing to the same
+ * sai-server.
+ *
+ * See p-comms.c for the secure stream template and callbacks for this.
+ */
+
+#include <libwebsockets.h>
+#include <string.h>
+#include <signal.h>
+
+#if defined(__linux__)
+#include <unistd.h>
+#endif
+
+#if defined(__APPLE__)
+#include <sys/stat.h> /* for mkdir() */
+#include <unistd.h> /* for chown() */
+#endif
+
+#if defined(WIN32)
+#include <initguid.h>
+#include <KnownFolders.h>
+#include <Shlobj.h>
+
+int getpid(void) { return 0; }
+
+#endif
+
+#include "p-private.h"
+
+static const char *config_dir = "/etc/sai/power";
+static int interrupted;
+static lws_state_notify_link_t nl;
+
+struct sai_power power;
+
+static const char * const default_ss_policy =
+ "{"
+ "\"retry\": [" /* named backoff / retry strategies */
+ "{\"default\": {"
+ "\"backoff\": [" "1000,"
+ "2000,"
+ "3000,"
+ "5000,"
+ "10000"
+ "],"
+ "\"conceal\":" "99999,"
+ "\"jitterpc\":" "20,"
+ "\"svalidping\":" "100,"
+ "\"svalidhup\":" "110"
+ "}}"
+ "],"
+
+ /*
+ * No certs / trust stores because we will validate using system trust
+ * store... metadata.url should be set at runtime to something like
+ * https://warmcat.com/sai
+ */
+
+ "\"s\": ["
+ /*
+ * The main connection to sai-server
+ */
+ "{\"sai_power\": {"
+ "\"endpoint\":" "\"${url}\","
+ "\"port\":" "443,"
+ "\"protocol\":" "\"ws\","
+ "\"ws_subprotocol\":" "\"com-warmcat-sai-power\","
+ "\"http_url\":" "\"\"," /* filled in by url */
+ "\"nailed_up\":" "true,"
+ "\"tls\":" "true,"
+ "\"retry\":" "\"default\","
+ "\"metadata\": ["
+ "{\"url\": \"\"}"
+ "]"
+ "}},"
+ /*
+ * The ws server that builders on the local subnet
+ * connect to for help with power operations
+ */
+ "{\"local\": {"
+ "\"server\":" "true,"
+ "\"port\":" "3333,"
+ "\"protocol\":" "\"ws\","
+ "\"ws_subprotocol\":" "\"com-warmcat-sai-power\","
+ "\"tls\":" "false,"
+ "\"metadata\": ["
+ "{\"path\": \"\"},"
+ "{\"method\": \"\"},"
+ "{\"mime\": \"\"}"
+ "]"
+ "}}"
+ "]}"
+;
+
+
+
+static const struct lws_protocols *pprotocols[] = {
+// &protocol_ws_power,
+ NULL
+};
+
+LWS_SS_USER_TYPEDEF
+ char payload[200];
+ size_t size;
+ size_t pos;
+} local_srv_t;
+
+static lws_ss_state_return_t
+local_srv_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, size_t *len,
+ int *flags)
+{
+ local_srv_t *g = (local_srv_t *)userobj;
+ lws_ss_state_return_t r = LWSSSSRET_OK;
+
+ if (g->size == g->pos)
+ return LWSSSSRET_TX_DONT_SEND;
+
+ if (*len > g->size - g->pos)
+ *len = g->size - g->pos;
+
+ if (!g->pos)
+ *flags |= LWSSS_FLAG_SOM;
+
+ memcpy(buf, g->payload + g->pos, *len);
+ g->pos += *len;
+
+ if (g->pos != g->size) /* more to do */
+ r = lws_ss_request_tx(lws_ss_from_user(g));
+ else
+ *flags |= LWSSS_FLAG_EOM;
+
+ lwsl_ss_user(lws_ss_from_user(g), "TX %zu, flags 0x%x, r %d", *len,
+ (unsigned int)*flags, (int)r);
+
+ return r;
+}
+
+static lws_ss_state_return_t
+local_srv_state(void *userobj, void *sh, lws_ss_constate_t state,
+ lws_ss_tx_ordinal_t ack)
+{
+ local_srv_t *g = (local_srv_t *)userobj;
+
+ lwsl_ss_user(lws_ss_from_user(g), "state %s", lws_ss_state_name(state));
+
+ switch ((int)state) {
+ case LWSSSCS_CREATING:
+ return lws_ss_request_tx(lws_ss_from_user(g));
+
+ case LWSSSCS_SERVER_TXN:
+
+ lwsl_ss_user(lws_ss_from_user(g), "LWSSSCS_SERVER_TXN");
+
+ /*
+ * A transaction is starting on an accepted connection. Say
+ * that we're OK with the transaction, prepare the user
+ * object with the response, and request tx to start sending it.
+ */
+ lws_ss_server_ack(lws_ss_from_user(g), 0);
+
+ if (lws_ss_set_metadata(lws_ss_from_user(g), "mime", "text/html", 9))
+ return LWSSSSRET_DISCONNECT_ME;
+
+ g->size = (size_t)lws_snprintf(g->payload, sizeof(g->payload),
+ "Hello World: %lu",
+ (unsigned long)lws_now_usecs());
+ g->pos = 0;
+
+ return lws_ss_request_tx_len(lws_ss_from_user(g),
+ (unsigned long)g->size);
+ }
+
+ return LWSSSSRET_OK;
+}
+
+
+LWS_SS_INFO("local", local_srv_t)
+ .tx = local_srv_tx,
+ .state = local_srv_state,
+};
+
+
+static int
+app_system_state_nf(lws_state_manager_t *mgr, lws_state_notify_link_t *link,
+ int current, int target)
+{
+ struct lws_context *cx = lws_system_context_from_system_mgr(mgr);
+
+ /*
+ * For the things we care about, let's notice if we are trying to get
+ * past them when we haven't solved them yet, and make the system
+ * state wait while we trigger the dependent action.
+ */
+ switch (target) {
+
+ case LWS_SYSTATE_OPERATIONAL:
+ if (current != LWS_SYSTATE_OPERATIONAL)
+ break;
+
+ lwsl_cx_user(cx, "LWS_SYSTATE_OPERATIONAL");
+
+ /* create our LAN-facing sai-power server / listener */
+
+ if (lws_ss_create(cx, 0, &ssi_local_srv_t, NULL, NULL, NULL, NULL))
+ return 1;
+
+ /*
+ * For each server... a single connection
+ */
+
+ lws_start_foreach_dll_safe(struct lws_dll2 *, mp, mp1,
+ power.sai_server_owner.head) {
+ saip_server_t *sps = lws_container_of(mp,
+ struct saip_server, list);
+
+ lwsl_user("%s: OPERATIONAL: server url %p %s\n", __func__, sps, sps->url);
+
+ if (lws_ss_create(cx, 0, &ssi_saip_server_link_t, sps,
+ &sps->ss, NULL, NULL)) {
+ lwsl_err("%s: failed to create secure stream\n",
+ __func__);
+ return -1;
+ }
+
+ } lws_end_foreach_dll_safe(mp, mp1);
+
+
+ break;
+ }
+
+ return 0;
+}
+
+/*
+ * The grace time is up, ask for the suspend
+ */
+
+void
+sul_idle_cb(lws_sorted_usec_list_t *sul)
+{
+
+}
+
+static lws_state_notify_link_t * const app_notifier_list[] = {
+ &nl, NULL
+};
+
+void sigint_handler(int sig)
+{
+ interrupted = 1;
+}
+
+
+int main(int argc, const char **argv)
+{
+ int logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE;
+ struct lws_context_creation_info info;
+#if defined(WIN32)
+ char temp[256], stg_config_dir[256];
+#endif
+ const char *p;
+
+ lws_context_info_defaults(&info, NULL);
+
+ if ((p = lws_cmdline_option(argc, argv, "-d")))
+ logs = atoi(p);
+
+ if ((p = lws_cmdline_option(argc, argv, "-c")))
+ config_dir = p;
+
+#if defined(__NetBSD__) || defined(__OpenBSD__)
+ if (lws_cmdline_option(argc, argv, "-D")) {
+ if (lws_daemonize("/var/run/sai_power.pid"))
+ return 1;
+ lws_set_log_level(logs, lwsl_emit_syslog);
+ } else
+#endif
+
+ lws_set_log_level(logs, NULL);
+
+#if defined(WIN32)
+ {
+ PWSTR wdi = NULL;
+
+ if (SHGetKnownFolderPath(&FOLDERID_ProgramData,
+ 0, NULL, &wdi) != S_OK) {
+ lwsl_err("%s: unable to get config dir\n", __func__);
+ return 1;
+ }
+
+ if (WideCharToMultiByte(CP_ACP, 0, wdi, -1, temp,
+ sizeof(temp), 0, NULL) <= 0) {
+ lwsl_err("%s: problem with string encoding\n", __func__);
+ return 1;
+ }
+
+ lws_snprintf(stg_config_dir, sizeof(stg_config_dir),
+ "%s\\sai\\power\\", temp);
+
+ config_dir = stg_config_dir;
+ CoTaskMemFree(wdi);
+ }
+#endif
+
+ /*
+ * Let's parse the global bits out of the config
+ */
+
+ lwsl_user("Sai Power - "
+ "Copyright (C) 2019-2025 Andy Green <andy@warmcat.com>\n");
+ lwsl_user(" sai-power [-c <config-file>]\n");
+
+ info.pprotocols = pprotocols;
+ info.uid = 883;
+ info.pt_serv_buf_size = 32 * 1024;
+ info.rlimit_nofile = 20000;
+
+ signal(SIGINT, sigint_handler);
+
+ info.pss_policies_json = default_ss_policy;
+ info.fd_limit_per_thread = 1 + 256 + 1;
+
+ /* hook up our lws_system state notifier */
+
+ nl.name = "sai-power";
+ nl.notify_cb = app_system_state_nf;
+ info.register_notifier_list = app_notifier_list;
+
+ /* create the lws context */
+
+ power.context = lws_create_context(&info);
+ if (!power.context) {
+ lwsl_err("lws init failed\n");
+ return 1;
+ }
+
+
+ lwsl_notice("%s: config dir %s\n", __func__, config_dir);
+ if (saip_config_global(&power, config_dir)) {
+ lwsl_err("%s: global config failed\n", __func__);
+
+ return 1;
+ }
+
+ power.vhost = lws_create_vhost(power.context, &info);
+ if (!power.vhost) {
+ lwsl_err("Failed to create tls vhost\n");
+ goto bail;
+ }
+
+ while (!lws_service(power.context, 0) && !interrupted)
+ ;
+
+bail:
+
+ /* destroy the connections to the servers */
+
+ lws_start_foreach_dll_safe(struct lws_dll2 *, p, p1,
+ power.sai_server_owner.head) {
+ struct saip_server *sps = lws_container_of(p,
+ struct saip_server, list);
+
+ lws_dll2_remove(&sps->list);
+ lws_ss_destroy(&sps->ss);
+
+ } lws_end_foreach_dll_safe(p, p1);
+
+ saip_config_destroy(&power);
+
+ lws_context_destroy(power.context);
+
+ return 0;
+}
diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt
index 74e4e8d..2e29af7 100644
--- a/src/server/CMakeLists.txt
+++ b/src/server/CMakeLists.txt
@@ -15,10 +15,10 @@ set(SRCS
)
set(requirements 1)
-require_lws_config(LWS_WITH_STRUCT_SQLITE3 1 requirements)
require_lws_config(LWS_WITH_SERVER 1 requirements)
require_lws_config(LWS_WITH_GENCRYPTO 1 requirements)
require_lws_config(LWS_WITH_UNIX_SOCK 1 requirements)
+require_lws_config(LWS_WITH_STRUCT_SQLITE3 1 requirements)
if (requirements)
add_executable(${SUB} ${SRCS})
diff --git a/src/server/s-comms.c b/src/server/s-comms.c
index 2233271..22ef381 100644
--- a/src/server/s-comms.c
+++ b/src/server/s-comms.c
@@ -115,7 +115,7 @@ sais_event_db_ensure_open(struct vhd *vhd, const char *event_uuid,
char filepath[256], saf[33];
sais_sqlite_cache_t *sc;
- lwsl_notice("%s: (sai-server) entry\n", __func__);
+ // lwsl_notice("%s: (sai-server) entry\n", __func__);
if (*ppdb)
return 0;
@@ -792,3 +792,138 @@ passthru:
const struct lws_protocols protocol_ws =
{ "com-warmcat-sai", callback_ws, sizeof(struct pss), 0 };
+
+
+ /*
+ * This is where the sai-power connections to us end up. They
+ * want to know summaries of platforms that have open jobs on any
+ * event, re-sent every time it might have changed.
+ */
+
+
+static int
+callback_ws_power(struct lws *wsi, enum lws_callback_reasons reason, void *user,
+ void *in, size_t len)
+{
+ struct vhd *vhd = (struct vhd *)lws_protocol_vh_priv_get(
+ lws_get_vhost(wsi), lws_get_protocol(wsi));
+ uint8_t buf[LWS_PRE + 8192], *start = &buf[LWS_PRE], *p = start,
+ *end = &buf[sizeof(buf) - LWS_PRE - 1];
+ struct pss_power *pss = (struct pss_power *)user;
+ int n;
+ size_t m;
+
+ (void)end;
+ (void)p;
+
+ switch (reason) {
+ case LWS_CALLBACK_PROTOCOL_INIT:
+ vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),
+ lws_get_protocol(wsi),
+ sizeof(struct vhd));
+
+ vhd->context = lws_get_context(wsi);
+ vhd->vhost = lws_get_vhost(wsi);
+
+ if (lws_pvo_get_str(in, "database", &vhd->sqlite3_path_lhs)) {
+ lwsl_err("%s: database pvo required\n", __func__);
+ return -1;
+ }
+
+ lws_snprintf((char *)buf, sizeof(buf), "%s-events.sqlite3",
+ vhd->sqlite3_path_lhs);
+
+ if (lws_struct_sq3_open(vhd->context, (char *)buf, 1,
+ &vhd->server.pdb)) {
+ lwsl_err("%s: Unable to open session db %s: %s\n",
+ __func__, vhd->sqlite3_path_lhs, sqlite3_errmsg(
+ vhd->server.pdb));
+
+ return -1;
+ }
+ break;
+
+ case LWS_CALLBACK_PROTOCOL_DESTROY:
+ goto passthru;
+
+ /*
+ * ws connections from sai-power clients
+ */
+
+ case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:
+ return 0;
+
+ case LWS_CALLBACK_ESTABLISHED:
+ pss->wsi = wsi;
+ pss->vhd = vhd;
+ if (!vhd)
+ return -1;
+
+ if (lws_hdr_total_length(wsi, WSI_TOKEN_GET_URI)) {
+ if (lws_hdr_copy(wsi, (char *)start, 64,
+ WSI_TOKEN_GET_URI) < 0)
+ return -1;
+ }
+#if defined(LWS_ROLE_H2)
+ else
+ if (lws_hdr_copy(wsi, (char *)start, 64,
+ WSI_TOKEN_HTTP_COLON_PATH) < 0)
+ return -1;
+#endif
+
+ lws_dll2_add_head(&pss->same, &vhd->sai_powers);
+
+ sais_platforms_with_tasks_pending(vhd);
+
+ break;
+
+ case LWS_CALLBACK_CLOSED:
+
+ lwsl_user("%s: CLOSED sai-power conn\n", __func__);
+ /* remove pss from vhd->sai_powers */
+ lws_dll2_remove(&pss->same);
+ break;
+
+ case LWS_CALLBACK_RECEIVE:
+ break;
+
+ case LWS_CALLBACK_SERVER_WRITEABLE:
+ if (!vhd) {
+ lwsl_notice("%s: no vhd\n", __func__);
+ break;
+ }
+
+ n = 0;
+ lws_start_foreach_dll(struct lws_dll2 *, px, vhd->pending_plats.head) {
+ sais_plat_t *pl = lws_container_of(px, sais_plat_t, list);
+
+ if (n)
+ *p++ = ',';
+ m = strlen(pl->plat);
+ if (lws_ptr_diff_size_t(end, p) < m + 2)
+ break;
+ memcpy(p, pl->plat, m);
+ p += m;
+ n = 1;
+
+ } lws_end_foreach_dll(px);
+
+
+ lwsl_hexdump_notice(start, lws_ptr_diff_size_t(p, start));
+
+ if (lws_write(pss->wsi, start, lws_ptr_diff_size_t(p, start),
+ LWS_WRITE_TEXT) < 0)
+ return -1;
+
+ break;
+
+ default:
+passthru:
+ break;
+ }
+
+ return lws_callback_http_dummy(wsi, reason, user, in, len);
+}
+
+const struct lws_protocols protocol_ws_power =
+ { "com-warmcat-sai-power", callback_ws_power, sizeof(struct pss_power), 0 };
diff --git a/src/server/s-notification.c b/src/server/s-notification.c
index 019d627..6028beb 100644
--- a/src/server/s-notification.c
+++ b/src/server/s-notification.c
@@ -557,6 +557,12 @@ sai_saifile_lejp_cb(struct lejp_ctx *ctx, char reason)
sais_event_db_close(pss->vhd, &pdb);
+ /*
+ * Recompute startable task platforms and broadcast to all sai-power,
+ * after there has been a change in tasks
+ */
+ sais_platforms_with_tasks_pending(pss->vhd);
+
// lwsl_notice("%s: New test '%s', '%s', '%s'\n", __func__,
// sn->t.taskname, sn->t.cmake, sn->t.packages);
diff --git a/src/server/s-private.h b/src/server/s-private.h
index 68101ec..f6bcd67 100644
--- a/src/server/s-private.h
+++ b/src/server/s-private.h
@@ -151,6 +151,13 @@ struct pss {
uint8_t ovstate; /* SOS_ substate when doing overview */
};
+struct pss_power {
+ struct vhd *vhd;
+ struct lws *wsi;
+
+ struct lws_dll2 same; /* owner: vhd.builders */
+};
+
typedef struct sais_sqlite_cache {
lws_dll2_t list;
char uuid[65];
@@ -159,6 +166,12 @@ typedef struct sais_sqlite_cache {
int refcount;
} sais_sqlite_cache_t;
+
+typedef struct sais_plat {
+ lws_dll2_t list;
+ const char *plat;
+} sais_plat_t;
+
struct vhd {
struct lws_context *context;
struct lws_vhost *vhost;
@@ -168,7 +181,11 @@ struct vhd {
char json_builders[8192];
/* pss lists */
- struct lws_dll2_owner builders;
+ struct lws_dll2_owner builders;
+ struct lws_dll2_owner sai_powers;
+ struct lws_dll2_owner pending_plats;
+
+ struct lwsac *ac_plats;
const char *sqlite3_path_lhs;
@@ -189,7 +206,7 @@ sai_lws_context_from_json(const char *config_dir,
struct lws_context_creation_info *info,
const struct lws_protocols **pprotocols,
const char *jpol);
-extern const struct lws_protocols protocol_ws;
+extern const struct lws_protocols protocol_ws, protocol_ws_power;
int
sai_notification_file_upload_cb(void *data, const char *name,
@@ -288,3 +305,6 @@ sais_resource_destroy_queued_by_cookie(sais_t *sais, const char *cookie);
void
sais_resource_rr_destroy(sai_resource_requisition_t *rr);
+
+int
+sais_platforms_with_tasks_pending(struct vhd *vhd);
diff --git a/src/server/s-sai.c b/src/server/s-sai.c
index c119d5e..b77c52c 100644
--- a/src/server/s-sai.c
+++ b/src/server/s-sai.c
@@ -55,6 +55,7 @@ static const char * const default_ss_policy =
static const struct lws_protocols
*pprotocols[] = {
&protocol_ws,
+ &protocol_ws_power,
#if defined(LWS_WITH_SYS_METRICS) && defined(LWS_WITH_PLUGINS_BUILTIN)
&lws_openmetrics_export_protocols[LWSOMPROIDX_PROX_HTTP_SERVER],
&lws_openmetrics_export_protocols[LWSOMPROIDX_PROX_WS_SERVER],
@@ -81,7 +82,7 @@ int main(int argc, const char **argv)
logs = atoi(p);
lws_set_log_level(logs, NULL);
- lwsl_user("Sai Server - Copyright (C) 2019-2020 Andy Green <andy@warmcat.com>\n");
+ lwsl_user("Sai Server - Copyright (C) 2019-2025 Andy Green <andy@warmcat.com>\n");
if ((p = lws_cmdline_option(argc, argv, "-c")))
conf = p;
diff --git a/src/server/s-task.c b/src/server/s-task.c
index 0261811..cf20cbb 100644
--- a/src/server/s-task.c
+++ b/src/server/s-task.c
@@ -329,6 +329,154 @@ bail:
return NULL;
}
+/*
+ * If the plat name is already listed, just return with 1.
+ * Otherwise add to the ac and linked-list for unique startable plat names and
+ * return 0.
+ */
+
+static int
+sais_find_or_add_pending_plat(struct vhd *vhd, const char *name)
+{
+ sais_plat_t *sp;
+
+ lws_start_foreach_dll(struct lws_dll2 *, p, vhd->pending_plats.head) {
+ sais_plat_t *pl = lws_container_of(p, sais_plat_t, list);
+
+ if (!strcmp(&pl->plat[1], name))
+ return 1;
+
+ } lws_end_foreach_dll(p);
+
+ /* platform name is new, make an entry in the ac */
+
+ sp = lwsac_use_zero(&vhd->ac_plats, sizeof(sais_plat_t) + strlen(name) + 1, 512);
+
+ sp->plat = (const char *)&sp[1]; /* start of overcommit */
+ memcpy(&sp[1], name, strlen(name) + 1);
+
+ lws_dll2_add_tail(&sp->list, &vhd->pending_plats);
+
+ return 0;
+}
+
+static void
+sais_destroy_pending_plat_list(struct vhd *vhd)
+{
+ /*
+ * We can just drop everything in the owner and drop the ac to destroy
+ */
+ lws_dll2_owner_clear(&vhd->pending_plats);
+ lwsac_free(&vhd->ac_plats);
+}
+
+static void
+sais_notify_all_sai_power(struct vhd *vhd)
+{
+ lws_start_foreach_dll(struct lws_dll2 *, p, vhd->sai_powers.head) {
+ struct pss_power *pss = lws_container_of(p, struct pss_power, same);
+
+ lws_callback_on_writable(pss->wsi);
+
+ } lws_end_foreach_dll(p);
+}
+
+/*
+ * Find out which platforms on this server have pending tasks
+ */
+
+int
+sais_platforms_with_tasks_pending(struct vhd *vhd)
+{
+ struct lwsac *ac = NULL;
+ char pf[128];
+ lws_dll2_owner_t o;
+ int n;
+
+ lwsl_err("%s: ++++++++ entry\n", __func__);
+
+ /* lose everything we were holding on to from last time */
+ sais_destroy_pending_plat_list(vhd);
+
+ /*
+ * Collect a list of events that still have any open tasks
+ */
+
+ lws_snprintf(pf, sizeof(pf)," and (state != 3 and state != 4 and state != 5) and created < %llu",
+ (unsigned long long)(lws_now_secs() - 10));
+
+ n = lws_struct_sq3_deserialize(vhd->server.pdb, pf, "created desc ",
+ lsm_schema_sq3_map_event, &o, &ac, 0, 10);
+
+ if (n < 0 || !o.head) {
+ /* error, or there are no events that aren't complete */
+ goto bail;
+ }
+
+
+ lwsl_err("%s: starting scan\n", __func__);
+
+ /*
+ * Iterate through the events looking at his event-specific database
+ * for platforms that have pending tasks...
+ */
+
+ lws_start_foreach_dll(struct lws_dll2 *, p, o.head) {
+ sai_event_t *e = lws_container_of(p, sai_event_t, list);
+ sqlite3 *pdb = NULL;
+ sqlite3_stmt *sm;
+ int n;
+
+ if (!sais_event_db_ensure_open(vhd, e->uuid, 0, &pdb)) {
+
+ if (sqlite3_prepare_v2(pdb, "select distinct platform "
+ "from tasks where "
+ "(state != 3 and state != 4 and state != 5)", -1, &sm,
+ NULL) != SQLITE_OK) {
+ lwsl_err("%s: Unable to %s\n",
+ __func__, sqlite3_errmsg(pdb));
+
+ goto bail;
+ }
+
+ do {
+ n = sqlite3_step(sm);
+ if (n == SQLITE_ROW) {
+ lwsl_err("%s: scanned plat %s\n", __func__, (const char *)sqlite3_column_text(sm, 0));
+ sais_find_or_add_pending_plat(vhd,
+ (const char *)sqlite3_column_text(sm, 0));
+ }
+ } while (n == SQLITE_ROW);
+
+ sqlite3_reset(sm);
+ sqlite3_finalize(sm);
+
+ if (n != SQLITE_DONE) {
+ n = sqlite3_extended_errcode(pdb);
+ if (!n)
+ lwsl_info("%s: failed\n", __func__);
+
+ lwsl_err("%s: %d: Unable to perform: %s\n",
+ __func__, n, sqlite3_errmsg(pdb));
+ }
+
+ sais_event_db_close(vhd, &pdb);
+ }
+
+ } lws_end_foreach_dll(p);
+
+ sais_notify_all_sai_power(vhd);
+
+ lwsac_free(&ac);
+
+ return 0;
+
+bail:
+ lwsac_free(&ac);
+
+ return 1;
+}
+
int
sais_task_cancel(struct vhd *vhd, const char *task_uuid)
{
@@ -359,6 +507,12 @@ sais_task_cancel(struct vhd *vhd, const char *task_uuid)
sais_taskchange(vhd->h_ss_websrv, task_uuid, SAIES_CANCELLED);
+ /*
+ * Recompute startable task platforms and broadcast to all sai-power,
+ * after there has been a change in tasks
+ */
+ sais_platforms_with_tasks_pending(vhd);
+
return 0;
}
@@ -413,6 +567,12 @@ sais_task_reset(struct vhd *vhd, const char *task_uuid)
lws_sul_schedule(vhd->context, 0, &vhd->sul_central, sais_central_cb, 1);
+ /*
+ * Recompute startable task platforms and broadcast to all sai-power,
+ * after there has been a change in tasks
+ */
+ sais_platforms_with_tasks_pending(vhd);
+
return 0;
}
diff --git a/src/server/s-websrv.c b/src/server/s-websrv.c
index 1db4f46..f573b6e 100644
--- a/src/server/s-websrv.c
+++ b/src/server/s-websrv.c
@@ -509,8 +509,8 @@ websrvss_srv_state(void *userobj, void *sh, lws_ss_constate_t state,
{
websrvss_srv_t *m = (websrvss_srv_t *)userobj;
- lwsl_user("%s: %p %s, ord 0x%x\n", __func__, m->ss,
- lws_ss_state_name((int)state), (unsigned int)ack);
+ // lwsl_user("%s: %p %s, ord 0x%x\n", __func__, m->ss,
+ // lws_ss_state_name((int)state), (unsigned int)ack);
switch (state) {
case LWSSSCS_DISCONNECTED:
diff --git a/src/server/s-ws-builder.c b/src/server/s-ws-builder.c
index 638bc72..febc3bd 100644
--- a/src/server/s-ws-builder.c
+++ b/src/server/s-ws-builder.c
@@ -229,7 +229,7 @@ sai_sql3_get_uint64_cb(void *user, int cols, char **values, char **name)
}
/*
- * Master received a communication from a builder
+ * Server received a communication from a builder
*/
int