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
26 changes: 25 additions & 1 deletion src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

namespace Avalonia.Controls.Gtk;

internal abstract class GtkWebViewAdapter : IWebViewAdapterWithFocus, IGtkWebViewPlatformHandle, IWebViewWithPrintWithOptions
internal abstract class GtkWebViewAdapter : IWebViewAdapterWithFocus, IGtkWebViewPlatformHandle, IWebViewWithPrintWithOptions,
IWebViewAdapterWithProcessTermination
Comment on lines +17 to +18

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The same web-process-terminated should exist in WPE linux backend. Can it be added there too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Wired up in 373ea51. WPE is first in LinuxDefaultOrder, so without this the event would never fire on the default Linux backend. web-process-terminated is registered on the shared glib WebKitWebView (Since 2.20), so the WPE adapter now raises the event from the same signal and reuses the shared reason mapping (WebKitWebProcessTermination). The handler follows the existing WPE convention and runs on the UI thread via the GLib pump. No runtime verification here since libWPEWebKit is not installed; the capability interface and mapping are covered by tests.

{
internal enum WebKitLoadEvent
{
Expand Down Expand Up @@ -45,6 +46,9 @@ internal enum WebKitLoadEvent
private static readonly unsafe IntPtr s_resourceLoadStartedCallback =
new((delegate* unmanaged[Cdecl]<IntPtr, IntPtr, IntPtr, IntPtr, void>)&ResourceLoadStartedCallback);

private static readonly unsafe IntPtr s_processTerminatedCallback =
new((delegate* unmanaged[Cdecl]<IntPtr, int, IntPtr, void>)&ProcessTerminatedCallback);

private readonly IntPtr _settings;
private IntPtr _webViewHandle;
private GtkSignal? _loadChangedSignal;
Expand All @@ -53,6 +57,7 @@ internal enum WebKitLoadEvent
private GtkSignal? _focusOutSignal;
private GtkSignal? _scriptMessageReceivedSignal;
private GtkSignal? _resourceLoadStarted;
private GtkSignal? _processTerminatedSignal;
private Uri _source = WebViewHelper.EmptyPage;
private bool _hostFocusChange;

Expand Down Expand Up @@ -135,6 +140,7 @@ protected GtkWebViewAdapter(GtkWebViewEnvironmentRequestedEventArgs args)
_focusInSignal = new GtkSignal(WebViewHandle, "focus-in-event", s_focusInCallback, this);
_focusOutSignal = new GtkSignal(WebViewHandle, "focus-out-event", s_focusOutCallback, this);
_resourceLoadStarted = new GtkSignal(WebViewHandle, "resource-load-started", s_resourceLoadStartedCallback, this);
_processTerminatedSignal = new GtkSignal(WebViewHandle, "web-process-terminated", s_processTerminatedCallback, this);
}

public IntPtr WebViewHandle => _webViewHandle;
Expand Down Expand Up @@ -185,6 +191,7 @@ public virtual Color DefaultBackground
public event EventHandler<WebResourceRequestedEventArgs>? WebResourceRequested;
public event EventHandler? GotFocus;
public event EventHandler<IWebViewAdapterWithFocus.LostFocusDirection>? LostFocus;
public event EventHandler<WebViewProcessTerminatedEventArgs>? ProcessTerminated;

/// <summary>
/// The GTK toplevel holding the web view, or zero when the adapter has none.
Expand Down Expand Up @@ -611,6 +618,22 @@ private static void ScriptMessageReceivedCallback(IntPtr widget, IntPtr jsResult
});
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void ProcessTerminatedCallback(IntPtr webView, int reason, IntPtr data)
{
if (!GtkSignal.TryGetState<GtkWebViewAdapter>(data, out var adapter)
|| adapter.ProcessTerminated is not { } handler)
{
return;
}

var reasonName = WebKitWebProcessTermination.GetReasonName(reason);
WebViewDispatcher.InvokeAsync(() =>
{
handler.Invoke(adapter, new WebViewProcessTerminatedEventArgs { Reason = reasonName });
});
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void ResourceLoadStartedCallback(IntPtr widget, IntPtr resource, IntPtr request, IntPtr data)
{
Expand Down Expand Up @@ -696,6 +719,7 @@ protected virtual void DisposeSafe(bool disposing)
Interlocked.Exchange(ref _focusOutSignal, null)?.Dispose();
Interlocked.Exchange(ref _scriptMessageReceivedSignal, null)?.Dispose();
Interlocked.Exchange(ref _resourceLoadStarted, null)?.Dispose();
Interlocked.Exchange(ref _processTerminatedSignal, null)?.Dispose();
}

var webView = Interlocked.Exchange(ref _webViewHandle, IntPtr.Zero);
Expand Down
23 changes: 23 additions & 0 deletions src/Avalonia.Controls.WebView.Core/IWebViewAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ public class WebViewNavigationEventArgs : EventArgs
public Uri? Request { get; init; }
}

