Skip to content

Recover from WebContent process termination on macOS by handling webViewWebContentProcessDidTerminate#5129

Open
wayneforrest wants to merge 1 commit intowailsapp:masterfrom
wayneforrest:feature/recover-from-webview-terminated
Open

Recover from WebContent process termination on macOS by handling webViewWebContentProcessDidTerminate#5129
wayneforrest wants to merge 1 commit intowailsapp:masterfrom
wayneforrest:feature/recover-from-webview-terminated

Conversation

@wayneforrest
Copy link
Copy Markdown
Contributor

@wayneforrest wayneforrest commented Apr 11, 2026

Description

Implemented automatic recovery when the web view process is killed.

Fixes #5130

Type of change

Please select the option that is relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Run the script, it kills the WebView, and app survives, the window re-centres on the screen.
Output:

2026-04-15 12:00:27.589 redacted-app-name[20456:1858304] [wails] webViewWebContentProcessDidTerminate fired — reloading webview (windowId=2)
2026-04-15 12:00:27.611 redacted-app-name[20456:1858304] [wails] webViewWebContentProcessDidTerminate fired — reloading webview (windowId=1)
12:00PM INF [AssetFileServerFS] Handling request url=/ file=.
12:00PM INF [AssetFileServerFS] Handling request url=/style.css file=style.css
12:00PM INF [AssetFileServerFS] Handling request url=/assets/index-BlidFdNW.js file=assets/index-BlidFdNW.js
12:00PM INF [AssetFileServerFS] Handling request url=/assets/index-aqfHKnGs.css file=assets/index-aqfHKnGs.css
12:00PM INF [AssetFileServerFS] Handling request url=/Inter-Medium.ttf file=Inter-Medium.ttf
12:00PM INF [AssetFileServerFS] Handling request url=/ file=.
12:00PM INF [AssetFileServerFS] Handling request url=/style.css file=style.css
12:00PM INF [AssetFileServerFS] Handling request url=/assets/index-aqfHKnGs.css file=assets/index-aqfHKnGs.css
12:00PM INF [AssetFileServerFS] Handling request url=/assets/index-BlidFdNW.js file=assets/index-BlidFdNW.js
WARN[12:00:27.698] [window] no primary screen detected, using defaults
INFO[12:00:27.699] [window] sized to 960x640
INFO[12:00:27.699] [screens] NSScreen returned 1 monitor(s):
INFO[12:00:27.699] [screens]   [0] frame=1920x1080@(0,0) workArea=1920x954@(0,96)
INFO[12:00:27.699] [theme] configureWindowVibrancy: set NSVisualEffectMaterialWindowBackground
#!/usr/bin/env bash
# Kill the WKWebView content process(es) for a macOS app and verify recovery.
# Usage: ./kill-webview.sh [app-name]   (default: redacted)

set -euo pipefail

APP_NAME="${1:-my-app-redacted"

APP_PID=$(pgrep -f "${APP_NAME}.dev.app/Contents/MacOS/${APP_NAME}" | head -1 || true)
if [ -z "${APP_PID}" ]; then
  APP_PID=$(pgrep -x "${APP_NAME}" | head -1 || true)
fi
if [ -z "${APP_PID}" ]; then
  echo "App '${APP_NAME}' is not running." >&2
  exit 1
fi
echo "App '${APP_NAME}' PID: ${APP_PID}"

# Find GPU + Networking helpers near the app PID (within 100 PIDs).
# These are reliably co-spawned with the app, unlike WebContent which can
# respawn later. They anchor us to the right "cluster" of helpers.
gpu_pid=$(pgrep -f "com.apple.WebKit.GPU" | awk -v app="$APP_PID" '{ d=$1-app; if (d>0 && d<100) print $1 }' | head -1)
net_pid=$(pgrep -f "com.apple.WebKit.Networking" | awk -v app="$APP_PID" '{ d=$1-app; if (d>0 && d<100) print $1 }' | head -1)
echo "  Anchor helpers: GPU=${gpu_pid:-none}  Networking=${net_pid:-none}"

if [ -z "${gpu_pid}" ] && [ -z "${net_pid}" ]; then
  echo "No WebKit GPU/Networking helpers near app PID — app may not have a WKWebView running." >&2
  exit 1
fi

anchor_pid=${gpu_pid:-$net_pid}

# Select WebContent PIDs in the same neighborhood as the anchor helpers.
CANDIDATE_PIDS=$(pgrep -f "com.apple.WebKit.WebContent" | awk -v anchor="$anchor_pid" '{ d=$1-anchor; if (d>-20 && d<20) print $1 }')
if [ -z "${CANDIDATE_PIDS}" ]; then
  echo "No WebContent processes clustered with anchor PID ${anchor_pid}." >&2
  exit 1
