Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 @@ -30875,6 +30875,12 @@
"value": "再読み込み"
}
},
"km": {
"stringUnit": {
"state": "translated",
"value": "ផ្ទុកឡើងវិញ"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
Expand Down
99 changes: 83 additions & 16 deletions Sources/Panels/BrowserPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2483,7 +2483,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 @@ -2948,8 +2948,8 @@ 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()
/// Fallback pool for utility callers that configure copied WebKit configurations outside a live panel.
private static let fallbackConfigurationProcessPool = WKProcessPool()

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
/// Popup windows owned by this panel (for lifecycle cleanup)
private var popupControllers: [BrowserPopupWindowController] = []
Expand Down Expand Up @@ -3099,11 +3099,15 @@ 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
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
private var pendingWebContentRecoveryURL: URL?
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

/// 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 @@ -3765,7 +3769,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 @@ -4003,12 +4008,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 @@ -4028,7 +4035,7 @@ final class BrowserPanel: Panel, ObservableObject {
static func configureWebViewConfiguration(
_ configuration: WKWebViewConfiguration,
websiteDataStore: WKWebsiteDataStore,
processPool: WKProcessPool = BrowserPanel.sharedProcessPool
processPool: WKProcessPool = BrowserPanel.fallbackConfigurationProcessPool
) {
configuration.processPool = processPool
configuration.mediaTypesRequiringUserActionForPlayback = []
Expand Down Expand Up @@ -4216,10 +4223,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 @@ -4664,7 +4674,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 @@ -5119,21 +5130,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 Down Expand Up @@ -5167,7 +5189,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 @@ -5187,7 +5210,15 @@ final class BrowserPanel: Panel, ObservableObject {
)
}

if shouldRestoreURL, let restoreURL {
if shouldShowManualRecovery, let restoreURL {
pendingWebContentRecoveryURL = restoreURL
hasRecoverableWebContentTermination = true
refreshNavigationAvailability()
} else {
clearWebContentTerminationRecovery()
}

if !shouldShowManualRecovery, shouldRestoreURL, let restoreURL {
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
navigateWithoutInsecureHTTPPrompt(
to: restoreURL,
recordTypedNavigation: false,
Expand All @@ -5211,6 +5242,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 @@ -5757,6 +5816,7 @@ final class BrowserPanel: Panel, ObservableObject {
preserveRestoredSessionHistory: Bool
) {
cancelHiddenWebViewDiscard()
clearWebContentTerminationRecovery()
if !preserveRestoredSessionHistory {
abandonRestoredSessionHistoryIfNeeded()
}
Expand Down Expand Up @@ -6039,6 +6099,8 @@ extension BrowserPanel {
isDownloading ||
activeDownloadCount != 0 ||
preferredDeveloperToolsVisible ||
hasRecoverableWebContentTermination ||
pendingWebContentRecoveryURL != nil ||
webView.superview != nil
}

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

loadingEndWorkItem?.cancel()
loadingEndWorkItem = nil
Expand Down Expand Up @@ -6122,7 +6185,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 @@ -6358,6 +6422,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 @@ -656,6 +656,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 @@ -1652,10 +1656,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
4 changes: 4 additions & 0 deletions cmux.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
4472A0034472A0034472A003 /* BrowserScreenshotSnapshotter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4472B0034472B0034472B003 /* BrowserScreenshotSnapshotter.swift */; };
A5008371 /* BrowserSearchOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008370 /* BrowserSearchOverlay.swift */; };
A5007422 /* BrowserWebAuthnSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5007423 /* BrowserWebAuthnSupport.swift */; };
C0DE49870000000000000001 /* BrowserWebContentProcessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE49870000000000000002 /* BrowserWebContentProcessTests.swift */; };
A5001534 /* BrowserWindowPortal.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001533 /* BrowserWindowPortal.swift */; };
C0DE35530000000000000101 /* BundledCLILinkageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE35530000000000000102 /* BundledCLILinkageTests.swift */; };
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */; };
Expand Down Expand Up @@ -712,6 +713,7 @@
4472B0034472B0034472B003 /* BrowserScreenshotSnapshotter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserScreenshotSnapshotter.swift; sourceTree = "<group>"; };
A5008370 /* BrowserSearchOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Find/BrowserSearchOverlay.swift; sourceTree = "<group>"; };
A5007423 /* BrowserWebAuthnSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserWebAuthnSupport.swift; sourceTree = "<group>"; };
C0DE49870000000000000002 /* BrowserWebContentProcessTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWebContentProcessTests.swift; sourceTree = "<group>"; };
A5001533 /* BrowserWindowPortal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWindowPortal.swift; sourceTree = "<group>"; };
C0DE35530000000000000102 /* BundledCLILinkageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundledCLILinkageTests.swift; sourceTree = "<group>"; };
F3000001A1B2C3D4E5F60718 /* CJKIMEInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CJKIMEInputTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1796,6 +1798,7 @@
A50019B3 /* SettingsSearchIndexTests.swift */,
D36090010000000000000004 /* SettingsWindowPresenterTests.swift */,
970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */,
C0DE49870000000000000002 /* BrowserWebContentProcessTests.swift */,
D3622001A1B2C3D4E5F60718 /* BrowserArrowKeyForwardingTests.swift */,
58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */,
D0B1000DA1B2C3D4E5F60001 /* CmuxWebViewDragRoutingTests.swift */,
Expand Down Expand Up @@ -2655,6 +2658,7 @@
C2B6A97D1F2E4C71A8B9D001 /* BrowserOmnibarPerformanceSupportTests.swift in Sources */,
D0B1000EA1B2C3D4E5F60001 /* BrowserPaneDropRoutingTests.swift in Sources */,
1F14445B9627DE9D3AF4FD2E /* BrowserPanelTests.swift in Sources */,
C0DE49870000000000000001 /* BrowserWebContentProcessTests.swift in Sources */,
C0DE35530000000000000101 /* BundledCLILinkageTests.swift in Sources */,
F3000000A1B2C3D4E5F60718 /* CJKIMEInputTests.swift in Sources */,
D3571002A1B2C3D4E5F60718 /* CJKIMEMarkedSelectionTests.swift in Sources */,
Expand Down
Loading
Loading