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.
This commit is contained in:
@@ -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<ClipboardImage | null> {
|
||||
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<ClipboardImage | null> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user