Improve watchdog for stalled downloads

This commit is contained in:
franzap
2026-02-05 13:23:43 -03:00
parent b5542064c3
commit cc31af4ee7
2 changed files with 94 additions and 18 deletions
@@ -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,
@@ -214,6 +214,7 @@ abstract class PackageManager extends StateNotifier<PackageManagerState> {
/// 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<PackageManagerState> {
'[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<PackageManagerState> {
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<PackageManagerState> {
filename: fileName,
updates: Updates.statusAndProgress,
requiresWiFi: false,
retries: 3,
retries: 10,
allowPause: true,
metaData: metaData,
displayName: displayName ?? appId,