From 81cec1c38bcf09265a03b519552ebcd3f4eaae02 Mon Sep 17 00:00:00 2001 From: can1357 Date: Mon, 25 May 2026 14:03:06 +0200 Subject: [PATCH] fix(clipboard): hardened WSL PowerShell fallback for headless environments - Raised PowerShell timeout to 8s and swallowed reap errors to prevent unhandled throws on WSL interop. - Fixed fallback logic so arboard is skipped when no display server is present on headless WSL. - Added test coverage for the headless WSL short-circuit path. --- packages/coding-agent/src/utils/clipboard.ts | 17 ++++++++++++++--- .../coding-agent/test/utils/clipboard.test.ts | 18 ++++++++++++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/src/utils/clipboard.ts b/packages/coding-agent/src/utils/clipboard.ts index ed24f769c..22438ee0a 100644 --- a/packages/coding-agent/src/utils/clipboard.ts +++ b/packages/coding-agent/src/utils/clipboard.ts @@ -1,6 +1,7 @@ import { execSync } from "node:child_process"; import type { ClipboardImage } from "@oh-my-pi/pi-natives"; import * as native from "@oh-my-pi/pi-natives"; +import { logger } from "@oh-my-pi/pi-utils"; function hasDisplay(): boolean { return process.platform !== "linux" || Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY); @@ -80,7 +81,7 @@ if ($img -ne $null) { } `; -const POWERSHELL_TIMEOUT_MS = 5000; +const POWERSHELL_TIMEOUT_MS = 8000; /** * Read a clipboard image through the Windows host's PowerShell. @@ -104,6 +105,12 @@ async function readImageViaPowerShell(): Promise { try { stdout = await new Response(proc.stdout).text(); await proc.exited; + } catch (err) { + // powershell.exe is a Windows process reached over WSL interop; if it + // doesn't reap cleanly, swallow the error so the dispatcher can fall + // through to the native bridge instead of throwing. + logger.warn("clipboard: powershell read failed", { error: String(err) }); + return null; } finally { clearTimeout(timer); } @@ -136,8 +143,12 @@ export async function readImageFromClipboard(): Promise { if (isWsl()) { const image = await readImageViaPowerShell(); if (image) return image; - // Fall through: arboard may still succeed on a future WSLg release. - } else if (!hasDisplay()) { + // Fall through: arboard may still succeed on a future WSLg release — + // but only when we actually have a display server. Headless WSL has + // no display, so arboard would reject anyway. + } + + if (!hasDisplay()) { return null; } diff --git a/packages/coding-agent/test/utils/clipboard.test.ts b/packages/coding-agent/test/utils/clipboard.test.ts index c1cc0546b..94d1e4b13 100644 --- a/packages/coding-agent/test/utils/clipboard.test.ts +++ b/packages/coding-agent/test/utils/clipboard.test.ts @@ -97,9 +97,10 @@ describe("readImageFromClipboard on WSL", () => { expect(nativeSpy).not.toHaveBeenCalled(); }); - it("falls back to the native bridge when PowerShell returns no payload", async () => { + it("falls back to the native bridge when PowerShell returns no payload and a display is present", async () => { setPlatform("linux"); process.env.WSL_INTEROP = "/run/WSL/1_interop"; + process.env.WAYLAND_DISPLAY = "wayland-0"; spyPowershell([], ""); const nativeSpy = vi.spyOn(native, "readImageFromClipboard").mockResolvedValue(null); @@ -110,9 +111,10 @@ describe("readImageFromClipboard on WSL", () => { expect(nativeSpy).toHaveBeenCalledTimes(1); }); - it("falls back to the native bridge when PowerShell exits non-zero", async () => { + it("falls back to the native bridge when PowerShell exits non-zero (with display)", async () => { setPlatform("linux"); process.env.WSL_DISTRO_NAME = "Ubuntu"; + process.env.DISPLAY = ":0"; spyPowershell([], "noise", 1); const nativeSpy = vi.spyOn(native, "readImageFromClipboard").mockResolvedValue(null); @@ -120,6 +122,18 @@ describe("readImageFromClipboard on WSL", () => { await readImageFromClipboard(); expect(nativeSpy).toHaveBeenCalledTimes(1); }); + + it("returns null without invoking arboard on headless WSL when PowerShell yields nothing", async () => { + setPlatform("linux"); + process.env.WSL_DISTRO_NAME = "Ubuntu"; + // No DISPLAY / WAYLAND_DISPLAY — arboard would reject, so we must short-circuit. + + spyPowershell([], ""); + const nativeSpy = vi.spyOn(native, "readImageFromClipboard"); + + expect(await readImageFromClipboard()).toBeNull(); + expect(nativeSpy).not.toHaveBeenCalled(); + }); }); describe("readImageFromClipboard dispatch", () => {