Skip to content

Commit 76c81bf

Browse files
committed
fix: ensure last-window close triggers clean shutdown and prevents unwanted tab/window restoration
1 parent aa3754f commit 76c81bf

File tree

2 files changed

+136
-111
lines changed

2 files changed

+136
-111
lines changed

src/pages/tab-bar.js

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -690,34 +690,37 @@ restoreTabs(persistedData) {
690690
this.destroyHoverCard(); // remove lingering hover card
691691
const tabElement = document.getElementById(tabId);
692692
if (!tabElement) return;
693-
693+
694694
// Get index of tab to close
695695
const tabIndex = this.tabs.findIndex(tab => tab.id === tabId);
696696
if (tabIndex === -1) return;
697-
698-
// Prevent closing the last tab - always keep at least the home tab
697+
698+
// If this is the last tab, close the entire window instead of
699+
// forcing a "home" tab. This matches normal browser behaviour.
699700
if (this.tabs.length === 1) {
700-
// Instead of closing, navigate to home
701-
this.updateTab(tabId, { url: "peersky://home", title: "Home" });
702-
this.navigateActiveTab("peersky://home");
703-
this.saveTabsState();
701+
try {
702+
const { ipcRenderer } = require('electron');
703+
ipcRenderer.send('close-window');
704+
} catch (error) {
705+
console.error('Failed to close window on last-tab close:', error);
706+
}
704707
return;
705708
}
706-
709+
707710
// Remove tab from DOM and array
708711
tabElement.remove();
709712
this.tabs.splice(tabIndex, 1);
710-
713+
711714
// Remove associated webview
712715
const webview = this.webviews.get(tabId);
713716
if (webview) {
714717
webview.remove();
715718
this.webviews.delete(tabId);
716719
}
717-
720+
718721
// Remove tab from group
719722
this.removeTabFromGroup(tabId);
720-
723+
721724
// If we closed the active tab, select another one
722725
if (this.activeTabId === tabId) {
723726
// Select the previous tab, or the next one if there is no previous
@@ -726,10 +729,10 @@ restoreTabs(persistedData) {
726729
this.selectTab(this.tabs[newTabIndex].id);
727730
}
728731
}
729-
732+
730733
// Save state after closing tab
731734
this.saveTabsState();
732-
735+
733736
// Dispatch event that tab was closed
734737
this.dispatchEvent(new CustomEvent("tab-closed", { detail: { tabId } }));
735738
}
@@ -1469,6 +1472,7 @@ restoreTabs(persistedData) {
14691472
this.saveTabsState();
14701473
}
14711474

