From 43ee5b09f7f544a2364f050447f8a62bf7ab3b5d Mon Sep 17 00:00:00 2001 From: asuc Date: Mon, 22 Jun 2026 01:04:38 +0200 Subject: [PATCH] fix: prevent crash on Linux without NetworkManager from connectivity_plus DBus FD leak On Linux systems without NetworkManager, connectivity_plus throws unhandled DBus exceptions on every checkConnectivity() call (called after each API request), leaking file descriptors until the app crashes with "Too many open files". Wrapped connectivity checks in try-catch with a 5s debounce and cancel the stream subscription on dispose. --- lib/providers/connectivity_provider.dart | 72 +++++++++++++++--------- lib/services/notification_service.dart | 8 ++- 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/lib/providers/connectivity_provider.dart b/lib/providers/connectivity_provider.dart index 2c6f1b969..f53061cb9 100644 --- a/lib/providers/connectivity_provider.dart +++ b/lib/providers/connectivity_provider.dart @@ -31,13 +31,19 @@ enum ConnectionState { @Riverpod(keepAlive: true) class ConnectivityStatus extends _$ConnectivityStatus { String? localUrl; + DateTime? _lastCheck; + static const Duration _minCheckInterval = Duration(seconds: 5); @override ConnectionState build() { ref.listen(userProvider, (previous, next) { checkLocalUrl(previous, next); }); - Connectivity().onConnectivityChanged.listen(onStateChange); + final sub = Connectivity().onConnectivityChanged.listen( + onStateChange, + onError: (error) => log('Connectivity stream error: $error'), + ); + ref.onDispose(sub.cancel); checkConnectivity(); return ConnectionState.mobile; } @@ -50,35 +56,49 @@ class ConnectivityStatus extends _$ConnectivityStatus { } Future onStateChange(List connectivityResult) async { - if (connectivityResult.contains(ConnectivityResult.ethernet)) { - state = ConnectionState.ethernet; - } else if (connectivityResult.contains(ConnectivityResult.wifi)) { - state = ConnectionState.wifi; - } else if (connectivityResult.contains(ConnectivityResult.mobile)) { - state = ConnectionState.mobile; - } else if (connectivityResult.contains(ConnectivityResult.none)) { - state = ConnectionState.offline; + try { + if (connectivityResult.contains(ConnectivityResult.ethernet)) { + state = ConnectionState.ethernet; + } else if (connectivityResult.contains(ConnectivityResult.wifi)) { + state = ConnectionState.wifi; + } else if (connectivityResult.contains(ConnectivityResult.mobile)) { + state = ConnectionState.mobile; + } else if (connectivityResult.contains(ConnectivityResult.none)) { + state = ConnectionState.offline; + } + final newUrl = ref.read(userProvider.select((value) => value?.credentials.localUrl)); + if (localUrl == newUrl) return; + localUrl = newUrl; + final localConnection = localUrl != null && localUrl?.isNotEmpty == true + ? await fetchSystemInfoDynamic(normalizeUrl(localUrl!)) + : null; + final correctServerResponse = + localConnection?.id == ref.read(userProvider.select((value) => value?.credentials.serverId)); + ref.read(localConnectionAvailableProvider.notifier).update((state) => correctServerResponse); + } catch (e) { + log('Error handling connectivity state change: $e'); } - final newUrl = ref.read(userProvider.select((value) => value?.credentials.localUrl)); - if (localUrl == newUrl) return; - localUrl = newUrl; - final localConnection = - localUrl != null && localUrl?.isNotEmpty == true ? await fetchSystemInfoDynamic(normalizeUrl(localUrl!)) : null; - final correctServerResponse = - localConnection?.id == ref.read(userProvider.select((value) => value?.credentials.serverId)); - ref.read(localConnectionAvailableProvider.notifier).update((state) => correctServerResponse); } Future checkConnectivity() async { - final connectivityResult = await Connectivity().checkConnectivity(); - final serverUrl = ref.read(serverUrlProvider); - final checkServer = await probeJellyfinUrl( - serverUrl ?? "", - ); - if (checkServer != null) { - onStateChange(connectivityResult); - } else { - onStateChange([ConnectivityResult.none]); + final now = DateTime.now(); + if (_lastCheck != null && now.difference(_lastCheck!) < _minCheckInterval) { + return; + } + _lastCheck = now; + try { + final connectivityResult = await Connectivity().checkConnectivity(); + final serverUrl = ref.read(serverUrlProvider); + final checkServer = await probeJellyfinUrl( + serverUrl ?? "", + ); + if (checkServer != null) { + onStateChange(connectivityResult); + } else { + onStateChange([ConnectivityResult.none]); + } + } catch (e) { + log('Failed to check connectivity: $e'); } } diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 7e256d7c0..be824cfca 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -64,8 +64,12 @@ class NotificationService { } static Future getInitialNotificationPayload() async { - final details = await _plugin.getNotificationAppLaunchDetails(); - return details?.notificationResponse?.payload; + try { + final details = await _plugin.getNotificationAppLaunchDetails(); + return details?.notificationResponse?.payload; + } catch (e) { + return null; + } } static Future requestPermission() async {