From 8b1cde2e3c60c1340c92b62d4b027d06df6537d3 Mon Sep 17 00:00:00 2001 From: Lorchie Date: Tue, 14 Jul 2026 00:55:49 +0200 Subject: [PATCH] fix: prevent EPIPE crash on closed stdout and skip process extensions in model registry - Swallow stdout/stderr stream errors and EPIPE in the main process and logger so a closed launching terminal can't crash the app via an unhandled error. - Skip process-type extensions in the Python model registry (they run through Electron's process runner, not the FastAPI generator registry). --- api/services/generator_registry.py | 10 +++++++++- electron/main/index.ts | 7 +++++++ electron/main/logger.ts | 12 +++++++++--- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/api/services/generator_registry.py b/api/services/generator_registry.py index b36c6c92..7f01126b 100644 --- a/api/services/generator_registry.py +++ b/api/services/generator_registry.py @@ -70,7 +70,15 @@ def _discover_extensions() -> Dict[str, Tuple[type, dict]]: continue try: - manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + + # Process extensions run via Electron's process runner, not this + # registry — skip them even when their entry file is generator.py. + if manifest.get("type", "model") != "model": + print(f"[Registry] Skipping '{ext_dir.name}': type " + f"'{manifest.get('type')}' is not handled by this registry") + continue + ext_id = manifest["id"] class_name = manifest["generator_class"] diff --git a/electron/main/index.ts b/electron/main/index.ts index 6ef0b46e..64a6e1b8 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -10,6 +10,12 @@ import { syncBuiltinExtensions } from './builtin-sync' let mainWindow: BrowserWindow | null = null let pythonBridge: PythonBridge | null = null +// When the launching terminal closes, stdout/stderr become broken pipes and +// every console.* write emits an unhandled 'error' (EPIPE) that would loop +// through the uncaughtException handler forever. Swallow stream errors. +process.stdout?.on('error', () => {}) +process.stderr?.on('error', () => {}) + function createWindow(): void { mainWindow = new BrowserWindow({ width: 1280, @@ -64,6 +70,7 @@ function createWindow(): void { app.setName('Modly') process.on('uncaughtException', (err) => { + if ((err as NodeJS.ErrnoException).code === 'EPIPE') return logger.error(`Uncaught exception: ${err.stack ?? err.message}`) mainWindow?.webContents.send('app:error', err.stack ?? err.message) }) diff --git a/electron/main/logger.ts b/electron/main/logger.ts index c5da2ea2..a85c855f 100644 --- a/electron/main/logger.ts +++ b/electron/main/logger.ts @@ -50,10 +50,16 @@ export function archiveCurrentSession(): void { } catch {} } +// Console writes throw EPIPE when the parent process' stdout/stderr pipe is +// gone (e.g. launching terminal closed). Never let logging crash the app. +function safeConsole(fn: (msg: string) => void, msg: string): void { + try { fn(msg) } catch {} +} + export const logger = { - info: (msg: string) => { console.log(msg); writeTo('modly.log', line('INFO', msg)) }, - warn: (msg: string) => { console.warn(msg); writeTo('modly.log', line('WARN', msg)) }, - error: (msg: string) => { console.error(msg); writeTo('modly.log', line('ERROR', msg)); writeTo('errors.log', line('ERROR', msg)) }, + info: (msg: string) => { safeConsole(console.log, msg); writeTo('modly.log', line('INFO', msg)) }, + warn: (msg: string) => { safeConsole(console.warn, msg); writeTo('modly.log', line('WARN', msg)) }, + error: (msg: string) => { safeConsole(console.error, msg); writeTo('modly.log', line('ERROR', msg)); writeTo('errors.log', line('ERROR', msg)) }, python: (msg: string) => { writeTo('runtime.log', line('RUNTIME', msg)) if (/error|exception|traceback|critical/i.test(msg)) {