1475+
// TODO: There are two handleGroupContextMenuAction() implementations. This one is overwritten by the later definition — decide which one to keep.
14721476
// Enhanced handleGroupContextMenuAction to work with groups from any window
14731477
handleGroupContextMenuAction(action, groupId) {
14741478
console.log(`Handling group action: ${action} for group: ${groupId}`);
@@ -2000,6 +2004,7 @@ restoreTabs(persistedData) {
20002004
document.body.appendChild(menu);
20012005
}
20022006

2007+
// TODO: This overrides the earlier enhanced version — remove or merge the logic.
20032008
// Handle group context menu actions
20042009
handleGroupContextMenuAction(action, groupId) {
20052010
switch (action) {

src/window-manager.js

Lines changed: 118 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -42,56 +42,68 @@ class WindowManager {
4242
this.saverInterval = DEFAULT_SAVE_INTERVAL;
4343
this.isSaving = false;
4444
this.isQuitting = false;
45-
this.isClosingLastWindow = false
4645
this.shutdownInProgress = false;
47-
this.saveQueue = Promise.resolve();
4846
this.finalSaveCompleted = false;
49-
this.isClearingState = false;
47+
this.isClearingState = false;
48+
this.saveQueue = Promise.resolve();
5049
this.registerListeners();
5150

52-
// Add signal handlers for graceful shutdown
53-
process.on('SIGINT', this.handleGracefulShutdown.bind(this));
54-
process.on('SIGTERM', this.handleGracefulShutdown.bind(this));
55-
56-
// Enhanced app event handlers for proper UI quit handling
57-
app.on('before-quit', (event) => {
58-
// If we're clearing state (windows closed), don't save
59-
if (this.isClearingState) {
60-
return;
61-
}
62-
63-
if (!this.finalSaveCompleted) {
64-
event.preventDefault();
65-
66-
if (!this.isQuitting && !this.shutdownInProgress) {
67-
this.handleGracefulShutdown();
51+
// Treat Ctrl+C / SIGTERM as explicit quits in dev:
52+
if (!app.isPackaged) {
53+
const handleSignal = (signal) => {
54+
if (this.shutdownInProgress) {
55+
console.log(`${signal} received while shutdown already in progress – ignoring.`);
56+
return;
6857
}
69-
}
70-
});
71-
72-
// Handle when all windows are closed (UI quit)
73-
app.on('window-all-closed', async () => {
74-
if (!this.isQuitting && !this.shutdownInProgress) {
75-
// On macOS, keep app running, on other platforms quit
76-
// Clear saved state since user closed all windows (didn't quit)
77-
this.isClearingState = true;
58+
7859
this.isQuitting = true;
7960
this.shutdownInProgress = true;
80-
this.finalSaveCompleted = true; // Prevent before-quit from trying to save
8161
this.stopSaver();
82-
await this.clearSavedState();
83-
84-
if (process.platform !== 'darwin') {
85-
app.quit();
86-
}
87-
}
88-
});
62+
63+
console.log(`${signal} received – saving session state WITHOUT clearing it, then exiting.`);
64+
65+
(async () => {
66+
try {
67+
await this.saveCompleteState();
68+
} catch (error) {
69+
console.error(`Error during ${signal} shutdown save:`, error);
70+
} finally {
71+
this.finalSaveCompleted = true;
72+
app.exit(0);
73+
}
74+
})();
75+
};
76+
77+
process.on('SIGINT', () => handleSignal('SIGINT'));
78+
process.on('SIGTERM', () => handleSignal('SIGTERM'));
79+
}
8980

90-
// Handle app activation (macOS specific)
91-
app.on('activate', () => {
92-
if (this.windows.size === 0 && !this.isQuitting) {
93-
this.open();
81+
app.on('before-quit', (event) => {
82+
// Avoid re-entering if something calls app.quit() again
83+
if (this.shutdownInProgress) {
84+
console.log('before-quit: shutdown already in progress, ignoring.');
85+
return;
9486
}
87+
88+
console.log('before-quit: performing final session save (without clearing).');
89+
this.isQuitting = true;
90+
this.shutdownInProgress = true;
91+
this.stopSaver();
92+
93+
// Prevent the default quit, we'll exit manually after the async save
94+
event.preventDefault();
95+
96+
(async () => {
97+
try {
98+
await this.saveCompleteState();
99+
} catch (error) {
100+
console.error('Error during final save in before-quit:', error);
101+
} finally {
102+
this.finalSaveCompleted = true;
103+
// Important: app.exit() does not re-emit 'before-quit'
104+
app.exit(0);
105+
}
106+
})();
95107
});
96108
}
97109

@@ -100,25 +112,22 @@ class WindowManager {
100112
this.open();
101113
});
102114

103-
// Add quit handler for UI quit button
115+
// Explicit quit from UI (custom Quit button, etc.).
116+
// We just call app.quit(); app.on('before-quit') will clear session files.
104117
ipcMain.on("quit-app", () => {
105118
console.log("Quit app requested from UI");
106-
this.handleGracefulShutdown();
119+
this.isQuitting = true;
120+
app.quit();
107121
});
108122

109-
// Add window close handler that checks if it's the last window
123+
// Close the current window and let Electron/main.js decide
124+
// whether the app should quit (non-macOS) or stay alive (macOS).
110125
ipcMain.on("close-window", (event) => {
111126
const senderId = event.sender.id;
112127
const window = this.findWindowBySenderId(senderId);
113128

114129
if (window) {
115-
// If this is the last window, trigger graceful shutdown
116-
if (this.windows.size === 1 && !this.isQuitting) {
117-
this.handleGracefulShutdown();
118-
} else {
119-
// Otherwise just close this window normally
120-
window.window.close();
121-
}
130+
window.window.close();
122131
}
123132
});
124133

@@ -447,7 +456,6 @@ class WindowManager {
447456

448457
// If this is the last window, mark that we're clearing state
449458
if (this.windows.size === 1) {
450-
this.isClearingState = true;
451459
this.stopSaver();
452460
}
453461
});
@@ -498,48 +506,6 @@ class WindowManager {
498506
console.error("Error clearing saved session state:", error);
499507
}
500508
}
501-
async handleGracefulShutdown() {
502-
if (this.shutdownInProgress) {
503-
console.log('Shutdown already in progress, ignoring additional signals');
504-
return;
505-
}
506-
507-
// Set flags IMMEDIATELY to block window closes and new saves
508-
this.shutdownInProgress = true;
509-
this.isQuitting = true;
510-
console.log('Graceful shutdown initiated...');
511-
512-
// Stop the periodic saver immediately
513-
this.stopSaver();
514-
515-
const forceExitTimeout = setTimeout(() => {
516-
console.log('Forced exit after timeout');
517-
process.exit(1);
518-
}, 8000);
519-
520-
try {
521-
console.log('Saving final state before exit...');
522-
await this.saveCompleteState();
523-
this.finalSaveCompleted = true;
524-
console.log('State saved successfully. Now safe to close windows.');
525-
526-
// ONLY AFTER successful save, close all windows
527-
console.log('Destroying windows...');
528-
const windowsToClose = Array.from(this.windows);
529-
for (const window of windowsToClose) {
530-
if (!window.window.isDestroyed()) {
531-
window.window.destroy();
532-
}
533-
}
534-
535-
} catch (error) {
536-
console.error('Error during shutdown:', error);
537-
} finally {
538-
clearTimeout(forceExitTimeout);
539-
console.log('Exiting application...');
540-
app.quit();
541-
}
542-
}
543509

544510
async saveCompleteState() {
545511
// Save both window positions/sizes and tab data
@@ -567,8 +533,26 @@ class WindowManager {
567533

568534
console.log(`Found ${validWindows.length} valid windows to save`);
569535

536+
// If there are no live windows…
570537
if (validWindows.length === 0) {
571-
console.warn('No valid windows to save!');
538+
// When the app is quitting (Cmd+Q, menu Quit, SIGINT, etc.), we MUST NOT clear
539+
// the session files. We just leave whatever was last saved on disk.
540+
if (this.isQuitting || this.shutdownInProgress) {
541+
console.warn('No valid windows to save during quit – leaving window state file untouched.');
542+
return;
543+
}
544+
545+
// But if the app is still running and the user has closed the last window,
546+
// we DO clear the file so the next launch starts fresh
547+
console.warn('No valid windows to save – clearing window state file so session does not restore.');
548+
try {
549+
const tempPath = PERSIST_FILE + ".tmp";
550+
await fs.outputJson(tempPath, [], { spaces: 2 });
551+
await fs.move(tempPath, PERSIST_FILE, { overwrite: true });
552+
console.log(`Wrote empty window state to ${PERSIST_FILE}`);
553+
} catch (error) {
554+
console.error("Error clearing window state file:", error);
555+
}
572556
return;
573557
}
574558

@@ -605,9 +589,17 @@ class WindowManager {
605589
}
606590
}
607591

592+
// If, for some reason, we ended up with nothing, also clear the file
608593
if (windowStates.length === 0) {
609-
console.error('Failed to save any window states!');
610-
// Don't write an empty file - keep the existing one
594+
console.warn('No window states collected during save – clearing window state file.');
595+
try {
596+
const tempPath = PERSIST_FILE + ".tmp";
597+
await fs.outputJson(tempPath, [], { spaces: 2 });
598+
await fs.move(tempPath, PERSIST_FILE, { overwrite: true });
599+
console.log(`Wrote empty window state to ${PERSIST_FILE}`);
600+
} catch (error) {
601+
console.error("Error clearing window state file:", error);
602+
}
611603
return;
612604
}
613605

@@ -627,13 +619,28 @@ class WindowManager {
627619

628620
try {
629621
const allTabsData = await this.getTabs();
622+
const TABS_FILE = path.join(USER_DATA_PATH, "tabs.json");
630623

624+
// 🔑 If there are no tabs/windows, clear the tabs file
631625
if (!allTabsData || Object.keys(allTabsData).length === 0) {
632-
console.warn("No tabs data to save - keeping existing file");
626+
// Same logic as window states: if we're quitting, do NOT clear the file.
627+
if (this.isQuitting || this.shutdownInProgress) {
628+
console.warn("No tabs data to save during quit – leaving tabs file untouched.");
629+
return;
630+
}
631+
632+
console.warn("No tabs data to save – clearing tabs file so session does not restore.");
633+
try {
634+
const tempPath = TABS_FILE + ".tmp";
635+
await fs.outputJson(tempPath, {}, { spaces: 2 });
636+
await fs.move(tempPath, TABS_FILE, { overwrite: true });
637+
console.log(`Wrote empty tabs data to ${TABS_FILE}`);
638+
} catch (error) {
639+
console.error("Error clearing tabs data file:", error);
640+
}
633641
return;
634642
}
635643

636-
const TABS_FILE = path.join(USER_DATA_PATH, "tabs.json");
637644
const windowCount = Object.keys(allTabsData).length;
638645

639646
console.log(`Saving tabs for ${windowCount} windows...`);
@@ -650,6 +657,19 @@ class WindowManager {
650657
}
651658

652659
async saveOpened(forceSave = false) {
660+
// Prevent saving when the last window has no tabs (closing last tab)
661+
if (
662+
this.windows.size === 1 &&
663+
!this.isQuitting &&
664+
!this.shutdownInProgress
665+
) {
666+
const onlyWindow = [...this.windows][0];
667+
if (onlyWindow && onlyWindow.savedTabs === null) {
668+
console.log("Preventing save: last window has no tabs (closing last tab).");
669+
return;
670+
}
671+
}
672+
653673
//Never save(periodic saves) during shutdown
654674
if (this.shutdownInProgress) {
655675
console.log("Shutdown in progress - saveOpened blocked");

0 commit comments

Comments
 (0)