@echo off setlocal enabledelayedexpansion cd /d "%~dp0" title Meistertracker REM ============================================================ REM Phase 1 (outer wrapper): Update code from git FIRST, then REM copy the now-updated START.bat to a temp file and re-execute REM from there. This ensures the temp copy always has the latest REM code, and git reset can never corrupt the running script. REM ============================================================ if not "%~1"=="--relaunched" ( echo. echo ======================================== echo Meistertracker echo ======================================== echo. echo [1/5] Updating code from git... set "IS_WORKTREE=0" where git >nul 2>&1 if !errorlevel! equ 0 ( set "GIT_DIR_VAL=" for /f "tokens=*" %%G in ('git rev-parse --git-dir 2^>nul') do set "GIT_DIR_VAL=%%G" if defined GIT_DIR_VAL ( set "WKT_CHECK=!GIT_DIR_VAL:.git/worktrees/=!" if not "!WKT_CHECK!"=="!GIT_DIR_VAL!" set "IS_WORKTREE=1" ) if "!IS_WORKTREE!"=="1" ( REM Auto-configure so a worktree run lives alongside prod: REM - different PORT (default 3001) so prod on 3000 keeps serving REM - different PM2 name so 'pm2 delete' won't touch prod REM - WORKTREE_MODE flag so the server can render a UI warning REM Env vars set here are inherited by the Phase 2 temp copy via cmd /c. if not defined PORT set "PORT=3001" REM Always append -worktree so forks with a custom PM2_PROCESS_NAME REM still get isolation. Skip if already suffixed (re-entrant runs). if not defined PM2_PROCESS_NAME ( set "PM2_PROCESS_NAME=meisterpilze-worktree" ) else ( set "PM2_NAME_TAIL=!PM2_PROCESS_NAME:~-9!" if not "!PM2_NAME_TAIL!"=="-worktree" set "PM2_PROCESS_NAME=!PM2_PROCESS_NAME!-worktree" ) set "WORKTREE_MODE=1" echo. echo +------------------------------------------+ echo ^| Running in git worktree ^| echo ^| Git pull will be skipped ^| echo +------------------------------------------+ echo -^> Port: !PORT! echo -^> PM2 name: !PM2_PROCESS_NAME! echo -^> Production on port 3000 will NOT be touched. ) else if exist ".git" ( git fetch origin >nul 2>&1 if !errorlevel! equ 0 ( git reset --hard origin/main &1 if !errorlevel! neq 0 ( echo WARNING: git reset had errors ^(locked files?^). Continuing with local code. ) ) else ( echo WARNING: git fetch failed. Continuing with local code. ) ) else ( echo WARNING: No .git directory found ^(ZIP download?^). Auto-update disabled. echo For auto-updates, re-download with: git clone https://github.com/loewenmaehne/meistertracker.git ) ) else ( echo WARNING: Git not found, skipping code update. ) REM Now copy the (possibly updated) START.bat to temp and run from there set "TMPBAT=%TEMP%\meisterpilze_start_%RANDOM%.bat" copy /y "%~f0" "!TMPBAT!" >nul cmd /c ""!TMPBAT!" --relaunched "%~dp0"" set "RC=!errorlevel!" del "!TMPBAT!" >nul 2>&1 if !RC! neq 0 ( pause ) exit /b !RC! ) REM ============================================================ REM Phase 2 (temp copy): Everything after git update REM ============================================================ cd /d "%~2" REM ---- Configuration ---- REM Override via env to run multiple instances under distinct PM2 names REM or to match a fork's branding. Must match the value server.js reads REM from PM2_PROCESS_NAME. if not defined PM2_PROCESS_NAME set "PM2_PROCESS_NAME=meisterpilze" if not defined PORT set "PORT=3000" set "NEED_PATH_REFRESH=0" REM ---- Drop administrator rights before anything can start pm2 ---- REM REM pm2's daemon, its pipe and the server it starts all inherit the token of REM whatever starts pm2 first. Started with administrator rights, they belong REM to Administrators, and an ordinary window can then neither stop the server REM nor send pm2 a command: taskkill is refused, and pm2 fails with REM "connect EPERM //./pipe/rpc.sock". REM REM The boot task is where that comes from. It runs this script through an REM S4U logon, and Windows does not apply a task's RunLevel to S4U: registered REM Limited, the boot run still gets the full administrator token. So neither REM re-registering the task nor asking the scheduler to end it could help -- REM and the task had long finished anyway, leaving pm2 behind. REM REM A run that finds itself elevated therefore hands itself to REM scripts\unelevated.ps1, which runs it again as a standard user. Not when REM Node.js or Git is missing: installing those is the one thing here that REM does need administrator rights. REM REM The second run deletes the marker file first thing. If it is still there REM afterwards, the helper never got as far as starting it -- and this run REM carries on with its rights rather than leave the machine without a server. REM REM Keep `exit /b` the last command of every block it sits in. Anything after REM it in the same block, even a line that never runs, makes cmd /c exit 0 -- REM and then Phase 1 takes a failed second run for a good one and does not REM stop to show the error. set "SELF=%~f0" set "HERE=%~2" if "%~3"=="--unelevated" ( if defined UNELEVATED_MARK del "!UNELEVATED_MARK!" >nul 2>&1 ) else ( call :check_admin if !errorlevel! equ 0 ( call :check_node if !errorlevel! equ 0 call :check_git if !errorlevel! equ 0 ( set "UNELEVATED_ARGS=/d /c ""!SELF!" --relaunched "!HERE!" --unelevated"" set "UNELEVATED_MARK=!TEMP!\meisterpilze_unelevated_!RANDOM!.mark" type nul > "!UNELEVATED_MARK!" powershell -NoProfile -ExecutionPolicy Bypass -File "!HERE!scripts\unelevated.ps1" set "UNELEVATED_RC=!errorlevel!" if exist "!UNELEVATED_MARK!" ( del "!UNELEVATED_MARK!" >nul 2>&1 echo WARNING: could not drop administrator rights; continuing with them. ) else ( exit /b !UNELEVATED_RC! ) ) ) ) REM ============================================================ REM Check and auto-install prerequisites REM ============================================================ REM ---- Check for admin rights (needed for winget installs) ---- call :check_admin if !errorlevel! neq 0 ( call :check_node if !errorlevel! neq 0 ( echo. echo ERROR: Node.js is missing and this terminal is not running as Administrator. echo winget install requires admin rights. Please right-click START.bat echo and choose "Run as administrator", then try again. echo. exit /b 1 ) ) where winget >nul 2>&1 if !errorlevel! neq 0 ( set "HAS_WINGET=0" ) else ( set "HAS_WINGET=1" ) REM ---- Check / Install Node.js ---- call :check_node if !errorlevel! neq 0 ( echo. echo Node.js is not installed. if "!HAS_WINGET!"=="1" ( echo Installing Node.js automatically via winget... echo. winget install OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements if !errorlevel! neq 0 ( echo. echo ERROR: Automatic Node.js installation failed. echo Please install manually from https://nodejs.org exit /b 1 ) set "NEED_PATH_REFRESH=1" ) else ( echo ERROR: Cannot auto-install ^(winget not available^). echo Please install Node.js manually from https://nodejs.org echo Then run this script again. echo. exit /b 1 ) ) REM ---- Check / Install Git ---- call :check_git if !errorlevel! neq 0 ( echo. echo Git is not installed. if "!HAS_WINGET!"=="1" ( echo Installing Git automatically via winget... echo. winget install Git.Git --accept-source-agreements --accept-package-agreements if !errorlevel! neq 0 ( echo. echo ERROR: Automatic Git installation failed. echo Please install manually from https://git-scm.com exit /b 1 ) set "NEED_PATH_REFRESH=1" ) else ( echo WARNING: Git not available. ) ) REM ---- Refresh PATH if we installed anything ---- if "!NEED_PATH_REFRESH!"=="1" ( echo. echo Refreshing system PATH... call :refresh_path ) REM ---- Verify Node.js is now available ---- call :check_node if !errorlevel! neq 0 ( echo. echo ERROR: Node.js is still not found after installation. echo Please close this window, open a NEW command prompt, and run START.bat again. echo ^(Windows needs a new terminal to pick up the new PATH.^) echo. exit /b 1 ) for /f "tokens=*" %%v in ('node --version') do set "NODE_VER=%%v" echo -^> Node.js %NODE_VER% found. REM ---- Ensure PM2 ---- call :check_pm2 if !errorlevel! neq 0 ( echo PM2 not found, installing globally... call npm install -g pm2 if !errorlevel! neq 0 ( echo ERROR: Failed to install PM2. exit /b 1 ) call :refresh_path ) call :check_pm2 if !errorlevel! neq 0 ( echo ERROR: PM2 is still not found after installation. echo Please close this window, open a NEW command prompt, and run START.bat again. exit /b 1 ) REM The version comes from pm2's package.json, not from `pm2 --version`. Every REM pm2 command, --version included, connects to the daemon first and starts REM one when it cannot -- so with the boot instance's daemon out of reach, each REM run left a second daemon behind that could never take over the pipe. Step 5 REM checks the daemon before the first pm2 command. set "PM2_VER=" set "PM2_PKG=" for /f "delims=" %%P in ('where pm2.cmd 2^>nul') do if not defined PM2_PKG set "PM2_PKG=%%~dpPnode_modules\pm2\package.json" if defined PM2_PKG if exist "!PM2_PKG!" ( for /f "delims=" %%v in ('node -p "require(process.argv[1]).version" "!PM2_PKG!" 2^>nul') do set "PM2_VER=%%v" ) if defined PM2_VER ( echo -^> PM2 !PM2_VER! found. ) else ( echo -^> PM2 found. ) REM ============================================================ REM Step 2: Install dependencies REM ============================================================ echo. echo [2/5] Installing dependencies... call npm install --omit=dev if !errorlevel! neq 0 ( echo ERROR: npm install failed. exit /b 1 ) REM ============================================================ REM Step 3: Backup data REM ============================================================ echo. echo [3/5] Backing up data... if not exist "backups" mkdir "backups" for /f "tokens=*" %%T in ('powershell -NoProfile -Command "Get-Date -Format yyyyMMdd_HHmmss"') do set "TIMESTAMP=%%T" if not defined TIMESTAMP set "TIMESTAMP=backup" if exist "meistertracker.db" ( set "BACKUP_FILE=backups\meistertracker_!TIMESTAMP!.db" set "BACKUP_OK=0" REM node runs the server, so it is always here. The sqlite3 CLI is not: REM Git for Windows does not ship it, so the raw-copy branch below was not REM a fallback at all, it was the path that always ran. And `copy` takes REM meistertracker.db alone: in WAL mode that file is only as current as the REM last checkpoint, so every snapshot silently dropped whatever was still REM sitting in meistertracker.db-wal. node "scripts\backup-db.js" "meistertracker.db" "!BACKUP_FILE!" >nul 2>&1 if !errorlevel! equ 0 ( echo -^> meistertracker.db backed up ^(WAL-consistent^). set "BACKUP_OK=1" ) if "!BACKUP_OK!"=="0" ( where sqlite3 >nul 2>&1 if !errorlevel! equ 0 ( sqlite3 meistertracker.db ".backup '!BACKUP_FILE!'" >nul 2>&1 if !errorlevel! equ 0 ( echo -^> meistertracker.db backed up ^(WAL-consistent via sqlite3^). set "BACKUP_OK=1" ) ) ) if "!BACKUP_OK!"=="0" ( copy /y "meistertracker.db" "!BACKUP_FILE!" >nul echo -^> WARNING: raw file copy only. Anything still in meistertracker.db-wal echo is NOT in this snapshot. Check that scripts\backup-db.js is present. ) ) else ( echo -^> No meistertracker.db found, skipping backup. ) REM The photos are not zipped here any more. Every start zipped all of REM data\photos again, onto this same disk, and no zip was ever deleted: REM the same pictures piling up until the disk is full. The server keeps a REM second copy under backups\photos as hard links, which cost no space, and REM the USB stick backup copies each photo once. An update does not touch REM photo files anyway; what it changes is the database, saved above. REM ============================================================ REM Step 4: Ensure TLS certificates REM ============================================================ echo. echo [4/5] Ensuring TLS certificates... call :ensure_certs REM ============================================================ REM Step 5: Start / Restart server via PM2 REM ============================================================ echo. echo [5/5] Starting server... REM ---- Can this window send pm2 a command at all? ---- REM Every pm2 command connects to the daemon over a named pipe first. A daemon REM started with administrator rights owns that pipe as Administrators, and REM this window may then read it but not write to it: pm2 fails with REM "connect EPERM //./pipe/rpc.sock" and a stack trace, which reads like the REM application crashing, after starting a second daemon that can never take REM the pipe over. So ask before the first pm2 command, and say what it is. REM Worktree runs ask too -- there is one daemon per machine, and they use it. set "PM2_ACCESS=" for /f "delims=" %%A in ('powershell -NoProfile -ExecutionPolicy Bypass -File "%CD%\scripts\pm2-access.ps1" 2^>nul') do set "PM2_ACCESS=%%A" if "!PM2_ACCESS!"=="denied" ( echo. echo ERROR: pm2 runs with administrator rights, and this window has none. echo. echo Every pm2 command from here is refused, so the server can be neither echo stopped nor updated. Its daemon was started elevated: by the boot echo task, before this script learned to drop those rights, or from a echo window run as administrator. echo. echo Once, in an Administrator PowerShell: echo ^& "$env:APPDATA\npm\pm2.cmd" kill echo then run this script again. echo. echo If this is back after the next restart, the boot run could not drop echo its rights. It wrote down why in %TEMP%\meistertracker-unelevated.log echo. pause exit /b 1 ) REM Match update_server.sh: always delete + start with --update-env so the REM new process picks up any .env changes made since the last launch. REM Through pm2 first, before anything is killed on the port: a process pm2 REM runs, killed from outside, is one pm2 starts straight back up. call pm2 describe %PM2_PROCESS_NAME% >nul 2>&1 if !errorlevel! equ 0 ( echo -^> Process found, deleting for clean restart... call pm2 delete %PM2_PROCESS_NAME% >nul 2>&1 ) REM Then kill whatever else still holds our port, and look again rather than REM assume: taskkill's refusal goes to nul, and a refused kill printed exactly REM what a successful one does. REM In worktree mode skip this entirely so a parallel run never reaches REM across into the production instance on a different port. if "%WORKTREE_MODE%"=="1" ( echo -^> Worktree mode: skipping stale-port cleanup ^(prod on 3000 untouched^). ) else ( for /f "tokens=5" %%P in ('netstat -ano 2^>nul ^| findstr /r "0.0.0.0:%PORT%.*LISTENING"') do ( echo -^> Killing stale process on port %PORT% ^(PID %%P^)... taskkill /PID %%P /F >nul 2>&1 ) set "PORT_HOLDER=" for /f "tokens=5" %%P in ('netstat -ano 2^>nul ^| findstr /r "0.0.0.0:%PORT%.*LISTENING"') do set "PORT_HOLDER=%%P" if defined PORT_HOLDER ( set "HOLDER_NAME=unknown program" for /f "tokens=1 delims=," %%N in ('tasklist /FI "PID eq !PORT_HOLDER!" /FO CSV /NH 2^>nul') do set "HOLDER_NAME=%%~N" echo. echo ERROR: port %PORT% is held by PID !PORT_HOLDER! ^(!HOLDER_NAME!^), and this window cannot stop it. echo. echo It is not run by the pm2 this window talks to - that one was asked echo first. To stop it, in an Administrator PowerShell: echo Stop-Process -Id !PORT_HOLDER! -Force echo then run this script again. echo. pause exit /b 1 ) ) echo -^> Starting instance... call pm2 start server.js --name %PM2_PROCESS_NAME% --update-env call pm2 save >nul 2>&1 REM Wait briefly for the process to initialize, then verify it stayed up ping -n 4 127.0.0.1 >nul 2>&1 call pm2 show %PM2_PROCESS_NAME% 2>nul | findstr /i /c:"online" >nul 2>&1 if !errorlevel! neq 0 ( echo. echo ERROR: Server process crashed on startup. echo. echo Recent error log: call pm2 logs %PM2_PROCESS_NAME% --lines 15 --nostream --err 2>nul echo. exit /b 1 ) REM Tag this commit as stable (last known-good deployment). REM Skip in worktree mode — that tag is for prod only and must not move REM when running a feature-branch instance. if not "%WORKTREE_MODE%"=="1" ( where git >nul 2>&1 if !errorlevel! equ 0 ( git tag -f stable HEAD >nul 2>&1 if !errorlevel! equ 0 ( echo -^> Tagged current commit as 'stable'. ) ) ) echo. echo ======================================== echo Server started successfully! if "%WORKTREE_MODE%"=="1" ( echo [WORKTREE INSTANCE - production on 3000 is separate] ) if exist "certs\server.crt" ( echo URL: https://localhost:%PORT% ) else ( echo URL: http://localhost:%PORT% echo. echo WARNING: No TLS certs found, running HTTP only. echo iOS camera scanning requires HTTPS. ) echo Run 'pm2 logs %PM2_PROCESS_NAME%' to see output. echo ======================================== echo. pause exit /b 0 REM ============================================================ REM Helper functions REM ============================================================ :check_node where node >nul 2>&1 exit /b %errorlevel% :check_git where git >nul 2>&1 exit /b %errorlevel% :check_pm2 where pm2 >nul 2>&1 exit /b %errorlevel% :check_admin REM Listing file system filters is for administrators only. Unlike REM `net session`, fltmc does not need the Server service to be running. fltmc >nul 2>&1 exit /b %errorlevel% :ensure_certs if exist "certs\server.key" if exist "certs\server.crt" ( echo -^> TLS certificates found. REM LE cert renewal is handled by the server on startup. exit /b 0 ) if exist "gen-cert.ps1" ( echo -^> TLS certificates missing, generating... if defined CERT_DOMAIN ( echo -^> Including domain in cert: %CERT_DOMAIN% powershell -NoProfile -ExecutionPolicy Bypass -File "%CD%\gen-cert.ps1" -Domain "%CERT_DOMAIN%" ) else ( powershell -NoProfile -ExecutionPolicy Bypass -File "%CD%\gen-cert.ps1" ) if exist "certs\server.key" if exist "certs\server.crt" ( echo -^> TLS certificates generated successfully. exit /b 0 ) echo -^> WARNING: Certificate generation failed. ) if exist "gen-cert.sh" ( where bash >nul 2>&1 if !errorlevel! equ 0 ( echo -^> Trying gen-cert.sh... bash "%CD%/gen-cert.sh" if exist "certs\server.key" if exist "certs\server.crt" ( echo -^> TLS certificates generated successfully. exit /b 0 ) ) ) echo -^> WARNING: Could not generate TLS certificates. echo Server will start in HTTP-only mode ^(mobile camera will not work^). exit /b 0 :refresh_path set "NEWPATH=" for /f "tokens=2*" %%A in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v Path 2^>nul') do set "NEWPATH=%%B" for /f "tokens=2*" %%A in ('reg query "HKCU\Environment" /v Path 2^>nul') do ( if defined NEWPATH ( set "NEWPATH=!NEWPATH!;%%B" ) else ( set "NEWPATH=%%B" ) ) if defined NEWPATH ( set "PATH=!NEWPATH!;%SystemRoot%\system32;%SystemRoot%" ) if exist "%ProgramFiles%\nodejs" set "PATH=!PATH!;%ProgramFiles%\nodejs" if exist "%ProgramFiles%\Git\cmd" set "PATH=!PATH!;%ProgramFiles%\Git\cmd" if exist "%ProgramFiles%\Git\usr\bin" set "PATH=!PATH!;%ProgramFiles%\Git\usr\bin" if exist "%LOCALAPPDATA%\Programs\Git\cmd" set "PATH=!PATH!;%LOCALAPPDATA%\Programs\Git\cmd" if exist "%APPDATA%\npm" set "PATH=!PATH!;%APPDATA%\npm" exit /b 0