From 9f4bfbdfee46c3b26da7e8dd310828ef6d1482fe Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Mon, 8 Jun 2026 06:14:02 +0530 Subject: [PATCH] feat(crash): add CrashWriter for local crash dumps (cap 20, prune oldest) --- src/main/updater/crash-writer.js | 59 ++++++++++++++++++++ tests/unit/main/updater/crash-writer.test.js | 56 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 src/main/updater/crash-writer.js create mode 100644 tests/unit/main/updater/crash-writer.test.js diff --git a/src/main/updater/crash-writer.js b/src/main/updater/crash-writer.js new file mode 100644 index 0000000..b52d029 --- /dev/null +++ b/src/main/updater/crash-writer.js @@ -0,0 +1,59 @@ +const fs = require('fs'); +const path = require('path'); +const MAX_DUMPS = 20; + +class CrashWriter { + constructor(dir) { + this.dir = dir; + this._counter = 0; + fs.mkdirSync(dir, { recursive: true }); + } + + handleUncaught(err, kind) { + try { + const filename = `${Date.now()}-${++this._counter}-${kind}.json`; + const payload = { + kind, + message: err && err.message, + stack: err && err.stack, + timestamp: new Date().toISOString(), + }; + fs.writeFileSync(path.join(this.dir, filename), JSON.stringify(payload, null, 2)); + this._prune(); + } catch (writeErr) { + console.error('[crash-writer] dump write failed:', writeErr.message); + } + } + + _prune() { + const files = fs.readdirSync(this.dir).sort(); + while (files.length > MAX_DUMPS) { + const oldest = files.shift(); + try { fs.unlinkSync(path.join(this.dir, oldest)); } catch (_) { /* ignore */ } + } + } + + list() { + if (!fs.existsSync(this.dir)) return []; + return fs.readdirSync(this.dir) + .filter((f) => f.endsWith('.json')) + .sort() + .reverse() + .map((filename) => { + const full = path.join(this.dir, filename); + const data = JSON.parse(fs.readFileSync(full, 'utf-8')); + return { filename, ...data }; + }); + } + + delete(filename) { + const full = path.join(this.dir, filename); + if (fs.existsSync(full)) fs.unlinkSync(full); + } + + path() { + return this.dir; + } +} + +module.exports = { CrashWriter }; \ No newline at end of file diff --git a/tests/unit/main/updater/crash-writer.test.js b/tests/unit/main/updater/crash-writer.test.js new file mode 100644 index 0000000..ef611a6 --- /dev/null +++ b/tests/unit/main/updater/crash-writer.test.js @@ -0,0 +1,56 @@ +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { CrashWriter } = require('../../../../src/main/updater/crash-writer'); + +function tmpDir() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'crash-test-')); +} + +describe('CrashWriter', () => { + test('writes a JSON dump for an uncaught exception', () => { + const dir = tmpDir(); + const writer = new CrashWriter(dir); + const err = new Error('boom'); + err.stack = 'Error: boom\n at test.js:1:1'; + writer.handleUncaught(err, 'uncaughtException'); + const files = fs.readdirSync(dir); + expect(files.length).toBe(1); + const data = JSON.parse(fs.readFileSync(path.join(dir, files[0]), 'utf-8')); + expect(data.kind).toBe('uncaughtException'); + expect(data.message).toBe('boom'); + expect(data.stack).toContain('test.js:1:1'); + fs.rmSync(dir, { recursive: true }); + }); + + test('caps at 20 dumps, pruning oldest', () => { + const dir = tmpDir(); + const writer = new CrashWriter(dir); + for (let i = 0; i < 25; i++) { + writer.handleUncaught(new Error(`e${i}`), 'unhandledRejection'); + } + const files = fs.readdirSync(dir); + expect(files.length).toBe(20); + fs.rmSync(dir, { recursive: true }); + }); + + test('returns the list of dumps', () => { + const dir = tmpDir(); + const writer = new CrashWriter(dir); + writer.handleUncaught(new Error('a'), 'uncaughtException'); + writer.handleUncaught(new Error('b'), 'uncaughtException'); + const list = writer.list(); + expect(list.length).toBe(2); + fs.rmSync(dir, { recursive: true }); + }); + + test('deletes a dump by filename', () => { + const dir = tmpDir(); + const writer = new CrashWriter(dir); + writer.handleUncaught(new Error('x'), 'uncaughtException'); + const list = writer.list(); + writer.delete(list[0].filename); + expect(writer.list().length).toBe(0); + fs.rmSync(dir, { recursive: true }); + }); +});