| #!/usr/bin/env node |
|
|
| const { spawn, exec, execSync } = require("child_process"); |
| const path = require("path"); |
| const fs = require("fs"); |
| const https = require("https"); |
| const os = require("os"); |
|
|
| |
| function createSpinner(text) { |
| const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; |
| let i = 0; |
| let interval = null; |
| let currentText = text; |
| return { |
| start() { |
| if (process.stdout.isTTY) { |
| process.stdout.write(`\r${frames[0]} ${currentText}`); |
| interval = setInterval(() => { |
| process.stdout.write(`\r${frames[i++ % frames.length]} ${currentText}`); |
| }, 80); |
| } |
| return this; |
| }, |
| stop() { |
| if (interval) { |
| clearInterval(interval); |
| interval = null; |
| } |
| if (process.stdout.isTTY) { |
| process.stdout.write("\r\x1b[K"); |
| } |
| }, |
| succeed(msg) { |
| this.stop(); |
| console.log(`✅ ${msg}`); |
| }, |
| fail(msg) { |
| this.stop(); |
| console.log(`❌ ${msg}`); |
| } |
| }; |
| } |
|
|
| const pkg = require("./package.json"); |
| const { ensureSqliteRuntime, buildEnvWithRuntime } = require("./hooks/sqliteRuntime"); |
| const { ensureTrayRuntime } = require("./hooks/trayRuntime"); |
| const args = process.argv.slice(2); |
|
|
| |
| |
| |
| try { ensureSqliteRuntime({ silent: true }); } catch {} |
|
|
| |
| try { ensureTrayRuntime({ silent: true }); } catch {} |
|
|
| |
| const APP_NAME = pkg.name; |
| const INSTALL_CMD_LATEST = `npm i -g ${APP_NAME}@latest --prefer-online`; |
|
|
| const DEFAULT_PORT = 20128; |
| const DEFAULT_HOST = "0.0.0.0"; |
| const MAX_PORT_ATTEMPTS = 10; |
| |
| const PROCESS_IDENTIFIERS = [ |
| '9router' |
| ]; |
|
|
| |
| let port = DEFAULT_PORT; |
| let host = DEFAULT_HOST; |
| let noBrowser = false; |
| let skipUpdate = false; |
| let showLog = false; |
| let trayMode = false; |
|
|
| for (let i = 0; i < args.length; i++) { |
| if (args[i] === "--port" || args[i] === "-p") { |
| port = parseInt(args[i + 1], 10) || DEFAULT_PORT; |
| i++; |
| } else if (args[i] === "--host" || args[i] === "-H") { |
| host = args[i + 1] || DEFAULT_HOST; |
| i++; |
| } else if (args[i] === "--no-browser" || args[i] === "-n") { |
| noBrowser = true; |
| } else if (args[i] === "--log" || args[i] === "-l") { |
| showLog = true; |
| } else if (args[i] === "--skip-update") { |
| skipUpdate = true; |
| } else if (args[i] === "--tray" || args[i] === "-t") { |
| trayMode = true; |
| process.env.TRAY_MODE = "1"; |
| } else if (args[i] === "--help" || args[i] === "-h") { |
| console.log(` |
| Usage: ${APP_NAME} [options] |
| |
| Options: |
| -p, --port <port> Port to run the server (default: ${DEFAULT_PORT}) |
| -H, --host <host> Host to bind (default: ${DEFAULT_HOST}) |
| -n, --no-browser Don't open browser automatically |
| -l, --log Show server logs (default: hidden) |
| -t, --tray Run in system tray mode (background) |
| --skip-update Skip auto-update check |
| -h, --help Show this help message |
| -v, --version Show version |
| `); |
| process.exit(0); |
| } else if (args[i] === "--version" || args[i] === "-v") { |
| console.log(pkg.version); |
| process.exit(0); |
| } |
| } |
|
|
| |
| if (skipUpdate && !trayMode && !process.stdin.isTTY) { |
| trayMode = true; |
| process.env.TRAY_MODE = "1"; |
| } |
|
|
| |
| const RUNTIME = process.execPath; |
|
|
| |
| function compareVersions(a, b) { |
| const partsA = a.split(".").map(Number); |
| const partsB = b.split(".").map(Number); |
| for (let i = 0; i < 3; i++) { |
| if (partsA[i] > partsB[i]) return 1; |
| if (partsA[i] < partsB[i]) return -1; |
| } |
| return 0; |
| } |
|
|
| |
| function getAppDataDir() { |
| return process.platform === "win32" |
| ? path.join(process.env.APPDATA || "", "9router") |
| : path.join(os.homedir(), ".9router"); |
| } |
|
|
| |
| function killByPidFile(pidFile) { |
| try { |
| if (!fs.existsSync(pidFile)) return; |
| const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10); |
| if (!pid) return; |
| try { |
| if (process.platform === "win32") { |
| execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore", windowsHide: true, timeout: 3000 }); |
| } else { |
| process.kill(pid, "SIGKILL"); |
| } |
| } catch { } |
| try { fs.unlinkSync(pidFile); } catch { } |
| } catch { } |
| } |
|
|
| |
| function killTunnelByPidFile() { |
| const tunnelDir = path.join(getAppDataDir(), "tunnel"); |
| killByPidFile(path.join(tunnelDir, "cloudflared.pid")); |
| killByPidFile(path.join(tunnelDir, "tailscale.pid")); |
| } |
|
|
| |
| function killCloudflaredByAppPort(appPort) { |
| if (!appPort) return []; |
| const portMatchers = [`localhost:${appPort}`, `127.0.0.1:${appPort}`]; |
| const pids = []; |
| try { |
| if (process.platform === "win32") { |
| const psCmd = `powershell -NonInteractive -WindowStyle Hidden -Command "Get-WmiObject Win32_Process -Filter 'Name=\\"cloudflared.exe\\"' | Select-Object ProcessId,CommandLine | ConvertTo-Csv -NoTypeInformation"`; |
| const output = execSync(psCmd, { encoding: "utf8", windowsHide: true, timeout: 5000 }); |
| const lines = output.split("\n").slice(1).filter(l => l.trim()); |
| lines.forEach(line => { |
| if (portMatchers.some(m => line.includes(m))) { |
| const match = line.match(/^"(\d+)"/); |
| if (match && match[1]) pids.push(match[1]); |
| } |
| }); |
| } else { |
| const output = execSync("ps -eo pid,command 2>/dev/null", { encoding: "utf8", timeout: 5000 }); |
| output.split("\n").forEach(line => { |
| if (line.includes("cloudflared") && portMatchers.some(m => line.includes(m))) { |
| const parts = line.trim().split(/\s+/); |
| const pid = parts[0]; |
| if (pid && !isNaN(pid)) pids.push(pid); |
| } |
| }); |
| } |
| } catch { } |
| return pids; |
| } |
|
|
| |
| function killAllAppProcesses(appPort) { |
| return new Promise((resolve) => { |
| try { |
| |
| killProxyByPidFile(); |
| |
| killTunnelByPidFile(); |
|
|
| const platform = process.platform; |
| let pids = []; |
|
|
| |
| pids.push(...killCloudflaredByAppPort(appPort)); |
|
|
| if (platform === "win32") { |
| |
| try { |
| const psCmd = `powershell -NonInteractive -WindowStyle Hidden -Command "Get-WmiObject Win32_Process -Filter 'Name=\\"node.exe\\"' | Select-Object ProcessId,CommandLine | ConvertTo-Csv -NoTypeInformation"`; |
| const output = execSync(psCmd, { |
| encoding: "utf8", |
| windowsHide: true, |
| timeout: 5000 |
| }); |
| const lines = output.split("\n").slice(1).filter(l => l.trim()); |
| lines.forEach(line => { |
| |
| |
| const cmd = line.toLowerCase(); |
| const isAppProcess = |
| (cmd.includes("node") && cmd.includes("9router") && (cmd.includes("cli.js") || cmd.includes("\\9router") || cmd.includes("/9router"))) |
| || cmd.includes("next-server"); |
| if (isAppProcess) { |
| const match = line.match(/^"(\d+)"/); |
| if (match && match[1] && match[1] !== process.pid.toString()) { |
| pids.push(match[1]); |
| } |
| } |
| }); |
| } catch (e) { |
| |
| } |
| } else { |
| |
| try { |
| const output = execSync('ps aux 2>/dev/null', { |
| encoding: 'utf8', |
| timeout: 5000 |
| }); |
| const lines = output.split('\n'); |
|
|
| lines.forEach(line => { |
| |
| |
| const cmd = line.toLowerCase(); |
| const isAppProcess = |
| (cmd.includes("node") && cmd.includes("9router") && (cmd.includes("cli.js") || cmd.includes("/9router"))) |
| || cmd.includes("next-server"); |
| if (isAppProcess) { |
| const parts = line.trim().split(/\s+/); |
| const pid = parts[1]; |
| if (pid && !isNaN(pid) && pid !== process.pid.toString()) { |
| pids.push(pid); |
| } |
| } |
| }); |
| } catch (e) { |
| |
| } |
| } |
|
|
| |
| if (pids.length > 0) { |
| pids.forEach(pid => { |
| try { |
| if (platform === "win32") { |
| execSync(`taskkill /F /PID ${pid} 2>nul`, { stdio: 'ignore', shell: true, windowsHide: true, timeout: 3000 }); |
| } else { |
| execSync(`kill -9 ${pid} 2>/dev/null`, { stdio: 'ignore', timeout: 3000 }); |
| } |
| } catch (err) { |
| |
| } |
| }); |
|
|
| |
| setTimeout(() => resolve(), 1000); |
| } else { |
| resolve(); |
| } |
| } catch (err) { |
| |
| resolve(); |
| } |
| }); |
| } |
|
|
| |
| function sleepSync(ms) { |
| try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { } |
| } |
|
|
| |
| function waitForExit(pid, timeoutMs) { |
| const deadline = Date.now() + timeoutMs; |
| while (Date.now() < deadline) { |
| try { process.kill(pid, 0); } catch { return true; } |
| sleepSync(100); |
| } |
| return false; |
| } |
|
|
| |
| |
| function killProxyByPidFile() { |
| try { |
| const pidFile = path.join(getAppDataDir(), "mitm", ".mitm.pid"); |
| if (!fs.existsSync(pidFile)) return; |
| const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10); |
| if (!pid) return; |
|
|
| if (process.platform === "win32") { |
| |
| try { execSync(`taskkill /T /PID ${pid}`, { stdio: "ignore", windowsHide: true, timeout: 2000 }); } catch { } |
| if (!waitForExit(pid, 1500)) { |
| try { execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore", windowsHide: true, timeout: 3000 }); } catch { } |
| } |
| |
| if (!waitForExit(pid, 500)) { |
| try { execSync(`powershell -NonInteractive -WindowStyle Hidden -Command "Stop-Process -Id ${pid} -Force"`, { stdio: "ignore", windowsHide: true, timeout: 3000 }); } catch { } |
| } |
| } else { |
| |
| try { execSync(`sudo -n kill -TERM ${pid} 2>/dev/null`, { stdio: "ignore", timeout: 2000 }); } |
| catch { try { process.kill(pid, "SIGTERM"); } catch { } } |
| if (!waitForExit(pid, 1500)) { |
| try { execSync(`sudo -n kill -9 ${pid} 2>/dev/null`, { stdio: "ignore", timeout: 2000 }); } |
| catch { try { process.kill(pid, "SIGKILL"); } catch { } } |
| } |
| } |
| try { fs.unlinkSync(pidFile); } catch { } |
| } catch { } |
| } |
|
|
| |
| function killProcessOnPort(port) { |
| return new Promise((resolve) => { |
| try { |
| const platform = process.platform; |
| let pid; |
|
|
| if (platform === "win32") { |
| try { |
| const output = execSync(`netstat -ano | findstr :${port}`, { |
| encoding: 'utf8', |
| shell: true, |
| windowsHide: true, |
| timeout: 5000 |
| }).trim(); |
| const lines = output.split('\n').filter(l => l.includes('LISTENING')); |
| if (lines.length > 0) { |
| pid = lines[0].trim().split(/\s+/).pop(); |
| execSync(`taskkill /F /PID ${pid} 2>nul`, { stdio: 'ignore', shell: true, windowsHide: true, timeout: 3000 }); |
| } |
| } catch (e) { |
| |
| } |
| } else { |
| |
| try { |
| const pidOutput = execSync(`lsof -ti:${port}`, { |
| encoding: 'utf8', |
| stdio: ['pipe', 'pipe', 'ignore'] |
| }).trim(); |
| if (pidOutput) { |
| pid = pidOutput.split('\n')[0]; |
| execSync(`kill -9 ${pid} 2>/dev/null`, { stdio: 'ignore', timeout: 3000 }); |
| } |
| } catch (e) { |
| |
| } |
| } |
|
|
| |
| setTimeout(() => resolve(), 500); |
| } catch (err) { |
| |
| resolve(); |
| } |
| }); |
| } |
|
|
|
|
| |
| function isRestrictedEnvironment() { |
| |
| if (process.env.CODESPACES === "true" || process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN) { |
| return "GitHub Codespaces"; |
| } |
|
|
| |
| if (fs.existsSync("/.dockerenv") || (fs.existsSync("/proc/1/cgroup") && fs.readFileSync("/proc/1/cgroup", "utf8").includes("docker"))) { |
| return "Docker"; |
| } |
|
|
| return null; |
| } |
|
|
| |
| function checkForUpdate() { |
| return new Promise((resolve) => { |
| if (skipUpdate) { |
| resolve(null); |
| return; |
| } |
|
|
| const spinner = createSpinner("Checking for updates...").start(); |
| let resolved = false; |
|
|
| const safetyTimeout = setTimeout(() => { |
| if (!resolved) { |
| resolved = true; |
| spinner.stop(); |
| resolve(null); |
| } |
| }, 8000); |
|
|
| const done = (version) => { |
| if (resolved) return; |
| resolved = true; |
| clearTimeout(safetyTimeout); |
| spinner.stop(); |
| resolve(version); |
| }; |
|
|
| const req = https.get(`https://registry.npmjs.org/${pkg.name}/latest`, { timeout: 3000 }, (res) => { |
| let data = ""; |
| res.on("data", chunk => data += chunk); |
| res.on("end", () => { |
| try { |
| const latest = JSON.parse(data); |
| if (latest.version && compareVersions(latest.version, pkg.version) > 0) { |
| done(latest.version); |
| } else { |
| done(null); |
| } |
| } catch (e) { |
| done(null); |
| } |
| }); |
| }); |
|
|
| req.on("error", () => done(null)); |
| req.on("timeout", () => { req.destroy(); done(null); }); |
| }); |
| } |
|
|
| |
| function openBrowser(url) { |
| const platform = process.platform; |
| let cmd; |
|
|
| if (platform === "darwin") { |
| cmd = `open "${url}"`; |
| } else if (platform === "win32") { |
| cmd = `start "" "${url}"`; |
| } else { |
| cmd = `xdg-open "${url}"`; |
| } |
|
|
| exec(cmd, { windowsHide: true }, (err) => { |
| if (err) { |
| console.log(`Open browser manually: ${url}`); |
| } |
| }); |
| } |
|
|
| |
| |
| const standaloneDir = path.join(__dirname, "app"); |
| const customServerPath = path.join(standaloneDir, "custom-server.js"); |
| const serverPath = fs.existsSync(customServerPath) |
| ? customServerPath |
| : path.join(standaloneDir, "server.js"); |
|
|
| if (!fs.existsSync(serverPath)) { |
| console.error("Error: Standalone build not found."); |
| console.error("Please run 'npm run build:cli' first."); |
| process.exit(1); |
| } |
|
|
| |
| checkForUpdate().then((latestVersion) => { |
| killAllAppProcesses(port).then(() => { |
| return killProcessOnPort(port); |
| }).then(() => { |
| startServer(latestVersion); |
| }); |
| }); |
|
|
| |
| async function showInterfaceMenu(latestVersion) { |
| const { selectMenu } = require("./src/cli/utils/input"); |
| const { clearScreen } = require("./src/cli/utils/display"); |
| const { getEndpoint } = require("./src/cli/utils/endpoint"); |
|
|
| clearScreen(); |
|
|
| const displayHost = host === DEFAULT_HOST ? "localhost" : host; |
|
|
| |
| let serverUrl; |
| try { |
| const { endpoint, tunnelEnabled } = await getEndpoint(port); |
| serverUrl = tunnelEnabled ? endpoint.replace(/\/v1$/, "") : `http://${displayHost}:${port}`; |
| } catch (e) { |
| serverUrl = `http://${displayHost}:${port}`; |
| } |
|
|
| const subtitle = `🚀 Server: \x1b[32m${serverUrl}\x1b[0m`; |
|
|
| const menuItems = []; |
|
|
| if (latestVersion) { |
| menuItems.push({ label: `Update to v${latestVersion} (current: v${pkg.version})`, icon: "⬆" }); |
| } |
|
|
| menuItems.push( |
| { label: "Web UI (Open in Browser)", icon: "🌐" }, |
| { label: "Terminal UI (Interactive CLI)", icon: "💻" }, |
| { label: "Hide to Tray (Background)", icon: "🔔" }, |
| { label: "Exit", icon: "🚪" } |
| ); |
|
|
| const selected = await selectMenu(`Choose Interface (v${pkg.version})`, menuItems, 0, subtitle); |
|
|
| const offset = latestVersion ? 1 : 0; |
|
|
| if (latestVersion && selected === 0) return "update"; |
| if (selected === offset) return "web"; |
| if (selected === offset + 1) return "terminal"; |
| if (selected === offset + 2) return "hide"; |
| return "exit"; |
| } |
|
|
| const MAX_RESTARTS = 2; |
| const RESTART_RESET_MS = 30000; |
|
|
| function startServer(latestVersion) { |
| const displayHost = host === DEFAULT_HOST ? "localhost" : host; |
| const url = `http://${displayHost}:${port}/dashboard`; |
|
|
| let restartCount = 0; |
| let serverStartTime = Date.now(); |
|
|
| const CRASH_LOG_LINES = 50; |
| let crashLog = []; |
|
|
| function spawnServer() { |
| serverStartTime = Date.now(); |
| crashLog = []; |
| const child = spawn(RUNTIME, ["--max-old-space-size=6144", serverPath], { |
| cwd: standaloneDir, |
| stdio: showLog ? "inherit" : ["ignore", "ignore", "pipe"], |
| detached: true, |
| windowsHide: true, |
| env: { |
| ...buildEnvWithRuntime(process.env), |
| PORT: port.toString(), |
| HOSTNAME: host |
| } |
| }); |
| if (!showLog && child.stderr) { |
| child.stderr.on("data", (data) => { |
| const lines = data.toString().split("\n").filter(Boolean); |
| crashLog.push(...lines); |
| if (crashLog.length > CRASH_LOG_LINES) crashLog = crashLog.slice(-CRASH_LOG_LINES); |
| }); |
| } |
| return child; |
| } |
|
|
| let server = spawnServer(); |
|
|
| |
| let isCleaningUp = false; |
| function cleanup() { |
| if (isCleaningUp) return; |
| isCleaningUp = true; |
| try { |
| |
| try { |
| const { killTray } = require("./src/cli/tray/tray"); |
| killTray(); |
| } catch (e) { } |
| |
| killProxyByPidFile(); |
| |
| killTunnelByPidFile(); |
| |
| if (server.pid) { |
| process.kill(server.pid, "SIGKILL"); |
| } |
| |
| process.kill(-server.pid, "SIGKILL"); |
| } catch (e) { } |
| } |
|
|
| |
| let isShuttingDown = false; |
| process.on("uncaughtException", (err) => { |
| if (isShuttingDown) return; |
| console.error("Error:", err.message); |
| }); |
|
|
| |
| process.on("SIGINT", () => { |
| if (isShuttingDown) return; |
| isShuttingDown = true; |
| console.log("\nExiting..."); |
| cleanup(); |
| setTimeout(() => process.exit(0), 100); |
| }); |
| process.on("SIGTERM", () => { |
| if (isShuttingDown) return; |
| isShuttingDown = true; |
| cleanup(); |
| setTimeout(() => process.exit(0), 100); |
| }); |
| process.on("SIGHUP", () => { |
| if (isShuttingDown) return; |
| isShuttingDown = true; |
| cleanup(); |
| setTimeout(() => process.exit(0), 100); |
| }); |
|
|
| |
| const initTrayIcon = () => { |
| try { |
| const { initTray } = require("./src/cli/tray/tray"); |
| initTray({ |
| port, |
| onQuit: () => { |
| isShuttingDown = true; |
| console.log("\n👋 Shutting down from tray..."); |
| cleanup(); |
| setTimeout(() => process.exit(0), 100); |
| }, |
| onOpenDashboard: () => openBrowser(url) |
| }); |
| } catch (err) { |
| |
| } |
| }; |
|
|
| |
| if (trayMode) { |
| |
| process.removeAllListeners("SIGHUP"); |
| process.on("SIGHUP", () => {}); |
|
|
| console.log(`\n🚀 ${pkg.name} v${pkg.version}`); |
| console.log(`Server: http://${displayHost}:${port}`); |
|
|
| setTimeout(() => { |
| initTrayIcon(); |
| console.log("\n💡 Router is now running in system tray. Close this terminal if you want."); |
| console.log(" Right-click tray icon to open dashboard or quit.\n"); |
| }, 2000); |
|
|
| return; |
| } |
|
|
| |
| setTimeout(async () => { |
| |
| initTrayIcon(); |
|
|
| try { |
| while (true) { |
| const choice = await showInterfaceMenu(latestVersion); |
|
|
| if (choice === "update") { |
| isShuttingDown = true; |
| const { clearScreen } = require("./src/cli/utils/display"); |
| clearScreen(); |
| console.log(`\n⬆ Update v${pkg.version} → v${latestVersion}\n`); |
| console.log(`Run this after exit:\n`); |
| console.log(` \x1b[33m${INSTALL_CMD_LATEST}\x1b[0m\n`); |
| cleanup(); |
| await killAllAppProcesses(port); |
| await killProcessOnPort(port); |
| setTimeout(() => process.exit(0), 200); |
| return; |
| } else if (choice === "web") { |
| openBrowser(url); |
| |
| const { pause } = require("./src/cli/utils/input"); |
| await pause("\nPress Enter to go back to menu..."); |
| } else if (choice === "terminal") { |
| |
| const { startTerminalUI } = require("./src/cli/terminalUI"); |
| await startTerminalUI(port); |
| |
| } else if (choice === "hide") { |
| const { clearScreen } = require("./src/cli/utils/display"); |
| clearScreen(); |
|
|
| |
| try { |
| const { enableAutoStart } = require("./src/cli/tray/autostart"); |
| enableAutoStart(__filename); |
| } catch (e) { } |
|
|
| if (process.platform === "darwin") { |
| |
| |
| process.removeAllListeners("SIGHUP"); |
| process.on("SIGHUP", () => {}); |
|
|
| console.log(`\n⏳ Switching to tray mode... (icon already visible in menu bar)`); |
| console.log(`🔔 9Router is running in tray (PID: ${process.pid})`); |
| console.log(` Server: http://${displayHost}:${port}`); |
| console.log(`\n💡 You can close this terminal. Right-click tray icon to quit.\n`); |
|
|
| |
| return; |
| } |
|
|
| |
| console.log(`\n⏳ Starting background process... (tray icon will appear in ~3s)`); |
|
|
| const bgProcess = spawn(process.execPath, [__filename, "--tray", "--skip-update", "-p", port.toString()], { |
| detached: true, |
| stdio: "ignore", |
| windowsHide: true, |
| env: { ...process.env } |
| }); |
| bgProcess.unref(); |
|
|
| console.log(`🔔 9Router is now running in background (PID: ${bgProcess.pid})`); |
| console.log(` Server: http://${displayHost}:${port}`); |
| console.log(`\n💡 You can close this terminal. Right-click tray icon to quit.\n`); |
|
|
| |
| cleanup(); |
| process.exit(0); |
| } else if (choice === "exit") { |
| isShuttingDown = true; |
| console.log("\nExiting..."); |
| cleanup(); |
| setTimeout(() => process.exit(0), 100); |
| } |
| } |
| } catch (err) { |
| console.error("Error:", err.message); |
| cleanup(); |
| process.exit(1); |
| } |
| }, 3000); |
|
|
| function attachServerEvents() { |
| server.on("error", (err) => { |
| console.error("Failed to start server:", err.message); |
| if (!isShuttingDown) tryRestart(); |
| else { cleanup(); process.exit(1); } |
| }); |
|
|
| server.on("close", (code) => { |
| if (isShuttingDown || code === 0) { |
| process.exit(code || 0); |
| return; |
| } |
| tryRestart(code); |
| }); |
| } |
|
|
| function tryRestart(code) { |
| const aliveMs = Date.now() - serverStartTime; |
| |
| if (aliveMs >= RESTART_RESET_MS) restartCount = 0; |
|
|
| if (restartCount >= MAX_RESTARTS) { |
| console.error(`\n⚠️ Server crashed ${MAX_RESTARTS} times. Disabling MIT and restarting...`); |
| try { |
| const dbPath = path.join(os.homedir(), process.platform === "win32" ? path.join("AppData", "Roaming", "9router", "db.json") : path.join(".9router", "db.json")); |
| if (fs.existsSync(dbPath)) { |
| const db = JSON.parse(fs.readFileSync(dbPath, "utf-8")); |
| if (db.settings) db.settings.mitmEnabled = false; |
| fs.writeFileSync(dbPath, JSON.stringify(db, null, 2)); |
| } |
| } catch { } |
| restartCount = 0; |
| server = spawnServer(); |
| attachServerEvents(); |
| return; |
| } |
|
|
| restartCount++; |
| const delay = Math.min(1000 * restartCount, 10000); |
| console.error(`\n⚠️ Server exited (code=${code ?? "unknown"}). Restarting in ${delay / 1000}s... (${restartCount}/${MAX_RESTARTS})`); |
| if (crashLog.length) { |
| console.error("\n--- Server crash log ---"); |
| crashLog.forEach(l => console.error(l)); |
| console.error("--- End crash log ---\n"); |
| } |
|
|
| setTimeout(() => { |
| server = spawnServer(); |
| attachServerEvents(); |
| }, delay); |
| } |
|
|
| attachServerEvents(); |
| } |
|
|