diff --git a/src/builder/CMakeLists.txt b/src/builder/CMakeLists.txt
index 1b1f9a3..8768b0d 100644
--- a/src/builder/CMakeLists.txt
+++ b/src/builder/CMakeLists.txt
@@ -3,6 +3,7 @@ set(CPACK_DEBIAN_BUILDER_PACKAGE_NAME "sai-builder")
set(SRCS
b-sai.c
+ b-win32-service.c
b-conf.c
b-ws-server.c
b-nspawn.c
diff --git a/src/builder/b-private.h b/src/builder/b-private.h
index 63ad022..f61c09b 100644
--- a/src/builder/b-private.h
+++ b/src/builder/b-private.h
@@ -320,6 +320,12 @@ suspender_destroy(void);
int
saib_reassess_idle_situation(void);
+int
+saib_app_run(int argc, const char **argv);
+
+void
+saib_app_stop(void);
+
#if defined(__APPLE__)
int
saib_need_wakelock(void);
diff --git a/src/builder/b-sai.c b/src/builder/b-sai.c
index 38dda21..3f968b8 100644
--- a/src/builder/b-sai.c
+++ b/src/builder/b-sai.c
@@ -424,8 +424,14 @@ sai_ns_destroy(struct sai_nspawn *ns)
free(ns);
}
+void saib_app_stop(void)
+{
+ interrupted = 1;
+ lws_cancel_service(builder.context);
+}
-int main(int argc, const char **argv)
+int
+saib_app_run(int argc, const char **argv)
{
int logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE;
struct lws_context_creation_info info;
@@ -664,3 +670,17 @@ bail:
return 0;
}
+
+#if defined(WIN32)
+extern int saib_service_run(int argc, const char **argv);
+#endif
+
+int main(int argc, const char **argv)
+{
+#if defined(WIN32)
+ if (lws_cmdline_option(argc, argv, "--service"))
+ return saib_service_run(argc, argv);
+#endif
+
+ return saib_app_run(argc, argv);
+}
diff --git a/src/builder/b-win32-service.c b/src/builder/b-win32-service.c
new file mode 100644
index 0000000..aacab5d
--- /dev/null
+++ b/src/builder/b-win32-service.c
@@ -0,0 +1,151 @@
+/*
+ * sai-builder - Windows Service support
+ *
+ * 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
+ */
+
+#if defined(WIN32)
+
+#include <windows.h>
+#include <tchar.h>
+#include <stdio.h>
+#include "b-private.h"
+
+SERVICE_STATUS g_ServiceStatus = {0};
+SERVICE_STATUS_HANDLE g_StatusHandle = NULL;
+
+VOID WINAPI ServiceMain(DWORD argc, LPTSTR *argv);
+VOID WINAPI ServiceCtrlHandler(DWORD);
+
+static int g_argc;
+static const char **g_argv;
+
+#define SERVICE_NAME TEXT("SaiBuilder")
+
+int saib_service_run(int argc, const char **argv)
+{
+ SERVICE_TABLE_ENTRY ServiceTable[] =
+ {
+ {SERVICE_NAME, (LPSERVICE_MAIN_FUNCTION) ServiceMain},
+ {NULL, NULL}
+ };
+
+ /* Capture arguments to pass to the service main */
+ g_argc = argc;
+ g_argv = argv;
+
+ /* This call blocks until the service is stopped */
+ if (StartServiceCtrlDispatcher(ServiceTable) == FALSE)
+ {
+ return GetLastError();
+ }
+
+ return 0;
+}
+
+VOID WINAPI ServiceMain(DWORD argc, LPTSTR *argv)
+{
+ int rc;
+
+ /*
+ * Note on arguments:
+ * argc/argv passed to ServiceMain contain arguments specified in the
+ * "Start parameters" of the Service Control Manager or via "sc start ... args".
+ * These are distinct from the process command line arguments passed to main().
+ *
+ * Typically, a service is configured via its binPath (e.g., "sai-builder.exe --service -c conf").
+ * These arguments are received by main() and captured in g_argc/g_argv.
+ * We use these captured arguments to start the app, ensuring configuration is preserved.
+ */
+
+ g_StatusHandle = RegisterServiceCtrlHandler(SERVICE_NAME, ServiceCtrlHandler);
+
+ if (g_StatusHandle == NULL)
+ {
+ return;
+ }
+
+ ZeroMemory(&g_ServiceStatus, sizeof(g_ServiceStatus));
+ g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
+ g_ServiceStatus.dwControlsAccepted = 0;
+ g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
+ g_ServiceStatus.dwWin32ExitCode = 0;
+ g_ServiceStatus.dwServiceSpecificExitCode = 0;
+ g_ServiceStatus.dwCheckPoint = 0;
+
+ if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
+ {
+ return;
+ }
+
+ /*
+ * We don't need a separate worker thread because ServiceMain runs in its own thread
+ * and we can block here. However, we need to handle the control requests.
+ */
+
+ g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
+ g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
+ g_ServiceStatus.dwWin32ExitCode = 0;
+ g_ServiceStatus.dwCheckPoint = 0;
+
+ if (SetServiceStatus(g_StatusHandle, &g_ServiceStatus) == FALSE)
+ {
+ return;
+ }
+
+ /* Run the app using the arguments passed to the process */
+ rc = saib_app_run(g_argc, g_argv);
+
+ g_ServiceStatus.dwControlsAccepted = 0;
+ g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
+
+ if (rc) {
+ g_ServiceStatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
+ g_ServiceStatus.dwServiceSpecificExitCode = (DWORD)rc;
+ } else {
+ g_ServiceStatus.dwWin32ExitCode = 0;
+ }
+
+ g_ServiceStatus.dwCheckPoint = 3;
+
+ SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
+}
+
+VOID WINAPI ServiceCtrlHandler(DWORD CtrlCode)
+{
+ switch (CtrlCode)
+ {
+ case SERVICE_CONTROL_STOP:
+ if (g_ServiceStatus.dwCurrentState != SERVICE_RUNNING)
+ break;
+
+ g_ServiceStatus.dwControlsAccepted = 0;
+ g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING;
+ g_ServiceStatus.dwWin32ExitCode = 0;
+ g_ServiceStatus.dwCheckPoint = 4;
+
+ SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
+
+ saib_app_stop();
+ break;
+
+ default:
+ break;
+ }
+}
+#endif