From f61bbe5b68453d3d02b3930ce30ff53818ff509c Mon Sep 17 00:00:00 2001 From: William Tanksley Jr Date: Mon, 26 Oct 2026 08:46:32 -0700 Subject: [PATCH] Fix Linux WebView login hang and User-Agent failure (#2106) (#2108) - Set NativeWebDialog.UserAgent in AdapterCreated instead of passing a full User-Agent header to ApplicationNameForUserAgent (which caused WebKit2GTK webkit_settings_set_user_agent assertion failures on Linux). - Set gtkArgs.ForceX11GdkBackend = true when requesting the GTK WebView environment. - Recognize GTK initialization failures (InvalidOperationException 'Unable to initialize GTK', libgtk-3/libgdk-3 DllNotFoundException, and exceptions originating from GtkNativeWebViewDialog/AvaloniaGtk) in WebView2LoginErrorMessage so async void NativeWebDialog.Show() failures fault BrowserLoginAsync instead of leaving tcs.Task waiting forever, allowing StartAsyncInternal to fall back to LoginExternalDialog. --- .../Dialogs/Login/AvaloniaLoginChoiceEager.cs | 21 ++++-- .../WebView2LoginErrorMessage.cs | 13 +++- .../WebView2LoginErrorMessageTests.cs | 69 +++++++++++++++++++ 3 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 Source/_Tests/LibationUiBase.Tests/WebView2LoginErrorMessageTests.cs diff --git a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs index 35dbbdba..54ef46c0 100644 --- a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs +++ b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs @@ -93,6 +93,15 @@ public class AvaloniaLoginChoiceEager : ILoginChoiceEager }; dialog.AdapterCreated += (s, e) => { + try + { + dialog.UserAgent = Configuration.Instance.GetDeviceRegistrationProfile().UserAgent; + } + catch (Exception ex) + { + Serilog.Log.Logger.Warning(ex, "Failed to set WebView User-Agent"); + } + if (dialog.TryGetCookieManager() is NativeWebViewCookieManager cookieManager) { foreach (System.Net.Cookie c in shoiceIn.SignInCookies ?? []) @@ -126,10 +135,13 @@ public class AvaloniaLoginChoiceEager : ILoginChoiceEager return null; } - // NativeWebDialog can fault on a posted dispatcher job (e.g. missing libwebkit2gtk on Linux), which bypasses a try/catch around Show(). + // NativeWebDialog.Show() is async void and can fault on a posted dispatcher job (e.g. missing libwebkit2gtk + // or GTK initialization failure on Linux), which bypasses a synchronous try/catch around Show() and would + // otherwise leave tcs.Task waiting forever. void onUnhandledException(object? sender, DispatcherUnhandledExceptionEventArgs e) { - if (!WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(e.Exception)) + if (!WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(e.Exception) + && !WebView2LoginErrorMessage.TryFindInTree(e.Exception, out _)) return; e.Handled = true; try @@ -166,7 +178,7 @@ public class AvaloniaLoginChoiceEager : ILoginChoiceEager else dialog.Show(); } - catch (Exception ex) when (WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(ex)) + catch (Exception ex) { tcs.TrySetException(ex); } @@ -190,11 +202,10 @@ public class AvaloniaLoginChoiceEager : ILoginChoiceEager break; case AppleWKWebViewEnvironmentRequestedEventArgs appleArgs: appleArgs.NonPersistentDataStore = true; - appleArgs.ApplicationNameForUserAgent = userAgent; break; case GtkWebViewEnvironmentRequestedEventArgs gtkArgs: gtkArgs.EphemeralDataManager = true; - gtkArgs.ApplicationNameForUserAgent = userAgent; + gtkArgs.ForceX11GdkBackend = true; break; } } diff --git a/Source/LibationUiBase/WebView2LoginErrorMessage.cs b/Source/LibationUiBase/WebView2LoginErrorMessage.cs index c1b60927..b5e210e4 100644 --- a/Source/LibationUiBase/WebView2LoginErrorMessage.cs +++ b/Source/LibationUiBase/WebView2LoginErrorMessage.cs @@ -61,10 +61,15 @@ public static class WebView2LoginErrorMessage if (e is DllNotFoundException dll && IsEmbeddedWebViewNativeDllFailure(dll)) return true; + // AvaloniaGtk.RunTask throws InvalidOperationException("Unable to initialize GTK") when GTK cannot start + // (for example on Wayland without X11 backend or when no GTK display is available). + if (e is InvalidOperationException && e.Message.Contains("Unable to initialize GTK", StringComparison.OrdinalIgnoreCase)) + return true; + if (!StackMentionsEmbeddedSignInBrowser(e)) continue; - if (e is UnauthorizedAccessException) + if (e is UnauthorizedAccessException or InvalidOperationException or EntryPointNotFoundException or TypeInitializationException) return true; if (e is COMException com @@ -81,7 +86,9 @@ public static class WebView2LoginErrorMessage private static bool IsEmbeddedWebViewNativeDllFailure(DllNotFoundException e) { if (e.Message.Contains("libwebkit2gtk", StringComparison.OrdinalIgnoreCase) - || e.Message.Contains("webkit2gtk", StringComparison.OrdinalIgnoreCase)) + || e.Message.Contains("webkit2gtk", StringComparison.OrdinalIgnoreCase) + || e.Message.Contains("libgtk-3", StringComparison.OrdinalIgnoreCase) + || e.Message.Contains("libgdk-3", StringComparison.OrdinalIgnoreCase)) return true; return StackMentionsEmbeddedSignInBrowser(e); @@ -118,7 +125,9 @@ public static class WebView2LoginErrorMessage && (stack.Contains("WebView2", StringComparison.Ordinal) || stack.Contains("CoreWebView2", StringComparison.Ordinal) || stack.Contains("NativeWebDialog", StringComparison.Ordinal) + || stack.Contains("GtkNativeWebViewDialog", StringComparison.Ordinal) || stack.Contains("GtkWebView", StringComparison.Ordinal) + || stack.Contains("AvaloniaGtk", StringComparison.Ordinal) || stack.Contains("WebViewControlAvalonia", StringComparison.Ordinal)); } } diff --git a/Source/_Tests/LibationUiBase.Tests/WebView2LoginErrorMessageTests.cs b/Source/_Tests/LibationUiBase.Tests/WebView2LoginErrorMessageTests.cs new file mode 100644 index 00000000..c384dd47 --- /dev/null +++ b/Source/_Tests/LibationUiBase.Tests/WebView2LoginErrorMessageTests.cs @@ -0,0 +1,69 @@ +using LibationUiBase; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Runtime.CompilerServices; + +namespace LibationUiBase.Tests; + +[TestClass] +public class WebView2LoginErrorMessageTests +{ + [TestMethod] + public void missing_webkit2gtk_dll_is_recognized_without_stack_trace() + { + var ex = new DllNotFoundException("Unable to load shared library 'libwebkit2gtk-4.1.so.0'"); + + Assert.IsTrue(WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(ex)); + } + + [TestMethod] + public void missing_gtk3_dll_is_recognized_without_stack_trace() + { + var ex = new DllNotFoundException("Unable to load shared library 'libgtk-3.so.0'"); + + Assert.IsTrue(WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(ex)); + } + + [TestMethod] + public void gtk_initialization_failure_is_recognized_without_stack_trace() + { + var ex = new InvalidOperationException("Unable to initialize GTK"); + + Assert.IsTrue(WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(ex)); + } + + [TestMethod] + public void invalid_operation_from_avalonia_gtk_webview_stack_is_recognized() + { + Exception? caught = null; + try + { + FakeAvaloniaGtkStack.ThrowFromGtkNativeWebViewDialog(); + } + catch (Exception ex) + { + caught = ex; + } + + Assert.IsNotNull(caught); + Assert.IsTrue(WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(caught)); + var agg = new AggregateException(new Exception("outer"), caught); + Assert.IsTrue(WebView2LoginErrorMessage.TryFindInTree(agg, out var match)); + Assert.IsNotNull(match); + } + + [TestMethod] + public void unrelated_invalid_operation_is_not_treated_as_webview_failure() + { + var ex = new InvalidOperationException("Something else went wrong"); + + Assert.IsFalse(WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(ex)); + } + + private static class FakeAvaloniaGtkStack + { + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowFromGtkNativeWebViewDialog() + => throw new InvalidOperationException("Avalonia.X11 is not referenced"); + } +}