// @effect-diagnostics nodeBuiltinImport:off -- Isolated D-Bus integration fixture, never the user's session bus. import * as NodeChildProcess from "node:child_process"; import * as NodeEvents from "node:events"; import * as NodeFSP from "node:fs/promises"; import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import * as NodeReadline from "node:readline"; import * as NodeURL from "node:url"; import { Message, NameFlag, Variant, sessionBus, type MessageBus } from "dbus-next"; import { expect, it, vi } from "vite-plus/test"; vi.mock("electron", () => ({ nativeImage: { createFromBuffer: () => ({ isEmpty: () => false, getSize: () => ({ width: 800, height: 600 }), }), }, })); import { LinuxCaptureConnection } from "./LinuxSnapShot.ts"; import { startNiriCaptureShortcut } from "./NiriCaptureShortcut.ts"; import { GnomeCaptureSetup } from "./GnomeCaptureSetup.ts"; import { GNOME_CAPTURE_UUID } from "./gnomeCaptureBundle.ts"; const hasDbus = NodeChildProcess.spawnSync("dbus-daemon", ["--version"]).status === 0 && NodeChildProcess.spawnSync("gdbus", ["help"]).status === 0; it.runIf(hasDbus)("captures through real D-Bus marshalling on a private bus", async () => { const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-private-dbus-")); let daemon: NodeChildProcess.ChildProcess | undefined; let server: MessageBus | undefined; const clients: LinuxCaptureConnection[] = []; let stopNiriShortcut: (() => void) | undefined; try { vi.stubEnv("XDG_CURRENT_DESKTOP", "GNOME"); vi.stubEnv("FLATPAK_ID", ""); vi.stubEnv("SNAP", ""); daemon = NodeChildProcess.spawn( "dbus-daemon", [ "--session", "--nofork", "--nopidfile", "--print-address", `--address=unix:path=${NodePath.join(directory, "bus")}`, ], { stdio: ["ignore", "pipe", "pipe"] }, ); const lines = NodeReadline.createInterface({ input: daemon.stdout! }); const [address] = await Promise.race([ NodeEvents.EventEmitter.once(lines, "line"), NodeEvents.EventEmitter.once(daemon, "exit").then(() => { throw new Error("Private D-Bus daemon failed to start."); }), ]); lines.close(); server = sessionBus({ busAddress: String(address) }); server.on("error", () => undefined); await server.requestName("org.freedesktop.portal.Desktop", NameFlag.DO_NOT_QUEUE); await server.requestName("org.gnome.Shell.Extensions.T3SnapShot", NameFlag.DO_NOT_QUEUE); await server.requestName("org.gnome.Shell", NameFlag.DO_NOT_QUEUE); await server.requestName("org.kde.KWin.ScreenShot2", NameFlag.DO_NOT_QUEUE); const png = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 0]); const imagePath = NodePath.join(directory, "image.png"); await NodeFSP.writeFile(imagePath, png); let portalVersion = 3; let extensionVersion = 1; let feedbackArgs: unknown[] | undefined; let activateTitle: unknown; let animateFrame: unknown[] | undefined; let target: unknown; let clientName: string | undefined; let shellExtensionState = 2; server.addMethodHandler((message: Message) => { if (message.member === "Register") { server!.send(Message.newMethodReturn(message)); } else if (message.member === "GetAll") { const properties = message.body[0] === "org.gnome.Shell.Extensions" ? { ShellVersion: new Variant("s", "50.0"), UserExtensionsEnabled: new Variant("b", true), } : message.body[0] === "org.kde.KWin.ScreenShot2" ? { Version: new Variant("u", 5) } : message.body[0] === "org.freedesktop.portal.Screenshot" ? { version: new Variant("u", portalVersion), AvailableTargets: new Variant("u", 8), } : { Version: new Variant("u", extensionVersion) }; server!.send(Message.newMethodReturn(message, "a{sv}", [properties])); } else if (message.member === "GetExtensionInfo") { server!.send( Message.newMethodReturn(message, "a{sv}", [ { state: new Variant("d", shellExtensionState), version: new Variant("d", 2) }, ]), ); } else if (message.member === "EnableExtension" || message.member === "DisableExtension") { expect(message.body).toEqual([GNOME_CAPTURE_UUID]); shellExtensionState = message.member === "EnableExtension" ? 1 : 2; server!.send(Message.newMethodReturn(message, "b", [true])); } else if (message.member === "Screenshot") { const options = message.body[1] as Record>; target = options.target?.value; const path = `/org/freedesktop/portal/desktop/request/${message.sender.slice(1).replaceAll(".", "_")}/${options.handle_token?.value}`; // The real transport also has to handle Response before the method reply. const signal = Message.newSignal( path, "org.freedesktop.portal.Request", "Response", "ua{sv}", [0, { uri: new Variant("s", NodeURL.pathToFileURL(imagePath).href) }], ); signal.destination = message.sender; server!.send(signal); server!.send(Message.newMethodReturn(message, "o", [path])); } else if (message.member === "Activate") { activateTitle = message.body[0]; server!.send(Message.newMethodReturn(message)); } else if (message.member === "Animate") { animateFrame = message.body; server!.send(Message.newMethodReturn(message)); } else if (message.member === "Capture" || message.member === "CaptureWithFeedback") { const feedback = message.member === "CaptureWithFeedback"; if (feedback) feedbackArgs = message.body; clientName = message.sender; server!.send( Message.newMethodReturn(message, feedback ? "aysb" : "ays", [ png, JSON.stringify({ title: "Editor", appName: "Editor", appIdentifier: "editor.desktop", processId: 42, bounds: { x: 0, y: 0, width: 800, height: 600 }, }), ...(feedback ? [true] : []), ]), ); } else return false; return true; }); const connect = () => { const client = new LinuxCaptureConnection(sessionBus({ busAddress: String(address) })); clients.push(client); return client; }; const setup = new GnomeCaptureSetup( { bundle: NodePath.resolve(import.meta.dirname, "../../gnome-extension"), dataHome: directory, }, sessionBus({ busAddress: String(address) }), ); try { expect((await setup.state()).status).toBe("disabled"); await setup.perform("enable-extension"); expect((await setup.state()).status).toBe("enabled"); await setup.perform("disable-extension"); expect((await setup.state()).status).toBe("disabled"); } finally { setup.close(); } const portal = connect(); expect(await portal.backend("com.t3tools.T3Code")).toBe("screenshot-portal"); expect(await portal.capturePortal()).toEqual({ png }); expect(target).toBe(8); portalVersion = 2; const extension = connect(); expect(await extension.backend("com.t3tools.T3Code")).toBe("gnome-extension"); expect(await extension.captureExtension("com.t3tools.T3Code")).toMatchObject({ png, window: { processId: 42 }, }); const owner = await server.call( new Message({ destination: "org.freedesktop.DBus", path: "/org/freedesktop/DBus", interface: "org.freedesktop.DBus", member: "GetNameOwner", signature: "s", body: ["com.t3tools.T3Code.SnapShot"], }), ); expect(owner?.body[0]).toBe(clientName); extension.close(); extensionVersion = 2; const updated = connect(); expect(await updated.backend("com.t3tools.T3Code")).toBe("gnome-extension"); const snapshot = await updated.captureExtension("com.t3tools.T3Code", { flash: true, animate: true, }); expect(snapshot.feedback?.animationStarted).toBe(true); await snapshot.feedback!.activate("T3 Code"); await snapshot.feedback!.animateTo({ x: 0.1, y: 0.8, width: 0.2, height: 0.1 }); await snapshot.feedback!.complete(); expect(feedbackArgs).toEqual([true, true]); expect(activateTitle).toBe("T3 Code"); expect(animateFrame).toEqual([0.1, 0.8, 0.2, 0.1]); vi.stubEnv("XDG_CURRENT_DESKTOP", "KDE"); const kde = connect(); expect(await kde.backend("com.t3tools.T3Code")).toBe("kde"); expect(kde.feedbackAvailable).toBe(false); kde.close(); const triggered = vi.fn(); const failed = vi.fn(); const niriBus = sessionBus({ busAddress: String(address) }); stopNiriShortcut = await startNiriCaptureShortcut( "com.t3tools.T3Code.NiriTest", triggered, failed, niriBus, ); const command = [ "call", "--session", "--dest", "com.t3tools.T3Code.NiriTest.SnapShot", "--object-path", "/com/t3tools/SnapShot", "--method", "com.t3tools.SnapShot.Capture", ]; // Exercise the actual command copied to Niri's configuration, including gdbus introspection. await new Promise((resolve, reject) => { NodeChildProcess.execFile( "gdbus", command, { env: { ...process.env, DBUS_SESSION_BUS_ADDRESS: String(address) }, timeout: 3_000, }, (error) => (error ? reject(error) : resolve()), ); }); expect(triggered).toHaveBeenCalledOnce(); expect(failed).not.toHaveBeenCalled(); const invalid = server.call( new Message({ destination: "com.t3tools.T3Code.NiriTest.SnapShot", path: "/com/t3tools/SnapShot", interface: "com.t3tools.SnapShot", member: "Capture", signature: "s", body: ["not allowed"], }), ); await expect(invalid).rejects.toThrow("Capture takes no arguments"); expect(triggered).toHaveBeenCalledOnce(); await expect( startNiriCaptureShortcut( "com.t3tools.T3Code.NiriTest", triggered, failed, sessionBus({ busAddress: String(address) }), ), ).rejects.toThrow("already owns"); stopNiriShortcut(); const restarted = await startNiriCaptureShortcut( "com.t3tools.T3Code.NiriTest", triggered, failed, sessionBus({ busAddress: String(address) }), ); restarted(); } finally { vi.unstubAllEnvs(); stopNiriShortcut?.(); for (const client of clients) client.close(); server?.disconnect(); if (daemon && daemon.exitCode === null) { const exited = NodeEvents.EventEmitter.once(daemon, "exit"); daemon.kill("SIGTERM"); await exited; } await NodeFSP.rm(directory, { recursive: true }); } });