Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -32081,6 +32081,12 @@
"value": "再読み込み"
}
},
"km": {
"stringUnit": {
"state": "translated",
"value": "ផ្ទុកឡើងវិញ"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
Expand Down
127 changes: 103 additions & 24 deletions Sources/Panels/BrowserPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2502,7 +2502,7 @@ actor BrowserSearchSuggestionService {
}

/// BrowserPanel provides a WKWebView-based browser panel.
/// All browser panels share a WKProcessPool for cookie sharing.
/// Each browser panel owns a WKProcessPool so WebContent crashes stay isolated.
private enum BrowserInsecureHTTPNavigationIntent {
case currentTab
case newTab
Expand Down Expand Up @@ -2967,9 +2967,6 @@ final class BrowserPortalAnchorView: NSView {

@MainActor
final class BrowserPanel: Panel, ObservableObject {
/// Shared process pool for cookie sharing across all browser panels
private static let sharedProcessPool = WKProcessPool()

/// Popup windows owned by this panel (for lifecycle cleanup)
private var popupControllers: [BrowserPopupWindowController] = []

Expand Down Expand Up @@ -3118,11 +3115,21 @@ final class BrowserPanel: Panel, ObservableObject {
/// The underlying web view
private(set) var webView: WKWebView
private var websiteDataStore: WKWebsiteDataStore
/// Isolate WebKit crashes to this browser panel while keeping popups in the same browsing context.
private let processPool: WKProcessPool
var webViewDidRequestClose: (() -> Void)?

/// Monotonic identity for the current WKWebView instance.
/// Incremented whenever we replace the underlying WKWebView after a process crash.
@Published private(set) var webViewInstanceID: UUID = UUID()
private(set) var hasRecoverableWebContentTermination = false {
willSet {
if newValue != hasRecoverableWebContentTermination {
objectWillChange.send()
}
}
}
private var pendingWebContentRecoveryURL: URL?

/// Prevent the omnibar from auto-focusing for a short window after explicit programmatic focus.
/// This avoids races where SwiftUI focus state steals first responder back from WebKit.
Expand Down Expand Up @@ -3796,7 +3803,8 @@ final class BrowserPanel: Panel, ObservableObject {

let replacement = Self.makeWebView(
profileID: profileID,
websiteDataStore: websiteDataStore
websiteDataStore: websiteDataStore,
processPool: processPool
)
replacement.pageZoom = desiredZoom
webViewInstanceID = UUID()
Expand Down Expand Up @@ -4034,12 +4042,14 @@ final class BrowserPanel: Panel, ObservableObject {

private static func makeWebView(
profileID: UUID,
websiteDataStore: WKWebsiteDataStore? = nil
websiteDataStore: WKWebsiteDataStore? = nil,
processPool: WKProcessPool
) -> CmuxWebView {
let config = WKWebViewConfiguration()
configureWebViewConfiguration(
config,
websiteDataStore: websiteDataStore ?? BrowserProfileStore.shared.websiteDataStore(for: profileID)
websiteDataStore: websiteDataStore ?? BrowserProfileStore.shared.websiteDataStore(for: profileID),
processPool: processPool
)

let webView = CmuxWebView(frame: .zero, configuration: config)
Expand All @@ -4059,9 +4069,11 @@ final class BrowserPanel: Panel, ObservableObject {
static func configureWebViewConfiguration(
_ configuration: WKWebViewConfiguration,
websiteDataStore: WKWebsiteDataStore,
processPool: WKProcessPool = BrowserPanel.sharedProcessPool
processPool: WKProcessPool? = nil
) {
configuration.processPool = processPool
if let processPool {
configuration.processPool = processPool
}
Comment thread
lawrencecchen marked this conversation as resolved.
configuration.mediaTypesRequiringUserActionForPlayback = []
// Ensure browser cookies/storage persist across navigations and launches.
// This reduces repeated consent/bot-challenge flows on sites like Google.
Expand Down Expand Up @@ -4253,10 +4265,13 @@ final class BrowserPanel: Panel, ObservableObject {
self.websiteDataStore = isRemoteWorkspace
? WKWebsiteDataStore(forIdentifier: remoteWebsiteDataStoreIdentifier ?? workspaceId)
: BrowserProfileStore.shared.websiteDataStore(for: resolvedProfileID)
let processPool = WKProcessPool()
self.processPool = processPool

let webView = Self.makeWebView(
profileID: resolvedProfileID,
websiteDataStore: websiteDataStore
websiteDataStore: websiteDataStore,
processPool: processPool
)
self.webView = webView
self.insecureHTTPAlertFactory = { NSAlert() }
Expand Down Expand Up @@ -4677,6 +4692,8 @@ final class BrowserPanel: Panel, ObservableObject {

webViewObservers.removeAll()
webViewCancellables.removeAll()
clearWebContentTerminationRecovery()
clearBrowserFocusMode(reason: "profileSwitch")
Comment thread
cursor[bot] marked this conversation as resolved.
faviconTask?.cancel()
faviconTask = nil
faviconRefreshGeneration &+= 1
Expand All @@ -4701,7 +4718,8 @@ final class BrowserPanel: Panel, ObservableObject {

let replacement = Self.makeWebView(
profileID: resolvedProfileID,
websiteDataStore: websiteDataStore
websiteDataStore: websiteDataStore,
processPool: processPool
)
replacement.pageZoom = desiredZoom
webViewInstanceID = UUID()
Expand Down Expand Up @@ -5156,21 +5174,32 @@ final class BrowserPanel: Panel, ObservableObject {
replaceWebViewPreservingState(
from: terminatedWebView,
websiteDataStore: websiteDataStore,
reason: "webcontent_process_terminated"
reason: "webcontent_process_terminated",
waitForManualRecovery: true
)
}

private func replaceWebViewPreservingState(
from oldWebView: WKWebView,
websiteDataStore: WKWebsiteDataStore,
reason: String
reason: String,
waitForManualRecovery: Bool = false
) {
guard oldWebView === webView else { return }

let wasRenderable = shouldRenderWebView
let restoreURL = Self.remoteProxyDisplayURL(for: oldWebView.url) ?? currentURL
let attemptedURL = Self.remoteProxyDisplayURL(for: navigationDelegate?.lastAttemptedURL)
?? navigationDelegate?.lastAttemptedURL
let liveURL = Self.remoteProxyDisplayURL(for: oldWebView.url)
?? currentURL
let restoreURL = (isMainFrameProvisionalNavigationActive ? attemptedURL : nil)
?? liveURL
?? attemptedURL
?? resolvedCurrentSessionHistoryURL()
let restoreURLString = restoreURL?.absoluteString
let shouldRestoreURL = wasRenderable && restoreURLString != nil && restoreURLString != blankURLString
let hasRecoveryTarget = restoreURLString != nil && restoreURLString != blankURLString
let shouldRestoreURL = wasRenderable && hasRecoveryTarget
let shouldShowManualRecovery = waitForManualRecovery && wasRenderable && hasRecoveryTarget
let history = sessionNavigationHistorySnapshot()
let historyCurrentURL = preferredURLStringForOmnibar()
let desiredZoom = max(minPageZoom, min(maxPageZoom, oldWebView.pageZoom))
Expand All @@ -5188,9 +5217,15 @@ final class BrowserPanel: Panel, ObservableObject {

webViewObservers.removeAll()
webViewCancellables.removeAll()
clearBrowserFocusMode(reason: reason)
faviconTask?.cancel()
faviconTask = nil
faviconRefreshGeneration &+= 1
loadingGeneration &+= 1
loadingEndWorkItem?.cancel()
loadingEndWorkItem = nil
isLoading = false
estimatedProgress = 0
cancelPendingInteractiveBrowserPrompts(reason: reason)
closeBackgroundPreloadHost(reason: reason)
BrowserWindowPortalRegistry.detach(webView: oldWebView)
Expand All @@ -5204,7 +5239,8 @@ final class BrowserPanel: Panel, ObservableObject {

let replacement = Self.makeWebView(
profileID: profileID,
websiteDataStore: websiteDataStore
websiteDataStore: websiteDataStore,
processPool: processPool
)
replacement.pageZoom = desiredZoom
webViewInstanceID = UUID()
Expand All @@ -5224,14 +5260,21 @@ final class BrowserPanel: Panel, ObservableObject {
)
}

if shouldRestoreURL, let restoreURL {
navigateWithoutInsecureHTTPPrompt(
to: restoreURL,
recordTypedNavigation: false,
preserveRestoredSessionHistory: true
)
} else {
if shouldShowManualRecovery, let restoreURL {
pendingWebContentRecoveryURL = restoreURL
hasRecoverableWebContentTermination = true
refreshNavigationAvailability()
} else {
clearWebContentTerminationRecovery()
if shouldRestoreURL, let restoreURL {
navigateWithoutInsecureHTTPPrompt(
to: restoreURL,
recordTypedNavigation: false,
preserveRestoredSessionHistory: true
)
} else {
refreshNavigationAvailability()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

if restoreDevTools {
Expand All @@ -5248,6 +5291,34 @@ final class BrowserPanel: Panel, ObservableObject {
#endif
}

@discardableResult
func recoverTerminatedWebContent(reason: String = "manual") -> Bool {
guard hasRecoverableWebContentTermination else { return false }
let recoveryURL = pendingWebContentRecoveryURL
clearWebContentTerminationRecovery()
#if DEBUG
cmuxDebugLog(
"browser.webcontent.recover panel=\(id.uuidString.prefix(5)) " +
"reason=\(reason) url=\(recoveryURL?.absoluteString ?? "nil")"
)
#endif
guard let recoveryURL else {
refreshNavigationAvailability()
return true
}
navigateWithoutInsecureHTTPPrompt(
to: recoveryURL,
recordTypedNavigation: false,
preserveRestoredSessionHistory: true
)
return true
}

private func clearWebContentTerminationRecovery() {
pendingWebContentRecoveryURL = nil
hasRecoverableWebContentTermination = false
}

#if DEBUG
func debugSimulateWebContentProcessTermination() {
replaceWebViewAfterContentProcessTermination(for: webView)
Expand Down Expand Up @@ -5795,6 +5866,7 @@ final class BrowserPanel: Panel, ObservableObject {
preserveRestoredSessionHistory: Bool
) {
cancelHiddenWebViewDiscard()
clearWebContentTerminationRecovery()
if !preserveRestoredSessionHistory {
abandonRestoredSessionHistoryIfNeeded()
}
Expand Down Expand Up @@ -6079,6 +6151,8 @@ extension BrowserPanel {
isDownloading ||
activeDownloadCount != 0 ||
preferredDeveloperToolsVisible ||
hasRecoverableWebContentTermination ||
pendingWebContentRecoveryURL != nil ||
webView.superview != nil
}

Expand Down Expand Up @@ -6112,6 +6186,7 @@ extension BrowserPanel {
developerToolsRestoreRetryAttempt = 0
preferredAttachedDeveloperToolsWidth = nil
preferredAttachedDeveloperToolsWidthFraction = nil
clearWebContentTerminationRecovery()

loadingEndWorkItem?.cancel()
loadingEndWorkItem = nil
Expand Down Expand Up @@ -6163,7 +6238,8 @@ extension BrowserPanel {

let replacement = Self.makeWebView(
profileID: profileID,
websiteDataStore: websiteDataStore
websiteDataStore: websiteDataStore,
processPool: processPool
Comment thread
cursor[bot] marked this conversation as resolved.
)
webViewInstanceID = UUID()
webView = replacement
Expand Down Expand Up @@ -6399,6 +6475,9 @@ extension BrowserPanel {

/// Reload the current page
func reload() {
if recoverTerminatedWebContent(reason: "reload") {
return
}
if restoreDiscardedWebViewIfNeeded(reason: "reload") {
return
}
Expand Down
31 changes: 31 additions & 0 deletions Sources/Panels/BrowserPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,10 @@ struct BrowserPanelView: View {
return
}

if panel.recoverTerminatedWebContent(reason: "toolbarReload") {
return
}

if currentEventIsCommandPointerActivation {
#if DEBUG
cmuxDebugLog("browser.reload.commandClickDuplicate panel=\(panel.id.uuidString.prefix(5))")
Expand Down Expand Up @@ -1749,10 +1753,37 @@ struct BrowserPanelView: View {
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.overlay {
if panel.hasRecoverableWebContentTermination {
webContentRecoveryOverlay
}
}
.layoutPriority(1)
.zIndex(0)
}

private var webContentRecoveryOverlay: some View {
ZStack {
Color(nsColor: browserChromeBackgroundColor)
.opacity(0.92)
Button(action: {
panel.recoverTerminatedWebContent(reason: "overlayButton")
}) {
Label(
String(localized: "browser.error.reload", defaultValue: "Reload"),
systemImage: "arrow.clockwise"
)
.font(.system(size: 13, weight: .medium))
.padding(.horizontal, 6)
}
.buttonStyle(.borderedProminent)
.controlSize(.regular)
.safeHelp(String(localized: "browser.reload", defaultValue: "Reload"))
.accessibilityIdentifier("BrowserWebContentRecoveryButton")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private func triggerFocusFlashAnimation() {
focusFlashAnimationGeneration &+= 1
let generation = focusFlashAnimationGeneration
Expand Down
12 changes: 12 additions & 0 deletions Sources/Panels/BrowserPopupWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,14 @@ final class BrowserPopupWindowController: NSObject, NSWindowDelegate {
}
}

fileprivate func handleWebContentProcessTermination(for terminatedWebView: WKWebView) {
guard terminatedWebView === webView else { return }
#if DEBUG
cmuxDebugLog("popup.webcontent.terminated depth=\(nestingDepth)")
#endif
closePopup()
}

fileprivate func requestNavigation(_ request: URLRequest, in webView: WKWebView) {
guard let url = request.url else { return }

Expand Down Expand Up @@ -666,6 +674,10 @@ private class PopupNavigationDelegate: NSObject, WKNavigationDelegate {
completionHandler(.performDefaultHandling, nil)
}

func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
controller?.handleWebContentProcessTermination(for: webView)
}

func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
#if DEBUG
cmuxDebugLog("popup.download.didBecome source=navigationAction")
Expand Down
Loading
Loading