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
65 changes: 64 additions & 1 deletion apps/server/src/process/externalLauncher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,69 @@ it.effect.skipIf(windowsHost)(
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

// Windows drive letters cannot resolve the POSIX xdg stubs.
it.effect.skipIf(windowsHost)(
"a Linux server without a display offers and opens the file manager on the desktop session's display",
() =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
for (const name of ["xdg-open", "xdg-mime"]) {
const filePath = path.join(binDir, name);
yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
yield* fileSystem.chmod(filePath, 0o755);
}

const fileManager = (sessionEnv: ReadonlyArray<string>) =>
Effect.gen(function* () {
const spawned: ChildProcess.StandardCommand[] = [];
const result = yield* Effect.gen(function* () {
const launcher = yield* ExternalLauncher.ExternalLauncher;
const editors = yield* launcher.resolveAvailableEditors();
const kind = yield* launcher.resolveFileManagerRevealKind();
const opened = yield* launcher
.launchEditor({ editor: "file-manager", cwd: "/workspace" })
.pipe(Effect.exit);
return { offered: editors.includes("file-manager"), kind, opened };
}).pipe(
Effect.provide(
layerTest({
platform: "linux",
env: { PATH: binDir },
onSpawn: (command) => spawned.push(command),
spawnResult: (command) =>
command.command === "busctl"
? { stdout: JSON.stringify({ type: "as", data: sessionEnv }) }
: command.command === "xdg-mime"
? { stdout: "org.kde.dolphin.desktop\n" }
: undefined,
}),
),
Effect.provideService(HostProcess.Environment, { PATH: binDir }),
);
const open = spawned.find((command) => command.command === "xdg-open");
return { ...result, open: open && { args: open.args, env: open.options.env } };
});

const session = yield* fileManager(["DISPLAY=:0", "HOME=/home/user"]);
assert.equal(session.offered, true);
assert.equal(session.kind, "files");
assert.equal(Exit.isSuccess(session.opened), true);
assert.deepEqual(session.open, {
args: ["/workspace"],
env: { PATH: binDir, DISPLAY: ":0" },
});

// No one is logged in to a desktop, so there is no screen to open it on.
const headless = yield* fileManager(["HOME=/home/user"]);
assert.equal(headless.offered, false);
assert.equal(headless.kind, undefined);
assert.equal(Exit.isFailure(headless.opened), true);
assert.equal(headless.open, undefined);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("launches an installed editor with platform-safe arguments", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
Expand Down Expand Up @@ -607,7 +670,7 @@ it.effect.skipIf(windowsHost)("does not open Zed with the OpenZFS zed daemon on
Effect.provide(
layerTest({
platform: "linux",
env: { HOME: home, PATH: binDir },
env: { HOME: home, PATH: binDir, DISPLAY: ":0" },
path: withSystemBin(path, systemBin),
onSpawn: (command) => spawned.push(command),
spawnResult: (command) => (command.args[0] === "--version" ? { exitCode: 1 } : undefined),
Expand Down
13 changes: 10 additions & 3 deletions apps/server/src/process/externalLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,16 +430,23 @@ const resolveBrowserLaunch = Effect.fn("externalLauncher.resolveBrowserLaunch")(
return buildBrowserLaunch(target, platform, env);
});

// Editors and the file manager open on the desktop session, so a boot-started
// server discovers and launches them with that session's display.
const readEditorEnv = Effect.fn("externalLauncher.readEditorEnv")(function* () {
const env = { ...(yield* readBrowserLaunchEnv), ...(yield* readCommandLookupEnv) };
return hasGraphicalLinuxSession(env) ? env : { ...env, ...(yield* readGraphicalSessionEnv()) };
});

const resolveAvailableEditors = Effect.fn("externalLauncher.resolveAvailableEditors")(function* () {
const platform = yield* HostProcess.Platform;
const env = { ...(yield* readBrowserLaunchEnv), ...(yield* readCommandLookupEnv) };
const env = yield* readEditorEnv();
return yield* buildAvailableEditors(platform, env).pipe(withPathDirectoryListings);
});

const resolveFileManagerRevealKind = Effect.fn("externalLauncher.resolveFileManagerRevealKind")(
function* () {
const platform = yield* HostProcess.Platform;
const env = { ...(yield* readBrowserLaunchEnv), ...(yield* readCommandLookupEnv) };
const env = yield* readEditorEnv();
return yield* fileManagerRevealKindForPlatform(platform, env);
},
);
Expand All @@ -465,7 +472,7 @@ const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* (
FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner
> {
const platform = yield* HostProcess.Platform;
const env = { ...(yield* readBrowserLaunchEnv), ...(yield* readCommandLookupEnv) };
const env = yield* readEditorEnv();
yield* Effect.annotateCurrentSpan({
"externalLauncher.editor": input.editor,
"externalLauncher.cwd": input.cwd,
Expand Down
Loading