diff --git a/docs/releases/UNRELEASED.md b/docs/releases/UNRELEASED.md index 1d7bb83..45f0be0 100644 --- a/docs/releases/UNRELEASED.md +++ b/docs/releases/UNRELEASED.md @@ -6,7 +6,10 @@ then reset this file. ## User-facing changes -- None yet. +- `desktop_launch` now points `WAYLAND_DISPLAY` at a non-existent socket (and + strips `WAYLAND_SOCKET`), so GTK/Qt/Electron/Flutter apps always fall back + to X11 and render inside the isolated lab display instead of opening on the + user's real Wayland desktop (where driving them moved the real cursor). ## Internal/release changes @@ -16,7 +19,9 @@ then reset this file. ### Tested -- None yet. +- Regression test asserting launched apps get `DISPLAY=` with + Wayland variables unset; full desktop-linux integration suite (Xvfb + + xdotool + xterm) passes. ### Not tested yet diff --git a/packages/desktop-linux/src/apps.ts b/packages/desktop-linux/src/apps.ts index c2fb3be..7ffc230 100644 --- a/packages/desktop-linux/src/apps.ts +++ b/packages/desktop-linux/src/apps.ts @@ -38,7 +38,17 @@ export async function launchApp(opts: LaunchAppOptions): Promise { const daemon = await startDaemon(opts.command, opts.args ?? [], { logDir: opts.logDir, cwd: opts.cwd, - env: { ...opts.env, DISPLAY: opts.display }, + env: { + ...opts.env, + DISPLAY: opts.display, + // Toolkits (GTK, Qt, Electron, Flutter) try Wayland first, which would + // open the app on the user's real desktop instead of the isolated lab + // display. Merely unsetting WAYLAND_DISPLAY is not enough: libwayland + // then falls back to the default "wayland-0" socket, so point it at a + // socket that cannot exist to force the X11 fallback. + WAYLAND_DISPLAY: "picklab-no-wayland", + WAYLAND_SOCKET: undefined, + }, }); const graceDeadline = Date.now() + LAUNCH_GRACE_MS; while (Date.now() < graceDeadline) { diff --git a/packages/desktop-linux/test/integration.test.ts b/packages/desktop-linux/test/integration.test.ts index d4192ec..eae6529 100644 --- a/packages/desktop-linux/test/integration.test.ts +++ b/packages/desktop-linux/test/integration.test.ts @@ -167,6 +167,34 @@ describe("screenshot output validation", () => { }); }); +describe("launchApp display isolation", () => { + it("redirects Wayland variables so apps render on the lab display", async () => { + const outFile = path.join(tmpRoot, "env-capture.txt"); + const binDir = path.join(tmpRoot, "env-capture-bin"); + writeExecutable( + path.join(binDir, "capture-env"), + "#!/bin/sh\n" + + "printf 'DISPLAY=%s\\nWAYLAND_DISPLAY=%s\\n' " + + '"$DISPLAY" "${WAYLAND_DISPLAY-unset}" > ' + + `'${outFile}'\n` + + "sleep 5\n", + ); + const app = await launchApp({ + display: DEAD_DISPLAY, + command: path.join(binDir, "capture-env"), + env: { ...process.env, WAYLAND_DISPLAY: "wayland-1" }, + logDir: path.join(tmpRoot, "env-capture-logs"), + }); + try { + const captured = fs.readFileSync(outFile, "utf8"); + expect(captured).toContain(`DISPLAY=${DEAD_DISPLAY}`); + expect(captured).toContain("WAYLAND_DISPLAY=picklab-no-wayland"); + } finally { + await stopPid(app.pid); + } + }); +}); + describe("launchApp early exit", () => { it("rejects with the log path when the command exits immediately", async () => { await expect(