Repair stale arm64 uiohook binary

This commit is contained in:
webadderall
2026-05-11 19:53:34 +10:00
parent be5bd4e761
commit 8c5ecc0a8b
2 changed files with 156 additions and 2 deletions
+71
View File
@@ -0,0 +1,71 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("electron", () => ({
app: {
getPath: vi.fn(() => "/tmp"),
setPath: vi.fn(),
isReady: vi.fn(() => true),
},
}));
import { repairBundledUiohookBinaryForCurrentArch } from "./interaction";
describe("repairBundledUiohookBinaryForCurrentArch", () => {
const tempRoots: string[] = [];
afterEach(async () => {
await Promise.all(
tempRoots.splice(0).map((tempRoot) => fs.rm(tempRoot, { recursive: true, force: true })),
);
});
it("promotes the bundled darwin-arm64 prebuild over a stale incompatible build", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-uiohook-"));
tempRoots.push(tempRoot);
const packageRoot = path.join(tempRoot, "uiohook-napi");
const prebuildPath = path.join(packageRoot, "prebuilds", "darwin-arm64", "node.napi.node");
const buildPath = path.join(packageRoot, "build", "Release", "uiohook_napi.node");
await fs.mkdir(path.dirname(prebuildPath), { recursive: true });
await fs.mkdir(path.dirname(buildPath), { recursive: true });
await fs.writeFile(prebuildPath, "arm64-prebuild");
await fs.writeFile(buildPath, "x64-build");
const log = vi.fn();
const repaired = repairBundledUiohookBinaryForCurrentArch(
Object.assign(new Error("mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')"), {
code: "ERR_DLOPEN_FAILED",
}),
{ packageRoot, platform: "darwin", arch: "arm64", log },
);
expect(repaired).toBe(true);
expect(await fs.readFile(buildPath, "utf8")).toBe("arm64-prebuild");
expect(log).toHaveBeenCalledWith(
"[CursorTelemetry] Repaired stale uiohook-napi binary using bundled darwin-arm64 prebuild.",
);
});
it("does not rewrite binaries for unrelated load failures", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-uiohook-"));
tempRoots.push(tempRoot);
const packageRoot = path.join(tempRoot, "uiohook-napi");
const buildPath = path.join(packageRoot, "build", "Release", "uiohook_napi.node");
await fs.mkdir(path.dirname(buildPath), { recursive: true });
await fs.writeFile(buildPath, "existing-build");
const repaired = repairBundledUiohookBinaryForCurrentArch(
Object.assign(new Error("some other dlopen failure"), {
code: "ERR_DLOPEN_FAILED",
}),
{ packageRoot, platform: "darwin", arch: "arm64" },
);
expect(repaired).toBe(false);
expect(await fs.readFile(buildPath, "utf8")).toBe("existing-build");
});
});
+85 -2
View File
@@ -1,4 +1,6 @@
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import type { HookMouseEvent, UiohookLike, UiohookModuleNamespace, CursorInteractionType } from "../types";
import {
isCursorCaptureActive,
@@ -54,8 +56,7 @@ function isUiohookLike(value: unknown): value is UiohookLike {
return typeof candidate?.on === "function" && typeof candidate?.start === "function";
}
function loadUiohookModule() {
const moduleExports = nodeRequire("uiohook-napi") as UiohookModuleNamespace;
function resolveUiohookModule(moduleExports: UiohookModuleNamespace) {
const defaultExport = moduleExports.default;
if (moduleExports.uIOhook) {
@@ -89,6 +90,88 @@ function loadUiohookModule() {
return null;
}
function shouldRepairBundledUiohookBinary(error: unknown): error is NodeJS.ErrnoException {
if (process.platform !== "darwin") {
return false;
}
if (process.arch !== "arm64") {
return false;
}
const candidate = error as NodeJS.ErrnoException | null;
return (
candidate?.code === "ERR_DLOPEN_FAILED" &&
typeof candidate.message === "string" &&
candidate.message.includes("incompatible architecture")
);
}
export function repairBundledUiohookBinaryForCurrentArch(
error: unknown,
options?: {
packageRoot?: string;
platform?: NodeJS.Platform;
arch?: string;
log?: (message: string) => void;
},
) {
const platform = options?.platform ?? process.platform;
const arch = options?.arch ?? process.arch;
if (platform !== "darwin" || arch !== "arm64") {
return false;
}
const candidate = error as NodeJS.ErrnoException | null;
if (
candidate?.code !== "ERR_DLOPEN_FAILED" ||
typeof candidate.message !== "string" ||
!candidate.message.includes("incompatible architecture")
) {
return false;
}
const packageRoot =
options?.packageRoot ?? path.dirname(nodeRequire.resolve("uiohook-napi/package.json"));
const prebuildPath = path.join(packageRoot, "prebuilds", `darwin-${arch}`, "node.napi.node");
const buildPath = path.join(packageRoot, "build", "Release", "uiohook_napi.node");
if (!fs.existsSync(prebuildPath)) {
return false;
}
try {
fs.mkdirSync(path.dirname(buildPath), { recursive: true });
fs.copyFileSync(prebuildPath, buildPath);
(options?.log ?? console.warn)(
"[CursorTelemetry] Repaired stale uiohook-napi binary using bundled darwin-arm64 prebuild.",
);
return true;
} catch {
return false;
}
}
function loadUiohookModule() {
try {
const moduleExports = nodeRequire("uiohook-napi") as UiohookModuleNamespace;
return resolveUiohookModule(moduleExports);
} catch (error) {
if (!shouldRepairBundledUiohookBinary(error)) {
throw error;
}
if (!repairBundledUiohookBinaryForCurrentArch(error)) {
throw error;
}
delete nodeRequire.cache[nodeRequire.resolve("uiohook-napi")];
const moduleExports = nodeRequire("uiohook-napi") as UiohookModuleNamespace;
return resolveUiohookModule(moduleExports);
}
}
export async function startInteractionCapture() {
if (!isCursorCaptureActive) {
return;