[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
99
electron/bridges/dirtyEditorGuard.cjs
Normal file
99
electron/bridges/dirtyEditorGuard.cjs
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Dirty-editor guard helper.
|
||||
*
|
||||
* Both the before-quit handler (electron/main.cjs) and the auto-update install
|
||||
* handler (electron/bridges/autoUpdateBridge.cjs) need to ask the renderer
|
||||
* whether any SFTP editor tab has unsaved changes before letting the process
|
||||
* exit. This module centralizes that one-shot request/response round-trip so
|
||||
* the two call sites stay in sync (#1215).
|
||||
*
|
||||
* The renderer side lives in application/app/useAppStartupEffects.ts: it
|
||||
* listens for "app:query-dirty-editors" and replies on
|
||||
* "app:dirty-editors-result" with { hasDirty: boolean }.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Ask a specific renderer whether it has unsaved editor changes.
|
||||
*
|
||||
* Sends "app:query-dirty-editors" to the given webContents and resolves with
|
||||
* the renderer's reply. Resolves `false` (fail-open) if the renderer never
|
||||
* answers within `timeoutMs`, if the webContents can't be messaged, or if the
|
||||
* send throws — in every one of those cases there is no usable UI to surface a
|
||||
* "save first" warning on, so blocking the quit would only strand the user.
|
||||
*
|
||||
* Only a reply whose `event.sender` is the exact `webContents` we queried is
|
||||
* accepted; replies from any other window are ignored so a stray/rogue message
|
||||
* can't decide the result. The listener and timer are always torn down before
|
||||
* resolving, so a late timeout can't override an already-received reply (and
|
||||
* vice versa).
|
||||
*
|
||||
* @param {import("electron").WebContents | null | undefined} webContents
|
||||
* @param {number} timeoutMs - Max time to wait for the renderer reply.
|
||||
* @param {{ ipcMain?: import("electron").IpcMain }} [options] - Inject ipcMain
|
||||
* for tests; defaults to electron's ipcMain.
|
||||
* @returns {Promise<boolean>} true when the renderer reports unsaved changes.
|
||||
*/
|
||||
function queryDirtyEditors(webContents, timeoutMs, options = {}) {
|
||||
const ipcMain = options.ipcMain || resolveIpcMain();
|
||||
|
||||
// No renderer to ask, or no ipcMain to listen with — fail open.
|
||||
if (!ipcMain || !webContents) return Promise.resolve(false);
|
||||
if (webContents.isDestroyed?.() || webContents.isCrashed?.()) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
let timeoutId = null;
|
||||
|
||||
const settle = (hasDirty) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
ipcMain.removeListener("app:dirty-editors-result", onResult);
|
||||
resolve(hasDirty);
|
||||
};
|
||||
|
||||
function onResult(evt, payload) {
|
||||
// Defence in depth: only the renderer we queried may decide the result.
|
||||
// Use `.on` (not `.once`) so a rogue reply from another window doesn't
|
||||
// consume the listener slot and let the real reply fall through. A
|
||||
// missing/falsy sender is anomalous and treated as a wrong-window reply.
|
||||
if (evt?.sender !== webContents) return;
|
||||
settle(payload?.hasDirty === true);
|
||||
}
|
||||
ipcMain.on("app:dirty-editors-result", onResult);
|
||||
|
||||
// Timeout fallback: if the renderer never replies (crash, unhandled
|
||||
// exception in its listener, etc.) we must not hang forever. Fail open.
|
||||
timeoutId = setTimeout(() => settle(false), timeoutMs);
|
||||
|
||||
try {
|
||||
webContents.send("app:query-dirty-editors");
|
||||
} catch (err) {
|
||||
// webContents.send can throw if the renderer was destroyed between the
|
||||
// isCrashed?.() check above and this call (a real race when the GPU
|
||||
// process is dying). Tear down synchronously and fail open.
|
||||
console.warn("[DirtyEditorGuard] Failed to query renderer for dirty editors:", err);
|
||||
settle(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily resolve electron's ipcMain. Kept out of module top-level so this file
|
||||
* can be required in a plain `node --test` process (where `electron` isn't a
|
||||
* loadable module) as long as the caller injects ipcMain.
|
||||
*/
|
||||
function resolveIpcMain() {
|
||||
try {
|
||||
return require("electron").ipcMain;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { queryDirtyEditors };
|
||||
Reference in New Issue
Block a user