mirror of
https://github.com/Ryder-Sun/Meldwork.git
synced 2026-11-07 06:56:03 -10:00
fix: write state compactly and durably, and record main-process faults
- The workspace state is rewritten synchronously on the main process for every save. It is now compact JSON written through the private atomic writer, which fsyncs before renaming (the old path could leave a truncated file after a crash). The Run Ledger snapshot is compact too. - Unhandled rejections and uncaught exceptions in the main process are written to a size-capped, secret-redacted meldwork-diagnostics.log in userData instead of disappearing, and a renderer that crashes is reloaded instead of leaving a blank window. Checks: run-ledger, local-workspace-state, local-workspace and main-security tests (185 pass). Full desktop suite: 1540 pass; frontend web and desktop builds pass (existing large-chunk warning only).
This commit is contained in:
1 parent
26d4be2404
commit
c3c090f83b
3 files changed
+34
-5
No files matched your search
@@ -1,6 +1,8 @@
|
||||
const electron = require('electron')
|
||||
const { app, BrowserWindow, dialog, ipcMain, Menu, Notification, protocol, shell } = electron
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const { redactSecrets } = require('./security/secret-redaction.cjs')
|
||||
const { AttachmentStore } = require('./attachments/attachment-store.cjs')
|
||||
const { ContentBlobStore } = require('./attachments/content-blob-store.cjs')
|
||||
const { ContextPackStore } = require('./collaboration/context-pack-store.cjs')
|
||||
@@ -77,6 +79,23 @@ const LOCAL_AGENT_KINDS = new Set([
|
||||
])
|
||||
const QUIT_CLEANUP_TIMEOUT_MS = 2500
|
||||
let mainWindow = null
|
||||
|
||||
// Faults outside a request handler are written to a small private log in
|
||||
// userData (secrets redacted, size-capped) instead of disappearing or
|
||||
// taking the app down, so instability reports have evidence.
|
||||
function recordMainProcessFault(kind, detail) {
|
||||
const text = String(detail?.stack || detail?.message || detail || '').slice(0, 4000)
|
||||
console.error(`[meldwork] ${kind}`, text)
|
||||
try {
|
||||
const file = path.join(app.getPath('userData'), 'meldwork-diagnostics.log')
|
||||
try {
|
||||
if (fs.statSync(file).size > 1024 * 1024) fs.renameSync(file, `${file}.1`)
|
||||
} catch { /* first entry */ }
|
||||
fs.appendFileSync(file, `${new Date().toISOString()} ${kind} ${redactSecrets(text)}\n`, { mode: 0o600 })
|
||||
} catch { /* diagnostics must never throw */ }
|
||||
}
|
||||
process.on('unhandledRejection', reason => recordMainProcessFault('unhandledRejection', reason))
|
||||
process.on('uncaughtException', error => recordMainProcessFault('uncaughtException', error))
|
||||
let workspace = null
|
||||
let workspaceChangedListener = null
|
||||
let workspaceRunFinishedListener = null
|
||||
@@ -677,6 +696,14 @@ function createWindow() {
|
||||
showOfflinePage(errorCode)
|
||||
})
|
||||
mainWindow.webContents.on('did-finish-load', flushPendingOpenGroup)
|
||||
// A crashed renderer used to leave a blank window; reload it so the
|
||||
// workspace (whose state lives in the main process) comes back.
|
||||
mainWindow.webContents.on('render-process-gone', (_event, details) => {
|
||||
recordMainProcessFault('render-process-gone', details?.reason || 'unknown')
|
||||
if (details?.reason !== 'clean-exit' && mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.reload()
|
||||
}
|
||||
})
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null
|
||||
})
|
||||
|
||||
@@ -311,7 +311,7 @@ class RunLedger {
|
||||
persist(runs = this.runs) {
|
||||
atomicWritePrivateFile(
|
||||
this.storagePath,
|
||||
`${JSON.stringify({ version: STORE_VERSION, runs }, null, 2)}\n`,
|
||||
`${JSON.stringify({ version: STORE_VERSION, runs })}\n`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const fs = require('node:fs')
|
||||
const { atomicWritePrivateFile } = require('../security/private-file.cjs')
|
||||
const path = require('node:path')
|
||||
const { RUN_STATUSES } = require('../runs/run-ledger-records.cjs')
|
||||
const { normalizeSessionMeta } = require('../runs/run-harness.cjs')
|
||||
@@ -247,11 +248,12 @@ function loadWorkspaceState(storagePath) {
|
||||
}
|
||||
}
|
||||
|
||||
// The whole workspace is rewritten on every save, synchronously on the main
|
||||
// process. Compact JSON cuts the bytes written (and the time the UI is
|
||||
// blocked) for long histories, and the private atomic writer fsyncs before
|
||||
// the rename so a crash cannot leave a truncated file behind.
|
||||
function saveWorkspaceState(storagePath, state) {
|
||||
fs.mkdirSync(path.dirname(storagePath), { recursive: true })
|
||||
const tempPath = `${storagePath}.tmp`
|
||||
fs.writeFileSync(tempPath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 })
|
||||
fs.renameSync(tempPath, storagePath)
|
||||
atomicWritePrivateFile(storagePath, `${JSON.stringify(state)}\n`)
|
||||
}
|
||||
|
||||
function durableWaitingRunSnapshots({ state, records, pendingGates, liveRunIds, liveGroupIds }) {
|
||||
|
||||
Reference in new issue
Block a user