Merge pull request #7761 from mbergo/fix-tab-switching-crash

Fix tab switching crash when creating new tabs (Issue #7716)
This commit is contained in:
mr. m
2025-04-22 14:24:20 +02:00
committed by GitHub
2 changed files with 348 additions and 53 deletions

View File

@@ -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) {

View File

@@ -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');
@@ -108,28 +118,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() {
@@ -2560,10 +2672,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() {