diff --git a/scripts/ctrl.bat b/scripts/ctrl.bat new file mode 100644 index 0000000..2fcd53e --- /dev/null +++ b/scripts/ctrl.bat @@ -0,0 +1,224 @@ +@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 - 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 ---- 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 +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