fix(server): stop detecting Windows PowerShell shims
This commit is contained in:
@@ -138,7 +138,7 @@ describe("executableExists", () => {
|
|||||||
expect(executableExists("/usr/local/bin/codex", exists)).toBe("/usr/local/bin/codex");
|
expect(executableExists("/usr/local/bin/codex", exists)).toBe("/usr/local/bin/codex");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("on Windows, falls back to .exe, .cmd, then .ps1 for extensionless paths", () => {
|
test("on Windows, falls back to .exe, then .cmd for extensionless paths", () => {
|
||||||
const originalPlatform = process.platform;
|
const originalPlatform = process.platform;
|
||||||
Object.defineProperty(process, "platform", { value: "win32", writable: true });
|
Object.defineProperty(process, "platform", { value: "win32", writable: true });
|
||||||
try {
|
try {
|
||||||
@@ -150,6 +150,18 @@ describe("executableExists", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("on Windows, ignores PowerShell scripts for extensionless paths", () => {
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
Object.defineProperty(process, "platform", { value: "win32", writable: true });
|
||||||
|
try {
|
||||||
|
const exists = (candidate: string) => candidate === "C:\\tools\\codex.ps1";
|
||||||
|
|
||||||
|
expect(executableExists("C:\\tools\\codex", exists)).toBeNull();
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(process, "platform", { value: originalPlatform, writable: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("returns null when no matching path exists", () => {
|
test("returns null when no matching path exists", () => {
|
||||||
expect(executableExists("/missing/codex", () => false)).toBeNull();
|
expect(executableExists("/missing/codex", () => false)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,25 +40,6 @@ export function isWindowsCommandScript(executablePath: string): boolean {
|
|||||||
return process.platform === "win32" && (extension === ".cmd" || extension === ".bat");
|
return process.platform === "win32" && (extension === ".cmd" || extension === ".bat");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isWindowsPowerShellScript(executablePath: string): boolean {
|
|
||||||
return process.platform === "win32" && extname(executablePath).toLowerCase() === ".ps1";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function windowsPowerShellScriptArgs(scriptPath: string, args: string[]): string[] {
|
|
||||||
const argvBase64 = Buffer.from(JSON.stringify(args), "utf16le").toString("base64");
|
|
||||||
const escapedScriptPath = scriptPath.replace(/'/g, "''");
|
|
||||||
const command = [
|
|
||||||
"$ErrorActionPreference = 'Stop'",
|
|
||||||
`$argvJson = [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String('${argvBase64}'))`,
|
|
||||||
"$argv = @((ConvertFrom-Json -InputObject $argvJson))",
|
|
||||||
`& '${escapedScriptPath}' @argv`,
|
|
||||||
"exit $LASTEXITCODE",
|
|
||||||
].join("; ");
|
|
||||||
const encodedCommand = Buffer.from(command, "utf16le").toString("base64");
|
|
||||||
|
|
||||||
return ["-NoProfile", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encodedCommand];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function probeExecutable(executablePath: string): Promise<boolean> {
|
async function probeExecutable(executablePath: string): Promise<boolean> {
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
let settled = false;
|
let settled = false;
|
||||||
@@ -78,19 +59,12 @@ async function probeExecutable(executablePath: string): Promise<boolean> {
|
|||||||
|
|
||||||
let child: ChildProcess;
|
let child: ChildProcess;
|
||||||
try {
|
try {
|
||||||
const isPowerShellScript = isWindowsPowerShellScript(executablePath);
|
child = spawn(executablePath, ["--version"], {
|
||||||
child = spawn(
|
stdio: "ignore",
|
||||||
isPowerShellScript ? "powershell.exe" : executablePath,
|
windowsHide: true,
|
||||||
isPowerShellScript
|
// Windows batch shims (.cmd/.bat) require cmd.exe; native binaries do not.
|
||||||
? windowsPowerShellScriptArgs(executablePath, ["--version"])
|
shell: isWindowsCommandScript(executablePath),
|
||||||
: ["--version"],
|
});
|
||||||
{
|
|
||||||
stdio: "ignore",
|
|
||||||
windowsHide: true,
|
|
||||||
// Windows batch shims (.cmd/.bat) require cmd.exe; native binaries do not.
|
|
||||||
shell: isWindowsCommandScript(executablePath),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
settle(false);
|
settle(false);
|
||||||
return;
|
return;
|
||||||
@@ -128,7 +102,7 @@ export function executableExists(
|
|||||||
): string | null {
|
): string | null {
|
||||||
if (exists(executablePath)) return executablePath;
|
if (exists(executablePath)) return executablePath;
|
||||||
if (process.platform === "win32" && !extname(executablePath)) {
|
if (process.platform === "win32" && !extname(executablePath)) {
|
||||||
for (const ext of [".exe", ".cmd", ".ps1"]) {
|
for (const ext of [".exe", ".cmd"]) {
|
||||||
const candidate = executablePath + ext;
|
const candidate = executablePath + ext;
|
||||||
if (exists(candidate)) return candidate;
|
if (exists(candidate)) return candidate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,24 +216,12 @@ describe.runIf(process.platform === "win32")("Windows spawn launch regression",
|
|||||||
expect(result.stdout.trim()).toBe("ARGV_OK");
|
expect(result.stdout.trim()).toBe("ARGV_OK");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("detects and launches a PowerShell shim from PATH without corrupting JSON args", async () => {
|
test("does not detect a PowerShell shim from PATH", async () => {
|
||||||
const fixture = makeFixture({ includeCmdShim: false });
|
const fixture = makeFixture({ includeCmdShim: false });
|
||||||
|
|
||||||
await withWindowsPathEntry(fixture.root, async () => {
|
await withWindowsPathEntry(fixture.root, async () => {
|
||||||
const detected = await findExecutable("claude");
|
const detected = await findExecutable("claude");
|
||||||
expect(detected?.toLowerCase()).toBe(fixture.powerShellShim.toLowerCase());
|
expect(detected).toBeNull();
|
||||||
|
|
||||||
const result = await runFixture({
|
|
||||||
command: detected!,
|
|
||||||
args: fixture.expectedArgs,
|
|
||||||
shell: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.error).toBeNull();
|
|
||||||
expect(result.code).toBe(0);
|
|
||||||
expect(result.signal).toBeNull();
|
|
||||||
expect(result.stderr).toBe("");
|
|
||||||
expect(result.stdout.trim()).toBe("ARGV_OK");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { execFile, spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
|
import { execFile, spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
import {
|
import { isWindowsCommandScript, quoteWindowsArgument, quoteWindowsCommand } from "./executable.js";
|
||||||
isWindowsCommandScript,
|
|
||||||
isWindowsPowerShellScript,
|
|
||||||
quoteWindowsArgument,
|
|
||||||
quoteWindowsCommand,
|
|
||||||
windowsPowerShellScriptArgs,
|
|
||||||
} from "./executable.js";
|
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
@@ -30,14 +24,6 @@ export function spawnProcess(
|
|||||||
options?: SpawnOptions,
|
options?: SpawnOptions,
|
||||||
): ChildProcess {
|
): ChildProcess {
|
||||||
const isWindows = process.platform === "win32";
|
const isWindows = process.platform === "win32";
|
||||||
if (isWindowsPowerShellScript(command)) {
|
|
||||||
return spawn("powershell.exe", windowsPowerShellScriptArgs(command, args), {
|
|
||||||
...options,
|
|
||||||
shell: false,
|
|
||||||
windowsHide: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const shell = isWindowsCommandScript(command) ? true : (options?.shell ?? isWindows);
|
const shell = isWindowsCommandScript(command) ? true : (options?.shell ?? isWindows);
|
||||||
|
|
||||||
const shouldQuoteForShell = isWindows && shell !== false;
|
const shouldQuoteForShell = isWindows && shell !== false;
|
||||||
@@ -57,18 +43,6 @@ export async function execCommand(
|
|||||||
options?: ExecCommandOptions,
|
options?: ExecCommandOptions,
|
||||||
): Promise<ExecCommandResult> {
|
): Promise<ExecCommandResult> {
|
||||||
const isWindows = process.platform === "win32";
|
const isWindows = process.platform === "win32";
|
||||||
if (isWindowsPowerShellScript(command)) {
|
|
||||||
return execFileAsync("powershell.exe", windowsPowerShellScriptArgs(command, args), {
|
|
||||||
cwd: options?.cwd,
|
|
||||||
env: options?.env,
|
|
||||||
encoding: options?.encoding ?? "utf8",
|
|
||||||
timeout: options?.timeout,
|
|
||||||
maxBuffer: options?.maxBuffer,
|
|
||||||
shell: false,
|
|
||||||
windowsHide: true,
|
|
||||||
}) as Promise<ExecCommandResult>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shell = isWindowsCommandScript(command) ? true : isWindows;
|
const shell = isWindowsCommandScript(command) ? true : isWindows;
|
||||||
const shouldQuoteForShell = isWindows && shell !== false;
|
const shouldQuoteForShell = isWindows && shell !== false;
|
||||||
const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command;
|
const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command;
|
||||||
|
|||||||
Reference in New Issue
Block a user