fi
echo "  Candidate WebContent PID(s): $(echo ${CANDIDATE_PIDS} | tr '\n' ' ')"

# Snapshot WebContent PIDs before kill.
BEFORE=$(pgrep -f "com.apple.WebKit.WebContent" | sort -n)

echo ""
echo "Killing WebContent...  "
for pid in ${CANDIDATE_PIDS}; do
  if sudo kill -9 "${pid}" 2>/dev/null; then
    echo "  killed ${pid}"
  else
    echo "  failed to kill ${pid}"
  fi
done

# Wait briefly for recovery to kick in.
sleep 2

AFTER=$(pgrep -f "com.apple.WebKit.WebContent" | sort -n)
NEW_PIDS=$(comm -13 <(echo "$BEFORE") <(echo "$AFTER") || true)

echo ""
echo "=== Recovery check ==="
if [ -n "${NEW_PIDS}" ]; then
  echo "NEW WebContent PID(s) spawned after kill:"
  echo "${NEW_PIDS}" | sed 's/^/  /'
  echo ""
  echo "Recovery fired — WKWebView respawned. Webview should be reloaded."
else
  echo "No new WebContent process appeared within 2s."
  echo "Either the killed PIDs were not app-name, or recovery did not fire."
fi

# Also confirm the anchor helpers are still alive — if they died too, we killed
# the wrong thing or the entire app crashed.
if [ -n "${gpu_pid}" ] && ! kill -0 "${gpu_pid}" 2>/dev/null; then
  echo "WARNING: GPU helper ${gpu_pid} is no longer alive."
fi
if [ -n "${net_pid}" ] && ! kill -0 "${net_pid}" 2>/dev/null; then
  echo "WARNING: Networking helper ${net_pid} is no longer alive."
fi
  • Windows
  • macOS
  • Linux

If you checked Linux, please specify the distro and version.

Test Configuration

Please paste the output of wails doctor. If you are unable to run this command, please describe your environment in as much detail as possible.

Checklist:

  • I have updated website/src/pages/changelog.mdx with details of this PR
  • My code follows the general coding style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Summary by CodeRabbit

  • New Features

    • Added a macOS event emitted when the WebView renderer process terminates.
    • Added automatic recovery: the web view now reloads after renderer termination to avoid blank/unresponsive windows.
  • Bug Fixes

    • Implemented macOS window reload and force-reload functionality (previously non-functional), improving reliability after renderer restarts.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 11, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fed9f2bb-016b-4d61-b1c4-4aad15f6650a

📥 Commits

Reviewing files that changed from the base of the PR and between c7bddab and 439c8b7.

📒 Files selected for processing (1)
  • v3/pkg/application/webview_window_darwin.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • v3/pkg/application/webview_window_darwin.go

Walkthrough

Adds a new macOS event mac:WebViewWebContentProcessDidTerminate, implements macOS WebviewWindow Reload() and ForceReload(), and adds recovery logic that emits the event and reloads the WKWebView when the WebContent process terminates.

Changes

Cohort / File(s) Summary
Changelog
v3/UNRELEASED_CHANGELOG.md
Documents the new macOS WebView termination event and recovery behavior.
Event System
v3/pkg/events/events.go, v3/pkg/events/events.txt, v3/pkg/events/events_darwin.h, v3/pkg/events/known_events.go, v3/internal/generator/collect/known_events.go
Registers WebViewWebContentProcessDidTerminate (ID 1259) across event mappings, adds to known-events registry, and updates MAX_EVENTS in Darwin header.
Runtime Types
v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.ts
Adds Types.Mac.WebViewWebContentProcessDidTerminate constant for JS runtime.
macOS Implementation
v3/pkg/application/webview_window_darwin.go, v3/pkg/application/webview_window_darwin.m
Implements reload() and forceReload() by invoking new C functions windowReload / windowForceReload. Adds -webViewWebContentProcessDidTerminate: delegate handler to emit the event and call [webView reload] for recovery.

Sequence Diagram(s)

sequenceDiagram
  participant WK as WKWebView (WebContent)
  participant Delegate as WebviewWindowDelegate
  participant App as Host App Event System
  participant JS as JS Runtime / Client
  WK->>Delegate: webViewWebContentProcessDidTerminate
  Delegate->>App: processWindowEvent(EventWebViewWebContentProcessDidTerminate)
  App->>JS: deliver "mac:WebViewWebContentProcessDidTerminate"
  Delegate->>WK: reload()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

MacOS, runtime, v3-alpha, go, size:L, lgtm

Suggested reviewers

  • leaanthony

Poem

