mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
8376e35022
* fix(auto-update): commit app to quit before quitAndInstall on macOS (#1215) macOS in-place auto-update downloaded and unpacked the new version but never installed it: the app appeared to close, ShipIt never ran, and no restart happened. A full uninstall + reinstall did not help; only a manual DMG replace worked. Root cause is a code-level coordination bug, not the release pipeline. The published mac zips are correctly Developer-ID signed, notarized, and stapled (Team H7WS5L2ML4, consistent across 1.1.17 and 1.1.20), and latest-mac.yml is well-formed — so Squirrel.Mac signature validation passes. The failure is that quitAndInstall() drives app.quit() while two normal-quit behaviors keep the process alive: 1. the main-window close handler hides to tray when close-to-tray is enabled (it only closes when isQuitting is true), and 2. the before-quit dirty-editor guard preventDefault()s the quit for a 5s renderer round-trip. Either keeps the parent process running, so Squirrel.Mac's ShipIt helper — which waits on the parent PID to die before swapping the bundle — lands in launchd "pending spawn / on-demand-only" limbo and the service is removed without installing. This matches the reporter's diagnosis exactly ("ShipIt 没有真正启动安装器", launchd on-demand-only). Fix: before quitAndInstall fires app.quit(), mark the app as quitting for an update via windowManager.setQuittingForUpdate(true). That sets isQuitting (bypassing close-to-tray) and the before-quit handler now returns early when isQuittingForUpdate() is true (skipping the dirty-editor round-trip), so the process exits cleanly and ShipIt can run. The same fix also covers the latent Windows NSIS case where close-to-tray would block an in-place update. If the install never actually quits the app (quitAndInstall throws, or returns without app.quit() on a Squirrel follow-up error / stale download), the quitting-for-update flags are rolled back — synchronously on throw, and via a short unref'd watchdog otherwise — so the app does not get stuck permanently bypassing close-to-tray and the quit guard. Tests: unit tests for the new windowManager flags and for the install handler — ordering (setQuittingForUpdate before quitAndInstall), tray cleanup still runs, no-op when the updater fails to load, rollback on synchronous throw, and watchdog rollback when the app never quits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(auto-update): keep dirty-editor guard during update install setQuittingForUpdate only bypasses close-to-tray (so the window actually closes and Squirrel.Mac's ShipIt can swap the bundle); it must NOT skip the unsaved-work guard, or clicking "Restart Now" with a dirty SFTP editor would silently lose edits. If the user cancels to save, the quit aborts and autoUpdateBridge's watchdog clears the quitting-for-update flags. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(auto-update): clear update-quit state when the quit is cancelled When the user clicks "Restart Now" with a dirty editor open, the before-quit guard cancels the quit (settle "stay"). The update path had already called setQuittingForUpdate(true) (which flips isQuitting=true to bypass close-to-tray for the install), so without clearing it the app stays in a quitting state — close-to-tray and other !isQuitting-gated behavior bypassed — until the 10s watchdog fires. Clear it immediately on the cancelled-quit path (#1215 review). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(auto-update): check for unsaved editors before quitAndInstall (#1215) The previous fix bypassed close-to-tray so the app process actually exits and Squirrel.Mac's ShipIt can swap the bundle, while keeping the before-quit dirty-editor guard as the unsaved-work safety net. But on macOS that net has a hole: quitAndInstall() closes the window FIRST and only then fires before-quit. Once setQuittingForUpdate(true) lets the main window truly close (instead of hiding to tray), the before-quit guard can run after the window is already gone — isReachableByUser is false, so it commits the quit and silently drops unsaved SFTP edits. Fix: move the dirty-editor check to the moment the user clicks "Restart Now", in the install handler, BEFORE setQuittingForUpdate / quitAndInstall — while the window and renderer are still alive: - dirty -> abort the install (don't set the quitting flags, don't quitAndInstall) and broadcast netcatty:update:needs-save so the renderer prompts the user to save and retry. - clean -> proceed with the existing flow (commit-to-quit, tray cleanup, quitAndInstall, watchdog). - no reachable main window / crashed renderer -> install directly (no user to ask), matching the before-quit fail-open path. The before-quit dirty guard is kept as defense-in-depth: if the window is still reachable it re-checks (clean, since we just verified), and if it's already gone it lets the quit through — which is now safe because the install handler already confirmed there were no unsaved editors. The request/reply/timeout round-trip is extracted into a shared helper, electron/bridges/dirtyEditorGuard.cjs (queryDirtyEditors), so the install handler and main.cjs's before-quit guard use one implementation. main.cjs's before-quit is refactored onto it (behavior preserved: sender-filtered reply, fail-open timeout, and the setQuittingForUpdate(false) rollback when the user cancels to save). The needs-save notice is BROADCAST to every window, not just the queried main window: "Restart to Update" can be clicked from the Settings window, which would otherwise see the click do nothing. preload exposes onUpdateNeedsSave; the subscription lives in useUpdateCheck (state layer), and both consumers — App.tsx (main window) and SettingsPage (settings window) — pass an onNeedsSave callback that shows an actionable toast ("save your editors, then click Restart Now again") in en / zh-CN / ru. Also lengthen the quitting-for-update rollback watchdog from 10s to 60s. On macOS quitAndInstall() can return while Squirrel is still pulling the downloaded ZIP from the local update server before it closes the windows; on a large/slow update that can exceed 10s. Clearing isQuitting that early would let the eventual native quit hit a non-quitting close-to-tray handler and strand the install again — the exact #1215 failure. The longer window only fires when the app is realistically stuck, at the cost of close-to-tray staying bypassed a little longer in the rare genuine-failure case. Tests: queryDirtyEditors (result / no-dirty / timeout / wrong-sender / dead or crashed webContents / send-throws / no-ipcMain paths); install handler pre-check (dirty -> no quitAndInstall + needs-save broadcast to all windows; clean -> quitAndInstall runs; no main window -> installs without asking). Existing install-handler tests updated for the now-async handler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
50 lines
1.9 KiB
JavaScript
50 lines
1.9 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
|
|
const WINDOW_MANAGER_PATH = require.resolve("./windowManager.cjs");
|
|
|
|
function loadFreshWindowManager() {
|
|
// windowManager keeps the quitting flags in module-level closures, so reload
|
|
// a fresh copy per test to avoid cross-test state leakage.
|
|
delete require.cache[WINDOW_MANAGER_PATH];
|
|
return require("./windowManager.cjs");
|
|
}
|
|
|
|
test("isQuittingForUpdate defaults to false", () => {
|
|
const wm = loadFreshWindowManager();
|
|
assert.equal(wm.isQuittingForUpdate(), false);
|
|
});
|
|
|
|
test("setQuittingForUpdate(true) flips the update-install flag and commits isQuitting", () => {
|
|
const wm = loadFreshWindowManager();
|
|
assert.equal(wm.getIsQuitting(), false);
|
|
wm.setQuittingForUpdate(true);
|
|
assert.equal(wm.isQuittingForUpdate(), true);
|
|
// Must also set the generic isQuitting flag so the main-window close handler
|
|
// bypasses close-to-tray during the update quit (#1215).
|
|
assert.equal(wm.getIsQuitting(), true);
|
|
});
|
|
|
|
test("setQuittingForUpdate(false) clears BOTH the update flag and isQuitting", () => {
|
|
const wm = loadFreshWindowManager();
|
|
wm.setQuittingForUpdate(true);
|
|
assert.equal(wm.isQuittingForUpdate(), true);
|
|
assert.equal(wm.getIsQuitting(), true);
|
|
// Rollback (failed install) must restore normal close behavior — resetting
|
|
// isQuitting too, otherwise close-to-tray / settings hiding stay disabled for
|
|
// the rest of the session (#1215 review).
|
|
wm.setQuittingForUpdate(false);
|
|
assert.equal(wm.isQuittingForUpdate(), false);
|
|
assert.equal(wm.getIsQuitting(), false);
|
|
});
|
|
|
|
test("setQuittingForUpdate coerces truthy/falsy values to booleans", () => {
|
|
const wm = loadFreshWindowManager();
|
|
wm.setQuittingForUpdate(1);
|
|
assert.equal(wm.isQuittingForUpdate(), true);
|
|
wm.setQuittingForUpdate(0);
|
|
assert.equal(wm.isQuittingForUpdate(), false);
|
|
wm.setQuittingForUpdate(undefined);
|
|
assert.equal(wm.isQuittingForUpdate(), false);
|
|
});
|