fix(nfc): make the Bolt Card reader an opt-in machine capability #115

Merged
padreug merged 2 commits from fix/nfc-opt-in into dev 2026-09-29 20:51:37 +00:00
Showing only changes of commit ce80d75f95 - Show all commits

fix(nfc): bail out when pcscd is absent instead of spinning the main thread

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.
Padreug 2026-09-29 22:49:45 +02:00

View file

@ -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