mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
- Updated global CSS to implement a new brand color palette and improve light/dark theme consistency. - Enhanced the MitmServerCard component to provide clearer user feedback regarding admin privileges. - Filtered LLM combos in the CombosPage to ensure only relevant data is displayed. - Improved APIPageClient layout for better usability and visual consistency. - Added functionality to save and load DNS tool states in the MITM manager. - Updated OAuth configuration URLs for Qwen to reflect the new endpoint structure. - Refined tunnel management logic to improve reliability and user experience.
184 lines
7.0 KiB
JavaScript
184 lines
7.0 KiB
JavaScript
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;
|
|
|
|
// Kill MITM server by PID file (MITM may run as admin/sudo)
|
|
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") {
|
|
execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore", windowsHide: true, timeout: 3000 });
|
|
} else {
|
|
try {
|
|
execSync(`sudo -n kill -9 ${pid} 2>/dev/null`, { stdio: "ignore", timeout: 3000 });
|
|
} catch {
|
|
try { process.kill(pid, "SIGKILL"); } catch { /* best effort */ }
|
|
}
|
|
}
|
|
try { fs.unlinkSync(mitmPidFile); } catch { /* best effort */ }
|
|
} catch { /* best effort */ }
|
|
}
|
|
|
|
// Collect PIDs of all 9router-related processes (excluding current)
|
|
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 isAppProcess = line.toLowerCase().includes("9router") || line.toLowerCase().includes("next-server");
|
|
if (isAppProcess) {
|
|
const match = line.match(/^"(\d+)"/);
|
|
if (match && match[1] && match[1] !== process.pid.toString()) pids.push(match[1]);
|
|
}
|
|
});
|
|
} catch { /* no processes */ }
|
|
|
|
try {
|
|
const cfCmd = `powershell -NonInteractive -WindowStyle Hidden -Command "Get-Process cloudflared -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id"`;
|
|
const cfOut = execSync(cfCmd, { encoding: "utf8", windowsHide: true, timeout: KILL_TIMEOUT_MS });
|
|
cfOut.split("\n").forEach(l => {
|
|
const pid = l.trim();
|
|
if (pid && !isNaN(pid)) pids.push(pid);
|
|
});
|
|
} catch { /* no cloudflared */ }
|
|
} 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");
|
|
if (isAppProcess) {
|
|
const parts = line.trim().split(/\s+/);
|
|
const pid = parts[1];
|
|
if (pid && !isNaN(pid) && pid !== process.pid.toString()) pids.push(pid);
|
|
}
|
|
});
|
|
} catch { /* no processes */ }
|
|
}
|
|
|
|
return pids;
|
|
}
|
|
|
|
// Copy updater.js into DATA_DIR so npm -g can overwrite node_modules safely
|
|
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;
|
|
}
|
|
// Production standalone: cwd is binAppDir (see bin/cli.js)
|
|
// Dev: cwd is app/
|
|
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 { /* recopy */ }
|
|
}
|
|
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
fs.copyFileSync(bundledPath, runtimePath);
|
|
return runtimePath;
|
|
} catch {
|
|
return bundledPath;
|
|
}
|
|
}
|
|
|
|
// Kill all app-related processes to release file locks (esp. on Windows)
|
|
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 { /* already dead */ }
|
|
});
|
|
|
|
if (pids.length > 0) {
|
|
await new Promise(r => setTimeout(r, PROCESS_WAIT_MS));
|
|
}
|
|
}
|
|
|
|
// Resolve npx/9router binary to relaunch after update (cross-platform)
|
|
function resolveRelaunchCommand() {
|
|
const isWin = process.platform === "win32";
|
|
// Prefer `npx 9router` — works regardless of global bin path changes after npm i -g
|
|
const npx = isWin ? "npx.cmd" : "npx";
|
|
return { cmd: npx, args: [UPDATER_CONFIG.npmPackageName] };
|
|
}
|
|
|
|
// Spawn detached headless updater (Node process) then exit current server
|
|
export function spawnUpdaterAndExit(packageName = UPDATER_CONFIG.npmPackageName) {
|
|
const updaterPath = ensureRuntimeUpdater(resolveBundledUpdaterPath());
|
|
const isTray = process.env.TRAY_MODE === "1";
|
|
const relaunch = resolveRelaunchCommand();
|
|
// Relaunch matching original env: tray stays tray, foreground stays foreground
|
|
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);
|
|
}
|