From 835424ed3f4b3bf3f4b38170dea08cf7404041b0 Mon Sep 17 00:00:00 2001 From: suportewmit-cmyk Date: Wed, 10 Jun 2026 21:48:13 -0300 Subject: [PATCH] Fix SherpaOnnx finalizer crash with DllNotFoundException Suppress fatal crashes caused by SherpaOnnx.OfflineTts finalizer calling SherpaOnnxDestroyOfflineTts when native DLL is unavailable. Changes: - PiperTextToSpeechClient: gracefully handle DllNotFoundException in constructor (TTS disabled gracefully), add GC.SuppressFinalize after dispose to prevent finalizer from calling native code during GC - App.xaml.cs: improved OnDomainUnhandledException logging for SherpaOnnx native DLL errors - Crash prevention relies on exe.config legacyUnhandledExceptionPolicy and GC.SuppressFinalize (IsTerminating is readonly in .NET 10) Fixes repeated crashes: System.DllNotFoundException: Unable to load DLL 'sherpa-onnx-c-api' at SherpaOnnx.OfflineTts.Finalize() --- src/OpenClaw.Tray.WinUI/App.xaml.cs | 49 ++++++++++++------- .../TextToSpeech/PiperTextToSpeechClient.cs | 34 +++++++++++-- 2 files changed, 59 insertions(+), 24 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs index 37ddf70a..e56300d9 100644 --- a/src/OpenClaw.Tray.WinUI/App.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs @@ -142,7 +142,7 @@ public IntPtr GetHubWindowHandle() private GatewayService? _gatewayService; private CancellationTokenSource? _deepLinkCts; private bool _isExiting; - + /// /// Cached connection status — sole writer is OnManagerStateChanged. /// Reads are safe from any thread; derives from the connection manager's state machine. @@ -174,12 +174,12 @@ public IntPtr GetHubWindowHandle() private DiagnosticsClipboardService? _diagnosticsClipboard; private ToastService? _toastService; - + // Node service (optional, enabled in settings) private NodeService? _nodeService; // MCP-only app capability — local testing/control, not exposed to gateway private AppCapability? _appCapability; - + // Keep-alive window to anchor WinUI runtime (prevents GC/threading issues) private Window? _keepAliveWindow; private SetupWindow? _setupWindow; @@ -233,10 +233,10 @@ public App() GatewayHostAccessLocalization.Format = (key, args) => LocalizationHelper.Format(key, args); InitializeComponent(); - + s_runMarker.Check(); s_runMarker.MarkStarted(); - + // Hook up crash handlers this.UnhandledException += OnUnhandledException; AppDomain.CurrentDomain.UnhandledException += OnDomainUnhandledException; @@ -294,7 +294,18 @@ private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExce private void OnDomainUnhandledException(object sender, System.UnhandledExceptionEventArgs e) { - _crashLogger.Log("DomainUnhandledException", e.ExceptionObject as Exception); + var ex = e.ExceptionObject as Exception; + _crashLogger.Log("DomainUnhandledException", ex); + + // Log SherpaOnnx finalizer errors clearly so they are visible in diagnostics. + // The actual crash prevention is handled by the exe.config + // legacyUnhandledExceptionPolicy setting and the GC.SuppressFinalize + // call in PiperTextToSpeechClient.Dispose(). + if (ex is DllNotFoundException dllEx && + dllEx.Message.Contains("sherpa-onnx", StringComparison.OrdinalIgnoreCase)) + { + Logger.Warn($"SherpaOnnx native DLL unavailable: {dllEx.Message}"); + } } private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) @@ -302,7 +313,7 @@ private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEv _crashLogger.Log("UnobservedTaskException", e.Exception); e.SetObserved(); // Prevent crash } - + private void OnProcessExit(object? sender, EventArgs e) { s_runMarker.MarkEnded(); @@ -679,7 +690,7 @@ private void InitializeKeepAliveWindow() _keepAliveWindow = new Window(); _keepAliveWindow.Content = new Microsoft.UI.Xaml.Controls.Grid(); _keepAliveWindow.AppWindow.IsShownInSwitchers = false; - + // Move off-screen and set minimal size _keepAliveWindow.AppWindow.MoveAndResize(new global::Windows.Graphics.RectInt32(-32000, -32000, 1, 1)); } @@ -688,10 +699,10 @@ private void InitializeTrayIcon() { // Initialize keep-alive window first to anchor WinUI runtime InitializeKeepAliveWindow(); - + // Pre-create tray menu window at startup to avoid creation crashes later InitializeTrayMenuWindow(); - + var iconPath = IconHelper.GetStatusIconPath(ConnectionStatus.Disconnected); _trayIcon = new TrayIcon(1, iconPath, BuildTrayTooltip()); _trayIcon.IsVisible = true; @@ -1002,15 +1013,15 @@ private void OnTrayMenuItemClicked(object? sender, string action) break; } } - + private void CopyDeviceIdToClipboard() { if (_nodeService?.FullDeviceId == null) return; - + try { CopyTextToClipboard(_nodeService.FullDeviceId); - + // Show toast confirming copy _toastService!.ShowToast(new ToastContentBuilder() .AddText(LocalizationHelper.GetString("Toast_DeviceIdCopied")) @@ -2208,7 +2219,7 @@ private void OnNodeStatusChanged(object? sender, ConnectionStatus status) { Logger.Info($"Node status: {status}"); AddRecentActivity($"Node mode {status}", category: "node", dashboardPath: "nodes"); - + // In node-only mode, surface node connection in main status indicator if (_settings?.EnableNodeMode == true) { @@ -2216,7 +2227,7 @@ private void OnNodeStatusChanged(object? sender, ConnectionStatus status) UpdateTrayIcon(); OnUiThread(UpdateStatusDetailWindow); } - + // Don't show "connected" toast if waiting for pairing - we'll show pairing status instead var nodeService = _nodeService; if (status == ConnectionStatus.Connected && nodeService?.IsPaired == true) @@ -2246,7 +2257,7 @@ private void OnNodeStatusChanged(object? sender, ConnectionStatus status) private void OnPairingStatusChanged(object? sender, OpenClaw.Shared.PairingStatusEventArgs args) { Logger.Info($"Pairing status: {args.Status}"); - + try { if (args.Status == OpenClaw.Shared.PairingStatus.Pending) @@ -2330,7 +2341,7 @@ public void ShowPairingPendingNotification(string deviceId, string? approvalComm "node-pairing-pending", deviceId); } - + private void OnNodeNotificationRequested(object? sender, OpenClaw.Shared.Capabilities.SystemNotifyArgs args) { AddRecentActivity(args.Title, category: "node", dashboardPath: "nodes", details: args.Body); @@ -3368,7 +3379,7 @@ private async Task ToggleChannelAsync(string channelName) await client.StartChannelAsync(channelName); AddRecentActivity($"Started channel: {channelName}", category: "channel", dashboardPath: "settings"); } - + // Refresh health await RunHealthCheckAsync(); } @@ -3480,7 +3491,7 @@ private void StartDeepLinkServer() { _deepLinkCts = new CancellationTokenSource(); var token = _deepLinkCts.Token; - + Task.Run(async () => { while (!token.IsCancellationRequested) diff --git a/src/OpenClaw.Tray.WinUI/Services/TextToSpeech/PiperTextToSpeechClient.cs b/src/OpenClaw.Tray.WinUI/Services/TextToSpeech/PiperTextToSpeechClient.cs index 2d595ba6..0022172b 100644 --- a/src/OpenClaw.Tray.WinUI/Services/TextToSpeech/PiperTextToSpeechClient.cs +++ b/src/OpenClaw.Tray.WinUI/Services/TextToSpeech/PiperTextToSpeechClient.cs @@ -25,12 +25,13 @@ public sealed class PiperTextToSpeechClient : IDisposable { private readonly IOpenClawLogger _logger; private readonly string _voiceId; - private readonly OfflineTts _tts; + private OfflineTts? _tts; private readonly SemaphoreSlim _gate = new(1, 1); private bool _disposed; + private bool _ttsAvailable; public string VoiceId => _voiceId; - public int SampleRate => _tts.SampleRate; + public int SampleRate => _tts?.SampleRate ?? 22050; public PiperTextToSpeechClient(IOpenClawLogger logger, PiperVoiceManager voices, string voiceId) { @@ -53,8 +54,18 @@ public PiperTextToSpeechClient(IOpenClawLogger logger, PiperVoiceManager voices, config.Model.Debug = 0; config.MaxNumSentences = 2; - _tts = new OfflineTts(config); - _logger.Info($"Piper voice '{_voiceId}' loaded (sample rate {_tts.SampleRate} Hz, {config.Model.NumThreads} threads)"); + try + { + _tts = new OfflineTts(config); + _ttsAvailable = true; + _logger.Info($"Piper voice '{_voiceId}' loaded (sample rate {_tts.SampleRate} Hz, {config.Model.NumThreads} threads)"); + } + catch (DllNotFoundException ex) + { + _logger.Warn($"Piper voice '{_voiceId}' unavailable: sherpa-onnx native library could not be loaded. TTS will be disabled. ({ex.Message})"); + _tts = null; + _ttsAvailable = false; + } } /// @@ -65,6 +76,8 @@ public async Task GenerateWavAsync(string text, float speed = 1.0f, Canc { if (_disposed) throw new ObjectDisposedException(nameof(PiperTextToSpeechClient)); if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("text must be non-empty", nameof(text)); + if (!_ttsAvailable || _tts == null) + throw new InvalidOperationException("Piper TTS is not available: sherpa-onnx native library could not be loaded."); await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); try @@ -131,7 +144,18 @@ public void Dispose() if (_disposed) return; _disposed = true; // slopwatch-ignore: SW003 Cleanup is best-effort; failure cannot improve caller state and the original outcome is preserved. - try { _tts.Dispose(); } catch { /* swallow */ } + try + { + if (_tts != null) + { + _tts.Dispose(); + // CRITICAL: Suppress the finalizer to prevent SherpaOnnxDestroyOfflineTts + // from being called during GC, which crashes the app when the native DLL + // is unavailable (DllNotFoundException in finalizer = instant crash). + GC.SuppressFinalize(_tts); + } + } + catch { /* swallow */ } // slopwatch-ignore: SW003 Cleanup is best-effort; failure cannot improve caller state and the original outcome is preserved. try { _gate.Dispose(); } catch { /* swallow */ } }