From ce80d75f95a38abce6fdf54ce511612af784ac1b Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 29 Sep 2026 22:49:45 +0200 Subject: [PATCH] fix(nfc): bail out when pcscd is absent instead of spinning the main thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nfc-pcsc's pcsclite binding does not fail when pcscd is not running — it retries SCardEstablishContext in a tight loop on the calling thread, which here is Electron's main thread. ~12k stat()s a second on /run/pcscd/pcscd.comm, event loop dead: the window never paints, the renderer is never reaped, and the watchdog can't fire because it needs the same event loop. The douro sat like that for 11 hours at 80% CPU (its CPUQuota ceiling), ignoring SIGTERM, with nothing in the journal after [StateStore]. Check the socket exists before touching the binding. This is what makes the "best-effort, every failure swallowed into a status callback" contract in the module header true, and it also covers pcscd dying at runtime on a machine that does have a reader. --- apps/machine/electron/nfc-service.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/apps/machine/electron/nfc-service.ts b/apps/machine/electron/nfc-service.ts index e94ddaf..b128321 100644 --- a/apps/machine/electron/nfc-service.ts +++ b/apps/machine/electron/nfc-service.ts @@ -14,6 +14,7 @@ */ import { execFile } from 'node:child_process' +import { existsSync } from 'node:fs' export type NfcState = 'ready' | 'reading' | 'error' | 'card-removed' | 'unavailable' export interface NfcStatus { @@ -140,12 +141,34 @@ function resetWedgedReader(): void { * Start listening for Bolt Card taps. Idempotent. Returns a stop function. * Never throws — failures surface via onStatus. */ +/** + * pcsc-lite's client socket, created by pcscd. + * + * When pcscd is NOT running, the pcsclite binding inside nfc-pcsc does not + * fail — it retries SCardEstablishContext in a tight loop on the calling + * thread (~12k stat()s per second on this path), and that thread is Electron's + * main thread. The event loop then stops turning entirely: the window never + * paints, the dead renderer is never reaped, and main.ts's watchdog can't fire + * either, so nothing recovers it. A douro with no reader fitted sat wedged + * like that for 11 hours, ignoring SIGTERM. + * + * Checking for the socket first is what makes the "best-effort" contract in + * this module's header actually true. It also covers pcscd dying at runtime on + * a machine that does have a reader. + */ +const PCSCD_SOCKET = '/run/pcscd/pcscd.comm' + export async function startNfcReader( onCard: CardHandler, onStatus: StatusHandler ): Promise<() => void> { if (stopFn) return stopFn + if (!existsSync(PCSCD_SOCKET)) { + onStatus({ state: 'unavailable', message: `pcscd not running (${PCSCD_SOCKET} absent)` }) + return () => {} + } + let mod: unknown try { // Non-literal specifier: nfc-pcsc ships no types; keep it `any` to tsc