Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 64 additions & 47 deletions apps/server/src/process/externalLauncher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,55 +358,72 @@ it.effect("reveals a file in File Explorer through PowerShell on Windows", () =>
// single `/select,"<path>"` switch. Mock argv assertions cannot prove this —
// only Windows' own PowerShell -> CreateProcess quoting chain can, so the
// test runs only where that chain exists.
// oxlint-disable-next-line t3code/no-global-process-runtime -- the skip decision needs the real host platform, outside any Effect runtime.
it.skipIf(process.platform !== "win32")(
"delivers the raw /select switch for spaced paths through real PowerShell",
{ timeout: 60_000 },
async () => {
const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-reveal-smoke-"));
try {
const recorderPath = NodePath.join(tempDir, "recorder.cmd");
const outputPath = NodePath.join(tempDir, "argv.txt");
NodeFS.writeFileSync(recorderPath, `@echo off\r\n>"${outputPath}" echo(%*\r\n`);

const target = "C:/workspace with spaces/media/author's clip.mp4";
const explorerTarget = target.replaceAll("/", "\\");
const source = ExternalLauncher.buildFileExplorerRevealPowerShellSource(
recorderPath,
explorerTarget,
);
const powerShellPath = `${process.env.SYSTEMROOT ?? "C:\\Windows"}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
NodeChildProcess.execFileSync(
powerShellPath,
[
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-EncodedCommand",
Buffer.from(source, "utf16le").toString("base64"),
],
{ timeout: 30_000 },
);
it.effect.skipIf(!windowsHost)(
"executes the reveal helper with production launch options and preserves the raw /select switch",
() =>
Effect.gen(function* () {
const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-reveal-smoke-"));
try {
const recorderPath = NodePath.join(tempDir, "recorder.cmd");
const outputPath = NodePath.join(tempDir, "argv.txt");
NodeFS.writeFileSync(recorderPath, `@echo off\r\n>"${outputPath}" echo(%*\r\n`);

const target = "C:/workspace with spaces/media/author's clip.mp4";
const explorerTarget = target.replaceAll("/", "\\");
const env = HostProcess.Environment.defaultValue();
let spawned: ChildProcess.StandardCommand | undefined;
yield* Effect.gen(function* () {
const launcher = yield* ExternalLauncher.ExternalLauncher;
yield* launcher.launchEditor({ editor: "file-manager", cwd: target, reveal: true });
}).pipe(
Effect.provide(
layerTest({
platform: "win32",
env: {
PATH: env.PATH ?? "",
SYSTEMROOT: env.SYSTEMROOT ?? "C:\\Windows",
PATHEXT: env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD",
},
onSpawn: (command) => {
spawned = command;
},
}),
),
);
assert.ok(spawned);
// Wait for the recording stub, not a GUI application, so process exit
// guarantees the output is ready without sleeps or polling.
const source = `${ExternalLauncher.buildFileExplorerRevealPowerShellSource(
recorderPath,
explorerTarget,
)} -Wait`;
const launch = spawned;
const exitCode = yield* Effect.promise(
() =>
new Promise<number | null>((resolve, reject) => {
const child = NodeChildProcess.spawn(
launch.command,
[...launch.args.slice(0, -1), Buffer.from(source, "utf16le").toString("base64")],
{
timeout: 30_000,
detached: launch.options.detached,
windowsHide: true,
stdio: "ignore",
},
);
child.once("error", reject);
child.once("exit", resolve);
}),
);
assert.equal(exitCode, 0);

// Start-Process returns before the recorder runs; wait for its output.
// The waits run outside the Effect runtime on purpose: the test
// exercises the real Windows process chain in real time.
// @effect-diagnostics-next-line globalTimers:off
const sleep = (millis: number) => new Promise((resolve) => setTimeout(resolve, millis));
// @effect-diagnostics-next-line globalDate:off
const deadline = Date.now() + 20_000;
// @effect-diagnostics-next-line globalDate:off
while (!NodeFS.existsSync(outputPath) && Date.now() < deadline) {
await sleep(100);
const recorded = NodeFS.readFileSync(outputPath, "utf8").trim();
assert.equal(recorded, `/select,"${explorerTarget}"`);
} finally {
NodeFS.rmSync(tempDir, { recursive: true, force: true });
}
await sleep(200);
const recorded = NodeFS.readFileSync(outputPath, "utf8").trim();
assert.equal(recorded, `/select,"${explorerTarget}"`);
} finally {
NodeFS.rmSync(tempDir, { recursive: true, force: true });
}
},
}),
{ timeout: 60_000 },
);

it.effect("does not advertise reveal on Windows when PowerShell is missing", () =>
Expand Down
19 changes: 13 additions & 6 deletions apps/server/src/process/externalLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ interface EditorLaunch {
readonly target: string;
readonly command: string;
readonly args: ReadonlyArray<string>;
readonly detached?: boolean;
}

interface ProcessLaunch {
Expand Down Expand Up @@ -636,11 +637,17 @@ const resolveFileManagerRevealLaunch = Effect.fn("resolveFileManagerRevealLaunch
}

if (platform === "win32") {
return fileExplorerRevealLaunch(
target,
normalizeWindowsFileManagerPath(target),
resolvePowerShellPath(env),
);
return {
...fileExplorerRevealLaunch(
target,
normalizeWindowsFileManagerPath(target),
resolvePowerShellPath(env),
),
// Windows PowerShell can exit successfully without executing its command
// when created with DETACHED_PROCESS. Unref still lets the helper outlive
// the request without detaching it from the Windows console.
detached: false,
};
}

if (
Expand Down Expand Up @@ -744,7 +751,7 @@ const launchEditorProcess = Effect.fn("externalLauncher.launchEditorProcess")(fu
command: spawnCommand.command,
args: spawnCommand.args,
options: {
detached: true,
detached: launch.detached ?? true,
shell: spawnCommand.shell,
stdin: "ignore",
stdout: "ignore",
Expand Down
Loading