Files
punktfunk/scripts/windows/web-run.cmd
T
enricobuehlerandClaude Opus 5 3bb30cb2f6 fix(windows/web-console): a bun exit no longer parks the console until the next sign-in
The PunktfunkWeb task registers RestartCount=10/PT1M, but Task Scheduler honours
restart-on-failure only when the action crashes or fails to start — never for a
plain non-zero exit. So any bun exit left the console down until the next boot or
interactive logon, with the host still streaming perfectly next to it. Seen on
glass on a 0.22.3 upgrade: `web setup` started the task one second after the new
service came up, bun exited at once (Last Run Result 0xFFFFFFFF), and the console
sat dead for hours while its box served clients — which reads to an operator as
"the host is gone".

web-run.cmd now supervises bun instead of exiting with it: restart on any exit,
indefinitely for a console that served a while and then died, but give up after
10 CONSECUTIVE fast exits so a genuinely broken install still surfaces as a failed
task. Any run lasting >= 60s resets that counter. It exits 0 rather than
respawning when the payload disappears, so an uninstall does not make it spin.

Uptime is measured with an unambiguous clock rather than parsed out of %TIME%,
which is locale-formatted (12-hour locales append " PM", many others use ',' as
the decimal separator) — the box this was found on prints 18:36:28,39.

stop_web_console() polls until :47992 is really free instead of sleeping a blind
second: both `schtasks /end` and `taskkill /F` are asynchronous, and web_setup
starts the new task immediately afterwards, where bun cannot bind a port the
corpse still holds. It re-ends the TASK halfway through, not just the listener —
with the launcher now supervising, killing bun alone would be undone by its own
restart loop.

The systemd unit had the same shape of hole: Restart=on-failure leaves a console
that exited 0 down. Now Restart=always, which an explicit `systemctl stop` still
overrides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:42:50 +02:00

128 lines
6.8 KiB
Batchfile