🐰 I peeked at WebViews late at night,
When content sleeps and breaks the sight,
I hop, emit a tiny chime,
Then nudge a reload — all fixed in time,
Hooray! No more blank-screen fright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: handling webViewWebContentProcessDidTerminate to recover from WebContent process termination on macOS.
Description check ✅ Passed The PR description covers the core requirement (bug fix for auto-recovery), references the linked issue, includes testing details and output, and notes self-review and testing completion. However, changelog and documentation update items remain unchecked.
Linked Issues check ✅ Passed The PR successfully implements automatic recovery when WebKit's renderer process terminates on macOS by adding event handling, implementing reload/forceReload methods, and emitting recovery events.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing WebContent process recovery on macOS: event system updates, native method implementations, handler registration, and Reload/ForceReload method completion.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
v3/pkg/application/webview_window_darwin.m (1)

792-801: Consider guarding against rapid crash→reload loops.

Auto-reload is good, but repeated renderer crashes can cause an unbounded tight loop. A small per-window debounce/backoff would improve resilience.

♻️ Suggested hardening
 - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
+    static NSTimeInterval const kMinReloadIntervalSeconds = 1.0;
+    static NSMutableDictionary<NSNumber *, NSDate *> *lastReloadByWindow = nil;
+    if (lastReloadByWindow == nil) {
+        lastReloadByWindow = [[NSMutableDictionary alloc] init];
+    }
+
+    NSNumber *windowKey = @(self.windowId);
+    NSDate *now = [NSDate date];
+    NSDate *last = [lastReloadByWindow objectForKey:windowKey];
+    if (last && [now timeIntervalSinceDate:last] < kMinReloadIntervalSeconds) {
+        return;
+    }
+    [lastReloadByWindow setObject:now forKey:windowKey];
+
     if( hasListeners(EventWebViewWebContentProcessDidTerminate) ) {
         processWindowEvent(self.windowId, EventWebViewWebContentProcessDidTerminate);
     }
     [webView reload];
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/pkg/application/webview_window_darwin.m` around lines 792 - 801, The
webViewWebContentProcessDidTerminate: handler currently calls [webView reload]
immediately which can enter a tight crash→reload loop; add a per-window
debounce/backoff: track per-window fields (e.g., _lastTerminateTimestamp and
_terminateCount on the window/controller that owns self.windowId), on each
invocation compute a delay = min(baseDelay * 2^_terminateCount, maxDelay) and if
now - _lastTerminateTimestamp < cooldown skip the reload, otherwise schedule the
reload after delay on the main thread and increment _terminateCount and set
_lastTerminateTimestamp; call processWindowEvent(self.windowId,
EventWebViewWebContentProcessDidTerminate) as before and reset
_terminateCount/_lastTerminateTimestamp to zero on a successful
webView:didFinishNavigation: (or similar) to restore normal behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@v3/pkg/application/webview_window_darwin.go`:
- Around line 1062-1074: The InvokeAsync closures in macosWebviewWindow.reload
and macosWebviewWindow.forceReload can run after the window is torn down; mirror
the execJS lifecycle guard by adding the same early-return check used there
inside each InvokeAsync closure (e.g., verify the window is not closed/destroyed
and nsWindow is valid) before calling C.windowReload or C.windowForceReload so
the C calls never run against a destroyed window; apply the same guard logic to
both reload and forceReload.

---

Nitpick comments:
In `@v3/pkg/application/webview_window_darwin.m`:
- Around line 792-801: The webViewWebContentProcessDidTerminate: handler
currently calls [webView reload] immediately which can enter a tight
crash→reload loop; add a per-window debounce/backoff: track per-window fields
(e.g., _lastTerminateTimestamp and _terminateCount on the window/controller that
owns self.windowId), on each invocation compute a delay = min(baseDelay *
2^_terminateCount, maxDelay) and if now - _lastTerminateTimestamp < cooldown
skip the reload, otherwise schedule the reload after delay on the main thread
and increment _terminateCount and set _lastTerminateTimestamp; call
processWindowEvent(self.windowId, EventWebViewWebContentProcessDidTerminate) as
before and reset _terminateCount/_lastTerminateTimestamp to zero on a successful
webView:didFinishNavigation: (or similar) to restore normal behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d2390a8e-edd4-4b45-b7a5-833083fe8c16

📥 Commits

Reviewing files that changed from the base of the PR and between bb4fbf9 and c7bddab.

📒 Files selected for processing (9)
  • v3/UNRELEASED_CHANGELOG.md
  • v3/internal/generator/collect/known_events.go
  • v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.ts
  • v3/pkg/application/webview_window_darwin.go
  • v3/pkg/application/webview_window_darwin.m
  • v3/pkg/events/events.go
  • v3/pkg/events/events.txt
  • v3/pkg/events/events_darwin.h
  • v3/pkg/events/known_events.go

Comment on lines 1062 to 1074
func (w *macosWebviewWindow) reload() {
//TODO: Implement
globalApplication.debug("reload called on WebviewWindow", "parentID", w.parent.id)
InvokeAsync(func() {
C.windowReload(w.nsWindow)
})
}

