@echo off setlocal EnableExtensions EnableDelayedExpansion rem ============================================================ rem Cyrene v2 local control script (Windows) - equivalent to scripts/ctrl.sh rem rem Usage: rem scripts\ctrl.bat start (default) rem scripts\ctrl.bat start start rem scripts\ctrl.bat restart restart rem scripts\ctrl.bat stop stop rem scripts\ctrl.bat status show status rem scripts\ctrl.bat build build cyrene.exe into the repo root rem rem Equivalent to ctrl.sh: rem - single binary, run from the repo root (reads .env PORT / DATA_DIR ...) rem - port comes from .env PORT, falls back to 10080 rem - log: %USERPROFILE%\cy.log (app logs to stderr); rem stdout fallback: %USERPROFILE%\cy.stdout.log rem - the previous run's logs are NEVER discarded: before each start they are rem archived as ..bak, newest %LOG_KEEP% kept. Needed rem because Start-Process -RedirectStandard* OVERWRITES its target (PowerShell rem has no append form), so every restart used to wipe the evidence in cy.log. rem - pid file: %USERPROFILE%\.cyrene.pid, else detected from the port rem - Start-Process -WindowStyle Hidden = detached start rem (the ctrl.sh equivalent of "setsid nohup ... & disown") rem - stop tries a graceful kill first, then force-kills (like kill, then kill -9) rem - health: GET http://127.0.0.1:/api/v1/health rem rem NOTE: build the frontend first (cd frontend\web && npm run build), rem otherwise the binary falls back to its embedded dist. rem rem This file is intentionally ASCII-only with CRLF line endings: batch rem parsing is codepage-sensitive, and cmd.exe requires CRLF. Windows tools rem are called by absolute path under %SystemRoot%\System32 so the script rem also works when an MSYS/MinGW PATH (Git Bash) comes first. rem ============================================================ set "SYS=%SystemRoot%\System32" rem ---- paths ---- set "REPO=%~dp0.." for %%I in ("%REPO%") do set "REPO=%%~fI" set "APP_BIN=%REPO%\cyrene.exe" if not exist "%APP_BIN%" set "APP_BIN=%USERPROFILE%\apps\cyrene\cyrene.exe" set "PID_FILE=%USERPROFILE%\.cyrene.pid" set "LOG_FILE=%USERPROFILE%\cy.log" set "STDOUT_FILE=%USERPROFILE%\cy.stdout.log" rem LOG_KEEP: how many archived logs to retain (one per start, oldest pruned first) set "LOG_KEEP=20" rem ---- port: .env PORT wins, fallback 10080 ---- set "GW_PORT=" if exist "%REPO%\.env" ( for /f "usebackq tokens=1,* delims==" %%A in ("%REPO%\.env") do ( if /i "%%A"=="PORT" if not defined GW_PORT set "GW_PORT=%%B" ) ) if not defined GW_PORT set "GW_PORT=10080" set "GW_PORT=%GW_PORT: =%" set "GW_PORT=%GW_PORT:"=%" set "HEALTH_URL=http://127.0.0.1:%GW_PORT%/api/v1/health" rem ---- dispatch ---- set "ACTION=%~1" if "%ACTION%"=="" set "ACTION=start" if /i "%ACTION%"=="build" goto :do_build if /i "%ACTION%"=="start" goto :do_start if /i "%ACTION%"=="stop" goto :do_stop if /i "%ACTION%"=="restart" goto :do_restart if /i "%ACTION%"=="status" goto :do_status echo Usage: scripts\ctrl.bat [start^|stop^|restart^|status^|build] (no arg = start) exit /b 1 rem ============================================================ rem build rem ============================================================ :do_build cd /d "%REPO%" || exit /b 1 echo Building -^> %REPO%\cyrene.exe go build -ldflags="-s -w" -trimpath -o cyrene.exe ./cmd/cyrene/ if errorlevel 1 ( echo Build FAILED. exit /b 1 ) echo Done. Note: if the frontend changed, run "cd frontend\web ^&^& npm run build" first. exit /b 0 rem ============================================================ rem start rem ============================================================ :do_start if not exist "%SYS%\curl.exe" ( echo [ERROR] %SYS%\curl.exe not found - the health check needs it. exit /b 1 ) call :is_running if defined RUNNING_PID ( echo cyrene is already running ^(PID !RUNNING_PID!^). Use "restart" to restart. exit /b 0 ) if not exist "%APP_BIN%" ( echo [ERROR] binary not found: %APP_BIN% echo build it first: scripts\ctrl.bat build exit /b 1 ) echo Starting cyrene -^> %APP_BIN% ^(port %GW_PORT%^) cd /d "%REPO%" || exit /b 1 call :rotate_logs powershell -NoProfile -Command "Start-Process -FilePath '%APP_BIN%' -WorkingDirectory '%REPO%' -RedirectStandardOutput '%STDOUT_FILE%' -RedirectStandardError '%LOG_FILE%' -WindowStyle Hidden" set /a TRIES=0 :start_wait set /a TRIES+=1 call :healthy if not errorlevel 1 ( echo Started: http://localhost:%GW_PORT% ^(log: %LOG_FILE%^) call :record_pid exit /b 0 ) if !TRIES! GEQ 20 ( echo [ERROR] port %GW_PORT% not ready after start - check %LOG_FILE% exit /b 1 ) "%SYS%\ping.exe" -n 2 127.0.0.1 >nul goto :start_wait rem ============================================================ rem stop rem ============================================================ :do_stop call :is_running if not defined RUNNING_PID ( echo cyrene is not running ^(no pid file, or the process exited^). del "%PID_FILE%" >nul 2>&1 exit /b 0 ) echo Sending stop signal -^> PID !RUNNING_PID! "%SYS%\taskkill.exe" /PID !RUNNING_PID! /T >nul 2>&1 set /a TRIES=0 :stop_wait set /a TRIES+=1 if !TRIES! GEQ 4 ( echo Did not exit in ~3s - force killing. "%SYS%\taskkill.exe" /PID !RUNNING_PID! /T /F >nul 2>&1 del "%PID_FILE%" >nul 2>&1 exit /b 0 ) "%SYS%\ping.exe" -n 2 127.0.0.1 >nul call :is_running if not defined RUNNING_PID ( echo Stopped. del "%PID_FILE%" >nul 2>&1 exit /b 0 ) goto :stop_wait rem ============================================================ rem restart rem ============================================================ :do_restart echo Restarting cyrene... call :do_stop "%SYS%\ping.exe" -n 2 127.0.0.1 >nul call :do_start exit /b %errorlevel% rem ============================================================ rem status rem ============================================================ :do_status call :is_running call :healthy if not errorlevel 1 ( echo cyrene: running ^(PID !RUNNING_PID!^) . port %GW_PORT% . health ok exit /b 0 ) if defined RUNNING_PID ( echo cyrene: process alive ^(PID !RUNNING_PID!^) but health not ready ^(%HEALTH_URL%^) exit /b 1 ) echo cyrene: not running exit /b 1 rem ============================================================ rem helpers rem ============================================================ rem healthy: errorlevel 0 = HTTP 2xx :healthy "%SYS%\curl.exe" -s -f --max-time 2 -o NUL "%HEALTH_URL%" >nul 2>&1 exit /b %errorlevel% rem find_pid_by_port -> PORT_PID (the ctrl.sh "ss" fallback) :find_pid_by_port set "PORT_PID=" set "NS_FILE=%TEMP%\cyrene-netstat-%RANDOM%.txt" "%SYS%\netstat.exe" -ano -p tcp > "%NS_FILE%" 2>nul for /f "tokens=2,5" %%A in ('%SYS%\findstr.exe /C:"LISTENING" "%NS_FILE%"') do ( if not defined PORT_PID ( echo %%A| "%SYS%\findstr.exe" /E /C:":%GW_PORT%" >nul && set "PORT_PID=%%B" ) ) del "%NS_FILE%" >nul 2>&1 exit /b 0 rem is_running -> RUNNING_PID (pid file first, then port probe) :is_running set "RUNNING_PID=" if exist "%PID_FILE%" ( set "STORED=" set /p STORED=<"%PID_FILE%" if defined STORED ( set "ALIVE=" for /f "tokens=2 delims=," %%P in ('%SYS%\tasklist.exe /FI "PID eq !STORED!" /NH /FO CSV 2^>nul') do ( if not defined ALIVE set "ALIVE=1" ) if defined ALIVE set "RUNNING_PID=!STORED!" ) ) if not defined RUNNING_PID ( call :find_pid_by_port if defined PORT_PID set "RUNNING_PID=!PORT_PID!" ) exit /b 0 rem record_pid: write the detected pid to the pid file :record_pid call :find_pid_by_port if defined PORT_PID ( >"%PID_FILE%" echo !PORT_PID! ) exit /b 0 rem rotate_logs: keep the previous run's log instead of letting it be overwritten. rem Start-Process -RedirectStandardOutput/-RedirectStandardError OVERWRITE their rem target (PowerShell has no append form), so without this every restart wiped rem cy.log -- i.e. each restart destroyed the evidence of the run before it. rem Archiving (not appending) also keeps the runs separable. Failures are ignored rem on purpose: a log we cannot archive must never block a start. :rotate_logs set "RUN_TS=" for /f "usebackq delims=" %%T in (`powershell -NoProfile -Command "Get-Date -Format yyyyMMdd-HHmmss"`) do if not defined RUN_TS set "RUN_TS=%%T" if not defined RUN_TS set "RUN_TS=unknown%RANDOM%" if exist "%LOG_FILE%" move /y "%LOG_FILE%" "%LOG_FILE%.%RUN_TS%.bak" >nul 2>&1 if exist "%STDOUT_FILE%" move /y "%STDOUT_FILE%" "%STDOUT_FILE%.%RUN_TS%.bak" >nul 2>&1 call :prune_logs "%LOG_FILE%" call :prune_logs "%STDOUT_FILE%" exit /b 0 rem prune_logs : keep only the newest %LOG_KEEP% archives of . rem Runs with %USERPROFILE% as the working directory so the bare names dir prints rem resolve (the repo may sit on another drive than %USERPROFILE%; pushd/popd then rem echo the directory, hence the >nul). :prune_logs set "PRUNE_BASE=%~1" pushd "%USERPROFILE%" >nul 2>&1 || exit /b 0 for /f "skip=%LOG_KEEP% delims=" %%F in ('dir /b /o-d /a-d "%PRUNE_BASE%.*.bak" 2^>nul') do del /q "%%~F" >nul 2>&1 popd >nul 2>&1 exit /b 0