@echo off
rem punktfunk web console launcher - the action the PunktfunkWeb scheduled task runs at boot.
rem
rem Lays out next to the installed payload: {app}\web\web-run.cmd, {app}\web\.output\... and
rem {app}\bun\bun.exe (so %~dp0 = {app}\web\). Auto-wires the console the same way the Linux
rem systemd unit does: it sources the host's mgmt bearer token + the console login password from
rem %ProgramData%\punktfunk\, points the /api proxy at the host's loopback HTTPS mgmt API, and serves
rem the (self-contained, no-node_modules) Nitro console over HTTPS (HTTP/1.1 over TLS) on :3000 with the
rem bundled bun, using the host's OWN identity cert. No env editing.
setlocal EnableExtensions
set "PFDATA=%ProgramData%\punktfunk"
set "TOKENFILE=%PFDATA%\mgmt-token"
set "PWFILE=%PFDATA%\web-password"
set "CERTFILE=%PFDATA%\cert.pem"
set "KEYFILE=%PFDATA%\key.pem"
rem The host's `serve` writes the mgmt token + its identity cert/key on first run. Until they exist
rem we have no credential and no TLS material, so WAIT rather than silently downgrading to plain HTTP.
rem
rem Wait in-process instead of exiting 1 and hoping the task's restart-on-failure retries: Task
rem Scheduler does not reliably restart on a plain non-zero exit code, so a console that started
rem before the host finished writing those files (the token lands at argument parse, the cert only
rem after the RSA-2048 keygen) used to stay down until the next reboot. ~5 min at 2 s, then give up
rem so a genuinely broken install still surfaces as a failed task rather than one that runs forever.
rem `timeout` needs a console this task does not have, so `ping -n 3` is the 2-second sleep.
set /a PFWAITS=0
:pfwait
if exist "%TOKENFILE%" if exist "%CERTFILE%" goto pfready
if %PFWAITS% GEQ 150 (
echo [punktfunk-web] gave up waiting for "%TOKENFILE%" + "%CERTFILE%" - is the punktfunk host service running?
exit /b 1
)
if %PFWAITS%==0 echo [punktfunk-web] waiting for the host service to write the mgmt token + identity cert...
set /a PFWAITS+=1
ping -n 3 127.0.0.1 >nul 2>&1
goto pfwait
:pfready
rem Both files are single KEY=VALUE lines (LF), written 0600/ACL'd: PUNKTFUNK_MGMT_TOKEN=... and
rem PUNKTFUNK_UI_PASSWORD=... . Split on the first '=' and import each into the environment.
for /f "usebackq tokens=1* delims==" %%A in ("%TOKENFILE%") do set "%%A=%%B"
if exist "%PWFILE%" for /f "usebackq tokens=1* delims==" %%A in ("%PWFILE%") do set "%%A=%%B"
rem Fixed deployment wiring (the Windows analogue of scripts/punktfunk-web.service).
set "PORT=47992"
set "HOST=0.0.0.0"
set "PUNKTFUNK_MGMT_URL=https://127.0.0.1:47990"
rem No NODE_TLS_REJECT_UNAUTHORIZED: the host's self-signed cert is accepted only for the loopback
rem proxy hop, scoped inside the proxy code (Bun per-request TLS), not process-wide.
rem Serve HTTPS (HTTP/1.1 over TLS) with the host's identity cert; mark the session cookie Secure.
set "PUNKTFUNK_UI_TLS_CERT=%CERTFILE%"
set "PUNKTFUNK_UI_TLS_KEY=%KEYFILE%"
set "PUNKTFUNK_UI_SECURE=1"
set "BUN=%~dp0..\bun\bun.exe"
set "SERVER=%~dp0.output\server\index.mjs"
if not exist "%BUN%" (
echo [punktfunk-web] bundled bun runtime missing at "%BUN%".
exit /b 1
)
rem Supervise bun instead of exiting with it. The task registers RestartCount=10/RestartInterval=PT1M,
rem but Task Scheduler honours restart-on-failure only when the action CRASHES or fails to start -
rem never for a plain non-zero exit - so before this loop any bun exit parked the console until the
rem next boot or interactive logon. Seen on glass 2026-07-30 (0.22.3 upgrade): `web setup` started the
rem task one second after the upgraded host service came up, bun exited at once (Last Run Result
rem 0xFFFFFFFF), and the console stayed down for hours on a box whose host was streaming perfectly -
rem which reads to the operator as "the host is gone". A single `schtasks /run /tn PunktfunkWeb`
rem revived it, i.e. whatever blocked the start was transient. That is exactly what a retry is for.
rem
rem Restart indefinitely for a console that served for a while and then died, but refuse to spin
rem forever on an install that can never work: PFFAILS counts only CONSECUTIVE fast exits and any run
rem lasting >= PFGOODSEC resets it, so a genuine crash-loop still surfaces as a failed task after
rem ~1 min of trying (the boot + logon triggers remain the outer backstop). Same shape as the Linux
rem unit's Restart= + StartLimitIntervalSec=0.
rem
rem INVARIANT for anything that STOPS the console (the installer's StopBunRuntimes, the host's
rem `stop_web_console`): end the TASK - `schtasks /end /tn PunktfunkWeb` / `Stop-ScheduledTask` -
rem which takes this launcher down with it. Killing bun.exe ALONE is now undone by the loop below,
rem which will relaunch it and re-lock the file you were trying to replace.
set /a PFMAXFAILS=10
set /a PFGOODSEC=60
rem `timeout` needs a console this task does not have; `ping -n N` sleeps about N-1 seconds.
set /a PFDELAYPINGS=6
set /a PFFAILS=0
:pfrun
call :pfnow PFT0
"%BUN%" "%SERVER%"
set "PFRC=%ERRORLEVEL%"
call :pfnow PFT1
set /a PFUPSEC=PFT1-PFT0
if %PFUPSEC% LSS 0 set /a PFUPSEC=0
if %PFUPSEC% GEQ %PFGOODSEC% (set /a PFFAILS=0) else (set /a PFFAILS+=1)
echo [punktfunk-web] bun exited rc=%PFRC% after %PFUPSEC%s - consecutive fast exits %PFFAILS%/%PFMAXFAILS%
if %PFFAILS% GEQ %PFMAXFAILS% (
echo [punktfunk-web] giving up after %PFMAXFAILS% consecutive fast exits - check that nothing else holds TCP 47992.
exit /b 1
)
rem The payload disappearing is a deliberate stop, not a crash: an uninstall deletes it, and an
rem upgrade replaces it. Exit 0 rather than respawning into a half-written {app} - `web setup` starts
rem the task again at the end of an upgrade, and an uninstall wants us gone.
if not exist "%BUN%" (
echo [punktfunk-web] "%BUN%" is gone - an install or uninstall is in progress; exiting instead of respawning.
exit /b 0
)
if not exist "%SERVER%" (
echo [punktfunk-web] "%SERVER%" is gone - an install or uninstall is in progress; exiting instead of respawning.
exit /b 0
)
ping -n %PFDELAYPINGS% 127.0.0.1 >nul 2>&1
goto pfrun
rem Unix seconds, for the uptime measurement above. Deliberately NOT parsed out of %TIME%: that is
rem locale-formatted - 12-hour locales append " PM" and many others use ',' as the decimal separator
rem - which makes hand-parsing it a portability trap in a launcher that ships worldwide. This runs
rem only after bun has already exited, so the ~0.3 s cost is irrelevant. `-Command` with an inline
rem expression is unaffected by a Restricted ExecutionPolicy (that gates script FILES). Should it
rem ever fail, PFN is empty, `set /a` reads that as 0, and the run is counted as a fast exit - the
rem conservative direction.
:pfnow
setlocal EnableExtensions
set "PFN="
for /f "usebackq tokens=* delims=" %%s in (`powershell -NoProfile -Command "[DateTimeOffset]::UtcNow.ToUnixTimeSeconds()"`) do set "PFN=%%s"
endlocal & set "%~1=%PFN%"
goto :eof