func (w *macosWebviewWindow) forceReload() {
//TODO: Implement
globalApplication.debug("force reload called on WebviewWindow", "parentID", w.parent.id)
InvokeAsync(func() {
C.windowForceReload(w.nsWindow)
})
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add lifecycle guards before async reload calls

InvokeAsync closures on Line 1064 and Line 1071 can run after teardown. Mirror the execJS guard pattern to avoid invoking reload against a destroyed window during shutdown/recovery.

🔧 Proposed fix
 func (w *macosWebviewWindow) reload() {
 	globalApplication.debug("reload called on WebviewWindow", "parentID", w.parent.id)
 	InvokeAsync(func() {
+		globalApplication.shutdownLock.Lock()
+		performingShutdown := globalApplication.performingShutdown
+		globalApplication.shutdownLock.Unlock()
+		if performingShutdown || w.nsWindow == nil || w.parent.isDestroyed() {
+			return
+		}
 		C.windowReload(w.nsWindow)
 	})
 }
 
 func (w *macosWebviewWindow) forceReload() {
 	globalApplication.debug("force reload called on WebviewWindow", "parentID", w.parent.id)
 	InvokeAsync(func() {
+		globalApplication.shutdownLock.Lock()
+		performingShutdown := globalApplication.performingShutdown
+		globalApplication.shutdownLock.Unlock()
+		if performingShutdown || w.nsWindow == nil || w.parent.isDestroyed() {
+			return
+		}
 		C.windowForceReload(w.nsWindow)
 	})
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (w *macosWebviewWindow) reload() {
//TODO: Implement
globalApplication.debug("reload called on WebviewWindow", "parentID", w.parent.id)
InvokeAsync(func() {
C.windowReload(w.nsWindow)
})
}
func (w *macosWebviewWindow) forceReload() {
//TODO: Implement
globalApplication.debug("force reload called on WebviewWindow", "parentID", w.parent.id)
InvokeAsync(func() {
C.windowForceReload(w.nsWindow)
})
}
func (w *macosWebviewWindow) reload() {
globalApplication.debug("reload called on WebviewWindow", "parentID", w.parent.id)
InvokeAsync(func() {
globalApplication.shutdownLock.Lock()
performingShutdown := globalApplication.performingShutdown
globalApplication.shutdownLock.Unlock()
if performingShutdown || w.nsWindow == nil || w.parent.isDestroyed() {
return
}
C.windowReload(w.nsWindow)
})
}
func (w *macosWebviewWindow) forceReload() {
globalApplication.debug("force reload called on WebviewWindow", "parentID", w.parent.id)
InvokeAsync(func() {
globalApplication.shutdownLock.Lock()
performingShutdown := globalApplication.performingShutdown
globalApplication.shutdownLock.Unlock()
if performingShutdown || w.nsWindow == nil || w.parent.isDestroyed() {
return
}
C.windowForceReload(w.nsWindow)
})
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/pkg/application/webview_window_darwin.go` around lines 1062 - 1074, The
InvokeAsync closures in macosWebviewWindow.reload and
macosWebviewWindow.forceReload can run after the window is torn down; mirror the
execJS lifecycle guard by adding the same early-return check used there inside
each InvokeAsync closure (e.g., verify the window is not closed/destroyed and
nsWindow is valid) before calling C.windowReload or C.windowForceReload so the C
calls never run against a destroyed window; apply the same guard logic to both
reload and forceReload.

@leaanthony
Copy link
Copy Markdown
Member

@wayneforrest please can you edit the PR description with the requested information. Thanks

@wayneforrest
Copy link
Copy Markdown
Contributor Author

@wayneforrest please can you edit the PR description with the requested information. Thanks

Hey @leaanthony , I have updated this, and also provided a script I used to test this.

@leaanthony leaanthony changed the base branch from v3-alpha to master April 29, 2026 13:08
@leaanthony
Copy link
Copy Markdown
Member

CI is failing on this PR — Cross-Compile checks for darwin/arm64 and windows/arm64 are not passing. Additionally, the PR shows a merge conflict with the target branch.

Please resolve the CI failures and rebase to fix the merge conflict, then we can proceed with testing.

@leaanthony leaanthony added the awaiting feedback More information is required from the requestor label May 3, 2026
@wayneforrest wayneforrest force-pushed the feature/recover-from-webview-terminated branch from 76fbaeb to 541d3f1 Compare May 3, 2026 03:33
@wayneforrest wayneforrest force-pushed the feature/recover-from-webview-terminated branch from 541d3f1 to dc8c1d8 Compare May 3, 2026 09:16
@wayneforrest
Copy link
Copy Markdown
Contributor Author

rebased onto master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting feedback More information is required from the requestor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v3] App Stuck On MacOS after OS sleep

3 participants