fix: add timeout diagnostics

This commit is contained in:
edde746
2026-03-15 05:06:16 +01:00
parent 2427a55901
commit 79888168ae
7 changed files with 28 additions and 3 deletions
+4
View File
@@ -501,6 +501,7 @@ class _SetupScreenState extends State<SetupScreen> {
// Check network connectivity early to fast-path airplane mode.
// Timeout guards against connectivity_plus hanging on some Android TV devices after force-close.
bool hasNetwork;
Sentry.addBreadcrumb(Breadcrumb(message: 'Checking network connectivity', category: 'setup'));
try {
final connectivityResult = await Connectivity().checkConnectivity().timeout(
const Duration(seconds: 3),
@@ -512,6 +513,8 @@ class _SetupScreenState extends State<SetupScreen> {
hasNetwork = true;
}
Sentry.addBreadcrumb(Breadcrumb(message: 'Network check done: hasNetwork=$hasNetwork', category: 'setup'));
if (hasNetwork) {
_setStatus(t.common.refreshingServers);
@@ -551,6 +554,7 @@ class _SetupScreenState extends State<SetupScreen> {
return;
}
Sentry.addBreadcrumb(Breadcrumb(message: 'Connecting to ${servers.length} server(s)', category: 'setup'));
_setStatus(t.common.connectingToServers);
// Populate per-server status for splash display
@@ -4,6 +4,7 @@ import 'dart:io';
import 'dart:math';
import 'package:web_socket_channel/io.dart';
import '../../utils/future_extensions.dart';
import '../../models/companion_remote/remote_command.dart';
import '../../models/companion_remote/remote_session.dart';
@@ -506,7 +507,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
}
try {
final winner = await completer.future.timeout(const Duration(seconds: 10));
final winner = await completer.future.namedTimeout(const Duration(seconds: 10), operation: 'CompanionRemote race connect');
cleanup();
// Now set up the proper managed connection on the winning address
@@ -20,6 +20,7 @@ import '../utils/app_logger.dart';
import '../utils/codec_utils.dart';
import '../utils/global_key_utils.dart';
import '../utils/plex_cache_parser.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
/// Context for a download that's been enqueued with background_downloader.
/// Carries metadata needed between enqueue and completion callback.
@@ -158,9 +159,11 @@ class DownloadManagerService {
/// then scans drift for orphaned items.
Future<void> recoverInterruptedDownloads() async {
try {
Sentry.addBreadcrumb(Breadcrumb(message: 'Initializing FileDownloader', category: 'downloads'));
await _initializeFileDownloader();
// Let background_downloader re-enqueue tasks killed by the OS
Sentry.addBreadcrumb(Breadcrumb(message: 'Rescheduling killed tasks', category: 'downloads'));
final (rescheduled, _) = await FileDownloader().rescheduleKilledTasks();
if (rescheduled.isNotEmpty) {
appLogger.i('Rescheduled ${rescheduled.length} killed download task(s)');
@@ -206,6 +209,7 @@ class DownloadManagerService {
}
// Scan drift for orphaned items stuck in 'downloading'
Sentry.addBreadcrumb(Breadcrumb(message: 'Scanning for orphaned downloads', category: 'downloads'));
final allDownloads = await _database.select(_database.downloadedMedia).get();
for (final item in allDownloads) {
if (item.status == DownloadStatus.downloading.index) {
+3
View File
@@ -6,6 +6,7 @@ import 'plex_client.dart';
import '../models/plex_config.dart';
import '../utils/app_logger.dart';
import '../utils/connection_constants.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'plex_auth_service.dart';
import 'storage_service.dart';
@@ -183,6 +184,7 @@ class MultiServerManager {
}
appLogger.i('Connecting to ${servers.length} servers...');
Sentry.addBreadcrumb(Breadcrumb(message: 'Connecting to ${servers.length} server(s)', category: 'servers'));
// Use provided client ID or generate a unique one for this app instance
final effectiveClientId = clientIdentifier ?? DateTime.now().millisecondsSinceEpoch.toString();
@@ -486,6 +488,7 @@ class MultiServerManager {
if (offline.isEmpty) return;
appLogger.d('Attempting reconnection for ${offline.length} offline servers');
Sentry.addBreadcrumb(Breadcrumb(message: 'Reconnecting ${offline.length} offline server(s)', category: 'servers'));
final futures = offline.map((serverId) {
final server = _servers[serverId];
+11
View File
@@ -0,0 +1,11 @@
import 'dart:async';
extension NamedTimeoutExtension<T> on Future<T> {
/// Like [Future.timeout], but the [TimeoutException] includes [operation]
/// so crash reports identify which call timed out.
Future<T> namedTimeout(Duration timeLimit, {required String operation}) {
return timeout(timeLimit, onTimeout: () {
throw TimeoutException('$operation timed out', timeLimit);
});
}
}
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import '../../utils/future_extensions.dart';
import 'package:flutter/services.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
@@ -91,7 +92,7 @@ class _NotInSessionViewState extends State<_NotInSessionView> {
final client = HttpClient();
client.connectionTimeout = const Duration(seconds: 5);
final request = await client.getUrl(Uri.parse(WatchTogetherPeerService.healthUrl));
final response = await request.close().timeout(const Duration(seconds: 5));
final response = await request.close().namedTimeout(const Duration(seconds: 5), operation: 'WatchTogether health check');
final body = await response.transform(const SystemEncoding().decoder).join();
client.close();
if (!mounted) return;
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:uuid/uuid.dart';
import '../../utils/future_extensions.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../../services/base_peer_service.dart';
@@ -280,7 +281,7 @@ class WatchTogetherPeerService with KeepaliveMixin {
_sendRaw({'type': 'join', 'sessionId': _sessionId, 'peerId': _myPeerId});
}
await completer.future.timeout(const Duration(seconds: 10));
await completer.future.namedTimeout(const Duration(seconds: 10), operation: 'WatchTogether reconnect');
appLogger.d('WatchTogether: Reconnected successfully');
} catch (e) {
appLogger.e('WatchTogether: Reconnect failed', error: e);