import { spawn } from "node:child_process"; import { createInterface } from "node:readline"; import { fileURLToPath } from "node:url"; import { checkSignal, type DesktopBackend } from "./backend.ts"; export function createWindowsBackend(): DesktopBackend { const child = spawn("powershell.exe", [ "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", fileURLToPath(new URL("./windows.ps1", import.meta.url)), ], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true }); const lines = createInterface({ input: child.stdout, crlfDelay: Infinity }); const responses = lines[Symbol.asyncIterator](); let stderr = ""; let failure: Error | undefined; child.stderr.setEncoding("utf8").on("data", (chunk: string) => { stderr = (stderr + chunk).slice(-16_384); }); // A failed spawn can also emit an error on stdin. child.stdin.on("error", (error) => { failure = error; }); const exited = new Promise((resolve) => { child.once("error", (error) => { failure = error; resolve(); }); child.once("close", resolve); }); return { async execute(action, signal) { checkSignal(signal); let abort: () => void = () => {}; const cancelled = new Promise((_, reject) => { abort = () => reject(new Error("Operation aborted")); signal?.addEventListener("abort", abort, { once: true }); }); try { if (failure) throw failure; child.stdin.write(`${JSON.stringify(action)}\n`); const line = await Promise.race([responses.next(), cancelled]); if (line.done) { await exited; throw failure ?? new Error(stderr.trim() || "Windows desktop helper disconnected"); } const response = JSON.parse(line.value) as { error?: string; png?: string }; if (response.error) throw new Error(response.error); if (action.action === "screenshot") { if (!response.png) throw new Error("Windows desktop helper returned no screenshot"); return Buffer.from(response.png, "base64"); } } finally { signal?.removeEventListener("abort", abort); } }, async close() { // EOF lets the helper release held inputs even after a cancelled request. child.stdin.end(); const timer = setTimeout(() => child.kill(), 5_000); try { await exited; } finally { clearTimeout(timer); lines.close(); } }, }; }