mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-23 07:32:20 +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>
176 lines
5.7 KiB
TypeScript
176 lines
5.7 KiB
TypeScript
/**
|
|
* Installer utilities — safe execFile wrapper for npm operations.
|
|
*
|
|
* Hard rule #13: never string-interpolate runtime values into shell commands.
|
|
* All npm invocations use execFile() with an explicit args array, never exec().
|
|
*/
|
|
|
|
import { execFile } from "node:child_process";
|
|
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
|
|
|
const DEFAULT_TIMEOUT_MS = 300_000; // 5 min — npm install can be slow
|
|
|
|
export interface NpmRunResult {
|
|
stdout: string;
|
|
stderr: string;
|
|
}
|
|
|
|
export class InstallError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public readonly friendly: string,
|
|
public readonly httpStatus: number = 500
|
|
) {
|
|
super(message);
|
|
this.name = "InstallError";
|
|
}
|
|
}
|
|
|
|
/** Classify raw npm/OS errors into user-friendly messages. */
|
|
function classifyError(
|
|
// execFile's callback error is an ExecFileException — it carries `signal`/`killed`
|
|
// (a timed-out/terminated child) on top of ErrnoException, which @types/node's
|
|
// ErrnoException itself does not declare. Widen the param so both are typed.
|
|
err: NodeJS.ErrnoException & {
|
|
stdout?: string;
|
|
stderr?: string;
|
|
signal?: NodeJS.Signals | null;
|
|
killed?: boolean;
|
|
}
|
|
): InstallError {
|
|
const raw = sanitizeErrorMessage(err.message);
|
|
const stderr = err.stderr ?? "";
|
|
|
|
if (err.code === "EACCES") {
|
|
return new InstallError(
|
|
raw,
|
|
"Sem permissão para instalar. Verifique as permissões da pasta de dados.",
|
|
403
|
|
);
|
|
}
|
|
if (err.code === "ENOENT" && err.message.includes("npm")) {
|
|
return new InstallError(
|
|
raw,
|
|
"Node.js/npm não está disponível no PATH. Instale Node ≥22.22.2.",
|
|
500
|
|
);
|
|
}
|
|
if (err.code === "ENOSPC" || stderr.includes("ENOSPC")) {
|
|
return new InstallError(raw, "Espaço em disco insuficiente.", 507);
|
|
}
|
|
if (err.signal === "SIGTERM" || err.code === "ETIMEDOUT" || err.killed) {
|
|
return new InstallError(raw, "Instalação demorou demais. Tente novamente.", 504);
|
|
}
|
|
if (
|
|
stderr.includes("ENOTFOUND") ||
|
|
stderr.includes("network") ||
|
|
stderr.includes("ECONNREFUSED") ||
|
|
stderr.includes("ERR_INVALID_URL")
|
|
) {
|
|
return new InstallError(
|
|
raw,
|
|
"Falha de rede ao instalar. Verifique a conexão e tente novamente.",
|
|
503
|
|
);
|
|
}
|
|
|
|
return new InstallError(raw, `Falha na instalação: ${raw}`, 500);
|
|
}
|
|
|
|
/**
|
|
* Validates a user-supplied service version (npm dist-tag or semver). Constrained
|
|
* to letters, digits and `. _ + -`, with a leading alphanumeric, so the value can
|
|
* never carry shell metacharacters once `runNpm` runs under a shell on Windows
|
|
* (see `buildNpmExecOptions`). Accepts `latest`, `next`, `1.2.3`, `1.2.3-beta.1`,
|
|
* `1.2.3+build.5`; rejects `latest && calc`, `$(id)`, spaces, leading `-`, etc.
|
|
*/
|
|
export const SERVICE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
|
|
export interface NpmExecOptions {
|
|
cwd?: string;
|
|
timeout: number;
|
|
env: NodeJS.ProcessEnv;
|
|
maxBuffer: number;
|
|
shell?: boolean;
|
|
windowsHide: boolean;
|
|
}
|
|
|
|
/**
|
|
* Builds the `execFile` options for {@link runNpm}.
|
|
*
|
|
* On Windows, npm is `npm.cmd` (a batch wrapper). Node 24 refuses to `execFile`
|
|
* a `.cmd` without a shell (nodejs/node#52554 — manifests as `spawn EINVAL`, see
|
|
* issue #5379), so we enable `shell` on win32 only.
|
|
*
|
|
* Enabling the shell means the shell — not `execFile` — splits the command line,
|
|
* so NO runtime value may be interpolated into argv (Hard Rule #13). The install
|
|
* prefix (a DATA_DIR path that can legitimately contain spaces, e.g.
|
|
* `C:\Users\John Doe\.omniroute\…`) is therefore exported as the
|
|
* `npm_config_prefix` environment variable — npm's documented env form of
|
|
* `--prefix` — never as an argv entry. With the prefix moved to the environment
|
|
* and the version constrained by {@link SERVICE_VERSION_PATTERN}, every remaining
|
|
* argv entry is a static, metacharacter-free flag.
|
|
*/
|
|
export function buildNpmExecOptions(
|
|
platform: NodeJS.Platform,
|
|
options: { cwd?: string; timeoutMs: number; prefix?: string }
|
|
): NpmExecOptions {
|
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
if (options.prefix) {
|
|
env.npm_config_prefix = options.prefix;
|
|
}
|
|
const execOptions: NpmExecOptions = {
|
|
cwd: options.cwd,
|
|
timeout: options.timeoutMs,
|
|
env,
|
|
maxBuffer: 10 * 1024 * 1024, // 10 MB for npm output
|
|
// Suppress the transient conhost.exe/cmd console window Windows briefly
|
|
// flashes open for spawned child processes (see #8131).
|
|
windowsHide: true,
|
|
};
|
|
if (platform === "win32") {
|
|
execOptions.shell = true;
|
|
}
|
|
return execOptions;
|
|
}
|
|
|
|
/**
|
|
* Runs npm with the given args array. Never uses shell interpolation: argv holds
|
|
* only static flags, and any install prefix is passed via `options.prefix`
|
|
* (exported as `npm_config_prefix`), not as an argv path. See
|
|
* {@link buildNpmExecOptions} for the Windows/Node-24 shell handling.
|
|
*/
|
|
export function runNpm(
|
|
args: string[],
|
|
options: { cwd?: string; timeoutMs?: number; prefix?: string } = {}
|
|
): Promise<NpmRunResult> {
|
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
// On Windows, npm is npm.cmd; on Unix it's npm.
|
|
const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
|
|
return new Promise((resolve, reject) => {
|
|
execFile(
|
|
npmBin,
|
|
args,
|
|
buildNpmExecOptions(process.platform, {
|
|
cwd: options.cwd,
|
|
timeoutMs,
|
|
prefix: options.prefix,
|
|
}),
|
|
(err, stdout, stderr) => {
|
|
if (err) {
|
|
const classified = classifyError(
|
|
Object.assign(err, { stdout, stderr }) as NodeJS.ErrnoException & {
|
|
stdout: string;
|
|
stderr: string;
|
|
}
|
|
);
|
|
reject(classified);
|
|
} else {
|
|
resolve({ stdout, stderr });
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|