Files
HealthLog/src/lib/documents/native-canvas-support.ts
T
Marc-André Bombeck 45da6c3c7d chore(release): v1.28.27 — runs on CPUs without AVX2
The bundled Skia x64 build (document thumbnails, scanned-PDF rasterization)
uses AVX2 instructions; on an x86-64 host without them — Celeron/Atom-class
NAS CPUs, pre-2013 Xeons, VMs masking CPU flags — the first canvas call is an
uncatchable SIGILL that kills the process and crash-loops the container (live
incident: a Synology DS1520+, exit 132 ~18 s after every boot, reported with
kernel traps and the correct root cause in hand).

The renderer is now gated on a one-time CPU-feature check before the module
ever loads: linux/x64 requires the avx2 cpuinfo flag; arm64 and non-Linux are
unaffected; unreadable cpuinfo fails open. Behind a closed gate, thumbnail
generation and PDF rasterization take their existing fail-soft paths (vault
shows type icons; PDFs read as text) and the boot-time thumbnail backfill
skips enqueueing. NATIVE_CANVAS=off|on overrides detection, documented in the
env example and whitelisted in compose.
2026-07-11 10:02:29 +02:00

80 lines
2.9 KiB
TypeScript

/**
* v1.28.27 — CPU-feature gate for the native canvas renderer.
*
* The bundled `@napi-rs/canvas` (Skia) x64 builds use AVX2 instructions. On an
* x86-64 host WITHOUT AVX2 — Celeron/Atom-class NAS CPUs (Goldmont Plus and
* older) are the common self-hosting case — the first canvas call dies with
* SIGILL (`trap invalid opcode … in skia.linux-x64-musl.node`), which kills the
* whole Node process and puts the container into an infinite restart loop
* (live incident: a Synology DS1520+ self-host, exit code 132 ~18 s after
* every boot once the thumbnail backfill touched Skia).
*
* A SIGILL cannot be caught in-process, so the only safe posture is to never
* load the module on an unsupported CPU. Every canvas consumer (document
* thumbnails, scanned-PDF rasterization) already has a fail-soft path — with
* the gate closed those features degrade cleanly (no thumbnails, PDFs read as
* text/native only) while the rest of the app runs normally.
*
* Detection: Linux x64 reads `/proc/cpuinfo` once and requires the `avx2`
* flag. Non-x64 (the arm64 image) and non-Linux (dev machines) are always
* supported — AVX is an x86 concept. The optional `NATIVE_CANVAS` env var
* overrides: `off` force-disables (e.g. to rule the renderer out while
* debugging), `on` force-enables (only sensible if detection misfires; on a
* CPU that truly lacks AVX2 this WILL crash the process). Unset or any other
* value → auto-detection.
*/
import { readFileSync } from "node:fs";
import { annotate } from "@/lib/logging/context";
let cached: boolean | null = null;
let announced = false;
/** Read the gate once per process; logs a single annotation when closed. */
export function nativeCanvasSupported(): boolean {
if (cached === null) {
cached = detect();
}
if (!cached && !announced) {
announced = true;
annotate({
action: { name: "documents.nativeCanvas.unsupported" },
meta: {
reason: "cpu_missing_avx2",
arch: process.arch,
platform: process.platform,
},
});
}
return cached;
}
function detect(): boolean {
const override = process.env.NATIVE_CANVAS?.trim().toLowerCase();
if (override === "off") return false;
if (override === "on") return true;
// AVX is an x86 instruction-set concept; the arm64 image and non-Linux dev
// hosts never hit the SIGILL class.
if (process.platform !== "linux" || process.arch !== "x64") return true;
try {
const cpuinfo = readFileSync("/proc/cpuinfo", "utf8");
const flagsLine = cpuinfo
.split("\n")
.find((line) => line.startsWith("flags"));
// No flags line = exotic kernel surface; fail OPEN to today's behavior
// rather than silently disabling a working renderer.
if (!flagsLine) return true;
return /\bavx2\b/.test(flagsLine);
} catch {
return true;
}
}
/** Test hook — the gate is process-cached by design. */
export function __resetNativeCanvasSupportForTests(): void {
cached = null;
announced = false;
}