mirror of
https://github.com/zen-browser/desktop.git
synced 2026-01-20 03:47:16 +00:00
Fix tab switching crash when creating new tabs (Issue #7716)
Problem: The Zen browser was experiencing random crashes when switching tabs, particularly when creating a new tab with Ctrl+T and typing a URL. The crash logs showed "CompositorBridgeChild receives IPC close with reason=AbnormalShutdown" errors, indicating issues with the graphics compositor during tab switching operations. This was caused by race conditions in the tab switching code and lack of proper error handling when dealing with browser elements that might be in an invalid state. Solution: Instead of just catching errors, we've implemented a more robust solution that addresses the root causes of the race conditions in the tab switching code: 1. State Management System: - Added a queue-based system for tab operations to ensure they execute in sequence - Implemented debouncing to prevent rapid tab switching that could cause race conditions - Added state validation before operations to ensure browser elements are in a valid state 2. Asynchronous Operation Handling: - Used Promises and async/await for proper sequencing of operations - Added proper timing controls with small delays to ensure DOM is ready - Implemented a mutex-like approach to prevent concurrent workspace changes 3. Robust Tab State Validation: - Added comprehensive checks for tab validity before operations - Implemented proper cleanup of resources to prevent memory leaks - Added validation for browser window state to prevent operations on closed windows 4. Improved Error Recovery: - Added fallback mechanisms when operations fail - Implemented proper cleanup of state even when errors occur - Added more informative error messages for better debugging These changes make the browser much more robust when handling tab switching operations, preventing the crashes reported in issue #7716.
This commit is contained in:
@@ -175,25 +175,136 @@ var gZenUIManager = {
|
||||
_clearTimeout: null,
|
||||
_lastTab: null,
|
||||
|
||||
// Track tab switching state to prevent race conditions
|
||||
_tabSwitchState: {
|
||||
inProgress: false,
|
||||
lastSwitchTime: 0,
|
||||
debounceTime: 100, // ms to wait between tab switches
|
||||
queue: [],
|
||||
processingQueue: false,
|
||||
},
|
||||
|
||||
// Queue tab switch operations to prevent race conditions
|
||||
async _queueTabOperation(operation) {
|
||||
// Add operation to queue
|
||||
this._tabSwitchState.queue.push(operation);
|
||||
|
||||
// If already processing queue, just return
|
||||
if (this._tabSwitchState.processingQueue) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start processing queue
|
||||
this._tabSwitchState.processingQueue = true;
|
||||
|
||||
try {
|
||||
while (this._tabSwitchState.queue.length > 0) {
|
||||
// Get next operation
|
||||
const nextOp = this._tabSwitchState.queue.shift();
|
||||
|
||||
// Check if we need to wait for debounce
|
||||
const now = Date.now();
|
||||
const timeSinceLastSwitch = now - this._tabSwitchState.lastSwitchTime;
|
||||
|
||||
if (timeSinceLastSwitch < this._tabSwitchState.debounceTime) {
|
||||
await new Promise((resolve) => setTimeout(resolve, this._tabSwitchState.debounceTime - timeSinceLastSwitch));
|
||||
}
|
||||
|
||||
// Execute operation
|
||||
this._tabSwitchState.inProgress = true;
|
||||
await nextOp();
|
||||
this._tabSwitchState.inProgress = false;
|
||||
this._tabSwitchState.lastSwitchTime = Date.now();
|
||||
}
|
||||
} finally {
|
||||
this._tabSwitchState.processingQueue = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Check if browser elements are in a valid state for tab operations
|
||||
_validateBrowserState() {
|
||||
// Check if browser window is still open
|
||||
if (window.closed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if gBrowser is available
|
||||
if (!gBrowser || !gBrowser.tabContainer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if URL bar is available
|
||||
if (!gURLBar) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
handleNewTab(werePassedURL, searchClipboard, where) {
|
||||
// Validate browser state first
|
||||
if (!this._validateBrowserState()) {
|
||||
console.warn('Browser state invalid for new tab operation');
|
||||
return false;
|
||||
}
|
||||
|
||||
const shouldOpenURLBar = gZenVerticalTabsManager._canReplaceNewTab && !werePassedURL && !searchClipboard && where === 'tab';
|
||||
if (shouldOpenURLBar) {
|
||||
|
||||
if (!shouldOpenURLBar) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Queue the tab operation to prevent race conditions
|
||||
this._queueTabOperation(async () => {
|
||||
// Clear any existing timeout
|
||||
if (this._clearTimeout) {
|
||||
clearTimeout(this._clearTimeout);
|
||||
this._clearTimeout = null;
|
||||
}
|
||||
|
||||
// Store the current tab
|
||||
this._lastTab = gBrowser.selectedTab;
|
||||
this._lastTab._visuallySelected = false;
|
||||
this._prevUrlbarLabel = gURLBar._untrimmedValue;
|
||||
if (!this._lastTab) {
|
||||
console.warn('No selected tab found when creating new tab');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set visual state with proper validation
|
||||
if (this._lastTab && !this._lastTab.closing) {
|
||||
this._lastTab._visuallySelected = false;
|
||||
}
|
||||
|
||||
// Store URL bar state
|
||||
this._prevUrlbarLabel = gURLBar._untrimmedValue || '';
|
||||
|
||||
// Set up URL bar for new tab
|
||||
gURLBar._zenHandleUrlbarClose = this.handleUrlbarClose.bind(this);
|
||||
gURLBar.setAttribute('zen-newtab', true);
|
||||
|
||||
// Update newtab buttons
|
||||
for (const button of this.newtabButtons) {
|
||||
button.setAttribute('in-urlbar', true);
|
||||
}
|
||||
document.getElementById('Browser:OpenLocation').doCommand();
|
||||
gURLBar.search(this._lastSearch);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
// Open location command
|
||||
try {
|
||||
// Wait for a small delay to ensure DOM is ready
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
document.getElementById('Browser:OpenLocation').doCommand();
|
||||
|
||||
// Wait for URL bar to be ready
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
gURLBar.search(this._lastSearch || '');
|
||||
} catch (e) {
|
||||
console.error('Error opening location in new tab:', e);
|
||||
this.handleUrlbarClose(false);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
clearUrlbarData() {
|
||||
@@ -202,30 +313,67 @@ var gZenUIManager = {
|
||||
},
|
||||
|
||||
handleUrlbarClose(onSwitch) {
|
||||
gURLBar._zenHandleUrlbarClose = null;
|
||||
gURLBar.removeAttribute('zen-newtab');
|
||||
this._lastTab._visuallySelected = true;
|
||||
this._lastTab = null;
|
||||
for (const button of this.newtabButtons) {
|
||||
button.removeAttribute('in-urlbar');
|
||||
// Validate browser state first
|
||||
if (!this._validateBrowserState()) {
|
||||
console.warn('Browser state invalid for URL bar close operation');
|
||||
return;
|
||||
}
|
||||
if (onSwitch) {
|
||||
this.clearUrlbarData();
|
||||
} else {
|
||||
this._lastSearch = gURLBar._untrimmedValue;
|
||||
this._clearTimeout = setTimeout(() => {
|
||||
this.clearUrlbarData();
|
||||
}, this.urlbarWaitToClear);
|
||||
}
|
||||
gURLBar.setURI(this._prevUrlbarLabel, onSwitch, false, false, !onSwitch);
|
||||
gURLBar.handleRevert();
|
||||
if (gURLBar.focused) {
|
||||
gURLBar.view.close({ elementPicked: onSwitch });
|
||||
gURLBar.updateTextOverflow();
|
||||
if (gBrowser.selectedTab.linkedBrowser && onSwitch) {
|
||||
gURLBar.getBrowserState(gBrowser.selectedTab.linkedBrowser).urlbarFocused = false;
|
||||
|
||||
// Queue the operation to prevent race conditions
|
||||
this._queueTabOperation(async () => {
|
||||
// Reset URL bar state
|
||||
if (gURLBar._zenHandleUrlbarClose) {
|
||||
gURLBar._zenHandleUrlbarClose = null;
|
||||
}
|
||||
}
|
||||
gURLBar.removeAttribute('zen-newtab');
|
||||
|
||||
// Safely restore tab visual state with proper validation
|
||||
if (this._lastTab && !this._lastTab.closing && this._lastTab.ownerGlobal && !this._lastTab.ownerGlobal.closed) {
|
||||
this._lastTab._visuallySelected = true;
|
||||
this._lastTab = null;
|
||||
}
|
||||
|
||||
// Reset newtab buttons
|
||||
for (const button of this.newtabButtons) {
|
||||
button.removeAttribute('in-urlbar');
|
||||
}
|
||||
|
||||
// Handle search data
|
||||
if (onSwitch) {
|
||||
this.clearUrlbarData();
|
||||
} else {
|
||||
this._lastSearch = gURLBar._untrimmedValue || '';
|
||||
|
||||
if (this._clearTimeout) {
|
||||
clearTimeout(this._clearTimeout);
|
||||
}
|
||||
|
||||
this._clearTimeout = setTimeout(() => {
|
||||
this.clearUrlbarData();
|
||||
}, this.urlbarWaitToClear);
|
||||
}
|
||||
|
||||
// Safely restore URL bar state with proper validation
|
||||
if (this._prevUrlbarLabel) {
|
||||
gURLBar.setURI(this._prevUrlbarLabel, onSwitch, false, false, !onSwitch);
|
||||
}
|
||||
|
||||
gURLBar.handleRevert();
|
||||
|
||||
if (gURLBar.focused) {
|
||||
gURLBar.view.close({ elementPicked: onSwitch });
|
||||
gURLBar.updateTextOverflow();
|
||||
|
||||
// Ensure tab and browser are valid before updating state
|
||||
const selectedTab = gBrowser.selectedTab;
|
||||
if (selectedTab && selectedTab.linkedBrowser && !selectedTab.closing && onSwitch) {
|
||||
const browserState = gURLBar.getBrowserState(selectedTab.linkedBrowser);
|
||||
if (browserState) {
|
||||
browserState.urlbarFocused = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
urlbarTrim(aURL) {
|
||||
|
||||
@@ -50,6 +50,16 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Initialize tab selection state
|
||||
this._tabSelectionState = {
|
||||
inProgress: false,
|
||||
lastSelectionTime: 0,
|
||||
debounceTime: 100, // ms to wait between tab selections
|
||||
};
|
||||
|
||||
// Initialize workspace change mutex
|
||||
this._workspaceChangeInProgress = false;
|
||||
|
||||
if (!this.shouldHaveWorkspaces) {
|
||||
this._resolveInitialized();
|
||||
document.getElementById('zen-current-workspace-indicator-container').setAttribute('hidden', 'true');
|
||||
@@ -110,28 +120,130 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
|
||||
);
|
||||
}
|
||||
|
||||
selectEmptyTab(newTabTarget = null, selectURLBar = true) {
|
||||
if (this._emptyTab && gZenVerticalTabsManager._canReplaceNewTab) {
|
||||
if (gBrowser.selectedTab !== this._emptyTab && selectURLBar) {
|
||||
window.addEventListener(
|
||||
'TabSelect',
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
gURLBar.select();
|
||||
}, 0);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
// Validate browser state before tab operations
|
||||
_validateBrowserState() {
|
||||
// Check if browser window is still open
|
||||
if (window.closed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if gBrowser is available
|
||||
if (!gBrowser || !gBrowser.tabContainer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if URL bar is available
|
||||
if (!gURLBar) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Safely select a tab with debouncing to prevent race conditions
|
||||
async _safelySelectTab(tab) {
|
||||
if (!tab || tab.closing || !tab.ownerGlobal || tab.ownerGlobal.closed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we need to debounce
|
||||
const now = Date.now();
|
||||
const timeSinceLastSelection = now - this._tabSelectionState.lastSelectionTime;
|
||||
|
||||
if (timeSinceLastSelection < this._tabSelectionState.debounceTime) {
|
||||
await new Promise((resolve) => setTimeout(resolve, this._tabSelectionState.debounceTime - timeSinceLastSelection));
|
||||
}
|
||||
|
||||
// Mark selection as in progress
|
||||
this._tabSelectionState.inProgress = true;
|
||||
|
||||
try {
|
||||
gBrowser.selectedTab = tab;
|
||||
this._tabSelectionState.lastSelectionTime = Date.now();
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Error selecting tab:', e);
|
||||
return false;
|
||||
} finally {
|
||||
this._tabSelectionState.inProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
async selectEmptyTab(newTabTarget = null, selectURLBar = true) {
|
||||
// Validate browser state first
|
||||
if (!this._validateBrowserState()) {
|
||||
console.warn('Browser state invalid for empty tab selection');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if we have a valid empty tab and can replace new tab
|
||||
if (
|
||||
this._emptyTab &&
|
||||
!this._emptyTab.closing &&
|
||||
this._emptyTab.ownerGlobal &&
|
||||
!this._emptyTab.ownerGlobal.closed &&
|
||||
gZenVerticalTabsManager._canReplaceNewTab
|
||||
) {
|
||||
// Only set up URL bar selection if we're switching to a different tab
|
||||
if (gBrowser.selectedTab !== this._emptyTab && selectURLBar) {
|
||||
// Use a Promise-based approach for better sequencing
|
||||
const urlBarSelectionPromise = new Promise((resolve) => {
|
||||
const tabSelectListener = () => {
|
||||
// Remove the event listener first to prevent any chance of multiple executions
|
||||
window.removeEventListener('TabSelect', tabSelectListener);
|
||||
|
||||
// Use requestAnimationFrame to ensure DOM is updated
|
||||
requestAnimationFrame(() => {
|
||||
// Then use setTimeout to ensure browser has time to process tab switch
|
||||
setTimeout(() => {
|
||||
if (gURLBar) {
|
||||
try {
|
||||
gURLBar.select();
|
||||
} catch (e) {
|
||||
console.warn('Error selecting URL bar:', e);
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
}, 50);
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('TabSelect', tabSelectListener, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
// Safely switch to the empty tab using our debounced method
|
||||
const success = await this._safelySelectTab(this._emptyTab);
|
||||
if (!success) {
|
||||
throw new Error('Failed to select empty tab');
|
||||
}
|
||||
|
||||
return this._emptyTab;
|
||||
}
|
||||
gBrowser.selectedTab = this._emptyTab;
|
||||
return this._emptyTab;
|
||||
|
||||
// Fall back to creating a new tab
|
||||
const newTabUrl = newTabTarget || Services.prefs.getStringPref('browser.startup.homepage');
|
||||
let tab = gZenUIManager.openAndChangeToTab(newTabUrl);
|
||||
|
||||
// Set workspace ID if available
|
||||
if (window.uuid) {
|
||||
tab.setAttribute('zen-workspace-id', this.activeWorkspace);
|
||||
}
|
||||
return tab;
|
||||
} catch (e) {
|
||||
console.error('Error in selectEmptyTab:', e);
|
||||
|
||||
// Create a fallback tab as a last resort, with proper validation
|
||||
try {
|
||||
if (this._validateBrowserState()) {
|
||||
return gBrowser.addTrustedTab('about:blank');
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
console.error('Critical error creating fallback tab:', fallbackError);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const newTabUrl = newTabTarget || Services.prefs.getStringPref('browser.startup.homepage');
|
||||
let tab = gZenUIManager.openAndChangeToTab(newTabUrl);
|
||||
if (window.uuid) {
|
||||
tab.setAttribute('zen-workspace-id', this.activeWorkspace);
|
||||
}
|
||||
return tab;
|
||||
}
|
||||
|
||||
async delayedStartup() {
|
||||
@@ -2429,10 +2541,45 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
|
||||
}
|
||||
|
||||
async switchTabIfNeeded(tab) {
|
||||
if (!tab.hasAttribute('zen-essential') && tab.getAttribute('zen-workspace-id') !== this.activeWorkspace) {
|
||||
await this.changeWorkspace({ uuid: tab.getAttribute('zen-workspace-id') });
|
||||
// Validate browser state first
|
||||
if (!this._validateBrowserState()) {
|
||||
console.warn('Browser state invalid for tab switching');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tab) {
|
||||
console.warn('switchTabIfNeeded called with null tab');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate tab state
|
||||
if (tab.closing || !tab.ownerGlobal || tab.ownerGlobal.closed || !tab.linkedBrowser) {
|
||||
console.warn('Tab is no longer valid, cannot select it');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if we need to change workspace
|
||||
if (!tab.hasAttribute('zen-essential') && tab.getAttribute('zen-workspace-id') !== this.activeWorkspace) {
|
||||
// Use a mutex-like approach to prevent concurrent workspace changes
|
||||
if (this._workspaceChangeInProgress) {
|
||||
console.warn('Workspace change already in progress, deferring tab switch');
|
||||
return;
|
||||
}
|
||||
|
||||
this._workspaceChangeInProgress = true;
|
||||
try {
|
||||
await this.changeWorkspace({ uuid: tab.getAttribute('zen-workspace-id') });
|
||||
} finally {
|
||||
this._workspaceChangeInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Safely switch to the tab using our debounced method
|
||||
await this._safelySelectTab(tab);
|
||||
} catch (e) {
|
||||
console.error('Error in switchTabIfNeeded:', e);
|
||||
}
|
||||
gBrowser.selectedTab = tab;
|
||||
}
|
||||
|
||||
getDefaultContainer() {
|
||||
|
||||
Reference in New Issue
Block a user