mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] electron: a dead stdout pipe no longer turns the crash handler into an infinite crash-report writer (ENG-247 candidate writer)
This commit is contained in:
@@ -6,9 +6,17 @@ const fs = require('fs');
|
||||
|
||||
const MAX_REPORTS = 30;
|
||||
const LOG_TAIL_BYTES = 64 * 1024;
|
||||
// A fault that repeats is the normal case, not the rare one, and each report costs a 64KB log read
|
||||
// plus a 68KB write. Without these two caps one stuck fault turns the reporter into a disk hog: a
|
||||
// live 1.7.6-exp2 loop wrote 30 identical reports in 36ms. Same fault inside the window is counted,
|
||||
// not rewritten, and a session can never spend more than CAP reports total.
|
||||
const DEDUPE_WINDOW_MS = 60_000;
|
||||
const MAX_REPORTS_PER_SESSION = 20;
|
||||
|
||||
let p_app = null;
|
||||
let p_notify = null;
|
||||
let p_written = 0;
|
||||
const p_lastByFingerprint = new Map();
|
||||
|
||||
function init(app, notifyFn) {
|
||||
p_app = app;
|
||||
@@ -43,7 +51,21 @@ function prune(dir) {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Kind plus the first stack frame: enough to tell two different faults apart, stable across laps of
|
||||
// the same one (the timestamps and line noise below it are not).
|
||||
function fingerprint(kind, details) {
|
||||
const stack = details && typeof details === 'object' ? String(details.stack || details.message || '') : String(details || '');
|
||||
return kind + '|' + stack.split('\n').slice(0, 2).join('|').slice(0, 300);
|
||||
}
|
||||
|
||||
function writeCrashReport(kind, details) {
|
||||
const now = Date.now();
|
||||
const fp = fingerprint(kind, details);
|
||||
const seen = p_lastByFingerprint.get(fp);
|
||||
if (seen && now - seen.at < DEDUPE_WINDOW_MS) { seen.count += 1; return null; }
|
||||
if (p_written >= MAX_REPORTS_PER_SESSION) return null;
|
||||
p_lastByFingerprint.set(fp, { at: now, count: 1 });
|
||||
p_written += 1;
|
||||
try {
|
||||
const dir = reportsDir();
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
@@ -56,6 +78,8 @@ function writeCrashReport(kind, details) {
|
||||
arch: process.arch,
|
||||
electron: process.versions.electron,
|
||||
details,
|
||||
// How many identical faults this report stands for, so dedupe hides nothing.
|
||||
repeats: (p_lastByFingerprint.get(fp) || {}).count || 1,
|
||||
backendLogTail: backendLogTail(),
|
||||
};
|
||||
fs.writeFileSync(file, JSON.stringify(report, null, 2));
|
||||
@@ -68,7 +92,8 @@ function writeCrashReport(kind, details) {
|
||||
}
|
||||
return file;
|
||||
} catch (err) {
|
||||
console.error('[crash-reports] failed to write report:', err && err.message);
|
||||
// Logging is exactly what may have failed upstream, so it cannot be allowed to throw from here.
|
||||
try { console.error('[crash-reports] failed to write report:', err && err.message); } catch (_) {}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+23
-2
@@ -1,4 +1,16 @@
|
||||
const { app, components, BrowserWindow, ipcMain, shell, session, dialog, crashReporter, powerMonitor, Menu, clipboard, globalShortcut } = require('electron');
|
||||
// Whoever was reading our stdout can go away (terminal closed, launcher exited) and then every
|
||||
// console call throws EPIPE. Unhandled, that lands in uncaughtException, whose FIRST line is a
|
||||
// console.error, which throws EPIPE again: a crash handler that crashes, forever, writing a 68KB
|
||||
// report per lap. Measured live on 1.7.6-exp2: 30 reports in 36ms, ~56MB/s of disk. Listening here
|
||||
// means the EPIPE is handled and never becomes a crash at all; after one, logging is simply off.
|
||||
function p_muteConsole() {
|
||||
const noop = () => {};
|
||||
for (const k of ['log', 'warn', 'error', 'info', 'debug', 'trace']) console[k] = noop;
|
||||
}
|
||||
for (const stream of [process.stdout, process.stderr]) {
|
||||
stream.on('error', (err) => { if (err && (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED')) p_muteConsole(); });
|
||||
}
|
||||
const whisperService = require('./voice/whisperService');
|
||||
const { createStreamingSession } = require('./voice/streamingSession');
|
||||
const whisperModels = require('./voice/whisperModels');
|
||||
@@ -104,9 +116,18 @@ try {
|
||||
// Capture every main-process throw we can. Without these, a throw inside an IPC handler or BrowserWindow event listener can die silently and look indistinguishable from a renderer crash in the trace.
|
||||
const crashReports = require('./crashReports');
|
||||
crashReports.init(app, null);
|
||||
// Reentrancy guard: anything this handler does can itself throw, and a handler that re-enters is an
|
||||
// infinite loop with a disk write in it. One lap at a time, and the logging is optional.
|
||||
let p_handlingUncaught = false;
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('[diag][main:uncaughtException]', err && err.stack || err);
|
||||
crashReports.writeCrashReport('main-uncaught-exception', { message: String(err && err.message || err), stack: String(err && err.stack || '') });
|
||||
if (p_handlingUncaught) return;
|
||||
p_handlingUncaught = true;
|
||||
try {
|
||||
try { console.error('[diag][main:uncaughtException]', err && err.stack || err); } catch (_) { p_muteConsole(); }
|
||||
crashReports.writeCrashReport('main-uncaught-exception', { message: String(err && err.message || err), stack: String(err && err.stack || '') });
|
||||
} finally {
|
||||
p_handlingUncaught = false;
|
||||
}
|
||||
});
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error('[diag][main:unhandledRejection]', reason && reason.stack || reason);
|
||||
|
||||
Reference in New Issue
Block a user