Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion workmanager_linux/lib/src/background_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import 'dart:io';

import 'package:workmanager_platform_interface/workmanager_platform_interface.dart';

import '../execution.dart';
import 'payload_store.dart';

Expand Down Expand Up @@ -85,16 +87,43 @@ class BackgroundTaskRunner {
);
return false;
}
final started = DateTime.now();
final taskInfo = TaskDebugInfo(
taskName: invocation.taskName,
inputData: inputData,
startTime: started,
);
WorkmanagerDebug.reportStatus(taskInfo, TaskStatus.started, null);
try {
return await WorkmanagerExecution.instance.runTask(
final success = await WorkmanagerExecution.instance.runTask(
invocation.taskName,
inputData,
);
WorkmanagerDebug.reportStatus(
taskInfo,
success ? TaskStatus.completed : TaskStatus.failed,
TaskResult(
success: success,
duration: DateTime.now().difference(started),
error: success ? null : 'handler returned false',
),
);
return success;
} on Object catch (error, stackTrace) {
stderr.writeln(
'workmanager_linux: background task "${invocation.taskName}" threw: '
'$error\n$stackTrace',
);
WorkmanagerDebug.reportStatus(
taskInfo,
TaskStatus.failed,
TaskResult(
success: false,
duration: DateTime.now().difference(started),
error: error.toString(),
),
);
WorkmanagerDebug.reportException(taskInfo, error, stackTrace);
return false;
}
}
Expand Down
160 changes: 160 additions & 0 deletions workmanager_platform_interface/lib/src/workmanager_debug.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright 2024 The Flutter Workmanager Authors. All rights reserved.
// Use of this source code is governed by a MIT-style license that can be
// found in the LICENSE file.

import 'package:workmanager_platform_interface/workmanager_platform_interface.dart'
show TaskStatus;

/// Information about a task for debugging purposes.
///
/// Mirrors `TaskDebugInfo` on the native (Android/iOS) debug API.
class TaskDebugInfo {
const TaskDebugInfo({
required this.taskName,
this.uniqueName,
this.inputData,
required this.startTime,
});

/// The task name the work was registered with.
final String taskName;

/// The unique name the work was registered with, when known.
final String? uniqueName;

/// The input data the task was registered with, when known.
final Map<String, dynamic>? inputData;

/// When the task started executing.
final DateTime startTime;
}

/// Result information for a completed task.
///
/// Mirrors `TaskResult` on the native (Android/iOS) debug API.
class TaskResult {
const TaskResult({
required this.success,
required this.duration,
this.error,
});

/// Whether the task handler reported success.
final bool success;

/// How long the task ran.
final Duration duration;

/// The failure message, when the task failed.
final String? error;
}

/// Abstract debug handler for Workmanager events.
///
/// Mirror of the native `WorkmanagerDebug` API (Android/iOS): set a handler
/// with [WorkmanagerDebug.setCurrent] and override the callbacks you care
/// about. The default handler does nothing.
///
/// ```dart
/// WorkmanagerDebug.setCurrent(LoggingDebugHandler());
/// ```
///
/// Available on every platform: Android/iOS implementations emit natively
/// (same handler contract, minus platform context); web, linux and windows
/// emit from their Dart execution paths.
abstract class WorkmanagerDebug {
const WorkmanagerDebug();

static WorkmanagerDebug _current = _NoopDebugHandler();

/// The currently registered debug handler.
static WorkmanagerDebug get current => _current;

/// Sets the global debug handler.
static void setCurrent(WorkmanagerDebug handler) {
_current = handler;
}

/// Restores the default no-op handler.
static void reset() {
_current = _NoopDebugHandler();
}

/// Called by platform implementations when a task status changes.
static void reportStatus(
TaskDebugInfo taskInfo,
TaskStatus status,
TaskResult? result,
) {
_current.onTaskStatusUpdate(taskInfo, status, result);
}

/// Called by platform implementations when an exception is encountered
/// during task processing.
static void reportException(
TaskDebugInfo? taskInfo,
Object exception,
StackTrace? stackTrace,
) {
_current.onExceptionEncountered(taskInfo, exception, stackTrace);
}

/// Called when a task status changes. Default: do nothing.
void onTaskStatusUpdate(
TaskDebugInfo taskInfo,
TaskStatus status,
TaskResult? result,
) {}

/// Called when an exception occurs during task processing. Default: do
/// nothing.
void onExceptionEncountered(
TaskDebugInfo? taskInfo,
Object exception,
StackTrace? stackTrace,
) {}
}

class _NoopDebugHandler extends WorkmanagerDebug {}

