From 62c167c7266743722d26f0d578b5bc2a5e00a7d9 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 29 Jul 2025 16:13:21 -0500 Subject: [PATCH 1/3] fix(macos): close mwebd on app close --- lib/main.dart | 32 ++++++++ lib/services/mwebd_service.dart | 136 ++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index 3ffda4012..e05610a6d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -615,6 +615,38 @@ class _MaterialAppWithThemeState extends ConsumerState // } break; case AppLifecycleState.detached: + // Clean shutdown of mwebd service with aggressive timeout for macOS. + if (Platform.isMacOS) { + // On macOS, use very short timeout since native StopServer is skipped. + try { + debugPrint("App detached on macOS, quick shutdown..."); + await ref.read(pMwebService).shutdown().timeout( + const Duration(seconds: 2), + onTimeout: () { + debugPrint("MwebdService shutdown timed out after 2 seconds on macOS"); + exit(0); + }, + ); + debugPrint("MwebdService shutdown completed on macOS"); + } catch (e, s) { + debugPrint("Error during MwebdService shutdown on macOS: $e\n$s"); + exit(0); + } + } else { + // Non-macOS platforms can use longer timeout. + try { + debugPrint("App detached, shutting down MwebdService..."); + await ref.read(pMwebService).shutdown().timeout( + const Duration(seconds: 5), + onTimeout: () { + debugPrint("MwebdService shutdown timed out after 5 seconds"); + }, + ); + debugPrint("MwebdService shutdown completed successfully"); + } catch (e, s) { + debugPrint("Error during MwebdService shutdown: $e\n$s"); + } + } break; case AppLifecycleState.hidden: break; diff --git a/lib/services/mwebd_service.dart b/lib/services/mwebd_service.dart index 89f2bee37..2d6e8604b 100644 --- a/lib/services/mwebd_service.dart +++ b/lib/services/mwebd_service.dart @@ -34,6 +34,9 @@ final class MwebdService { final Mutex _torConnectingLock = Mutex(); + // Track active log stream controllers for cleanup during shutdown. + final Set> _activeLogControllers = {}; + static final instance = MwebdService._(); MwebdService._() { @@ -204,6 +207,9 @@ final class MwebdService { String leftover = ''; Timer? timer; + // Track this controller for cleanup during shutdown. + _activeLogControllers.add(controller); + final path = "${(await StackFileSystem.applicationMwebdDirectory(net == CryptoCurrencyNetwork.main ? "mainnet" : "testnet")).path}" "${Platform.pathSeparator}logs" @@ -237,11 +243,141 @@ final class MwebdService { controller.onCancel = () { timer?.cancel(); + _activeLogControllers.remove(controller); controller.close(); }; return controller.stream; } + + /// Shutdown all mwebd servers and clean up resources. + /// + /// This method should be called when the app is terminating to prevent hanging. + Future shutdown() async { + final stopwatch = Stopwatch()..start(); + Logging.instance.i("MwebdService shutdown() started"); + + await _updateLock.protect(() async { + // Cancel stream subscriptions to prevent further events. + try { + await _torStatusListener.cancel(); + Logging.instance.i("Canceled tor status listener"); + } catch (e, s) { + Logging.instance.w( + "Error canceling tor status listener", + error: e, + stackTrace: s, + ); + } + + try { + await _torPreferenceListener.cancel(); + Logging.instance.i("Canceled tor preference listener"); + } catch (e, s) { + Logging.instance.w( + "Error canceling tor preference listener", + error: e, + stackTrace: s, + ); + } + + // Cancel all active log stream controllers and their timers. + final logControllers = List.from(_activeLogControllers); + for (final controller in logControllers) { + try { + await controller.close(); + Logging.instance.i("Closed log stream controller"); + } catch (e, s) { + Logging.instance.w( + "Error closing log stream controller", + error: e, + stackTrace: s, + ); + } + } + _activeLogControllers.clear(); + + // Stop all servers and clean up clients with timeout protection. + final stopFutures = []; + for (final entry in _map.values) { + stopFutures.add(_shutdownServerSafely(entry)); + } + + // Wait for all shutdowns with overall timeout. + try { + await Future.wait(stopFutures).timeout( + const Duration(seconds: 10), + onTimeout: () { + Logging.instance.w("Timeout waiting for mwebd servers to stop"); + return []; // Return a dummy list. + }, + ); + } catch (e, s) { + Logging.instance.w( + "Error during mwebd servers shutdown", + error: e, + stackTrace: s, + ); + } + + _map.clear(); + + final elapsedMs = stopwatch.elapsedMilliseconds; + Logging.instance.i("MwebdService shutdown() completed in ${elapsedMs}ms"); + + // Warn if shutdown took too long (could indicate hanging). + if (elapsedMs > 3000) { + Logging.instance.w("MwebdService shutdown took ${elapsedMs}ms - longer than expected"); + } + }); + } + + /// Safely shutdown a server/client pair with timeout protection. + Future _shutdownServerSafely( + ({MwebdServer server, MwebClient client}) entry, + ) async { + final serverStopwatch = Stopwatch()..start(); + Logging.instance.i("Starting shutdown of mwebd server/client pair"); + + try { + // Clean up client first. + final clientStopwatch = Stopwatch()..start(); + await entry.client.cleanup().timeout( + const Duration(seconds: 3), + onTimeout: () { + Logging.instance.w("Timeout cleaning up mweb client after 3s"); + }, + ); + Logging.instance.i("Client cleanup completed in ${clientStopwatch.elapsedMilliseconds}ms"); + } catch (e, s) { + Logging.instance.w( + "Error cleaning up mweb client", + error: e, + stackTrace: s, + ); + } + + try { + // Stop server with timeout protection. + final serverShutdownStopwatch = Stopwatch()..start(); + await entry.server.stopServer().timeout( + const Duration(seconds: 5), + onTimeout: () { + Logging.instance.w("Timeout stopping mwebd server after 5s"); + }, + ); + Logging.instance.i("Server stop completed in ${serverShutdownStopwatch.elapsedMilliseconds}ms"); + } catch (e, s) { + Logging.instance.w( + "Error stopping mwebd server", + error: e, + stackTrace: s, + ); + } + + final totalMs = serverStopwatch.elapsedMilliseconds; + Logging.instance.i("Server/client pair shutdown completed in ${totalMs}ms"); + } } // ============================================================================ From e7737d60beb3559a71f2114f1ea9ed2ba45172b5 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 30 Jul 2025 15:13:02 -0500 Subject: [PATCH 2/3] Revert "fix(macos): close mwebd on app close" This reverts commit 62c167c7266743722d26f0d578b5bc2a5e00a7d9. --- lib/main.dart | 32 -------- lib/services/mwebd_service.dart | 136 -------------------------------- 2 files changed, 168 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index e05610a6d..3ffda4012 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -615,38 +615,6 @@ class _MaterialAppWithThemeState extends ConsumerState // } break; case AppLifecycleState.detached: - // Clean shutdown of mwebd service with aggressive timeout for macOS. - if (Platform.isMacOS) { - // On macOS, use very short timeout since native StopServer is skipped. - try { - debugPrint("App detached on macOS, quick shutdown..."); - await ref.read(pMwebService).shutdown().timeout( - const Duration(seconds: 2), - onTimeout: () { - debugPrint("MwebdService shutdown timed out after 2 seconds on macOS"); - exit(0); - }, - ); - debugPrint("MwebdService shutdown completed on macOS"); - } catch (e, s) { - debugPrint("Error during MwebdService shutdown on macOS: $e\n$s"); - exit(0); - } - } else { - // Non-macOS platforms can use longer timeout. - try { - debugPrint("App detached, shutting down MwebdService..."); - await ref.read(pMwebService).shutdown().timeout( - const Duration(seconds: 5), - onTimeout: () { - debugPrint("MwebdService shutdown timed out after 5 seconds"); - }, - ); - debugPrint("MwebdService shutdown completed successfully"); - } catch (e, s) { - debugPrint("Error during MwebdService shutdown: $e\n$s"); - } - } break; case AppLifecycleState.hidden: break; diff --git a/lib/services/mwebd_service.dart b/lib/services/mwebd_service.dart index 2d6e8604b..89f2bee37 100644 --- a/lib/services/mwebd_service.dart +++ b/lib/services/mwebd_service.dart @@ -34,9 +34,6 @@ final class MwebdService { final Mutex _torConnectingLock = Mutex(); - // Track active log stream controllers for cleanup during shutdown. - final Set> _activeLogControllers = {}; - static final instance = MwebdService._(); MwebdService._() { @@ -207,9 +204,6 @@ final class MwebdService { String leftover = ''; Timer? timer; - // Track this controller for cleanup during shutdown. - _activeLogControllers.add(controller); - final path = "${(await StackFileSystem.applicationMwebdDirectory(net == CryptoCurrencyNetwork.main ? "mainnet" : "testnet")).path}" "${Platform.pathSeparator}logs" @@ -243,141 +237,11 @@ final class MwebdService { controller.onCancel = () { timer?.cancel(); - _activeLogControllers.remove(controller); controller.close(); }; return controller.stream; } - - /// Shutdown all mwebd servers and clean up resources. - /// - /// This method should be called when the app is terminating to prevent hanging. - Future shutdown() async { - final stopwatch = Stopwatch()..start(); - Logging.instance.i("MwebdService shutdown() started"); - - await _updateLock.protect(() async { - // Cancel stream subscriptions to prevent further events. - try { - await _torStatusListener.cancel(); - Logging.instance.i("Canceled tor status listener"); - } catch (e, s) { - Logging.instance.w( - "Error canceling tor status listener", - error: e, - stackTrace: s, - ); - } - - try { - await _torPreferenceListener.cancel(); - Logging.instance.i("Canceled tor preference listener"); - } catch (e, s) { - Logging.instance.w( - "Error canceling tor preference listener", - error: e, - stackTrace: s, - ); - } - - // Cancel all active log stream controllers and their timers. - final logControllers = List.from(_activeLogControllers); - for (final controller in logControllers) { - try { - await controller.close(); - Logging.instance.i("Closed log stream controller"); - } catch (e, s) { - Logging.instance.w( - "Error closing log stream controller", - error: e, - stackTrace: s, - ); - } - } - _activeLogControllers.clear(); - - // Stop all servers and clean up clients with timeout protection. - final stopFutures = []; - for (final entry in _map.values) { - stopFutures.add(_shutdownServerSafely(entry)); - } - - // Wait for all shutdowns with overall timeout. - try { - await Future.wait(stopFutures).timeout( - const Duration(seconds: 10), - onTimeout: () { - Logging.instance.w("Timeout waiting for mwebd servers to stop"); - return []; // Return a dummy list. - }, - ); - } catch (e, s) { - Logging.instance.w( - "Error during mwebd servers shutdown", - error: e, - stackTrace: s, - ); - } - - _map.clear(); - - final elapsedMs = stopwatch.elapsedMilliseconds; - Logging.instance.i("MwebdService shutdown() completed in ${elapsedMs}ms"); - - // Warn if shutdown took too long (could indicate hanging). - if (elapsedMs > 3000) { - Logging.instance.w("MwebdService shutdown took ${elapsedMs}ms - longer than expected"); - } - }); - } - - /// Safely shutdown a server/client pair with timeout protection. - Future _shutdownServerSafely( - ({MwebdServer server, MwebClient client}) entry, - ) async { - final serverStopwatch = Stopwatch()..start(); - Logging.instance.i("Starting shutdown of mwebd server/client pair"); - - try { - // Clean up client first. - final clientStopwatch = Stopwatch()..start(); - await entry.client.cleanup().timeout( - const Duration(seconds: 3), - onTimeout: () { - Logging.instance.w("Timeout cleaning up mweb client after 3s"); - }, - ); - Logging.instance.i("Client cleanup completed in ${clientStopwatch.elapsedMilliseconds}ms"); - } catch (e, s) { - Logging.instance.w( - "Error cleaning up mweb client", - error: e, - stackTrace: s, - ); - } - - try { - // Stop server with timeout protection. - final serverShutdownStopwatch = Stopwatch()..start(); - await entry.server.stopServer().timeout( - const Duration(seconds: 5), - onTimeout: () { - Logging.instance.w("Timeout stopping mwebd server after 5s"); - }, - ); - Logging.instance.i("Server stop completed in ${serverShutdownStopwatch.elapsedMilliseconds}ms"); - } catch (e, s) { - Logging.instance.w( - "Error stopping mwebd server", - error: e, - stackTrace: s, - ); - } - - final totalMs = serverStopwatch.elapsedMilliseconds; - Logging.instance.i("Server/client pair shutdown completed in ${totalMs}ms"); - } } // ============================================================================ From 99f66cc1f982c78ceb4dd3a14ce33d910755bad8 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 30 Jul 2025 15:15:01 -0500 Subject: [PATCH 3/3] fix(macos): shutdown mwebd and force close app on app close request --- lib/main.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index 3ffda4012..0eb4c224c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'dart:ui'; import 'package:coinlib_flutter/coinlib_flutter.dart'; import 'package:compat/compat.dart' as lib_monero_compat; @@ -621,6 +622,21 @@ class _MaterialAppWithThemeState extends ConsumerState } } + @override + Future didRequestAppExit() async { + debugPrint("didRequestAppExit called"); + if (Platform.isMacOS) { + // On macOS, mwebd fails to shut down, hanging the app on close. + // + // Exiting is a hack fix for this issue. + + // await ref.read(pMwebService).shutdown(); + // Something like the above would probably be prudent to make. + exit(0); + } + return AppExitResponse.exit; + } + /// should only be called on android currently Future getOpenFile() async { // update provider with new file content state