Skip to content
Merged
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
23 changes: 22 additions & 1 deletion workmanager/lib/src/workmanager_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ class Workmanager {
static ProgressListener? _progressListener;
static late final WorkmanagerFlutterApi _flutterApi;

/// Dart→native API proxy used by background isolates to signal readiness
/// (see [executeTask]). Constructed lazily so the messenger is captured on
/// the isolate that actually runs a background task, after
/// [WidgetsFlutterBinding.ensureInitialized] bound it to the engine.
static late final WorkmanagerHostApi _hostApi = WorkmanagerHostApi();

/// The callback dispatcher registered via [initialize], kept so in-process
/// (main-engine) one-off tasks can lazily register their task handler on
/// first execution without spawning a second Flutter engine.
Expand Down Expand Up @@ -225,7 +231,22 @@ class Workmanager {
_flutterApi = _WorkmanagerFlutterApiImpl();
WorkmanagerFlutterApi.setUp(_flutterApi);

await _flutterApi.backgroundChannelInitialized();
// Signal the native worker running this isolate that the task handlers
// are now registered on this engine's messenger. The native side waits
// for this signal before invoking [WorkmanagerFlutterApi.executeTask];
// because the signal is sent from Dart *after* setUp, the follow-up
// executeTask call can never race isolate startup (a regression
// introduced by the Pigeon migration, which flipped the handshake to
// native-initiated — see #732/#738).
//
// Deliberately not awaited: engines without a native receiver for this
// call (web, desktop headless) never answer, and the worker must not
// depend on the reply. Errors are swallowed for the same reason.
unawaited(
_hostApi
.notifyBackgroundChannelInitialized()
.then((_) {}, onError: (Object _) {}),
);
}

/// Schedule a one-off task.
Expand Down
92 changes: 92 additions & 0 deletions workmanager/test/background_channel_ready_signal_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:workmanager/workmanager.dart';
import 'package:workmanager_apple/workmanager_apple.dart';

/// Host channel the Dart side uses to signal that its task handlers are
/// registered. Native workers wait for this signal before invoking
/// [WorkmanagerFlutterApi.executeTask], so the handshake never races isolate
/// startup (regression introduced by the Pigeon migration — see #732/#738).
const String _readyChannel =
'dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi'
'.notifyBackgroundChannelInitialized';

/// Fake platform that records initialize() calls without touching real
/// platform channels. Extends [WorkmanagerApple] so Workmanager's platform
/// auto-selection does not replace it on the test host.
class _FakeApplePlatform extends WorkmanagerApple {
@override
Future<void> initialize(
Function callbackDispatcher, {
@Deprecated(
'Use WorkmanagerDebug handlers instead. This parameter has no effect.')
bool isInDebugMode = false,
}) async {}
}

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

setUp(() {
WorkmanagerPlatform.instance = _FakeApplePlatform();
});

test('executeTask signals readiness and does not wait for the reply',
() async {
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
final codec = WorkmanagerFlutterApi.pigeonChannelCodec;

var readySignals = 0;
// Simulate a native side that receives the signal but never answers (the
// case on platforms without a native receiver, e.g. desktop headless).
// Recording the invocation is enough; the worker must not depend on the
// reply.
final never = Completer<ByteData?>();
messenger.setMockMessageHandler(_readyChannel, (ByteData? message) {
readySignals++;
expect(message, isNull);
return never.future;
});

final executedTasks = <String>[];
void callbackDispatcher() {
Workmanager().executeTask((taskName, inputData) async {
executedTasks.add(taskName);
return true;
});
}

await Workmanager().initialize(callbackDispatcher);
expect(readySignals, 0,
reason: 'no signal may be sent before a task executes');

callbackDispatcher();

// Exactly one readiness signal per executeTask call, sent after the task
// handler was registered (setUp runs before the signal in executeTask).
expect(readySignals, 1);

// The task handler must still be executable while the signal is
// unanswered: simulate the native side invoking executeTask once the
// signal was sent.
ByteData? reply;
await messenger.handlePlatformMessage(
'dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerFlutterApi'
'.executeTask',
codec.encodeMessage(<Object?>[
'dev.fluttercommunity.test.oneOff',
<String?, Object?>{'foo': 'bar'},
]),
(data) {
reply = data;
},
);
expect(reply, isNotNull);
expect(codec.decodeMessage(reply), [true]);
expect(executedTasks, ['dev.fluttercommunity.test.oneOff']);
});
}
1 change: 1 addition & 0 deletions workmanager/test/workmanager_test.mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Do not manually edit this file.

// ignore_for_file: no_leading_underscores_for_library_prefixes

import 'dart:async' as _i3;

