| import { spawn, execSync } from "child_process"; |
| import path from "path"; |
| import fs from "fs"; |
| import os from "os"; |
| import { UPDATER_CONFIG } from "@/shared/constants/config"; |
|
|
| const KILL_TIMEOUT_MS = 5000; |
| const PROCESS_WAIT_MS = 1500; |
|
|
| |
| function killMitmByPidFile() { |
| try { |
| const mitmPidFile = path.join( |
| process.platform === "win32" |
| ? path.join(process.env.APPDATA || "", "9router") |
| : path.join(os.homedir(), ".9router"), |
| "mitm", |
| ".mitm.pid" |
| ); |
| if (!fs.existsSync(mitmPidFile)) return; |
| const pid = parseInt(fs.readFileSync(mitmPidFile, "utf8").trim(), 10); |
| if (!pid) return; |
|
|
| if (process.platform === "win32") { |
| |
| try { execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore", windowsHide: true, timeout: 3000 }); } catch { |
| 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 -9 ${pid} 2>/dev/null`, { stdio: "ignore", timeout: 3000 }); |
| } catch { |
| try { process.kill(pid, "SIGKILL"); } catch { } |
| } |
| } |
| try { fs.unlinkSync(mitmPidFile); } catch { } |
| } catch { } |
| } |
|
|
| |
| function collectAppPids() { |
| const pids = []; |
| const platform = process.platform; |
|
|
| 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: KILL_TIMEOUT_MS }); |
| const lines = output.split("\n").slice(1).filter(l => l.trim()); |
| lines.forEach(line => { |
| const lower = line.toLowerCase(); |
| |
| const isAppProcess = lower.includes("9router") || |
| lower.includes("next-server") || |
| lower.includes("\\bin\\app\\") || |
| lower.includes("/bin/app/") || |
| lower.includes("cli.js"); |
| if (isAppProcess) { |
| const match = line.match(/^"(\d+)"/); |
| if (match && match[1] && match[1] !== process.pid.toString()) pids.push(match[1]); |
| } |
| }); |
| } catch { } |
|
|
| |
| for (const procName of ["cloudflared", "tray_windows_release"]) { |
| try { |
| const cmd = `powershell -NonInteractive -WindowStyle Hidden -Command "Get-Process ${procName} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id"`; |
| const out = execSync(cmd, { encoding: "utf8", windowsHide: true, timeout: KILL_TIMEOUT_MS }); |
| out.split("\n").forEach(l => { |
| const pid = l.trim(); |
| if (pid && !isNaN(pid)) pids.push(pid); |
| }); |
| } catch { } |
| } |
| } else { |
| try { |
| const output = execSync("ps aux 2>/dev/null", { encoding: "utf8", timeout: KILL_TIMEOUT_MS }); |
| output.split("\n").forEach(line => { |
| const isAppProcess = line.includes("9router") || |
| line.includes("next-server") || |
| line.includes("cloudflared") || |
| line.includes("/bin/app/") || |
| line.includes("tray_darwin") || |
| line.includes("tray_linux"); |
| if (isAppProcess) { |
| const parts = line.trim().split(/\s+/); |
| const pid = parts[1]; |
| if (pid && !isNaN(pid) && pid !== process.pid.toString()) pids.push(pid); |
| } |
| }); |
| } catch { } |
| } |
|
|
| return pids; |
| } |
|
|
| |
| function getDataDir() { |
| if (process.env.DATA_DIR) return process.env.DATA_DIR; |
| if (process.platform === "win32") { |
| return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "9router"); |
| } |
| return path.join(os.homedir(), ".9router"); |
| } |
|
|
| function resolveBundledUpdaterPath() { |
| if (process.env.UPDATER_SCRIPT_PATH && fs.existsSync(process.env.UPDATER_SCRIPT_PATH)) { |
| return process.env.UPDATER_SCRIPT_PATH; |
| } |
| |
| |
| const fromCwd = path.join(process.cwd(), "src", "lib", "updater", "updater.js"); |
| if (fs.existsSync(fromCwd)) return fromCwd; |
| const fromParent = path.join(process.cwd(), "..", "src", "lib", "updater", "updater.js"); |
| if (fs.existsSync(fromParent)) return fromParent; |
| return fromCwd; |
| } |
|
|
| function ensureRuntimeUpdater(bundledPath) { |
| try { |
| if (!bundledPath || !fs.existsSync(bundledPath)) return bundledPath; |
| const runtimeDir = path.join(getDataDir(), "runtime", "updater"); |
| const runtimePath = path.join(runtimeDir, "updater.js"); |
| if (fs.existsSync(runtimePath)) { |
| try { |
| if (fs.statSync(bundledPath).size === fs.statSync(runtimePath).size) return runtimePath; |
| } catch { } |
| } |
| fs.mkdirSync(runtimeDir, { recursive: true }); |
| fs.copyFileSync(bundledPath, runtimePath); |
| return runtimePath; |
| } catch { |
| return bundledPath; |
| } |
| } |
|
|
| |
| export async function killAppProcesses() { |
| killMitmByPidFile(); |
| const pids = collectAppPids(); |
| const platform = process.platform; |
|
|
| 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 { } |
| }); |
|
|
| if (pids.length > 0) { |
| await new Promise(r => setTimeout(r, PROCESS_WAIT_MS)); |
| } |
| } |
|
|
| |
| function resolveRelaunchCommand() { |
| const isWin = process.platform === "win32"; |
| |
| const npx = isWin ? "npx.cmd" : "npx"; |
| return { cmd: npx, args: [UPDATER_CONFIG.npmPackageName] }; |
| } |
|
|
| |
| export function spawnUpdaterAndExit(packageName = UPDATER_CONFIG.npmPackageName) { |
| const updaterPath = ensureRuntimeUpdater(resolveBundledUpdaterPath()); |
| const isTray = process.env.TRAY_MODE === "1"; |
| const relaunch = resolveRelaunchCommand(); |
| |
| const relaunchArgs = isTray |
| ? [...relaunch.args, "--tray", "--skip-update"] |
| : [...relaunch.args, "--skip-update"]; |
|
|
| spawn(process.execPath, [updaterPath], { |
| detached: true, |
| stdio: "ignore", |
| windowsHide: true, |
| env: { |
| ...process.env, |
| UPDATER_PKG_NAME: packageName, |
| UPDATER_PORT: String(UPDATER_CONFIG.statusPort), |
| UPDATER_TAIL_LINES: String(UPDATER_CONFIG.statusLogTailLines), |
| UPDATER_RETRIES: String(UPDATER_CONFIG.installRetries), |
| UPDATER_RETRY_DELAY_MS: String(UPDATER_CONFIG.installRetryDelayMs), |
| UPDATER_LINGER_MS: String(UPDATER_CONFIG.lingerAfterDoneMs), |
| UPDATER_WAIT_MIN_MS: String(UPDATER_CONFIG.waitForExitMinMs), |
| UPDATER_WAIT_MAX_MS: String(UPDATER_CONFIG.waitForExitMaxMs), |
| UPDATER_WAIT_CHECK_MS: String(UPDATER_CONFIG.waitForExitCheckMs), |
| UPDATER_APP_PORT: String(UPDATER_CONFIG.appPort), |
| UPDATER_RELAUNCH: "1", |
| UPDATER_RELAUNCH_CMD: relaunch.cmd, |
| UPDATER_RELAUNCH_ARGS: JSON.stringify(relaunchArgs), |
| }, |
| }).unref(); |
|
|
| setTimeout(() => process.exit(0), UPDATER_CONFIG.exitDelayMs); |
| } |
|
|