From 6d3a0d874045ab32dad342bcfe811ff8e68abe48 Mon Sep 17 00:00:00 2001 From: Eddy Date: Mon, 27 Apr 2026 15:58:10 +0200 Subject: [PATCH] fix: Bridge-EPIPE-Endlosschleife + monitor_events-Schneeball verhindern [appimage] Ursache: Bridge schrieb bei EPIPE-Fehler (Tauri-Pipe geschlossen) einen monitor_event, der selbst wieder EPIPE warf -> uncaughtException-Loop. Ergebnis: 1.082.260 identische Fehler-Eintraege, DB auf 293 MB angewachsen, App hing beim Start am Index-Scan dieser Tabelle. Bridge (scripts/claude-bridge.js): - crashHandlerActive-Flag verhindert Re-Eintreten der Handler - isPipeError() erkennt EPIPE/ERR_STREAM_DESTROYED/ERR_STREAM_WRITE_AFTER_END - Bei Pipe-Fehler: process.exit(0) statt Schreibversuch - stdout/stderr error-Listener als Erstausloeser-Sperre - Alle sendEvent/sendMonitorEvent-Aufrufe in try/catch isoliert Schema (src-tauri/src/db.rs): - Trigger cleanup_old_monitor_events war AFTER INSERT (lief bei jedem Insert) -> bei 1 Mio Zeilen O(n) DELETE pro Event = O(n^2)-Schneeball - Neuer Trigger: WHEN COUNT > 50000, behaelt juengste 30000, loescht >7d alt - DROP TRIGGER vor CREATE damit Migration auf bestehenden DBs greift Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/claude-bridge.js | 49 ++++++++++++++++++++++++++++++++-------- src-tauri/src/db.rs | 15 ++++++++++-- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/scripts/claude-bridge.js b/scripts/claude-bridge.js index 115955a..1a59559 100644 --- a/scripts/claude-bridge.js +++ b/scripts/claude-bridge.js @@ -1156,6 +1156,10 @@ process.on('exit', () => { cleanupDaemon(); }); // WICHTIG: err.stack ist ein lazy Getter der bei jedem Zugriff neu formatiert wird // und bei V8-OOM selbst einen OOM-Abort auslösen kann. Daher: einmal lesen, kürzen, // try/catch drumrum — der Handler darf nicht selbst crashen. +// +// Re-Entrancy-Schutz: Wenn der Crash aus dem Schreiben auf stdout/stderr kommt +// (EPIPE — Tauri liest nicht mehr), darf der Handler nicht erneut schreiben, +// sonst Endlosschleife mit ~5 Events/s in monitor_events (siehe DB-Crash 04/2026). function safeStack(err) { try { const raw = (err && err.stack) ? String(err.stack) : ''; @@ -1164,30 +1168,55 @@ function safeStack(err) { return '[stack nicht lesbar]'; } } +let crashHandlerActive = false; +function isPipeError(err) { + if (!err) return false; + const code = err.code || (err.cause && err.cause.code); + return code === 'EPIPE' || code === 'ERR_STREAM_DESTROYED' || code === 'ERR_STREAM_WRITE_AFTER_END'; +} process.on('uncaughtException', (err) => { + if (crashHandlerActive) return; + crashHandlerActive = true; try { + // Pipe ist tot → kein Schreibversuch, sonst Loop. Stiller Exit ist hier korrekt. + if (isPipeError(err)) { + process.exit(0); + } const msg = (err && err.message) ? String(err.message).slice(0, 500) : String(err).slice(0, 500); const stack = safeStack(err); - process.stderr.write(`❌ Unbehandelter Fehler: ${msg}\n${stack}\n`); - sendEvent('bridge-error', { type: 'uncaughtException', message: msg, stack }); - sendMonitorEvent('error', `Bridge Crash: ${msg}`, {}); - } catch (inner) { - try { process.stderr.write(`❌ Handler-Fehler: ${inner && inner.message}\n`); } catch {} + try { process.stderr.write(`❌ Unbehandelter Fehler: ${msg}\n${stack}\n`); } catch {} + try { sendEvent('bridge-error', { type: 'uncaughtException', message: msg, stack }); } catch {} + try { sendMonitorEvent('error', `Bridge Crash: ${msg}`, {}); } catch {} + } finally { + crashHandlerActive = false; } }); process.on('unhandledRejection', (reason) => { + if (crashHandlerActive) return; + crashHandlerActive = true; try { + if (isPipeError(reason)) { + process.exit(0); + } const msg = reason instanceof Error ? String(reason.message).slice(0, 500) : String(reason).slice(0, 500); - process.stderr.write(`❌ Unhandled Promise Rejection: ${msg}\n`); - sendEvent('bridge-error', { type: 'unhandledRejection', message: msg }); - sendMonitorEvent('error', `Unhandled Rejection: ${msg}`, {}); - } catch (inner) { - try { process.stderr.write(`❌ Handler-Fehler: ${inner && inner.message}\n`); } catch {} + try { process.stderr.write(`❌ Unhandled Promise Rejection: ${msg}\n`); } catch {} + try { sendEvent('bridge-error', { type: 'unhandledRejection', message: msg }); } catch {} + try { sendMonitorEvent('error', `Unhandled Rejection: ${msg}`, {}); } catch {} + } finally { + crashHandlerActive = false; } }); +// stdout/stderr-Streams: EPIPE als Erstauslöser abfangen, damit der globale +// uncaughtException-Pfad gar nicht erst antritt. +for (const stream of [process.stdout, process.stderr]) { + stream.on('error', (err) => { + if (isPipeError(err)) process.exit(0); + }); +} + // Bereit-Signal (im stdio-Modus sofort senden, im Daemon-Modus pro Client bei Connect) if (!IS_DAEMON) { sendEvent('ready', { version: '1.2.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS }); diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 6f5c27b..e38a11e 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -210,12 +210,23 @@ impl Database { CREATE INDEX IF NOT EXISTS idx_monitor_timestamp ON monitor_events(timestamp DESC); CREATE INDEX IF NOT EXISTS idx_monitor_type ON monitor_events(event_type); - -- Automatisch alte Monitor-Events löschen (älter als 7 Tage) + -- Threshold-basierter Cleanup: greift nur wenn die Tabelle mehr als + -- 50_000 Zeilen hat, behaelt dann die juengsten 30_000. Loescht ausserdem + -- Eintraege aelter als 7 Tage. Verhindert die Endlosschleife aus 04/2026 + -- (Bridge-EPIPE-Crash schrieb 5 Events/s und der Trigger scannte bei + -- jedem Insert die ganze Tabelle = O(n^2)-Schneeball). + DROP TRIGGER IF EXISTS cleanup_old_monitor_events; CREATE TRIGGER IF NOT EXISTS cleanup_old_monitor_events AFTER INSERT ON monitor_events + WHEN (SELECT COUNT(*) FROM monitor_events) > 50000 BEGIN DELETE FROM monitor_events - WHERE timestamp < datetime('now', '-7 days'); + WHERE timestamp < datetime('now', '-7 days') + OR id IN ( + SELECT id FROM monitor_events + ORDER BY timestamp ASC + LIMIT MAX((SELECT COUNT(*) FROM monitor_events) - 30000, 0) + ); END; -- Projekte (für schnellen Wechsel)