fix(dev): preserve command arguments in exclusive runners
Use the existing pinned cross-spawn dependency at the command-spawn boundary instead of forcing every Windows executable and argument vector through cmd.exe. Preserve native executable arguments while retaining PATH/PATHEXT and command-shim support. Validation: public runner argv and lock/cancellation tests passed on POSIX. Windows-only tests cover an executable path with spaces, bare node, npm, and the local turbo shim; native Windows execution remains unverified. Fixes #2319
This commit is contained in:
parent
b7169c8b74
commit
fd9588089d
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import spawn from "cross-spawn";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
|
|
@ -74,7 +74,6 @@ function runCommand() {
|
|||
child = spawn(command[0], command.slice(1), {
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
shell: process.platform === "win32",
|
||||
stdio: "inherit",
|
||||
});
|
||||
child.once("error", reject);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { spawn } from "node:child_process";
|
||||
import { readFile, utimes, writeFile } from "node:fs/promises";
|
||||
import { copyFile, mkdir, readFile, utimes, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
|
|
@ -116,6 +116,66 @@ it("does not treat an old database timestamp as permission to replace a live own
|
|||
}
|
||||
});
|
||||
|
||||
it("preserves executable arguments through the public runner", async () => {
|
||||
const directory = await createTestTempDirectory("rea-runner-argv-");
|
||||
const payload = 'spaces, "quotes", & ^ % ! (parens)';
|
||||
const code = "process.stdout.write(JSON.stringify(process.argv.slice(1)))";
|
||||
expect(
|
||||
await execute(directory, process.execPath, ["-e", code, payload]),
|
||||
).toMatchObject({
|
||||
code: 0,
|
||||
stdout: JSON.stringify([payload]),
|
||||
stderr: "",
|
||||
});
|
||||
});
|
||||
|
||||
it.runIf(process.platform === "win32")(
|
||||
"launches an executable from a path containing spaces",
|
||||
async () => {
|
||||
const directory = await createTestTempDirectory("rea-runner-spaced-exe-");
|
||||
const executable = join(directory, "node runtime", "node.exe");
|
||||
await mkdir(join(directory, "node runtime"), { recursive: true });
|
||||
await copyFile(process.execPath, executable);
|
||||
const payload = 'argument with spaces and "quotes" & ^ % !';
|
||||
const code = "process.stdout.write(JSON.stringify(process.argv.slice(1)))";
|
||||
expect(
|
||||
await execute(directory, executable, ["-e", code, payload]),
|
||||
).toMatchObject({
|
||||
code: 0,
|
||||
stdout: JSON.stringify([payload]),
|
||||
stderr: "",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform === "win32")(
|
||||
"resolves bare node and Windows command shims without losing arguments",
|
||||
async () => {
|
||||
const directory = await createTestTempDirectory("rea-runner-shims-");
|
||||
const payload = 'argument with spaces and "quotes" & ^ % !';
|
||||
const code = "process.stdout.write(JSON.stringify(process.argv.slice(1)))";
|
||||
expect(
|
||||
await execute(directory, "node", ["-e", code, payload]),
|
||||
).toMatchObject({
|
||||
code: 0,
|
||||
stdout: JSON.stringify([payload]),
|
||||
stderr: "",
|
||||
});
|
||||
|
||||
const npm = await execute(directory, "npm", ["--version"]);
|
||||
expect(npm).toMatchObject({ code: 0, stderr: "" });
|
||||
expect(npm.stdout.trim()).toMatch(/^\d+\.\d+\.\d+/u);
|
||||
|
||||
const turbo = await execute(
|
||||
directory,
|
||||
join(process.cwd(), "node_modules", ".bin", "turbo"),
|
||||
["--version"],
|
||||
);
|
||||
expect(turbo).toMatchObject({ code: 0, stderr: "" });
|
||||
expect(turbo.stdout.trim()).toMatch(/^\d+\.\d+\.\d+/u);
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"recovers after abrupt owner death and keeps simultaneous contenders exclusive",
|
||||
async () => {
|
||||
|
|
@ -219,6 +279,30 @@ function launch(directory: string, command: string) {
|
|||
return { child, ready, closed };
|
||||
}
|
||||
|
||||
function execute(
|
||||
directory: string,
|
||||
command: string,
|
||||
arguments_: string[] = [],
|
||||
) {
|
||||
return new Promise<{
|
||||
code: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}>((resolve, reject) => {
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[runnerPath, "test", command, ...arguments_],
|
||||
{ cwd: directory, stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk) => (stdout += chunk.toString()));
|
||||
child.stderr.on("data", (chunk) => (stderr += chunk.toString()));
|
||||
child.once("error", reject);
|
||||
child.once("close", (code) => resolve({ code, stdout, stderr }));
|
||||
});
|
||||
}
|
||||
|
||||
function withTimeout<T>(promise: Promise<T>, message: string): Promise<T> {
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
return Promise.race([
|
||||
|
|
|
|||
Loading…
Reference in New Issue