From cc31af4ee7af5776f57cff87cd3c782ce443a8c3 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Thu, 5 Feb 2026 13:23:43 -0300 Subject: [PATCH] Improve watchdog for stalled downloads --- .../package_manager/install_operation.dart | 15 ++- .../package_manager/package_manager.dart | 97 ++++++++++++++++--- 2 files changed, 94 insertions(+), 18 deletions(-) diff --git a/lib/services/package_manager/install_operation.dart b/lib/services/package_manager/install_operation.dart index 5e09527..5cbfc19 100644 --- a/lib/services/package_manager/install_operation.dart +++ b/lib/services/package_manager/install_operation.dart @@ -7,7 +7,7 @@ const staleOperationThreshold = Duration(days: 7); const batchQueueDelayMs = 50; /// Dart-side watchdog timeout (fallback if native events stop arriving) -const watchdogTimeout = Duration(minutes: 5); +const watchdogTimeout = Duration(minutes: 1); /// How often the watchdog timer checks for stale operations const watchdogCheckInterval = Duration(seconds: 30); @@ -40,18 +40,21 @@ class DownloadQueued extends InstallOperation { class Downloading extends InstallOperation { final double progress; final String taskId; + final DateTime startedAt; - const Downloading({ + Downloading({ required super.target, required this.progress, required this.taskId, - }); + DateTime? startedAt, + }) : startedAt = startedAt ?? DateTime.now(); Downloading copyWith({double? progress}) { return Downloading( target: target, progress: progress ?? this.progress, taskId: taskId, + startedAt: startedAt, ); } } @@ -242,7 +245,10 @@ extension InstallOperationX on InstallOperation { /// Whether this operation needs watchdog monitoring (waiting for native events) bool get needsWatchdog => - this is Verifying || this is Installing || this is SystemProcessing; + this is Downloading || + this is Verifying || + this is Installing || + this is SystemProcessing; /// Whether this operation is in the verification phase bool get isVerifying => this is Verifying; @@ -255,6 +261,7 @@ extension InstallOperationX on InstallOperation { /// Get start time for watchdog-monitored operations DateTime? get startedAt => switch (this) { + Downloading(:final startedAt) => startedAt, Verifying(:final startedAt) => startedAt, Installing(:final startedAt) => startedAt, SystemProcessing(:final startedAt) => startedAt, diff --git a/lib/services/package_manager/package_manager.dart b/lib/services/package_manager/package_manager.dart index f86d720..bbff87c 100644 --- a/lib/services/package_manager/package_manager.dart +++ b/lib/services/package_manager/package_manager.dart @@ -214,6 +214,7 @@ abstract class PackageManager extends StateNotifier { /// Check for operations stuck too long (Dart-side fallback if native events stop). void _checkForStaleOperations() { final now = DateTime.now(); + var needsQueueProcessing = false; for (final entry in state.operations.entries) { final appId = entry.key; @@ -228,19 +229,43 @@ abstract class PackageManager extends StateNotifier { '[PackageManager] Watchdog: $appId stuck in ${op.runtimeType}, ' 'transitioning to error', ); - setOperation( - appId, - OperationFailed( - target: op.target, - type: FailureType.installFailed, - message: 'Operation timed out (no response from system)', - filePath: op.filePath, - ), - ); - if (op is Installing || op is SystemProcessing) { - clearInstallSlot(appId); + + // Use appropriate failure type and cleanup based on operation type + if (op is Downloading) { + activeDownloads.remove(appId); + // Try to cancel the stuck task + unawaited( + _downloader.cancelTaskWithId(op.taskId).catchError((_) => false), + ); + setOperation( + appId, + OperationFailed( + target: op.target, + type: FailureType.downloadFailed, + message: 'Download timed out (no response from server)', + ), + ); + needsQueueProcessing = true; + } else { + setOperation( + appId, + OperationFailed( + target: op.target, + type: FailureType.installFailed, + message: 'Operation timed out (no response from system)', + filePath: op.filePath, + ), + ); + if (op is Installing || op is SystemProcessing) { + clearInstallSlot(appId); + needsQueueProcessing = true; + } } } + + if (needsQueueProcessing) { + scheduleProcessQueue(); + } } // ═══════════════════════════════════════════════════════════════════════════ @@ -384,11 +409,55 @@ abstract class PackageManager extends StateNotifier { try { final task = await _downloader.taskForId(op.taskId); if (task is DownloadTask) { - await _downloader.pause(task); + final paused = await _downloader.pause(task); + if (!paused) { + // Pause failed - task may be stuck, transition to failed state + debugPrint( + '[PackageManager] Pause returned false for $appId, marking as failed', + ); + activeDownloads.remove(appId); + setOperation( + appId, + OperationFailed( + target: op.target, + type: FailureType.downloadFailed, + message: 'Download stalled. Please try again.', + ), + ); + // Cancel the stuck task to clean up + try { + await _downloader.cancelTaskWithId(op.taskId); + } catch (_) {} + scheduleProcessQueue(); + } + } else { + // Task not found - download is in zombie state + debugPrint( + '[PackageManager] Task not found for $appId, marking as failed', + ); + activeDownloads.remove(appId); + setOperation( + appId, + OperationFailed( + target: op.target, + type: FailureType.downloadFailed, + message: 'Download lost. Please try again.', + ), + ); + scheduleProcessQueue(); } } catch (e) { debugPrint('[PackageManager] Failed to pause download for $appId: $e'); - // Download library will handle the state via callbacks + activeDownloads.remove(appId); + setOperation( + appId, + OperationFailed( + target: op.target, + type: FailureType.downloadFailed, + message: 'Download error: $e', + ), + ); + scheduleProcessQueue(); } } @@ -635,7 +704,7 @@ abstract class PackageManager extends StateNotifier { filename: fileName, updates: Updates.statusAndProgress, requiresWiFi: false, - retries: 3, + retries: 10, allowPause: true, metaData: metaData, displayName: displayName ?? appId,