import 'package:mockito/mockito.dart' as _i1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ class BackgroundWorker(

private var initializationWatchdog: DartInitializationWatchdog? = null

/**
* Guards [executeBackgroundTask] against duplicate Dart readiness signals
* (the Dart side could call notifyBackgroundChannelInitialized more than
* once, e.g. when executeTask runs twice in the same isolate).
*/
private val taskStarted = AtomicBoolean(false)

/**
* The plugin instance attached to this worker's engine. The Dart task
* runs on that engine, so its `reportProgress` calls arrive at this
Expand Down Expand Up @@ -177,13 +184,15 @@ class BackgroundWorker(
)

// The worker's result future is only resolved once the Dart
// side acknowledges the initialized background channel. If
// the engine fails to start the Dart isolate, or the
// acknowledgement is lost, the worker would otherwise stay
// in the RUNNING state forever (see #732). Arm a watchdog
// that fails the worker when the acknowledgement does not
// arrive in time; it is disarmed on acknowledgement and on
// stop.
// side signals that its background-channel handlers are
// registered. That signal is a Dart→native call
// (WorkmanagerHostApi.notifyBackgroundChannelInitialized),
// routed here by the plugin bound to this worker's engine. If
// the engine fails to start the Dart isolate, or the signal is
// lost, the worker would otherwise stay in the RUNNING state
// forever (see #732). Arm a watchdog that fails the worker
// when the signal does not arrive in time; it is disarmed on
// the signal and on stop.
val watchdog =
DartInitializationWatchdog(
handler = mainHandler,
Expand All @@ -200,12 +209,14 @@ class BackgroundWorker(
initializationWatchdog = watchdog
watchdog.arm()

// Initialize the background channel
flutterApi.backgroundChannelInitialized {
// Channel is initialized, now execute the task
watchdog.disarm()
executeBackgroundTask()
}
// The Dart isolate runs the callback dispatcher as its entry
// point; once its task handlers are set up it calls
// notifyBackgroundChannelInitialized, which lands in
// [onDartBackgroundChannelInitialized] and kicks off
// execution. Nothing is sent to Dart from here: a native→Dart
// message sent before the isolate registered its handlers is
// silently lost, which is what left workers stuck in RUNNING
// (pre-0.10.9) or failing the watchdog (0.10.9, #738).
}
}

Expand Down Expand Up @@ -387,6 +398,30 @@ class BackgroundWorker(
StopReasonUtils.STOP_REASON_UNKNOWN
}

/**
* Called by the plugin attached to this worker's engine when the Dart
* isolate signals that its task handlers are registered (the Dart side
* calls `WorkmanagerHostApi.notifyBackgroundChannelInitialized` from
* `Workmanager().executeTask`, after `WorkmanagerFlutterApi.setUp`).
*
* Only now is the task sent down to Dart: because the signal originates
* from Dart, its handlers are guaranteed to be registered when the
* follow-up [WorkmanagerFlutterApi.executeTask] call arrives — the
* handshake can never race isolate startup (see #732/#738).
*/
fun onDartBackgroundChannelInitialized() {
if (isStopped) return
mainHandler.post {
// A stopped or torn-down worker (watchdog fired, WM stop) must
// ignore a late signal.
if (isStopped || engine == null) return@post
if (taskStarted.compareAndSet(false, true)) {
initializationWatchdog?.disarm()
executeBackgroundTask()
}
}
}

private fun executeBackgroundTask() {
// Convert payload to the format expected by Pigeon (Map<String?, Object?>)
val pigeonPayload = payload.mapKeys { it.key as String? }.mapValues { it.value as Object? }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import java.util.concurrent.atomic.AtomicBoolean
/**
* Bounds the Dart engine initialization handshake of a [BackgroundWorker].
*
* The worker's result future is only resolved once the Dart side acknowledges
* `backgroundChannelInitialized`. If the engine fails to start the Dart
* isolate, or the acknowledgement is lost, the worker would otherwise stay in
* the RUNNING state forever (see #732). This watchdog fires [timeoutAction]
* when the acknowledgement does not arrive within [timeoutMillis].
* The worker's result future is only resolved once the Dart side signals
* that its background-channel handlers are registered
* (`WorkmanagerHostApi.notifyBackgroundChannelInitialized`). If the engine
* fails to start the Dart isolate, or the signal is lost, the worker would
* otherwise stay in the RUNNING state forever (see #732). This watchdog fires
* [timeoutAction] when the signal does not arrive within [timeoutMillis].
*
* [arm] and [disarm] are idempotent and safe to call from any thread: the
* action fires at most once, and only while the watchdog is armed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,18 @@ class WorkmanagerPlugin :
callback(Result.success(Unit))
}

override fun notifyBackgroundChannelInitialized(callback: (Result<Unit>) -> Unit) {
val worker = boundWorker
if (worker != null) {
// The signal originates from the Dart isolate running on this
// plugin's engine, which is the engine of the bound worker (or of
// a worker that has not bound itself yet — then there is nothing
// to execute and the signal is ignored).
worker.onDartBackgroundChannelInitialized()
}
callback(Result.success(Unit))
}

override fun setProgressListener(
enabled: Boolean,
callback: (Result<Unit>) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,23 @@ interface WorkmanagerHostApi {
* progress support the call is a no-op.
*/
fun setProgressListener(enabled: Boolean, callback: (Result<Unit>) -> Unit)
/**
* Signals the native side that the background isolate's task handlers are
* registered on this engine's messenger.
*
* Called from Dart by `Workmanager().executeTask`, after
* [WorkmanagerFlutterApi]'s handlers have been set up. Native background
* workers wait for this signal before invoking
* [WorkmanagerFlutterApi.executeTask]; because the signal is sent from
* Dart only after setUp completed, the follow-up `executeTask` call can
* never race isolate startup (regression introduced by the Pigeon
* migration, which flipped the handshake to native-initiated — see
* #732/#738).
*
* Platforms and engines that never execute a Dart background task (the app
* engine on Android, web) may ignore the call.
*/
fun notifyBackgroundChannelInitialized(callback: (Result<Unit>) -> Unit)

companion object {
/** The codec used by WorkmanagerHostApi. */
Expand Down Expand Up @@ -1585,6 +1602,23 @@ interface WorkmanagerHostApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.notifyBackgroundChannelInitialized$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.notifyBackgroundChannelInitialized{ result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(WorkmanagerApiPigeonUtils.wrapError(error))
} else {
reply.reply(WorkmanagerApiPigeonUtils.wrapResult(null))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,20 @@ public class WorkmanagerPlugin: WorkmanagerPluginBase, FlutterPlugin, Workmanage
completion(.success(WorkInfoStore.workInfoData(forUniqueName: uniqueName)))
}

func notifyBackgroundChannelInitialized(completion: @escaping (Result<Void, Error>) -> Void) {
// Dart-side readiness signal introduced with the re-inverted handshake
// (see the pigeon definition): the background isolate calls this from
// Workmanager().executeTask after registering its task handlers.
//
// Apple workers still gate task execution on the reply of the legacy
// native-initiated `backgroundChannelInitialized` call (the kick that
// lazily runs the callbackDispatcher on the main engine), so there is
// nothing to do here yet. Aligning the Apple headless-engine flow with
// the Dart-initiated handshake (mirroring the Android fix for
// #732/#738) is tracked as a follow-up.
completion(.success(()))
}

// MARK: - WorkmanagerHostApi implementation (iOS)

#if os(iOS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,21 @@ protocol WorkmanagerHostApi {
/// messenger of the engine that made this call. On platforms without
/// progress support the call is a no-op.
func setProgressListener(enabled: Bool, completion: @escaping (Result<Void, Error>) -> Void)
/// Signals the native side that the background isolate's task handlers are
/// registered on this engine's messenger.
///
/// Called from Dart by `Workmanager().executeTask`, after
/// [WorkmanagerFlutterApi]'s handlers have been set up. Native background
/// workers wait for this signal before invoking
/// [WorkmanagerFlutterApi.executeTask]; because the signal is sent from
/// Dart only after setUp completed, the follow-up `executeTask` call can
/// never race isolate startup (regression introduced by the Pigeon
/// migration, which flipped the handshake to native-initiated — see
/// #732/#738).
///
/// Platforms and engines that never execute a Dart background task (the app
/// engine on Android, web) may ignore the call.
func notifyBackgroundChannelInitialized(completion: @escaping (Result<Void, Error>) -> Void)
}

/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
Expand Down Expand Up @@ -1435,6 +1450,35 @@ class WorkmanagerHostApiSetup {
} else {
setProgressListenerChannel.setMessageHandler(nil)
}
/// Signals the native side that the background isolate's task handlers are
/// registered on this engine's messenger.
///
/// Called from Dart by `Workmanager().executeTask`, after
/// [WorkmanagerFlutterApi]'s handlers have been set up. Native background
/// workers wait for this signal before invoking
/// [WorkmanagerFlutterApi.executeTask]; because the signal is sent from
/// Dart only after setUp completed, the follow-up `executeTask` call can
/// never race isolate startup (regression introduced by the Pigeon
/// migration, which flipped the handshake to native-initiated — see
/// #732/#738).
///
/// Platforms and engines that never execute a Dart background task (the app
/// engine on Android, web) may ignore the call.
let notifyBackgroundChannelInitializedChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.notifyBackgroundChannelInitialized\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
notifyBackgroundChannelInitializedChannel.setMessageHandler { _, reply in
api.notifyBackgroundChannelInitialized { result in
switch result {
case .success:
reply(wrapResult(nil))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
notifyBackgroundChannelInitializedChannel.setMessageHandler(nil)
}
}
}
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
Expand Down
Loading
Loading