Files
openwork/apps/desktop/electron/open-external.mjs
T

66 lines
2.2 KiB
JavaScript

import { spawn } from "node:child_process";
const DEFAULT_TIMEOUT_MS = 4000;
function describeError(error) {
if (error instanceof Error && error.message.trim()) return error.message;
return String(error ?? "unknown error");
}
async function defaultOpenExternal(url) {
const electron = await import("electron");
if (typeof electron.shell?.openExternal !== "function") {
throw new Error("Electron shell.openExternal is unavailable");
}
await electron.shell.openExternal(url);
}
export async function openExternalUrl(url, deps = {}) {
const env = deps.env ?? process.env;
if (env.OPENWORK_SIMULATE_OPEN_EXTERNAL_FAILURE === "1") {
const message = "simulated failure";
// why: enables evals to prove the failure UX without breaking a real machine.
console.error("[shell] openExternal failed:", message);
return { ok: false, error: message };
}
const timeoutMs = Number.isFinite(deps.timeoutMs) ? deps.timeoutMs : DEFAULT_TIMEOUT_MS;
const openExternal = deps.openExternal ?? defaultOpenExternal;
let timeoutId = null;
try {
// why: shell.openExternal can hang forever on Windows machines with broken https URL associations; silence is the bug we're fixing.
await Promise.race([
Promise.resolve().then(() => openExternal(url)),
new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error(`timed out after ${timeoutMs}ms`));
}, timeoutMs);
}),
]);
return { ok: true };
} catch (error) {
const message = describeError(error);
console.error("[shell] openExternal failed:", message);
const platform = deps.platform ?? process.platform;
if (platform === "win32") {
const spawnProcess = deps.spawnProcess ?? spawn;
try {
console.error("[shell] attempting rundll32 browser fallback");
const child = spawnProcess("rundll32", ["url.dll,FileProtocolHandler", url], {
detached: true,
stdio: "ignore",
});
if (typeof child?.unref === "function") child.unref();
} catch (spawnError) {
console.error("[shell] rundll32 browser fallback failed:", describeError(spawnError));
}
}
return { ok: false, error: message };
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}