Skip to content
Open
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
71 changes: 71 additions & 0 deletions apps/server/src/process/externalLauncher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,80 @@ it.effect("reveals a file in File Explorer through PowerShell on Windows", () =>
"$ProgressPreference = 'SilentlyContinue'; Start-Process 'explorer.exe' -ArgumentList ('/select,\"' + 'C:\\workspace with spaces\\media\\author''s clip.mp4' + '\"')",
);
assert.equal(spawned.options.shell, false);

yield* Effect.gen(function* () {
const launcher = yield* ExternalLauncher.ExternalLauncher;
yield* launcher.launchEditor({
editor: "file-manager",
cwd: "C:/workspace with spaces/media/author's clip.mp4",
});
}).pipe(
Effect.provide(
testLayer({
platform: "win32",
env: { PATH: binDir, PATHEXT: ".COM;.EXE;.BAT;.CMD" },
onSpawn: (command) => {
spawned = command;
},
}),
),
);
assert.deepEqual(spawned.args, ["C:\\workspace with spaces\\media\\author's clip.mp4"]);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect.skipIf(!windowsHost)(
"executes the Windows reveal helper with the launcher's process options",
() =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-reveal-launch-" });
const recorderPath = path.join(tempDir, "recorder.cmd");
const outputPath = path.join(tempDir, "argv.txt");
yield* fileSystem.writeFileString(recorderPath, `@echo off\r\n>"${outputPath}" echo(%*\r\n`);
let helper: ChildProcessSpawner.ChildProcessHandle | undefined;
const recordingSpawner = ChildProcessSpawner.make((command) =>
Effect.gen(function* () {
assert.ok(ChildProcess.isStandardCommand(command));
if (!ChildProcess.isStandardCommand(command))
throw new Error("Expected a standard command");
const source = Buffer.from(command.args.at(-1) ?? "", "base64").toString("utf16le");
// Substitute only Explorer, preserving the production launch options.
// Wait for the recorder so its output is complete when the helper exits.
const recordingSource =
source.replace("'explorer.exe'", `'${recorderPath.replaceAll("'", "''")}'`) + " -Wait";
helper = yield* spawner.spawn(
ChildProcess.make(
command.command,
[
...command.args.slice(0, -1),
Buffer.from(recordingSource, "utf16le").toString("base64"),
],
command.options,
),
);
return helper;
}),
);
const launcher = yield* ExternalLauncher.make.pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, recordingSpawner),
);
yield* launcher.launchEditor({
editor: "file-manager",
cwd: "C:/workspace with spaces/author's file.md",
reveal: true,
});
assert.ok(helper);
assert.equal(yield* helper.exitCode, 0);
assert.equal(
(yield* fileSystem.readFileString(outputPath)).trim(),
`/select,"C:\\workspace with spaces\\author's file.md"`,
);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

// Real-chain smoke check for the Explorer selection contract: runs the exact
// PowerShell source the reveal launch encodes, against a stub that records
// the raw argument tail it receives, and asserts a spaced path arrives as the
Expand Down
26 changes: 17 additions & 9 deletions apps/server/src/process/externalLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ interface EditorLaunch {
readonly target: string;
readonly command: string;
readonly args: ReadonlyArray<string>;
readonly detached?: boolean;
}

interface ProcessLaunch {
Expand Down Expand Up @@ -565,9 +566,11 @@ const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* (
target: input.cwd,
command,
args:
command === "explorer.exe" && env.WSL_DISTRO_NAME !== undefined
? [resolveWslFileManagerPath(input.cwd, env.WSL_DISTRO_NAME)]
: [input.cwd],
platform === "win32"
? [normalizeWindowsFileManagerPath(input.cwd)]
: command === "explorer.exe" && env.WSL_DISTRO_NAME !== undefined
? [resolveWslFileManagerPath(input.cwd, env.WSL_DISTRO_NAME)]
: [input.cwd],
};
});

Expand Down Expand Up @@ -626,11 +629,16 @@ 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 running its command
// when detached with ignored stdio. The helper is still unref'ed below.
detached: false,
};
}

if (
Expand Down Expand Up @@ -731,7 +739,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