fix: tunnel stability, NSSM service installer, cleanup old service scripts

This commit is contained in:
ssbg04 committed 2026-09-01 00:49:23 +08:00
1 parent 906df823b5
commit 3dcad8878b
10 files changed
+30890 -472

No files matched your search

@@ -1,37 +0,0 @@
@echo off
:: Check for Admin Privileges
net session >nul 2>&1
if %errorlevel% neq 0 (
echo =========================================================================
echo [ATTENTION] Administrator privileges required!
echo Please right-click this file and select "Run as administrator".
echo =========================================================================
echo.
pause
exit /b 1
)
echo =========================================================================
echo Fixing Cloudflare Tunnel: Removing Conflicting Windows Background Service
echo =========================================================================
echo.
echo [1/3] Stopping existing Cloudflared service...
net stop Cloudflared >nul 2>&1
sc stop Cloudflared >nul 2>&1
echo [2/3] Uninstalling old Cloudflared Windows service...
"%~dp0bin\cloudflared.exe" service uninstall >nul 2>&1
sc delete Cloudflared >nul 2>&1
echo [3/3] Terminating any stale background cloudflared processes...
taskkill /F /IM cloudflared.exe >nul 2>&1
echo.
echo =========================================================================
echo [SUCCESS] Conflicting service removed!
echo The TIS RMS server / start_tunnel will now handle the tunnel cleanly
echo without any 502 Bad Gateway or socket collisions.
echo =========================================================================
echo.
pause
+183
View File
@@ -0,0 +1,183 @@
@echo off
setlocal EnableDelayedExpansion
:: ============================================================
:: TIS RMS - NSSM Service Installer
:: Installs Node.js backend as a Windows Service using NSSM.
:: Also fixes ghost Cloudflared service and firewall rules.
:: Run as Administrator (right-click -> Run as administrator)
:: ============================================================
NET SESSION >nul 2>&1
IF %ERRORLEVEL% NEQ 0 (
echo [ERROR] This script must be run as Administrator.
echo Right-click the file and choose "Run as administrator"
pause
exit /b 1
)
:: ── Config ────────────────────────────────────────────────────
set BACKEND_DIR=F:\SumbrerongBato\tis_rms_server\backend
set NSSM=%BACKEND_DIR%\bin\nssm.exe
set CLOUDFLARED=%BACKEND_DIR%\bin\cloudflared.exe
set SERVICE_NAME=TIS.RMS.ServerService
set LOG_DIR=%BACKEND_DIR%\logs
set NODE_PORT=18484
echo.
echo ============================================================
echo TIS RMS - NSSM Service Installer
echo ============================================================
echo.
:: ── 0. Verify NSSM exists ────────────────────────────────────
IF NOT EXIST "%NSSM%" (
echo [ERROR] nssm.exe not found at: %NSSM%
pause
exit /b 1
)
echo [OK] NSSM found: %NSSM%
:: ── 1. Find node.exe ─────────────────────────────────────────
echo.
echo [1/7] Locating node.exe...
set NODE_EXE=
:: Check bundled node first
IF EXIST "%BACKEND_DIR%\node.exe" set NODE_EXE=%BACKEND_DIR%\node.exe
IF NOT DEFINED NODE_EXE IF EXIST "C:\Program Files\nodejs\node.exe" set NODE_EXE=C:\Program Files\nodejs\node.exe
IF NOT DEFINED NODE_EXE IF EXIST "C:\Program Files (x86)\nodejs\node.exe" set NODE_EXE=C:\Program Files (x86)\nodejs\node.exe
:: Try PATH lookup
IF NOT DEFINED NODE_EXE (
for /f "delims=" %%i in ('where node.exe 2^>nul') do (
IF NOT DEFINED NODE_EXE set NODE_EXE=%%i
)
)
IF NOT DEFINED NODE_EXE (
echo [ERROR] node.exe not found. Install Node.js from https://nodejs.org first.
pause
exit /b 1
)
echo [OK] Found: %NODE_EXE%
:: ── 2. Remove ghost Cloudflared Windows Service ──────────────
echo.
echo [2/7] Checking for ghost Cloudflared Windows service...
sc query Cloudflared >nul 2>&1
IF %ERRORLEVEL% EQU 0 (
echo Found! Stopping and removing...
sc stop Cloudflared >nul 2>&1
timeout /t 3 /nobreak >nul
sc delete Cloudflared >nul 2>&1
echo [OK] Ghost Cloudflared service removed.
) ELSE (
echo [OK] No ghost service found.
)
:: ── 3. Stop and remove existing TIS RMS service (clean reinstall) ──
echo.
echo [3/7] Removing existing "%SERVICE_NAME%" service (if any)...
sc query "%SERVICE_NAME%" >nul 2>&1
IF %ERRORLEVEL% EQU 0 (
echo Stopping existing service...
"%NSSM%" stop "%SERVICE_NAME%" >nul 2>&1
timeout /t 3 /nobreak >nul
"%NSSM%" remove "%SERVICE_NAME%" confirm >nul 2>&1
echo [OK] Old service removed.
) ELSE (
echo [OK] No existing service to remove.
)
:: ── 4. Create logs directory ──────────────────────────────────
echo.
echo [4/7] Creating logs directory...
IF NOT EXIST "%LOG_DIR%" mkdir "%LOG_DIR%"
echo [OK] %LOG_DIR%
:: ── 5. Install service via NSSM ───────────────────────────────
echo.
echo [5/7] Installing "%SERVICE_NAME%" via NSSM...
"%NSSM%" install "%SERVICE_NAME%" "%NODE_EXE%"
"%NSSM%" set "%SERVICE_NAME%" AppDirectory "%BACKEND_DIR%"
"%NSSM%" set "%SERVICE_NAME%" AppParameters "index.js"
:: Logging
"%NSSM%" set "%SERVICE_NAME%" AppStdout "%LOG_DIR%\server.log"
"%NSSM%" set "%SERVICE_NAME%" AppStderr "%LOG_DIR%\server.log"
"%NSSM%" set "%SERVICE_NAME%" AppRotateFiles 1
"%NSSM%" set "%SERVICE_NAME%" AppRotateBytes 5242880
:: Auto-start on boot
"%NSSM%" set "%SERVICE_NAME%" Start SERVICE_AUTO_START
:: Restart on crash: 5s, 10s, 30s
"%NSSM%" set "%SERVICE_NAME%" AppExit Default Restart
"%NSSM%" set "%SERVICE_NAME%" AppRestartDelay 5000
:: Display name and description
"%NSSM%" set "%SERVICE_NAME%" DisplayName "TIS RMS Backend Server"
"%NSSM%" set "%SERVICE_NAME%" Description "TIS Record Management System - Node.js Backend API Server with Cloudflare Tunnel"
:: Shutdown: send Ctrl+C then wait 10s before kill
"%NSSM%" set "%SERVICE_NAME%" AppStopMethodSkip 0
"%NSSM%" set "%SERVICE_NAME%" AppStopMethodConsole 5000
"%NSSM%" set "%SERVICE_NAME%" AppStopMethodWindow 0
"%NSSM%" set "%SERVICE_NAME%" AppStopMethodThreads 5000
echo [OK] Service installed.
:: ── 6. Configure firewall rules ───────────────────────────────
echo.
echo [6/7] Configuring Windows Firewall...
:: cloudflared.exe - all profiles
netsh advfirewall firewall delete rule name="TIS-RMS-Cloudflared" >nul 2>&1
netsh advfirewall firewall add rule name="TIS-RMS-Cloudflared" dir=in action=allow program="%CLOUDFLARED%" enable=yes profile=any >nul 2>&1
netsh advfirewall firewall add rule name="TIS-RMS-Cloudflared" dir=out action=allow program="%CLOUDFLARED%" enable=yes profile=any >nul 2>&1
:: Node port - all profiles
netsh advfirewall firewall delete rule name="TIS-RMS-NodeServer-Inbound" >nul 2>&1
netsh advfirewall firewall delete rule name="TIS-RMS-NodeServer-Outbound" >nul 2>&1
netsh advfirewall firewall add rule name="TIS-RMS-NodeServer-Inbound" dir=in action=allow protocol=TCP localport=%NODE_PORT% enable=yes profile=any >nul 2>&1
netsh advfirewall firewall add rule name="TIS-RMS-NodeServer-Outbound" dir=out action=allow protocol=TCP localport=%NODE_PORT% enable=yes profile=any >nul 2>&1
:: URL ACL reservation (allows non-admin binding)
netsh http add urlacl url=http://+:%NODE_PORT%/ user=Everyone >nul 2>&1
echo [OK] Firewall rules set for all network profiles (Public/Private/Domain).
:: ── 7. Start the service ──────────────────────────────────────
echo.
echo [7/7] Starting service...
"%NSSM%" start "%SERVICE_NAME%"
timeout /t 4 /nobreak >nul
:: Verify
sc query "%SERVICE_NAME%" | findstr /i "RUNNING" >nul 2>&1
IF %ERRORLEVEL% EQU 0 (
echo [OK] Service is RUNNING.
) ELSE (
echo [WARN] Service may still be starting up. Check status in the Server Manager app.
)
:: ── Done ──────────────────────────────────────────────────────
echo.
echo ============================================================
echo Installation Complete!
echo.
echo Service Name : %SERVICE_NAME%
echo Node.js : %NODE_EXE%
echo Backend Dir : %BACKEND_DIR%
echo Logs : %LOG_DIR%\server.log
echo Port : %NODE_PORT%
echo Auto-Start : Yes (starts on Windows boot)
echo.
echo The WPF Server Manager can now Start/Stop/Restart the
echo service using the Service page.
echo.
echo To uninstall later, run: uninstall_service.bat
echo ============================================================
echo.
pause
File diff suppressed because it is too large. Load diff
+7 -8
View File
@@ -5,15 +5,14 @@
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon index.js",
"service:install": "scripts\\install_service.bat",
"service:uninstall": "scripts\\uninstall_service.bat",
"service:manage": "scripts\\manage_service.bat",
"service:start": "net start TIS_RMSServer",
"service:stop": "net stop TIS_RMSServer",
"service:status": "sc query TIS_RMSServer",
"tunnel": "bin\\cloudflared.exe tunnel --url http://localhost:18484"
"test": "echo \"Error: no test specified\" && exit 1",
"service:install": "install_service.bat",
"service:uninstall": "uninstall_service.bat",
"service:start": "bin\\nssm.exe start TIS.RMS.ServerService",
"service:stop": "bin\\nssm.exe stop TIS.RMS.ServerService",
"service:restart": "bin\\nssm.exe restart TIS.RMS.ServerService",
"service:status": "sc query TIS.RMS.ServerService"
},
"keywords": [],
"author": "",
-106
View File
@@ -1,106 +0,0 @@
@echo off
:: ============================================================================
:: TIS_RMS Backend - Windows Service Installer (NSSM / SC)
:: ============================================================================
SETLOCAL EnableDelayedExpansion
title TIS RMS Server - Windows Service Setup
:: Check for Administrator Privileges
net session >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] This script requires Administrator privileges.
echo Please right-click install_service.bat and select "Run as administrator".
echo.
pause
exit /b 1
)
SET SERVICE_NAME=TIS_RMSServer
SET DISPLAY_NAME=TIS RMS Server Manager Service
SET DESCRIPTION=Backend Node.js service for TIS RMS (School Record Management System).
SET BACKEND_DIR=%~dp0..
pushd "%BACKEND_DIR%"
SET BACKEND_DIR=%CD%
popd
echo ============================================================================
echo Installing %DISPLAY_NAME% (%SERVICE_NAME%)...
echo Directory: %BACKEND_DIR%
echo ============================================================================
:: Locate Node.js executable
FOR /F "tokens=*" %%g IN ('where node 2^>nul') DO (
SET NODE_EXE=%%g
GOTO :NODE_FOUND
)
:NODE_FOUND
IF "%NODE_EXE%"=="" (
IF EXIST "C:\Program Files\nodejs\node.exe" (
SET "NODE_EXE=C:\Program Files\nodejs\node.exe"
) ELSE (
echo [ERROR] Node.js not found in PATH or standard location.
echo Please install Node.js before proceeding.
pause
exit /b 1
)
)
echo [OK] Using Node.js at: %NODE_EXE%
:: Ensure logs directory exists
IF NOT EXIST "%BACKEND_DIR%\logs" mkdir "%BACKEND_DIR%\logs"
:: Check if NSSM executable exists in backend or PATH
SET NSSM_EXE=
IF EXIST "%BACKEND_DIR%\bin\nssm.exe" (
SET "NSSM_EXE=%BACKEND_DIR%\bin\nssm.exe"
) ELSE (
FOR /F "tokens=*" %%g IN ('where nssm 2^>nul') DO (
SET NSSM_EXE=%%g
GOTO :NSSM_FOUND
)
)
:NSSM_FOUND
IF NOT "%NSSM_EXE%"=="" (
echo [OK] Using NSSM binary at: %NSSM_EXE%
:: Stop and remove existing service if present
"%NSSM_EXE%" stop %SERVICE_NAME% >nul 2>&1
"%NSSM_EXE%" remove %SERVICE_NAME% confirm >nul 2>&1
:: Install service using NSSM
"%NSSM_EXE%" install %SERVICE_NAME% "%NODE_EXE%" "\"%BACKEND_DIR%\index.js\""
"%NSSM_EXE%" set %SERVICE_NAME% AppDirectory "%BACKEND_DIR%"
"%NSSM_EXE%" set %SERVICE_NAME% DisplayName "%DISPLAY_NAME%"
"%NSSM_EXE%" set %SERVICE_NAME% Description "%DESCRIPTION%"
"%NSSM_EXE%" set %SERVICE_NAME% Start SERVICE_AUTO_START
"%NSSM_EXE%" set %SERVICE_NAME% AppStdout "%BACKEND_DIR%\logs\service_out.log"
"%NSSM_EXE%" set %SERVICE_NAME% AppStderr "%BACKEND_DIR%\logs\service_err.log"
"%NSSM_EXE%" set %SERVICE_NAME% AppRotateFiles 1
"%NSSM_EXE%" set %SERVICE_NAME% AppRotateBytes 10485760
:: Start the service
echo Starting service %SERVICE_NAME%...
"%NSSM_EXE%" start %SERVICE_NAME%
) ELSE (
echo [INFO] NSSM executable not found. Using Windows Service Control (sc.exe) fallback...
:: Use PowerShell to create/register Windows Service if nssm is not present
powershell -Command "New-Service -Name '%SERVICE_NAME%' -BinaryPathName '\"%NODE_EXE%\" \"%BACKEND_DIR%\index.js\"' -DisplayName '%DISPLAY_NAME%' -Description '%DESCRIPTION%' -StartupType Automatic" >nul 2>&1
sc start %SERVICE_NAME% >nul 2>&1
)
echo.
echo ============================================================================
echo [SUCCESS] %SERVICE_NAME% installed successfully!
echo Service status:
sc query %SERVICE_NAME% | findstr /I "STATE"
echo ============================================================================
echo.
pause
-118
View File
@@ -1,118 +0,0 @@
@echo off
:: ============================================================================
:: TIS_RMS Backend - Interactive Windows Service Control CLI
:: ============================================================================
SETLOCAL EnableDelayedExpansion
title TIS RMS Server Manager CLI
SET SERVICE_NAME=TIS_RMSServer
SET BACKEND_DIR=%~dp0..
pushd "%BACKEND_DIR%"
SET BACKEND_DIR=%CD%
popd
:: Check for Administrator Privileges
net session >nul 2>&1
if %errorlevel% neq 0 (
echo [WARNING] Running without Administrator privileges.
echo Some actions (Install, Uninstall, Start, Stop) may fail if user isn't Admin.
echo.
)
:MENU
cls
echo ============================================================================
echo TIS RMS SERVER - WINDOWS SERVICE CONTROL
echo ============================================================================
echo Service Name: %SERVICE_NAME%
echo Backend Path: %BACKEND_DIR%
echo ----------------------------------------------------------------------------
echo Current Status:
sc query %SERVICE_NAME% | findstr /I "STATE"
echo ============================================================================
echo.
echo [1] Start Service (net start %SERVICE_NAME%)
echo [2] Stop Service (net stop %SERVICE_NAME%)
echo [3] Restart Service (nssm restart %SERVICE_NAME%)
echo [4] Service Status Details (sc query %SERVICE_NAME%)
echo [5] Install Service (Run install_service.bat)
echo [6] Uninstall Service (Run uninstall_service.bat)
echo [7] View Service Logs (Open stdout/stderr log files)
echo [8] Exit
echo.
SET /P CHOICE="Enter your choice [1-8]: "
IF "%CHOICE%"=="1" GOTO START_SVC
IF "%CHOICE%"=="2" GOTO STOP_SVC
IF "%CHOICE%"=="3" GOTO RESTART_SVC
IF "%CHOICE%"=="4" GOTO STATUS_SVC
IF "%CHOICE%"=="5" GOTO INSTALL_SVC
IF "%CHOICE%"=="6" GOTO UNINSTALL_SVC
IF "%CHOICE%"=="7" GOTO LOGS_SVC
IF "%CHOICE%"=="8" GOTO END
echo Invalid choice. Try again.
timeout /t 2 >nul
GOTO MENU
:START_SVC
echo Starting %SERVICE_NAME%...
net start %SERVICE_NAME%
pause
GOTO MENU
:STOP_SVC
echo Stopping %SERVICE_NAME%...
net stop %SERVICE_NAME%
pause
GOTO MENU
:RESTART_SVC
echo Restarting %SERVICE_NAME%...
net stop %SERVICE_NAME% >nul 2>&1
net start %SERVICE_NAME%
pause
GOTO MENU
:STATUS_SVC
cls
echo ============================================================================
echo Detailed Status for %SERVICE_NAME%:
echo ============================================================================
sc query %SERVICE_NAME%
echo.
pause
GOTO MENU
:INSTALL_SVC
call "%~dp0install_service.bat"
GOTO MENU
:UNINSTALL_SVC
call "%~dp0uninstall_service.bat"
GOTO MENU
:LOGS_SVC
cls
echo ============================================================================
echo Service Log Files:
echo ============================================================================
IF EXIST "%BACKEND_DIR%\logs\service_out.log" (
echo Opening stdout log: %BACKEND_DIR%\logs\service_out.log
start "" notepad "%BACKEND_DIR%\logs\service_out.log"
) ELSE (
echo [INFO] Stdout log not created yet.
)
IF EXIST "%BACKEND_DIR%\logs\service_err.log" (
echo Opening stderr log: %BACKEND_DIR%\logs\service_err.log
start "" notepad "%BACKEND_DIR%\logs\service_err.log"
) ELSE (
echo [INFO] Stderr log not created yet.
)
pause
GOTO MENU
:END
exit /b 0
-55
View File
@@ -1,55 +0,0 @@
@echo off
:: ============================================================================
:: TIS_RMS Backend - Windows Service Uninstaller
:: ============================================================================
SETLOCAL EnableDelayedExpansion
title TIS RMS Server - Remove Windows Service
:: Check for Administrator Privileges
net session >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] This script requires Administrator privileges.
echo Please right-click uninstall_service.bat and select "Run as administrator".
echo.
pause
exit /b 1
)
SET SERVICE_NAME=TIS_RMSServer
SET BACKEND_DIR=%~dp0..
pushd "%BACKEND_DIR%"
SET BACKEND_DIR=%CD%
popd
echo ============================================================================
echo Uninstalling %SERVICE_NAME%...
echo ============================================================================
:: Check for NSSM
SET NSSM_EXE=
IF EXIST "%BACKEND_DIR%\bin\nssm.exe" (
SET "NSSM_EXE=%BACKEND_DIR%\bin\nssm.exe"
) ELSE (
FOR /F "tokens=*" %%g IN ('where nssm 2^>nul') DO (
SET NSSM_EXE=%%g
GOTO :NSSM_FOUND
)
)
:NSSM_FOUND
IF NOT "%NSSM_EXE%"=="" (
"%NSSM_EXE%" stop %SERVICE_NAME% >nul 2>&1
"%NSSM_EXE%" remove %SERVICE_NAME% confirm
) ELSE (
net stop %SERVICE_NAME% >nul 2>&1
sc delete %SERVICE_NAME%
)
echo.
echo ============================================================================
echo [SUCCESS] Service %SERVICE_NAME% removed.
echo ============================================================================
echo.
pause
+173 -148
View File
@@ -6,15 +6,24 @@ const fs = require('fs');
const dns = require('dns');
const https = require('https');
let tunnelProcess = null;
let isStarting = false;
let monitorTimer = null;
let lastInternetCheck = false;
let lastCheckedTime = null;
let lastLogMessage = '';
let tunnelState = 'stopped'; // 'stopped', 'starting', 'connected', 'error', 'no_internet', 'disabled'
let consecutiveFailures = 0;
let detectedTunnelHostname = null;
// ─── State ─────────────────────────────────────────────────────────────────
let tunnelProcess = null;
let isStarting = false;
let monitorTimer = null;
let signalHandlersSet = false;
let lastInternetCheck = false;
let lastCheckedTime = null;
let lastLogMessage = '';
let tunnelState = 'stopped'; // 'stopped'|'starting'|'connected'|'error'|'no_internet'|'disabled'
let consecutiveInternetFails = 0;
let detectedTunnelHostname = null;
// Backoff config
const BACKOFF_MIN_MS = 5_000; // 5 s initial wait after a crash
const BACKOFF_MAX_MS = 120_000; // 2 min cap
let crashCount = 0;
let restartTimer = null;
/**
* Finds the cloudflared executable path.
@@ -53,48 +62,63 @@ function getCloudflaredPath() {
return null;
}
/**
* Robust check for internet connectivity.
* Tests multiple DNS endpoints and falls back to HTTPS.
*/
function checkInternet(timeoutMs = 5000) {
/** Robust internet check — parallel DNS + HTTPS fallback, resolves on first success. */
function checkInternet(timeoutMs = 6000) {
return new Promise((resolve) => {
let isResolved = false;
let resolved = false;
const done = (val) => {
if (!isResolved) {
isResolved = true;
clearTimeout(timer);
if (!resolved) {
resolved = true;
clearTimeout(globalTimer);
resolve(val);
}
};
const timer = setTimeout(() => done(false), timeoutMs);
// Try DNS resolution on multiple reliable hosts in parallel
dns.lookup('cloudflare.com', (err1) => {
if (!err1) return done(true);
dns.lookup('1.1.1.1', (err2) => {
if (!err2) return done(true);
// Fallback HTTPS request
const req = https.get('https://1.1.1.1', { timeout: 3000 }, (res) => {
res.destroy();
done(true);
});
req.on('error', () => done(false));
req.on('timeout', () => {
req.destroy();
done(false);
});
});
});
const globalTimer = setTimeout(() => done(false), timeoutMs);
// Parallel DNS lookups
dns.lookup('cloudflare.com', (e) => { if (!e) done(true); });
dns.lookup('8.8.8.8', (e) => { if (!e) done(true); });
dns.lookup('one.one.one.one', (e) => { if (!e) done(true); });
// HTTPS fallback
try {
const req = https.get('https://1.1.1.1', { timeout: 4000 }, (res) => { res.destroy(); done(true); });
req.on('error', () => {});
req.on('timeout', () => { req.destroy(); });
} catch (_) {}
});
}
/** Returns exponential backoff delay for the next restart attempt. */
function getBackoffMs() {
return Math.min(BACKOFF_MIN_MS * Math.pow(2, Math.max(0, crashCount - 1)), BACKOFF_MAX_MS);
}
/**
* Starts the Cloudflare Tunnel child process.
* Schedules the next startTunnel() call with exponential backoff.
* Safe to call multiple times — only one pending restart is ever queued.
*/
function scheduleRestart() {
if (restartTimer) return;
const isEnabled = process.env.CLOUDFLARE_TUNNEL_ENABLED !== 'false';
if (!isEnabled) return;
const delay = getBackoffMs();
console.log(`[Tunnel] Will retry in ${(delay / 1000).toFixed(0)} s (crash #${crashCount})...`);
restartTimer = setTimeout(async () => {
restartTimer = null;
if (tunnelState === 'disabled' || tunnelState === 'no_internet') return;
if (tunnelProcess || isStarting) return;
const hasInternet = await checkInternet();
if (hasInternet) {
await startTunnel();
} else {
console.warn('[Tunnel] No internet at restart time — will retry on next monitor tick');
tunnelState = 'no_internet';
}
}, delay);
}
/**
* Launches the cloudflared child process with crash detection and supervised restart.
*/
async function startTunnel() {
if (tunnelProcess || isStarting) return;
@@ -111,113 +135,118 @@ async function startTunnel() {
const DEFAULT_TOKEN = 'eyJhIjoiZWRhMWQ4ZTc1MzNjMjBiMDcyNmM0ZGU1OWE5YTMxYzgiLCJ0IjoiZjJhOGYyYmMtMWE1YS00MmNmLWJjZTUtZWMzYzAxNzY4M2IyIiwicyI6Ik5EbGtOMkZpTldNdFpEYzVNUzAwTUdFMUxXSTFNalV0WW1RNVl6VXlaV1EzTVRWaiJ9';
const rawToken = process.env.CLOUDFLARE_TUNNEL_TOKEN || DEFAULT_TOKEN;
const token = rawToken.replace(/[\r\n\s'"=]/g, '').trim();
const port = process.env.PORT || 18484;
const token = rawToken.replace(/[\r\n\s'"=]/g, '').trim();
// Use default auto-negotiated protocol (QUIC with HTTP/2 fallback) and --no-autoupdate
const args = token
? ['tunnel', '--no-autoupdate', 'run', '--token', token]
: ['tunnel', '--no-autoupdate', '--url', `http://localhost:${port}`];
: ['tunnel', '--no-autoupdate', '--url', `http://localhost:${process.env.PORT || 18484}`];
console.log(`[Tunnel] Launching Cloudflare tunnel: "${binPath}" ${token ? 'tunnel --no-autoupdate run --token [CONFIGURED]' : args.join(' ')}`);
console.log(`[Tunnel] Launching cloudflared (crash #${crashCount})...`);
tunnelState = 'starting';
const startedAt = Date.now();
try {
tunnelProcess = spawn(binPath, args, {
const proc = spawn(binPath, args, {
windowsHide: true,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, NO_AUTOUPDATE: 'true' }
});
tunnelProcess = proc;
isStarting = false;
console.log(`[Tunnel] Process started (PID: ${proc.pid})`);
const pid = tunnelProcess.pid;
console.log(`[Tunnel] Process started (PID: ${pid})`);
const handleLog = (data) => {
for (const line of data.toString().split('\n')) {
const t = line.trim();
if (!t) continue;
lastLogMessage = t;
const hostMatch = t.match(/"hostname":\s*"([^"]+)"/);
if (hostMatch?.[1]) detectedTunnelHostname = hostMatch[1];
const handleLogStream = (data) => {
const str = data.toString();
const lines = str.trim().split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
lastLogMessage = trimmed;
const isConn =
t.includes('Registered tunnel connection') ||
(t.includes('Connection') && t.includes('registered')) ||
t.includes('Tunnel connection curve preferences') ||
t.includes('protocol=quic') ||
t.includes('protocol=http2');
// Extract configured hostname if present in ingress logs
const hostMatch = trimmed.match(/"hostname":\s*"([^"]+)"/);
if (hostMatch && hostMatch[1]) {
detectedTunnelHostname = hostMatch[1];
if (isConn && tunnelState !== 'connected') {
tunnelState = 'connected';
crashCount = 0; // healthy → reset backoff
console.log('[Tunnel] ✔ Connected to Cloudflare edge network.');
}
if (trimmed.includes('Registered tunnel connection') ||
(trimmed.includes('Connection') && trimmed.includes('registered')) ||
trimmed.includes('Tunnel connection curve preferences') ||
trimmed.includes('protocol=quic') ||
trimmed.includes('protocol=http2')) {
if (tunnelState !== 'connected') {
tunnelState = 'connected';
console.log(`[Tunnel] Status: Connected to Cloudflare edge network.`);
}
consecutiveFailures = 0;
if (t.includes('Invalid token') || t.includes('failed to authenticate')) {
console.error('[Tunnel] ✖ Auth failure — check CLOUDFLARE_TUNNEL_TOKEN');
tunnelState = 'error';
}
}
};
proc.stdout.on('data', handleLog);
proc.stderr.on('data', handleLog);
tunnelProcess.stdout.on('data', handleLogStream);
tunnelProcess.stderr.on('data', handleLogStream);
tunnelProcess.on('error', (err) => {
proc.on('error', (err) => {
console.error('[Tunnel] Process error:', err.message);
tunnelState = 'error';
tunnelState = 'error';
lastLogMessage = err.message;
tunnelProcess = null;
isStarting = false;
if (tunnelProcess === proc) tunnelProcess = null;
isStarting = false;
crashCount++;
scheduleRestart();
});
tunnelProcess.on('exit', (code, signal) => {
proc.on('exit', (code, signal) => {
console.log(`[Tunnel] Process exited (Code: ${code}, Signal: ${signal})`);
tunnelProcess = null;
if (tunnelProcess === proc) tunnelProcess = null;
isStarting = false;
if (tunnelState !== 'no_internet' && tunnelState !== 'disabled') {
tunnelState = 'stopped';
// Detect crash: process lived < 30 s
const uptimeMs = Date.now() - startedAt;
if (uptimeMs < 30_000) {
crashCount++;
console.warn(`[Tunnel] Crash detected (uptime ${(uptimeMs / 1000).toFixed(1)} s, crash #${crashCount})`);
} else {
crashCount = 0; // lived long enough → reset
}
if (tunnelState === 'disabled' || tunnelState === 'no_internet') return;
tunnelState = 'stopped';
scheduleRestart();
});
// If process stays alive for 3 seconds, mark as connected
// Infer connected if still alive after 5 s with no explicit log confirmation
setTimeout(() => {
if (tunnelProcess && tunnelState === 'starting') {
if (tunnelProcess === proc && tunnelState === 'starting') {
tunnelState = 'connected';
console.log(`[Tunnel] Status: Connected to Cloudflare edge network.`);
consecutiveFailures = 0;
crashCount = 0;
console.log('[Tunnel] ✔ Connected (inferred — process alive after 5 s).');
}
}, 3000);
}, 5000);
} catch (err) {
console.error('[Tunnel] Failed to start tunnel process:', err.message);
tunnelState = 'error';
console.error('[Tunnel] Failed to spawn process:', err.message);
tunnelState = 'error';
lastLogMessage = err.message;
tunnelProcess = null;
} finally {
isStarting = false;
isStarting = false;
crashCount++;
scheduleRestart();
}
}
/**
* Stops the Cloudflare Tunnel child process cleanly and non-blockingly.
*/
/** Kills the cloudflared child process cleanly and clears any pending restart timer. */
function stopTunnel() {
if (restartTimer) { clearTimeout(restartTimer); restartTimer = null; }
if (!tunnelProcess) return;
const proc = tunnelProcess;
tunnelProcess = null;
tunnelState = 'stopped';
tunnelState = 'stopped';
try {
console.log(`[Tunnel] Stopping tunnel process (PID: ${proc.pid})...`);
if (process.platform === 'win32') {
const killer = spawn('taskkill', ['/pid', String(proc.pid), '/T', '/F'], {
windowsHide: true,
stdio: 'ignore'
});
killer.on('error', () => {
try { proc.kill('SIGTERM'); } catch (_) {}
windowsHide: true, stdio: 'ignore'
});
killer.on('error', () => { try { proc.kill('SIGTERM'); } catch (_) {} });
} else {
proc.kill('SIGTERM');
}
@@ -226,9 +255,7 @@ function stopTunnel() {
}
}
/**
* Periodic check loop: checks internet and maintains tunnel state resiliently.
*/
/** Internet watchdog — restarts tunnel if net comes back, suspends after persistent outage. */
async function checkAndMaintainTunnel() {
const isEnabled = process.env.CLOUDFLARE_TUNNEL_ENABLED !== 'false';
if (!isEnabled) {
@@ -237,79 +264,77 @@ async function checkAndMaintainTunnel() {
return;
}
const hasInternet = await checkInternet(5000);
const hasInternet = await checkInternet(6000);
lastInternetCheck = hasInternet;
lastCheckedTime = new Date().toISOString();
lastCheckedTime = new Date().toISOString();
if (hasInternet) {
consecutiveFailures = 0;
if (!tunnelProcess && !isStarting) {
console.log('[Tunnel] Internet connection available. Starting Cloudflare tunnel...');
consecutiveInternetFails = 0;
// Only start if nothing is running or pending
if (!tunnelProcess && !isStarting && !restartTimer) {
if (tunnelState === 'no_internet') crashCount = 0; // reset backoff after net recovery
console.log('[Tunnel] Internet available — starting tunnel...');
await startTunnel();
}
} else {
consecutiveFailures++;
console.warn(`[Tunnel] Internet check failed (${consecutiveFailures}/3)`);
// Only suspend tunnel after 3 consecutive failures (approx 90s)
if (consecutiveFailures >= 3 && tunnelProcess) {
console.warn('[Tunnel] Persistent internet loss. Suspending tunnel...');
consecutiveInternetFails++;
console.warn(`[Tunnel] Internet check failed (${consecutiveInternetFails}/3)`);
if (consecutiveInternetFails >= 3 && tunnelProcess) {
console.warn('[Tunnel] Persistent internet loss — suspending tunnel');
tunnelState = 'no_internet';
stopTunnel();
}
}
}
/**
* Initializes auto tunnel activation on server startup.
*/
/** Initializes the supervised tunnel service on server startup. */
function startAutoTunnel() {
const isEnabled = process.env.CLOUDFLARE_TUNNEL_ENABLED !== 'false';
if (!isEnabled) {
console.log('[Tunnel] Auto tunnel is disabled via CLOUDFLARE_TUNNEL_ENABLED=false');
console.log('[Tunnel] Auto tunnel disabled (CLOUDFLARE_TUNNEL_ENABLED=false)');
tunnelState = 'disabled';
return;
}
console.log('[Tunnel] Initializing Auto Cloudflare Tunnel Service...');
// Initial check immediately
console.log('[Tunnel] Initializing supervised Cloudflare Tunnel service...');
checkAndMaintainTunnel();
// Periodic check interval (default: 30 seconds to prevent aggressive CPU/DNS churn)
const intervalMs = parseInt(process.env.CLOUDFLARE_TUNNEL_CHECK_INTERVAL, 10) || 30000;
const intervalMs = parseInt(process.env.CLOUDFLARE_TUNNEL_CHECK_INTERVAL, 10) || 30_000;
if (monitorTimer) clearInterval(monitorTimer);
monitorTimer = setInterval(checkAndMaintainTunnel, intervalMs);
// Ensure clean shutdown on parent process exit
const cleanExit = () => {
if (monitorTimer) clearInterval(monitorTimer);
stopTunnel();
};
process.on('exit', cleanExit);
process.on('SIGINT', () => { cleanExit(); process.exit(0); });
process.on('SIGTERM', () => { cleanExit(); process.exit(0); });
process.on('beforeExit', cleanExit);
// Register signal handlers exactly once to avoid duplicate-listener warnings
if (!signalHandlersSet) {
signalHandlersSet = true;
const cleanExit = () => {
if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; }
if (restartTimer) { clearTimeout(restartTimer); restartTimer = null; }
stopTunnel();
};
process.on('exit', cleanExit);
process.on('SIGINT', () => { cleanExit(); process.exit(0); });
process.on('SIGTERM', () => { cleanExit(); process.exit(0); });
process.on('beforeExit', cleanExit);
}
}
/**
* Returns current tunnel status.
*/
/** Returns current tunnel status including crash diagnostics. */
function getTunnelStatus() {
const binPath = getCloudflaredPath();
return {
enabled: process.env.CLOUDFLARE_TUNNEL_ENABLED !== 'false',
hasExecutable: !!binPath,
enabled: process.env.CLOUDFLARE_TUNNEL_ENABLED !== 'false',
hasExecutable: !!binPath,
executablePath: binPath,
hasInternet: lastInternetCheck,
isRunning: !!tunnelProcess,
pid: tunnelProcess ? tunnelProcess.pid : null,
status: tunnelState,
hostname: detectedTunnelHostname,
publicUrl: detectedTunnelHostname ? `https://${detectedTunnelHostname}` : null,
lastChecked: lastCheckedTime,
lastLog: lastLogMessage
hasInternet: lastInternetCheck,
isRunning: !!tunnelProcess,
pid: tunnelProcess ? tunnelProcess.pid : null,
status: tunnelState,
hostname: detectedTunnelHostname,
publicUrl: detectedTunnelHostname ? `https://${detectedTunnelHostname}` : null,
lastChecked: lastCheckedTime,
lastLog: lastLogMessage,
crashCount,
nextRetryMs: restartTimer ? getBackoffMs() : null,
};
}
+4
View File
@@ -1,4 +1,8 @@
@echo off
:: NOTE: This script is kept as an emergency fallback only.
:: The Cloudflare Tunnel is now managed automatically by Node.js (tunnelService.js).
:: Use install_service.bat + WPF Server Manager instead of running this manually.
::
title TIS RMS Cloudflare Tunnel
echo =========================================================
echo Starting TIS RMS Cloudflare Tunnel on port 18484
+28
View File
@@ -0,0 +1,28 @@
@echo off
:: ============================================================
:: TIS RMS - NSSM Service Uninstaller
:: Run as Administrator
:: ============================================================
NET SESSION >nul 2>&1
IF %ERRORLEVEL% NEQ 0 (
echo [ERROR] Run as Administrator.
pause
exit /b 1
)
set BACKEND_DIR=F:\SumbrerongBato\tis_rms_server\backend
set NSSM=%BACKEND_DIR%\bin\nssm.exe
set SERVICE_NAME=TIS.RMS.ServerService
echo.
echo Stopping and removing "%SERVICE_NAME%"...
"%NSSM%" stop "%SERVICE_NAME%" >nul 2>&1
timeout /t 3 /nobreak >nul
"%NSSM%" remove "%SERVICE_NAME%" confirm
echo.
echo [OK] Service removed. Node.js will no longer start on boot.
echo You can re-install anytime by running install_service.bat
echo.
pause