From aa47871e471046dd76a811da4b845ec4979a00b7 Mon Sep 17 00:00:00 2001 From: Gagan Date: Mon, 13 Jul 2026 18:16:12 +0530 Subject: [PATCH] fix(watchers): ignore .git/node_modules in apps watcher, degrade watcher fd errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The apps-dir watcher recursed into anything an installed app ships; ignore .git and node_modules segments like the workspace watcher does. (The skills watcher already bounds itself with depth: 1.) fs.watch failures (EMFILE/ENOSPC) escape chokidar's error handler as uncaught exceptions from Node watcher internals and killed the main process with the native error dialog. Catch them at the process level and log instead — watching is degradable; other uncaught exceptions keep the default dialog behavior. --- apps/x/apps/main/src/main.ts | 20 +++++++++++++++++++- apps/x/packages/core/src/apps/server.ts | 6 ++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts index 1e1d3874..c0f7d39f 100644 --- a/apps/x/apps/main/src/main.ts +++ b/apps/x/apps/main/src/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, desktopCapturer, protocol, net, shell, session, safeStorage, type Session } from "electron"; +import { app, BrowserWindow, desktopCapturer, dialog, protocol, net, shell, session, safeStorage, type Session } from "electron"; import path from "node:path"; import { setupIpcHandlers, @@ -74,6 +74,24 @@ const APP_LAUNCHED_AT = Date.now(); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +// fs.watch failures (EMFILE fd exhaustion, ENOSPC watch limits) surface as +// uncaught exceptions from Node's watcher internals, bypassing chokidar's +// 'error' handlers. Watching is a degradable feature — log and keep running. +// Everything else keeps Electron's default behavior (native error dialog), +// which we replicate since registering any listener suppresses it. +process.on('uncaughtException', (err) => { + const code = (err as NodeJS.ErrnoException | undefined)?.code; + if ((code === 'EMFILE' || code === 'ENOSPC') && (err?.stack ?? '').includes('FSWatcher')) { + console.error('[Main] file watcher error (non-fatal):', err); + return; + } + console.error('[Main] uncaught exception:', err); + dialog.showErrorBox( + 'A JavaScript error occurred in the main process', + err?.stack ?? String(err), + ); +}); + // run this as early in the main process as possible if (started) app.quit(); diff --git a/apps/x/packages/core/src/apps/server.ts b/apps/x/packages/core/src/apps/server.ts index 58565e7b..e73f26b9 100644 --- a/apps/x/packages/core/src/apps/server.ts +++ b/apps/x/packages/core/src/apps/server.ts @@ -719,6 +719,12 @@ async function startWatcher(): Promise { if (watcher) return; const w = chokidar.watch(APPS_DIR, { ignoreInitial: true, + // Installed apps may ship .git/node_modules trees — thousands of files + // no consumer renders, at one watch fd per file (chokidar v4, no fsevents). + ignored: (watchedPath: string) => { + const segments = path.relative(APPS_DIR, watchedPath).split(path.sep); + return segments.includes('.git') || segments.includes('node_modules'); + }, awaitWriteFinish: { stabilityThreshold: 180, pollInterval: 50 }, }); w.on('all', (eventName, absolutePath) => {