public sealed class WebViewProcessTerminatedEventArgs : EventArgs
{
/// <summary>
/// Platform-specific detail about the web content process termination, or null if the platform doesn't provide one.
/// </summary>
public string? Reason { get; init; }
}

public sealed class WebViewNavigationCompletedEventArgs : WebViewNavigationEventArgs
{
public bool IsSuccess { get; init; } = true;
Expand Down Expand Up @@ -390,6 +398,21 @@ internal interface IWebViewAdapterWithInputRedirect : IWebViewAdapter
event Action<RoutedEventArgs> Input;
}

internal interface IWebViewAdapterWithProcessTermination : IWebViewAdapter
{
/// <summary>
/// ProcessTerminated dispatches when the web content process backing the webview is gone,
/// for example after a crash or when the operating system reclaims its memory.
/// The event is not raised for recoverable conditions, such as process unresponsiveness,
/// nor when only subframe processes exit and the top-level document survives.
/// </summary>
/// <remarks>
/// Raised by the WebView2, WKWebView, WebKitGTK and WPE WebKit adapters.
/// The headless, browser and Android adapters do not raise it.
/// </remarks>
event EventHandler<WebViewProcessTerminatedEventArgs>? ProcessTerminated;
}

internal interface IWebViewAdapterWithCommands
{
void Copy();
Expand Down
14 changes: 14 additions & 0 deletions src/Avalonia.Controls.WebView.Core/Linux/WpeWebViewAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal sealed unsafe class WpeWebViewAdapter
IWebViewAdapterWithCookieManager,
IWebViewAdapterWithVisibility,
IWebViewAdapterWithGpuSurface,
IWebViewAdapterWithProcessTermination,
ILinuxWpePlatformHandle
{
private IntPtr _webView;
Expand All @@ -35,6 +36,7 @@ internal sealed unsafe class WpeWebViewAdapter

// Pin callback delegates to prevent GC
private readonly GSignalCallback _loadChangedCallback;
private readonly GSignalCallback _processTerminatedCallback;
private readonly GSignalDecidePolicyCallback _decidePolicyCallback;
private readonly GSignalCreateCallback _createCallback;
private readonly GSignalScriptMessageCallback _scriptMessageCallback;
Expand All @@ -45,6 +47,7 @@ private WpeWebViewAdapter()
{
// Pin callbacks as instance fields
_loadChangedCallback = OnLoadChanged;
_processTerminatedCallback = OnProcessTerminated;
_decidePolicyCallback = OnDecidePolicy;
_createCallback = OnCreate;
_scriptMessageCallback = OnScriptMessageReceived;
Expand Down Expand Up @@ -149,6 +152,7 @@ private void Initialize(LinuxWpeWebViewEnvironmentRequestedEventArgs args, Contr
ConnectSignal(_webView, "load-failed", Marshal.GetFunctionPointerForDelegate(_loadFailedCallback), selfPtr);
ConnectSignal(_webView, "decide-policy", Marshal.GetFunctionPointerForDelegate(_decidePolicyCallback), selfPtr);
ConnectSignal(_webView, "create", Marshal.GetFunctionPointerForDelegate(_createCallback), selfPtr);
ConnectSignal(_webView, "web-process-terminated", Marshal.GetFunctionPointerForDelegate(_processTerminatedCallback), selfPtr);

// 4. Register the message handler, connecting before registering so that no message can arrive unhandled.
ConnectSignal(_userContentManager, $"script-message-received::{WebViewHelper.PostAvWebViewMessageName}",
Expand Down Expand Up @@ -228,6 +232,15 @@ private static int OnLoadFailed(IntPtr webView, int loadEvent, IntPtr failingUri
return 0; // FALSE = don't suppress default handling
}

private static void OnProcessTerminated(IntPtr webView, int reason, IntPtr userData)
{
var adapter = (WpeWebViewAdapter)GCHandle.FromIntPtr(userData).Target!;
adapter.ProcessTerminated?.Invoke(adapter, new WebViewProcessTerminatedEventArgs
{
Reason = WebKitWebProcessTermination.GetReasonName(reason)
});
}

// WebKitPolicyDecisionType
private const int WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION = 0;
private const int WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION = 1;
Expand Down Expand Up @@ -724,6 +737,7 @@ private static void OnGetAllCookiesFinished(IntPtr sourceObject, IntPtr result,
public event EventHandler<WebViewNewWindowRequestedEventArgs>? NewWindowRequested;
public event EventHandler<WebMessageReceivedEventArgs>? WebMessageReceived;
public event EventHandler<WebResourceRequestedEventArgs>? WebResourceRequested;
public event EventHandler<WebViewProcessTerminatedEventArgs>? ProcessTerminated;

// --- Static info ---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,29 @@ internal unsafe class WKNavigationDelegate : NSManagedObjectBase
s_willPresentNotification = &OnDidFinishNavigation;
private static readonly delegate* unmanaged[Cdecl]<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, void>
s_decidePolicyForNavigationAction = &OnDecidePolicyForNavigationAction;
private static readonly delegate* unmanaged[Cdecl]<IntPtr, IntPtr, IntPtr, void>
s_webContentProcessDidTerminate = &OnWebContentProcessDidTerminate;

static WKNavigationDelegate()
{
var delegateClass = AllocateClassPair("ManagedWKNavigationDelegate");

var willPresentNotificationSel = Libobjc.sel_getUid("webView:didFinishNavigation:");
var didReceiveNotificationResponse = Libobjc.sel_getUid("webView:decidePolicyForNavigationAction:decisionHandler:");
var webContentProcessDidTerminateSel = Libobjc.sel_getUid("webViewWebContentProcessDidTerminate:");

// All WKNavigationDelegate methods are optional.
AddProtocol(delegateClass, "WKNavigationDelegate",
new ProtocolMethod(willPresentNotificationSel, "v@:@@", false),
new ProtocolMethod(didReceiveNotificationResponse, "v@:@@@?", false));
new ProtocolMethod(didReceiveNotificationResponse, "v@:@@@?", false),
new ProtocolMethod(webContentProcessDidTerminateSel, "v@:@", false));

AddMethod(delegateClass, willPresentNotificationSel, new IntPtr(s_willPresentNotification), "v@:@@");

AddMethod(delegateClass, didReceiveNotificationResponse, new IntPtr(s_decidePolicyForNavigationAction), "v@:@@@");

AddMethod(delegateClass, webContentProcessDidTerminateSel, new IntPtr(s_webContentProcessDidTerminate), "v@:@");

RegisterManagedMembers(delegateClass);

Libobjc.objc_registerClassPair(delegateClass);
Expand All @@ -43,6 +49,7 @@ public WKNavigationDelegate() : base(s_class)

public event EventHandler? DidFinishNavigation;
public event EventHandler<DecidePolicyNavigationEventArgs>? DecidePolicyNavigation;
public event EventHandler? WebContentProcessDidTerminate;

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void OnDidFinishNavigation(IntPtr self, IntPtr sel, IntPtr webView, IntPtr navigation)
Expand All @@ -51,6 +58,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<WKNavigationDelegate>(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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ namespace Avalonia.Controls.Macios;
[SupportedOSPlatform("macos")]
[SupportedOSPlatform("ios")]
internal class MaciosWebViewAdapter : IWebViewAdapterWithFocus, IWebViewAdapterWithInputRedirect,
IWebViewAdapterWithCookieManager, IWebViewAdapterWithCommands, IWebViewWithPrint, IAppleWKWebViewPlatformHandle
IWebViewAdapterWithCookieManager, IWebViewAdapterWithCommands, IWebViewAdapterWithProcessTermination,
IWebViewWithPrint, IAppleWKWebViewPlatformHandle
{
private readonly string _scriptHandlerMessageName;
private readonly NSString _scriptHandlerMessageNameNative;
Expand Down Expand Up @@ -79,6 +80,7 @@ public MaciosWebViewAdapter(AppleWKWebViewEnvironmentRequestedEventArgs options)
_navDelegate = new WKNavigationDelegate();
_navDelegate.DidFinishNavigation += OnDelegateOnDidFinishNavigation;
_navDelegate.DecidePolicyNavigation += OnDelegateOnDecidePolicyNavigation;
_navDelegate.WebContentProcessDidTerminate += OnDelegateOnWebContentProcessDidTerminate;

_webView = new WKWebView(_config);
// The view is released explicitly in ReleaseWebView, never by the wrapper.
Expand Down Expand Up @@ -177,6 +179,7 @@ public Color DefaultBackground
public event EventHandler<WebResourceRequestedEventArgs>? WebResourceRequested;
public event EventHandler? GotFocus;
public event EventHandler<IWebViewAdapterWithFocus.LostFocusDirection>? LostFocus;
public event EventHandler<WebViewProcessTerminatedEventArgs>? ProcessTerminated;
public event Action<RoutedEventArgs>? Input;
public bool GoBack() => _webView.GoBack() != default;
public bool GoForward() => _webView.GoForward() != default;
Expand Down Expand Up @@ -229,6 +232,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;
Expand Down Expand Up @@ -327,6 +331,12 @@ private void OnDelegateOnDidFinishNavigation(object? sender, EventArgs args)
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)
{
// WKWebView doesn't report a termination reason.
ProcessTerminated?.Invoke(this, new WebViewProcessTerminatedEventArgs { Reason = null });
}

private void OnWebViewOnResignFirstResponder(object? o, EventArgs eventArgs)
{
LostFocus?.Invoke(this, IWebViewAdapterWithFocus.LostFocusDirection.Unknown);
Expand Down
23 changes: 23 additions & 0 deletions src/Avalonia.Controls.WebView.Core/WebKitWebProcessTermination.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Avalonia.Controls;

/// <summary>
/// Maps <c>WebKitWebProcessTerminationReason</c> values, shared by the WebKitGTK and WPE WebKit ports.
/// </summary>
internal static class WebKitWebProcessTermination
{
// WebKitWebProcessTerminationReason
private enum Reason
{
Crashed = 0,
ExceededMemoryLimit = 1,
TerminatedByApi = 2
}

internal static string GetReasonName(int reason) => reason switch
{
(int)Reason.Crashed => "Crashed",
(int)Reason.ExceededMemoryLimit => "ExceededMemoryLimit",
(int)Reason.TerminatedByApi => "TerminatedByApi",
_ => $"Unknown({reason})"
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal partial interface ICoreWebView2
void add_PermissionRequested([MarshalAs(UnmanagedType.Interface)] IntPtr eventHandler, out EventRegistrationToken token);
void remove_PermissionRequested(EventRegistrationToken token);

void add_ProcessFailed([MarshalAs(UnmanagedType.Interface)] IntPtr eventHandler, out EventRegistrationToken token);
void add_ProcessFailed([MarshalAs(UnmanagedType.Interface)] ICoreWebView2ProcessFailedEventHandler eventHandler, out EventRegistrationToken token);
void remove_ProcessFailed(EventRegistrationToken token);

void AddScriptToExecuteOnDocumentCreated([MarshalAs(UnmanagedType.LPWStr)] string javaScript, [MarshalAs(UnmanagedType.Interface)] ICoreWebView2AddScriptToExecuteOnDocumentCreatedCompletedHandler handler);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;

namespace Avalonia.Controls.Win.WebView2.Interop;

internal enum COREWEBVIEW2_PROCESS_FAILED_KIND
{
COREWEBVIEW2_PROCESS_FAILED_KIND_BROWSER_PROCESS_EXITED,
COREWEBVIEW2_PROCESS_FAILED_KIND_RENDER_PROCESS_EXITED,
COREWEBVIEW2_PROCESS_FAILED_KIND_RENDER_PROCESS_UNRESPONSIVE,
COREWEBVIEW2_PROCESS_FAILED_KIND_FRAME_RENDER_PROCESS_EXITED,
COREWEBVIEW2_PROCESS_FAILED_KIND_UTILITY_PROCESS_EXITED,
COREWEBVIEW2_PROCESS_FAILED_KIND_SANDBOX_HELPER_PROCESS_EXITED,
COREWEBVIEW2_PROCESS_FAILED_KIND_GPU_PROCESS_EXITED,
COREWEBVIEW2_PROCESS_FAILED_KIND_PPAPI_PLUGIN_PROCESS_EXITED,
COREWEBVIEW2_PROCESS_FAILED_KIND_PPAPI_BROKER_PROCESS_EXITED,
COREWEBVIEW2_PROCESS_FAILED_KIND_UNKNOWN_PROCESS_EXITED
}

[GeneratedComInterface(Options = ComInterfaceOptions.ComObjectWrapper)]
[Guid("8155A9A4-1474-4A86-8CAE-151B0FA6B8CA")]
internal partial interface ICoreWebView2ProcessFailedEventArgs
{
COREWEBVIEW2_PROCESS_FAILED_KIND GetProcessFailedKind();
}

[GeneratedComInterface(Options = ComInterfaceOptions.ManagedObjectWrapper)]
[Guid("79E0AEA4-990B-42D9-AA1D-0FCC2E5BC7F1")]
internal partial interface ICoreWebView2ProcessFailedEventHandler
{
void Invoke([MarshalAs(UnmanagedType.Interface)] ICoreWebView2 sender, [MarshalAs(UnmanagedType.Interface)] ICoreWebView2ProcessFailedEventArgs args);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ namespace Avalonia.Controls.Win.WebView2;

[SupportedOSPlatform("windows6.1")] // win7
internal abstract partial class WebView2BaseAdapter(ICoreWebView2Controller controller)
: IWebViewAdapterWithCookieManager, IWebViewAdapterWithFocus, IWindowsWebView2PlatformHandle, IWebViewWithPrintWithOptions
: IWebViewAdapterWithCookieManager, IWebViewAdapterWithFocus, IWebViewAdapterWithProcessTermination,
IWindowsWebView2PlatformHandle, IWebViewWithPrintWithOptions
{
private EventHandler<WebResourceRequestedEventArgs>? _webResourceRequested;
private Action? _subscriptions;
Expand Down Expand Up @@ -86,6 +87,7 @@ public event EventHandler<WebResourceRequestedEventArgs>? WebResourceRequested

public event EventHandler? GotFocus;
public event EventHandler<IWebViewAdapterWithFocus.LostFocusDirection>? LostFocus;
public event EventHandler<WebViewProcessTerminatedEventArgs>? ProcessTerminated;

public Color DefaultBackground
{
Expand Down Expand Up @@ -291,6 +293,7 @@ private static class HResult
internal EventHandler<WebViewNewWindowRequestedEventArgs>? GetNewWindowRequested() => NewWindowRequested;
internal EventHandler? GetGotFocus() => GotFocus;
internal EventHandler<IWebViewAdapterWithFocus.LostFocusDirection>? GetLostFocus() => LostFocus;
internal EventHandler<WebViewProcessTerminatedEventArgs>? GetProcessTerminated() => ProcessTerminated;

public async Task InitializeAsync(WindowsWebView2EnvironmentRequestedEventArgs environmentArgs)
{
Expand Down Expand Up @@ -344,6 +347,7 @@ private Action AddHandlers(ICoreWebView2 webView)
webView.add_WebMessageReceived(callbacks, out var webMessageReceivedToken);
webView.add_WebResourceRequested(callbacks, out var webResourceRequestedToken);
webView.add_NewWindowRequested(callbacks, out var newWindowRequestedToken);
webView.add_ProcessFailed(callbacks, out var processFailedToken);
controller.add_MoveFocusRequested(callbacks, out var moveFocusRequestedToken);
controller.add_GotFocus(callbacks, out var gotFocusToken);
RegisterCallbacks(callbacks);
Expand All @@ -355,6 +359,7 @@ private Action AddHandlers(ICoreWebView2 webView)
webView.remove_WebMessageReceived(webMessageReceivedToken);
webView.remove_WebResourceRequested(webResourceRequestedToken);
webView.remove_NewWindowRequested(newWindowRequestedToken);
webView.remove_ProcessFailed(processFailedToken);
controller.remove_MoveFocusRequested(moveFocusRequestedToken);
controller.remove_GotFocus(gotFocusToken);
UnregisterCallbacks();
Expand Down
Loading