diff --git a/electron/native-bridge/capture/linuxNativeCaptureSession.test.ts b/electron/native-bridge/capture/linuxNativeCaptureSession.test.ts index 0d900a7d7..b773aef5f 100644 --- a/electron/native-bridge/capture/linuxNativeCaptureSession.test.ts +++ b/electron/native-bridge/capture/linuxNativeCaptureSession.test.ts @@ -156,6 +156,8 @@ async function record(sourceKind?: string) { } beforeEach(() => { + // The portal helper is only looked up in a Wayland session (#1491). + vi.stubEnv("XDG_SESSION_TYPE", "wayland"); helper = new FakeHelper(); spawnMock.mockReset(); spawnMock.mockReturnValue(helper as unknown as ReturnType); @@ -169,6 +171,7 @@ beforeEach(() => { afterEach(() => { vi.restoreAllMocks(); + vi.unstubAllEnvs(); }); describe("LinuxNativeCaptureSession", () => { diff --git a/electron/native-bridge/cursor/recording/factory.ts b/electron/native-bridge/cursor/recording/factory.ts index facf1f078..4bafd3852 100644 --- a/electron/native-bridge/cursor/recording/factory.ts +++ b/electron/native-bridge/cursor/recording/factory.ts @@ -1,4 +1,5 @@ import type { Rectangle } from "electron"; +import { isWaylandSession } from "../../../wayland"; import { MacNativeCursorRecordingSession } from "./macNativeCursorRecordingSession"; import { PipeWireCursorRecordingSession } from "./pipeWireCursorRecordingSession"; import type { CursorRecordingSession } from "./session"; @@ -36,7 +37,7 @@ export function createCursorRecordingSession( }); } - if (options.platform === "linux") { + if (options.platform === "linux" && isWaylandSession(process.env)) { // The ScreenCast portal's METADATA cursor mode is the only source of a real // pointer position on Wayland: `screen.getCursorScreenPoint()` returns // {0,0} there, so TelemetryRecordingSession produced well-formed recordings @@ -50,7 +51,7 @@ export function createCursorRecordingSession( }); } - // Anything else (a future platform, or a test harness): capture cursor + // Anything else (X11, a future platform, or a test harness): capture cursor // positions via Electron's `screen` API on an interval. No cursor // sprites/assets and no clicks, just position telemetry. return new TelemetryRecordingSession({ diff --git a/electron/native-bridge/cursor/recording/pipeWireCursorRecordingSession.test.ts b/electron/native-bridge/cursor/recording/pipeWireCursorRecordingSession.test.ts index 0058079d3..da1950c03 100644 --- a/electron/native-bridge/cursor/recording/pipeWireCursorRecordingSession.test.ts +++ b/electron/native-bridge/cursor/recording/pipeWireCursorRecordingSession.test.ts @@ -33,7 +33,12 @@ vi.mock("node:fs", async (importOriginal) => { }); import { spawn } from "node:child_process"; -import { PipeWireCursorRecordingSession } from "./pipeWireCursorRecordingSession"; +import { createCursorRecordingSession } from "./factory"; +import { + findPipeWireCursorHelperPath, + PipeWireCursorRecordingSession, +} from "./pipeWireCursorRecordingSession"; +import { TelemetryRecordingSession } from "./telemetryRecordingSession"; /** Minimal stand-in for the helper process: stdio pipes plus kill bookkeeping. */ class FakeHelper extends EventEmitter { @@ -93,7 +98,13 @@ function flushStdout() { return new Promise((resolve) => setTimeout(resolve, 0)); } +const realPlatform = process.platform; + beforeEach(() => { + // The helper is only looked up in a Wayland session (#1491); pin one for the session tests. + Object.defineProperty(process, "platform", { value: "linux" }); + vi.stubEnv("XDG_SESSION_TYPE", "wayland"); + vi.stubEnv("WAYLAND_DISPLAY", undefined); helper = new FakeHelper(); spawnMock.mockReset(); spawnMock.mockReturnValue(helper as unknown as ReturnType); @@ -107,6 +118,30 @@ beforeEach(() => { afterEach(() => { vi.restoreAllMocks(); + vi.unstubAllEnvs(); + Object.defineProperty(process, "platform", { value: realPlatform }); +}); + +describe("X11 session (#1491)", () => { + const cursorOptions = { + getDisplayBounds: () => null, + maxSamples: 100, + platform: "linux" as const, + sampleIntervalMs: 33, + }; + + it("finds the portal helper in a Wayland session", () => { + expect(findPipeWireCursorHelperPath()).not.toBeNull(); + expect(createCursorRecordingSession(cursorOptions)).toBeInstanceOf( + PipeWireCursorRecordingSession, + ); + }); + + it("treats the helper as absent on X11, so capture falls back to Chromium", () => { + vi.stubEnv("XDG_SESSION_TYPE", "x11"); + expect(findPipeWireCursorHelperPath()).toBeNull(); + expect(createCursorRecordingSession(cursorOptions)).toBeInstanceOf(TelemetryRecordingSession); + }); }); describe("PipeWireCursorRecordingSession", () => { diff --git a/electron/native-bridge/cursor/recording/pipeWireCursorRecordingSession.ts b/electron/native-bridge/cursor/recording/pipeWireCursorRecordingSession.ts index 7dd222ced..492a74aea 100644 --- a/electron/native-bridge/cursor/recording/pipeWireCursorRecordingSession.ts +++ b/electron/native-bridge/cursor/recording/pipeWireCursorRecordingSession.ts @@ -3,6 +3,7 @@ import { accessSync, constants as fsConstants } from "node:fs"; import path from "node:path"; import type { Readable, Writable } from "node:stream"; import type { CursorRecordingData } from "../../../../src/native/contracts"; +import { isWaylandSession } from "../../../wayland"; import { NdjsonLineReader, PipeWireCursorAccumulator, @@ -70,6 +71,10 @@ function helperCandidates() { } export function findPipeWireCursorHelperPath() { + // The helper is a ScreenCast portal client, and KDE's portal has no ScreenCast outside + // Wayland: on X11 the take died on "CreateSession: ... Other" (#1491). Without the helper + // every caller falls back to Chromium's capture, which X11 serves without a portal. + if (!isWaylandSession(process.env)) return null; for (const candidate of helperCandidates()) { try { accessSync(candidate, fsConstants.X_OK);