From 6b364aeaac36745f52f5a02cb40ad680617be98a Mon Sep 17 00:00:00 2001 From: Mark Zielinski Date: Thu, 23 Jul 2026 22:31:18 +0000 Subject: [PATCH] Surface web content process termination as a WebContentProcessTerminated event When the web content process backing a WKWebView terminates - after a crash or after the operating system reclaims its memory - the native view stays alive but renders nothing, and WebKit performs no recovery on its own. The only signal is the WKNavigationDelegate webViewWebContentProcessDidTerminate: callback, which the managed delegate did not implement, so embedders had no way to observe the loss and reload. The managed WKNavigationDelegate now implements that callback and the macOS adapter surfaces it through a new IWebViewAdapterWithProcessTermination capability, exposed as a WebContentProcessTerminated event on NativeWebView and NativeWebDialog. The event follows the existing capability-interface pattern, so platforms without an equivalent signal are unaffected; WebView2's ProcessFailed can back the same event on Windows later. --- .../IWebViewAdapter.cs | 10 +++++++ .../Interop/WebKit/WKNavigationDelegate.cs | 14 ++++++++++ .../Macios/MaciosWebViewAdapter.cs | 11 +++++++- .../NativeWebDialog.cs | 20 ++++++++++++++ .../NativeWebView.cs | 27 +++++++++++++++++++ 5 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs index c8e67f2..eaf074c 100644 --- a/src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs @@ -385,6 +385,16 @@ internal interface IWebViewAdapterWithInputRedirect : IWebViewAdapter event Action Input; } +internal interface IWebViewAdapterWithProcessTermination : IWebViewAdapter +{ + /// + /// WebContentProcessTerminated dispatches after the web content process backing the webview terminates, + /// for example after a crash or after the operating system reclaims its memory. The native control stays + /// alive but renders nothing until the page is reloaded. + /// + event EventHandler? WebContentProcessTerminated; +} + internal interface IWebViewAdapterWithCommands { void Copy(); diff --git a/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKNavigationDelegate.cs b/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKNavigationDelegate.cs index 91b889e..a391ee4 100644 --- a/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKNavigationDelegate.cs +++ b/src/Avalonia.Controls.WebView.Core/Macios/Interop/WebKit/WKNavigationDelegate.cs @@ -14,6 +14,8 @@ internal unsafe class WKNavigationDelegate : NSManagedObjectBase s_willPresentNotification = &OnDidFinishNavigation; private static readonly delegate* unmanaged[Cdecl] s_decidePolicyForNavigationAction = &OnDecidePolicyForNavigationAction; + private static readonly delegate* unmanaged[Cdecl] + s_webContentProcessDidTerminate = &OnWebContentProcessDidTerminate; static WKNavigationDelegate() { @@ -31,6 +33,10 @@ static WKNavigationDelegate() result = Libobjc.class_addMethod(delegateClass, didReceiveNotificationResponse, s_decidePolicyForNavigationAction, "v@:@@@"); Debug.Assert(result == 1); + var webContentProcessDidTerminateSel = Libobjc.sel_getUid("webViewWebContentProcessDidTerminate:"); + result = Libobjc.class_addMethod(delegateClass, webContentProcessDidTerminateSel, s_webContentProcessDidTerminate, "v@:@"); + Debug.Assert(result == 1); + result = RegisterManagedMembers(delegateClass) ? 1 : 0; Debug.Assert(result == 1); @@ -45,6 +51,7 @@ public WKNavigationDelegate() : base(s_class) public event EventHandler? DidFinishNavigation; public event EventHandler? DecidePolicyNavigation; + public event EventHandler? WebContentProcessDidTerminate; [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] private static void OnDidFinishNavigation(IntPtr self, IntPtr sel, IntPtr webView, IntPtr navigation) @@ -53,6 +60,13 @@ private static void OnDidFinishNavigation(IntPtr self, IntPtr sel, IntPtr webVie managed?.DidFinishNavigation?.Invoke(managed, EventArgs.Empty); } + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static void OnWebContentProcessDidTerminate(IntPtr self, IntPtr sel, IntPtr webView) + { + var managed = ReadManagedSelf(self); + managed?.WebContentProcessDidTerminate?.Invoke(managed, EventArgs.Empty); + } + private static readonly IntPtr s_actionRequest = Libobjc.sel_getUid("request"); private static readonly IntPtr s_navigationType = Libobjc.sel_getUid("navigationType"); private static readonly IntPtr s_targetFrame = Libobjc.sel_getUid("targetFrame"); diff --git a/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs index 133662c..c93637e 100644 --- a/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Macios/MaciosWebViewAdapter.cs @@ -18,7 +18,8 @@ namespace Avalonia.Controls.Macios; [SupportedOSPlatform("macos")] [SupportedOSPlatform("ios")] internal class MaciosWebViewAdapter : IWebViewAdapterWithFocus, IWebViewAdapterWithInputRedirect, - IWebViewAdapterWithCookieManager, IWebViewAdapterWithCommands, IWebViewWithPrint, IAppleWKWebViewPlatformHandle + IWebViewAdapterWithCookieManager, IWebViewAdapterWithCommands, IWebViewAdapterWithProcessTermination, + IWebViewWithPrint, IAppleWKWebViewPlatformHandle { private const string DefaultPostAvWebViewMessageName = "postAvWebViewMessage"; @@ -73,6 +74,7 @@ public MaciosWebViewAdapter(AppleWKWebViewEnvironmentRequestedEventArgs options) _navDelegate = new WKNavigationDelegate(); _navDelegate.DidFinishNavigation += OnDelegateOnDidFinishNavigation; _navDelegate.DecidePolicyNavigation += OnDelegateOnDecidePolicyNavigation; + _navDelegate.WebContentProcessDidTerminate += OnDelegateOnWebContentProcessDidTerminate; _webView = new WKWebView(_config) { NavigationDelegate = _navDelegate }; _webView.Opaque = false; @@ -157,6 +159,7 @@ public Color DefaultBackground public event EventHandler? WebResourceRequested; public event EventHandler? GotFocus; public event EventHandler? LostFocus; + public event EventHandler? WebContentProcessTerminated; public event Action? Input; public bool GoBack() => _webView.GoBack() != default; public bool GoForward() => _webView.GoForward() != default; @@ -203,6 +206,7 @@ public void Dispose() _scriptHandler.DidReceiveScriptMessage -= OnScriptHandlerOnDidReceiveScriptMessage; _navDelegate.DidFinishNavigation -= OnDelegateOnDidFinishNavigation; _navDelegate.DecidePolicyNavigation -= OnDelegateOnDecidePolicyNavigation; + _navDelegate.WebContentProcessDidTerminate -= OnDelegateOnWebContentProcessDidTerminate; _webView.PerformKeyEquivalent -= WebViewOnPerformKeyEquivalent; _webView.BecomeFirstResponder -= OnWebViewOnBecomeFirstResponder; _webView.ResignFirstResponder -= OnWebViewOnResignFirstResponder; @@ -297,6 +301,11 @@ private async void OnDelegateOnDidFinishNavigation(object? sender, EventArgs arg NavigationCompleted?.Invoke(this, new WebViewNavigationCompletedEventArgs { Request = Uri.TryCreate(url!.AbsoluteString, UriKind.Absolute, out var uri) ? uri : null, IsSuccess = true }); } + private void OnDelegateOnWebContentProcessDidTerminate(object? sender, EventArgs args) + { + WebContentProcessTerminated?.Invoke(this, EventArgs.Empty); + } + private void OnWebViewOnResignFirstResponder(object? o, EventArgs eventArgs) { LostFocus?.Invoke(this, IWebViewAdapterWithFocus.LostFocusDirection.Unknown); diff --git a/src/Avalonia.Controls.WebView/NativeWebDialog.cs b/src/Avalonia.Controls.WebView/NativeWebDialog.cs index 56ab3ed..bcfa6c4 100644 --- a/src/Avalonia.Controls.WebView/NativeWebDialog.cs +++ b/src/Avalonia.Controls.WebView/NativeWebDialog.cs @@ -365,6 +365,17 @@ public bool Move(int x, int y) public event EventHandler? AdapterDestroyed; public event EventHandler? EnvironmentRequested; + /// + /// WebContentProcessTerminated dispatches after the web content process backing the webview terminates, + /// for example after a crash or after the operating system reclaims its memory. The dialog stays open + /// but renders nothing until the page is reloaded, so handle this event to again. + /// + /// + /// Raised by WebKit-based adapters (WKWebView on macOS and iOS). Other platforms do not raise this + /// event yet. + /// + public event EventHandler? WebContentProcessTerminated; + /// public Core.NativeWebViewCommandManager? TryGetCommandManager() => TryGetAdapter() switch { @@ -465,6 +476,8 @@ private void DialogImplOnAdapterDestroyed(object? sender, Core.WebViewAdapterEve adapter.WebMessageReceived -= WebViewAdapterOnWebMessageReceived; adapter.WebResourceRequested -= WebViewAdapterOnWebResourceRequested; adapter.NewWindowRequested -= WebViewAdapterOnNewWindowRequested; + if (adapter is Core.IWebViewAdapterWithProcessTermination withProcessTermination) + withProcessTermination.WebContentProcessTerminated -= WebViewAdapterOnWebContentProcessTerminated; _dialogInitialized = false; AdapterDestroyed?.Invoke(this, e); } @@ -492,6 +505,8 @@ private void DialogImplOnAdapterCreated(object? sender, Core.WebViewAdapterEvent adapter.WebResourceRequested += WebViewAdapterOnWebResourceRequested; if (_newWindowRequested is not null) adapter.NewWindowRequested += WebViewAdapterOnNewWindowRequested; + if (adapter is Core.IWebViewAdapterWithProcessTermination withProcessTermination) + withProcessTermination.WebContentProcessTerminated += WebViewAdapterOnWebContentProcessTerminated; if (_initialSource is Uri url) adapter.Source = url; else if (_initialSource is ValueTuple pair) @@ -528,5 +543,10 @@ private void WebViewAdapterOnNewWindowRequested(object? sender, Core.WebViewNewW { _newWindowRequested?.Invoke(this, e); } + + private void WebViewAdapterOnWebContentProcessTerminated(object? sender, EventArgs e) + { + WebContentProcessTerminated?.Invoke(this, e); + } } } diff --git a/src/Avalonia.Controls.WebView/NativeWebView.cs b/src/Avalonia.Controls.WebView/NativeWebView.cs index 31ccf23..2567fa7 100644 --- a/src/Avalonia.Controls.WebView/NativeWebView.cs +++ b/src/Avalonia.Controls.WebView/NativeWebView.cs @@ -247,6 +247,18 @@ public event EventHandler? WebResourceReques /// public event EventHandler? AdapterDestroyed; + /// + /// WebContentProcessTerminated dispatches after the web content process backing the webview terminates, + /// for example after a crash or after the operating system reclaims its memory. The native control stays + /// alive but renders nothing until the page is reloaded, so handle this event to + /// or again. + /// + /// + /// Raised by WebKit-based adapters (WKWebView on macOS and iOS). Other platforms do not raise this + /// event yet. + /// + public event EventHandler? WebContentProcessTerminated; + /// public Uri Source { @@ -428,6 +440,12 @@ private void WebViewAdapterOnNewWindowRequested(object? sender, Core.WebViewNewW _newWindowRequested?.Invoke(this, e); } + private void WebViewAdapterOnWebContentProcessTerminated(object? sender, EventArgs e) + { + Core.WebViewDispatcher.VerifyAccess(); + WebContentProcessTerminated?.Invoke(this, e); + } + #if AVALONIA protected override async void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { @@ -563,6 +581,10 @@ private void ControlHostImplOnAdapterDeinitialized(object? sender, Core.IWebView { withInput.Input -= WithInputOnInput; } + if (adapter is Core.IWebViewAdapterWithProcessTermination withProcessTermination) + { + withProcessTermination.WebContentProcessTerminated -= WebViewAdapterOnWebContentProcessTerminated; + } AdapterDestroyed?.Invoke(this, new Core.WebViewAdapterEventArgs(adapter)); } @@ -595,6 +617,11 @@ private void ControlHostImplOnAdapterCreated(object? sender, Core.IWebViewAdapte withInput.Input += WithInputOnInput; } + if (adapter is Core.IWebViewAdapterWithProcessTermination withProcessTermination) + { + withProcessTermination.WebContentProcessTerminated += WebViewAdapterOnWebContentProcessTerminated; + } + if (_initialSource is Uri url) { adapter.Navigate(url);