mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 15:12:23 +03:00
* fix(windows): add windowsHide to all child process spawns (#8131) On Windows, child processes spawned without windowsHide: true cause transient conhost.exe/cmd console windows to flash open. Audited all spawn/exec/execFile/execSync/execFileSync call sites and added windowsHide: true where missing. Files patched: - src/mitm/manager.ts (MITM server spawn) - src/mitm/systemCommands.ts (sudo/system command spawn) - src/mitm/inspector/systemProxyConfig.ts (execFile wrapper) - src/shared/services/cliRuntime.ts (CLI spawn + npm execFileSync) - src/lib/plugins/loader.ts (plugin host spawn) - src/lib/providerModels/cursorAgent.ts (cursor binary spawn) - src/lib/cloudflaredTunnel.ts (cloudflared spawn) Unix-only call sites (shell: /bin/bash, which) are unaffected. electron/main.js already had windowsHide: true. * fix(windows): cover remaining spawn sites missed by #8131 windowsHide sweep Extends the #8131 windowsHide audit to the three call sites the original sweep missed: ServiceSupervisor.start() and processManager.startProcess() (both spawn() embedded-service child processes), and installers/utils.ts::buildNpmExecOptions() (the execFile() options runNpm() uses to install services). All three now always set windowsHide: true so no transient conhost.exe/cmd console window flashes open on Windows. The two spawn() options objects are factored into small, pure, exported builder functions (buildServiceSpawnOptions, buildCliproxyapiSpawnOptions) so the regression test can assert on the constructed options directly, since both call sites use a bare named `import { spawn } from "node:child_process"` that ESM live-binding semantics make unmockable without --experimental-test-module-mocks (not currently enabled repo-wide). Bumps config/quality/file-size-baseline.json for cloudflaredTunnel.ts 934->935 (the PR's own +1 windowsHide line at the existing spawn options object). Co-Authored-By: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local> Co-authored-by: Probe Test <probe@example.com> Co-authored-by: Dingding-leo <Dingding-leo@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
177 lines
4.6 KiB
TypeScript
177 lines
4.6 KiB
TypeScript
import { spawn, type ChildProcess } from "child_process";
|
|
import fs from "fs/promises";
|
|
import fsSync from "fs";
|
|
import path from "path";
|
|
import os from "os";
|
|
import { setToolStatus, getVersionManagerTool } from "@/lib/db/versionManager";
|
|
|
|
const DEFAULT_PORT = 8317;
|
|
const GRACEFUL_TIMEOUT_MS = 5000;
|
|
|
|
/**
|
|
* Builds the `spawn()` options for the cliproxyapi child process.
|
|
* `windowsHide: true` suppresses the transient conhost.exe/cmd console
|
|
* window Windows briefly flashes open for spawned child processes (#8131).
|
|
* Exported (rather than inlined) so a unit test can assert on it directly
|
|
* instead of mocking `node:child_process`.
|
|
*/
|
|
export function buildCliproxyapiSpawnOptions(): {
|
|
detached: boolean;
|
|
stdio: ["ignore", "pipe", "pipe"];
|
|
env: NodeJS.ProcessEnv;
|
|
windowsHide: boolean;
|
|
} {
|
|
return {
|
|
detached: false,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
env: { ...process.env },
|
|
windowsHide: true,
|
|
};
|
|
}
|
|
|
|
function defaultConfigDir(): string {
|
|
return process.env.CLIPROXYAPI_CONFIG_DIR || path.join(os.homedir(), ".cli-proxy-api");
|
|
}
|
|
|
|
async function writeConfig(
|
|
configDir: string,
|
|
port: number,
|
|
overrides?: Record<string, unknown>
|
|
): Promise<string> {
|
|
await fs.mkdir(configDir, { recursive: true });
|
|
const configPath = path.join(configDir, "config.yaml");
|
|
const config = `port: ${port}
|
|
host: 127.0.0.1
|
|
log_level: warn
|
|
`;
|
|
await fs.writeFile(configPath, config);
|
|
return configPath;
|
|
}
|
|
|
|
export async function startProcess(
|
|
binaryPath: string,
|
|
port?: number,
|
|
configDir?: string
|
|
): Promise<{ pid: number; port: number }> {
|
|
const existing = await getVersionManagerTool("cliproxyapi");
|
|
if (existing?.pid) {
|
|
const alive = isProcessRunning(existing.pid);
|
|
if (alive) return { pid: existing.pid, port: existing.port };
|
|
}
|
|
|
|
const actualPort = port || DEFAULT_PORT;
|
|
const actualConfigDir = configDir || defaultConfigDir();
|
|
await writeConfig(actualConfigDir, actualPort);
|
|
|
|
const child = spawn(
|
|
binaryPath,
|
|
["-c", path.join(actualConfigDir, "config.yaml")],
|
|
buildCliproxyapiSpawnOptions()
|
|
);
|
|
|
|
child.stdout?.on("data", () => {});
|
|
child.stderr?.on("data", () => {});
|
|
|
|
child.on("error", async (err) => {
|
|
await setToolStatus("cliproxyapi", "error", undefined, err.message);
|
|
});
|
|
|
|
child.on("exit", async (code) => {
|
|
if (code !== 0 && code !== null) {
|
|
await setToolStatus("cliproxyapi", "stopped", undefined, `Process exited with code ${code}`);
|
|
}
|
|
});
|
|
|
|
const pid = child.pid;
|
|
await setToolStatus("cliproxyapi", "running", pid);
|
|
|
|
return { pid, port: actualPort };
|
|
}
|
|
|
|
export function isProcessRunning(pid: number): boolean {
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function stopProcess(pid: number): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
if (!isProcessRunning(pid)) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
process.kill(pid, "SIGTERM");
|
|
} catch {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const timer = setTimeout(() => {
|
|
try {
|
|
process.kill(pid, "SIGKILL");
|
|
} catch {}
|
|
clearInterval(check);
|
|
resolve();
|
|
}, GRACEFUL_TIMEOUT_MS);
|
|
|
|
const check = setInterval(() => {
|
|
if (!isProcessRunning(pid)) {
|
|
clearTimeout(timer);
|
|
clearInterval(check);
|
|
resolve();
|
|
}
|
|
}, 200);
|
|
});
|
|
}
|
|
|
|
export async function restartProcess(
|
|
binaryPath: string,
|
|
port?: number,
|
|
configDir?: string,
|
|
currentPid?: number | null
|
|
): Promise<{ pid: number; port: number }> {
|
|
if (currentPid) {
|
|
await stopProcess(currentPid);
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
}
|
|
return startProcess(binaryPath, port, configDir);
|
|
}
|
|
|
|
export async function getProcessInfo(pid: number): Promise<{
|
|
pid: number;
|
|
alive: boolean;
|
|
memoryUsage?: number;
|
|
}> {
|
|
if (!isProcessRunning(pid)) {
|
|
return { pid, alive: false };
|
|
}
|
|
|
|
try {
|
|
if (process.platform === "linux" || process.platform === "android") {
|
|
const statusFile = `/proc/${pid}/status`;
|
|
const content = await fs.readFile(statusFile, "utf-8");
|
|
const match = content.match(/VmRSS:\s+(\d+)\s+kB/);
|
|
if (match) {
|
|
return { pid, alive: true, memoryUsage: parseInt(match[1], 10) * 1024 };
|
|
}
|
|
} else if (process.platform === "darwin") {
|
|
const { execFile } = await import("child_process");
|
|
const { promisify } = await import("util");
|
|
const execFileAsync = promisify(execFile);
|
|
const { stdout } = await execFileAsync("ps", ["-o", "rss=", "-p", String(pid)]);
|
|
const rssKb = parseInt(stdout.trim(), 10);
|
|
if (!isNaN(rssKb)) {
|
|
return { pid, alive: true, memoryUsage: rssKb * 1024 };
|
|
}
|
|
}
|
|
return { pid, alive: true };
|
|
} catch {
|
|
return { pid, alive: true };
|
|
}
|
|
}
|