/// Prints debug information to the console.
class LoggingDebugHandler extends WorkmanagerDebug {
const LoggingDebugHandler();

@override
void onTaskStatusUpdate(
TaskDebugInfo taskInfo,
TaskStatus status,
TaskResult? result,
) {
final buffer = StringBuffer()
..write('[workmanager] ${taskInfo.taskName} -> $status');
if (taskInfo.uniqueName != null) {
buffer.write(' (${taskInfo.uniqueName})');
}
if (result != null) {
buffer.write(
' — ${result.success ? 'OK' : 'FAILED'} in '
'${result.duration.inMilliseconds}ms',
);
if (result.error != null) {
buffer.write(': ${result.error}');
}
}
// ignore: avoid_print
print(buffer.toString());
}

@override
void onExceptionEncountered(
TaskDebugInfo? taskInfo,
Object exception,
StackTrace? stackTrace,
) {
// ignore: avoid_print
print(
'[workmanager] exception in ${taskInfo?.taskName ?? 'unknown task'}: '
'$exception',
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ library workmanager_platform_interface;
export 'src/workmanager_platform_interface.dart';
export 'src/work_info.dart';
export 'src/pigeon/workmanager_api.g.dart';
export 'src/workmanager_debug.dart';
export 'src/stop_reason.dart';
112 changes: 112 additions & 0 deletions workmanager_platform_interface/test/workmanager_debug_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:workmanager_platform_interface/workmanager_platform_interface.dart';

void main() {
tearDown(WorkmanagerDebug.reset);

test('default handler is a no-op', () {
expect(WorkmanagerDebug.current, isA<WorkmanagerDebug>());
// Must not throw.
WorkmanagerDebug.reportStatus(
_taskInfo(),
TaskStatus.started,
null,
);
WorkmanagerDebug.reportException(_taskInfo(), StateError('boom'), null);
});

test('setCurrent routes status updates to the handler', () {
final events = <(TaskDebugInfo, TaskStatus, TaskResult?)>[];
WorkmanagerDebug.setCurrent(_RecordingHandler(events));

final taskInfo = _taskInfo();
WorkmanagerDebug.reportStatus(taskInfo, TaskStatus.started, null);
WorkmanagerDebug.reportStatus(
taskInfo,
TaskStatus.completed,
TaskResult(success: true, duration: const Duration(milliseconds: 42)),
);

expect(events, hasLength(2));
expect(events[0].$1.taskName, 'my-task');
expect(events[0].$2, TaskStatus.started);
expect(events[1].$2, TaskStatus.completed);
expect(events[1].$3?.success, isTrue);
expect(events[1].$3?.duration, const Duration(milliseconds: 42));
});

test('setCurrent routes exceptions to the handler', () {
final exceptions = <Object>[];
WorkmanagerDebug.setCurrent(
_RecordingHandler(<(TaskDebugInfo, TaskStatus, TaskResult?)>[],
exceptions: exceptions),
);

final error = StateError('boom');
WorkmanagerDebug.reportException(_taskInfo(), error, null);

expect(exceptions, [error]);
});

test('reset restores the no-op handler', () {
WorkmanagerDebug.setCurrent(
_RecordingHandler(<(TaskDebugInfo, TaskStatus, TaskResult?)>[]),
);
WorkmanagerDebug.reset();
expect(WorkmanagerDebug.current, isA<WorkmanagerDebug>());
// Must not throw with a cleared handler.
WorkmanagerDebug.reportStatus(_taskInfo(), TaskStatus.failed, null);
});

test('LoggingDebugHandler does not throw on either callback', () {
final handler = LoggingDebugHandler();
handler.onTaskStatusUpdate(
_taskInfo(),
TaskStatus.failed,
TaskResult(
success: false,
duration: const Duration(milliseconds: 500),
error: 'nope',
),
);
handler.onExceptionEncountered(
_taskInfo(),
StateError('boom'),
null,
);
expect(handler, isA<WorkmanagerDebug>());
});
}

TaskDebugInfo _taskInfo() => TaskDebugInfo(
taskName: 'my-task',
uniqueName: 'my-unique',
inputData: <String, dynamic>{'key': 'value'},
startTime: DateTime(2026, 1, 1),
);

class _RecordingHandler extends WorkmanagerDebug {
_RecordingHandler(this.events, {List<Object>? exceptions})
: exceptions = exceptions ?? <Object>[];

final List<(TaskDebugInfo, TaskStatus, TaskResult?)> events;
final List<Object> exceptions;

@override
void onTaskStatusUpdate(
TaskDebugInfo taskInfo,
TaskStatus status,
TaskResult? result,
) {
events.add((taskInfo, status, result));
}

@override
void onExceptionEncountered(
TaskDebugInfo? taskInfo,
Object exception,
StackTrace? stackTrace,
) {
exceptions.add(exception);
}
}
33 changes: 33 additions & 0 deletions workmanager_web/lib/workmanager_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,15 @@ class WorkmanagerWeb extends WorkmanagerPlatform {
_tasks.remove(uniqueName);
}
final started = DateTime.now();
final taskInfo = TaskDebugInfo(
taskName: taskName,
uniqueName: uniqueName,
inputData: rawInputData is Map
? Map<String, dynamic>.from(rawInputData)
: null,
startTime: started,
);
WorkmanagerDebug.reportStatus(taskInfo, TaskStatus.started, null);
Object? result;
String executedIn;
String? errorMessage;
Expand Down Expand Up @@ -629,6 +638,14 @@ class WorkmanagerWeb extends WorkmanagerPlatform {
}
if (errorMessage == null) {
final elapsed = DateTime.now().difference(started).inMilliseconds;
WorkmanagerDebug.reportStatus(
taskInfo,
TaskStatus.completed,
TaskResult(
success: true,
duration: Duration(milliseconds: elapsed),
),
);
_emit(
'executed',
'Task "$taskName" executed in ${elapsed}ms via $executedIn.',
Expand All @@ -637,6 +654,22 @@ class WorkmanagerWeb extends WorkmanagerPlatform {
taskName: taskName,
result: result,
);
} else {
final elapsed = DateTime.now().difference(started).inMilliseconds;
WorkmanagerDebug.reportStatus(
taskInfo,
TaskStatus.failed,
TaskResult(
success: false,
duration: Duration(milliseconds: elapsed),
error: errorMessage,
),
);
WorkmanagerDebug.reportException(
taskInfo,
Exception(errorMessage),
null,
);
}
await _notifyServiceWorkerExecuted(
uniqueName,
Expand Down
Loading