diff --git a/packages/server/src/utils/executable.ts b/packages/server/src/utils/executable.ts index 051a4994b..e259a2171 100644 --- a/packages/server/src/utils/executable.ts +++ b/packages/server/src/utils/executable.ts @@ -35,7 +35,7 @@ async function enumerateCandidates(name: string): Promise { }); } -function isWindowsCommandScript(executablePath: string): boolean { +export function isWindowsCommandScript(executablePath: string): boolean { const extension = extname(executablePath).toLowerCase(); return process.platform === "win32" && (extension === ".cmd" || extension === ".bat"); } @@ -141,8 +141,11 @@ function escapeWindowsCmdValue(value: string): string { const unquoted = isQuoted ? value.slice(1, -1) : value; const escaped = unquoted.replace(/%/g, "%%").replace(/([&|^<>()!])/g, "^$1"); - if (isQuoted || escaped.includes(" ")) { - return `"${escaped}"`; + if (isQuoted || /[\s"]/u.test(unquoted)) { + const quoted = escaped + .replace(/(\\*)"/g, (_match, slashes: string) => `${slashes}${slashes}\\"`) + .replace(/\\+$/u, (slashes) => `${slashes}${slashes}`); + return `"${quoted}"`; } return escaped; diff --git a/packages/server/src/utils/spawn.ts b/packages/server/src/utils/spawn.ts index bab7b047a..f7a0b0881 100644 --- a/packages/server/src/utils/spawn.ts +++ b/packages/server/src/utils/spawn.ts @@ -1,7 +1,7 @@ import { execFile, spawn, type ChildProcess, type SpawnOptions } from "node:child_process"; import { promisify } from "node:util"; -import { quoteWindowsArgument, quoteWindowsCommand } from "./executable.js"; +import { isWindowsCommandScript, quoteWindowsArgument, quoteWindowsCommand } from "./executable.js"; const execFileAsync = promisify(execFile); @@ -24,13 +24,15 @@ export function spawnProcess( options?: SpawnOptions, ): ChildProcess { const isWindows = process.platform === "win32"; + const shell = isWindowsCommandScript(command) ? true : (options?.shell ?? isWindows); - const resolvedCommand = isWindows ? quoteWindowsCommand(command) : command; - const resolvedArgs = isWindows ? args.map(quoteWindowsArgument) : args; + const shouldQuoteForShell = isWindows && shell !== false; + const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command; + const resolvedArgs = shouldQuoteForShell ? args.map(quoteWindowsArgument) : args; return spawn(resolvedCommand, resolvedArgs, { ...options, - shell: options?.shell ?? isWindows, + shell, windowsHide: true, }); } @@ -41,8 +43,10 @@ export async function execCommand( options?: ExecCommandOptions, ): Promise { const isWindows = process.platform === "win32"; - const resolvedCommand = isWindows ? quoteWindowsCommand(command) : command; - const resolvedArgs = isWindows ? args.map(quoteWindowsArgument) : args; + const shell = isWindowsCommandScript(command) ? true : isWindows; + const shouldQuoteForShell = isWindows && shell !== false; + const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command; + const resolvedArgs = shouldQuoteForShell ? args.map(quoteWindowsArgument) : args; return execFileAsync(resolvedCommand, resolvedArgs, { cwd: options?.cwd, @@ -50,7 +54,7 @@ export async function execCommand( encoding: options?.encoding ?? "utf8", timeout: options?.timeout, maxBuffer: options?.maxBuffer, - shell: isWindows, + shell, windowsHide: true, }) as Promise; }