From 5c1153e1871cc572a116e94260fffc8d5f452d17 Mon Sep 17 00:00:00 2001 From: rmcrackan Date: Mon, 30 Mar 2026 14:22:09 -0400 Subject: [PATCH] better error messages when webview is the problem. shouldn't appear like a scan error --- .../Dialogs/Login/AvaloniaLoginChoiceEager.cs | 32 ++++---- .../ViewModels/MainVM.Import.cs | 22 ++++-- .../ViewModels/ProductsDisplayViewModel.cs | 22 ++++-- .../WebView2LoginErrorMessage.cs | 75 +++++++++++++++++++ .../Dialogs/Login/WebLoginDialog.cs | 62 +++++++++------ .../Dialogs/Login/WinformLoginChoiceEager.cs | 3 + Source/LibationWinForms/Form1.ScanManual.cs | 22 ++++-- 7 files changed, 185 insertions(+), 53 deletions(-) create mode 100644 Source/LibationUiBase/WebView2LoginErrorMessage.cs diff --git a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs index d4ff039f..f4c1b208 100644 --- a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs +++ b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs @@ -5,6 +5,7 @@ using Avalonia.Platform; using Avalonia.Threading; using Dinah.Core; using LibationFileManager; +using LibationUiBase; using LibationUiBase.Forms; using System; using System.Threading.Tasks; @@ -29,8 +30,22 @@ public class AvaloniaLoginChoiceEager : ILoginChoiceEager { try { - if (Configuration.Instance.UseWebView && await BrowserLoginAsync(choiceIn) is ChoiceOut external) - return external; + if (Configuration.Instance.UseWebView) + { + try + { + if (await BrowserLoginAsync(choiceIn) is ChoiceOut external) + return external; + } + catch (Exception ex) when (WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(ex)) + { + await MessageBox.ShowAdminAlert( + App.MainWindow, + WebView2LoginErrorMessage.ExplainerBody, + WebView2LoginErrorMessage.Caption, + ex); + } + } } catch (Exception ex) { @@ -44,19 +59,6 @@ public class AvaloniaLoginChoiceEager : ILoginChoiceEager } private async Task BrowserLoginAsync(ChoiceIn shoiceIn) - { - try - { - return await BrowserLoginAsyncCore(shoiceIn); - } - catch (Exception ex) - { - Serilog.Log.Logger.Warning(ex, "In-app browser failed; falling back to external browser"); - return null; - } - } - - private async Task BrowserLoginAsyncCore(ChoiceIn shoiceIn) { TaskCompletionSource tcs = new(); diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Import.cs b/Source/LibationAvalonia/ViewModels/MainVM.Import.cs index 1c69294c..d8f2af1f 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.Import.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.Import.cs @@ -3,6 +3,7 @@ using AudibleUtilities; using Avalonia.Controls; using Avalonia.Input; using LibationFileManager; +using LibationUiBase; using LibationUiBase.Forms; using ReactiveUI; using System; @@ -221,11 +222,22 @@ public partial class MainVM } catch (Exception ex) { - await MessageBox.ShowAdminAlert( - MainWindow, - "Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator", - "Error importing library", - ex); + if (WebView2LoginErrorMessage.TryFindInTree(ex, out var webViewEx) && webViewEx is not null) + { + await MessageBox.ShowAdminAlert( + MainWindow, + WebView2LoginErrorMessage.ExplainerBody, + WebView2LoginErrorMessage.Caption, + webViewEx); + } + else + { + await MessageBox.ShowAdminAlert( + MainWindow, + "Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator", + "Error importing library", + ex); + } } } diff --git a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs index 25fb1b3e..3a96a0c5 100644 --- a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs +++ b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs @@ -6,6 +6,7 @@ using Avalonia.Threading; using DataLayer; using Dinah.Core.Collections.Generic; using LibationFileManager; +using LibationUiBase; using LibationUiBase.Forms; using LibationUiBase.GridView; using ReactiveUI; @@ -434,11 +435,22 @@ public class ProductsDisplayViewModel : ViewModelBase } catch (Exception ex) { - await MessageBox.ShowAdminAlert( - null, - "Error scanning library. You may still manually select books to remove from Libation's library.", - "Error scanning library", - ex); + if (WebView2LoginErrorMessage.TryFindInTree(ex, out var webViewEx) && webViewEx is not null) + { + await MessageBox.ShowAdminAlert( + null, + WebView2LoginErrorMessage.ExplainerBody, + WebView2LoginErrorMessage.Caption, + webViewEx); + } + else + { + await MessageBox.ShowAdminAlert( + null, + "Error scanning library. You may still manually select books to remove from Libation's library.", + "Error scanning library", + ex); + } } } diff --git a/Source/LibationUiBase/WebView2LoginErrorMessage.cs b/Source/LibationUiBase/WebView2LoginErrorMessage.cs new file mode 100644 index 00000000..ce56ee5a --- /dev/null +++ b/Source/LibationUiBase/WebView2LoginErrorMessage.cs @@ -0,0 +1,75 @@ +using System; +using System.Runtime.InteropServices; + +namespace LibationUiBase; + +/// +/// User-facing copy and exception matching when embedded sign-in browser fails (often mistaken for a "library scan" bug). +/// Shared by WinForms and Avalonia; stack markers include WebView2 and Avalonia's NativeWebDialog. +/// +public static class WebView2LoginErrorMessage +{ + public const string Caption = "Sign-in browser could not start"; + + public static string ExplainerBody => + "Libation could not start the in-app sign-in browser. On Windows this uses Microsoft WebView2. " + + "This is a local sign-in or system setup issue — not a failure of the library scan itself.\r\n\r\n" + + "Things to try:\r\n" + + "• On Windows: install or repair the Microsoft Edge WebView2 Runtime from https://developer.microsoft.com/microsoft-edge/webview2/\r\n" + + "• In Libation Settings, try turning off the option to use the embedded browser and use external browser sign-in instead.\r\n" + + "• Check that security software is not blocking Libation or the embedded browser.\r\n" + + "• Ensure your account can write to local app data folders (permissions).\r\n" + + "• If you run as Administrator, try running Libation as a normal user (or the reverse).\r\n\r\n" + + "After sign-in works, use Import again to scan your library."; + + public static bool IsWebView2SignInInfrastructureFailure(Exception ex) + { + for (var e = ex; e is not null; e = e.InnerException) + { + if (!StackMentionsEmbeddedSignInBrowser(e)) + continue; + + if (e is UnauthorizedAccessException) + return true; + + if (e is COMException com + && (com.HResult == unchecked((int)0x8000FFFF) || com.HResult == unchecked((int)0x80070005))) + return true; + } + + return false; + } + + public static bool TryFindInTree(Exception ex, out Exception? match) + { + Exception? found = null; + walk(ex); + match = found; + return found is not null; + + void walk(Exception? e) + { + if (e is null || found is not null) + return; + if (IsWebView2SignInInfrastructureFailure(e)) + found = e; + if (found is not null) + return; + if (e is AggregateException agg) + { + foreach (var inner in agg.InnerExceptions) + walk(inner); + } + walk(e.InnerException); + } + } + + private static bool StackMentionsEmbeddedSignInBrowser(Exception e) + { + var stack = e.StackTrace; + return stack is not null + && (stack.Contains("WebView2", StringComparison.Ordinal) + || stack.Contains("CoreWebView2", StringComparison.Ordinal) + || stack.Contains("NativeWebDialog", StringComparison.Ordinal)); + } +} diff --git a/Source/LibationWinForms/Dialogs/Login/WebLoginDialog.cs b/Source/LibationWinForms/Dialogs/Login/WebLoginDialog.cs index a22c1b8d..ff7c65b3 100644 --- a/Source/LibationWinForms/Dialogs/Login/WebLoginDialog.cs +++ b/Source/LibationWinForms/Dialogs/Login/WebLoginDialog.cs @@ -1,8 +1,10 @@ using AudibleApi; using Dinah.Core; +using LibationUiBase; using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.WinForms; using System; +using System.Threading.Tasks; using System.Windows.Forms; namespace LibationWinForms.Login; @@ -30,34 +32,48 @@ public partial class WebLoginDialog : Form ArgumentValidator.EnsureNotNullOrWhiteSpace(choiceIn?.LoginUrl, nameof(choiceIn)); this.Load += async (_, _) => { - //enable private browsing - var env = await CoreWebView2Environment.CreateAsync(); - var options = env.CreateCoreWebView2ControllerOptions(); - options.IsInPrivateModeEnabled = true; - await webView.EnsureCoreWebView2Async(env, options); - - webView.CoreWebView2.Settings.UserAgent = Resources.User_Agent; - - //Load init cookies - foreach (System.Net.Cookie cookie in choiceIn.SignInCookies ?? []) + try { - if (string.IsNullOrEmpty(cookie.Value)) - continue; - try - { - webView.CoreWebView2.CookieManager.AddOrUpdateCookie(webView.CoreWebView2.CookieManager.CreateCookieWithSystemNetCookie(cookie)); - } - catch (Exception ex) - { - Serilog.Log.Logger.Error(ex, $"Failed to set cookie {cookie.Name} for domain {cookie.Domain}"); - } + await initWebViewAndNavigateAsync(choiceIn); + } + catch (Exception ex) when (WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(ex)) + { + MessageBoxLib.ShowAdminAlert(this, WebView2LoginErrorMessage.ExplainerBody, WebView2LoginErrorMessage.Caption, ex); + DialogResult = DialogResult.Cancel; + Close(); } - - webView.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded; - Invoke(() => webView.Source = new Uri(choiceIn.LoginUrl)); }; } + private async Task initWebViewAndNavigateAsync(ChoiceIn choiceIn) + { + // enable private browsing + var env = await CoreWebView2Environment.CreateAsync(); + var options = env.CreateCoreWebView2ControllerOptions(); + options.IsInPrivateModeEnabled = true; + await webView.EnsureCoreWebView2Async(env, options); + + webView.CoreWebView2.Settings.UserAgent = Resources.User_Agent; + + // Load init cookies + foreach (System.Net.Cookie cookie in choiceIn.SignInCookies ?? []) + { + if (string.IsNullOrEmpty(cookie.Value)) + continue; + try + { + webView.CoreWebView2.CookieManager.AddOrUpdateCookie(webView.CoreWebView2.CookieManager.CreateCookieWithSystemNetCookie(cookie)); + } + catch (Exception ex) + { + Serilog.Log.Logger.Error(ex, $"Failed to set cookie {cookie.Name} for domain {cookie.Domain}"); + } + } + + webView.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded; + Invoke(() => webView.Source = new Uri(choiceIn.LoginUrl)); + } + private void WebView_NavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs e) { if (e.Uri.Contains("/ap/maplanding") is true) diff --git a/Source/LibationWinForms/Dialogs/Login/WinformLoginChoiceEager.cs b/Source/LibationWinForms/Dialogs/Login/WinformLoginChoiceEager.cs index bf0207f7..6fbe4966 100644 --- a/Source/LibationWinForms/Dialogs/Login/WinformLoginChoiceEager.cs +++ b/Source/LibationWinForms/Dialogs/Login/WinformLoginChoiceEager.cs @@ -1,6 +1,7 @@ using AudibleApi; using AudibleUtilities; using LibationFileManager; +using LibationUiBase; using LibationWinForms.Dialogs.Login; using System; using System.Threading.Tasks; @@ -36,6 +37,8 @@ public class WinformLoginChoiceEager : ILoginChoiceEager catch (Exception ex) { Serilog.Log.Logger.Error(ex, $"Failed to run {nameof(WebLoginDialog)}"); + if (WebView2LoginErrorMessage.IsWebView2SignInInfrastructureFailure(ex)) + MessageBoxLib.ShowAdminAlert(Owner, WebView2LoginErrorMessage.ExplainerBody, WebView2LoginErrorMessage.Caption, ex); } } diff --git a/Source/LibationWinForms/Form1.ScanManual.cs b/Source/LibationWinForms/Form1.ScanManual.cs index a683654c..44e65a9d 100644 --- a/Source/LibationWinForms/Form1.ScanManual.cs +++ b/Source/LibationWinForms/Form1.ScanManual.cs @@ -1,6 +1,7 @@ using ApplicationServices; using AudibleUtilities; using LibationFileManager; +using LibationUiBase; using LibationWinForms.Dialogs; using System; using System.Collections.Generic; @@ -87,11 +88,22 @@ public partial class Form1 } catch (Exception ex) { - MessageBoxLib.ShowAdminAlert( - this, - "Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator", - "Error importing library", - ex); + if (WebView2LoginErrorMessage.TryFindInTree(ex, out var webViewEx) && webViewEx is not null) + { + MessageBoxLib.ShowAdminAlert( + this, + WebView2LoginErrorMessage.ExplainerBody, + WebView2LoginErrorMessage.Caption, + webViewEx); + } + else + { + MessageBoxLib.ShowAdminAlert( + this, + "Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator", + "Error importing library", + ex); + } } }