From 20779dbb11a93fdcf090cbd817ccc52d38794f8a Mon Sep 17 00:00:00 2001 From: Littlegnal <8847263+littleGnAl@users.noreply.github.com> Date: Fri, 19 Apr 2024 18:03:28 +0800 Subject: [PATCH] fix: Prevent creation of multiple isolates when IrisMethodChannel.initialize is called multiple times simultaneously (#98) A case like: ```dart for (int i = 0; i < 5; ++i) { irisMethodChannel.initilize([]); } ``` If we do not guard the initialization only once, multiple isolates will be created. --- lib/src/iris_method_channel.dart | 57 +++++----- .../io/iris_method_channel_internal_io.dart | 105 +++++++++--------- test/iris_method_channel_test.dart | 43 +++++++ .../fake_platform_binding_delegate_io.dart | 9 ++ 4 files changed, 136 insertions(+), 78 deletions(-) diff --git a/lib/src/iris_method_channel.dart b/lib/src/iris_method_channel.dart index 2567dc4..00ded2f 100644 --- a/lib/src/iris_method_channel.dart +++ b/lib/src/iris_method_channel.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async/async.dart' show AsyncMemoizer; import 'package:flutter/foundation.dart' show VoidCallback, debugPrint, visibleForTesting; import 'package:flutter/services.dart' show MethodChannel; @@ -24,6 +25,8 @@ class IrisMethodChannel { @visibleForTesting final ScopedObjects scopedEventHandlers = ScopedObjects(); + AsyncMemoizer? _initializeCallOnce; + void _setuponDetachedFromEngineListener() { _channel.setMethodCallHandler((call) async { if (call.method == 'onDetachedFromEngine_fromPlatform') { @@ -43,41 +46,44 @@ class IrisMethodChannel { return null; } - _setuponDetachedFromEngineListener(); - - final initilizationResult = - await _irisMethodChannelInternal.initilize(args); - - _irisMethodChannelInternal.setIrisEventMessageListener((eventMessage) { - bool handled = false; - for (final sub in scopedEventHandlers.values) { - final scopedObjects = sub as DisposableScopedObjects; - for (final es in scopedObjects.values) { - final EventHandlerHolder eh = es as EventHandlerHolder; - // We need the event handlers with the same _EventHandlerHolderKey consume the message. - for (final e in eh.getEventHandlers()) { - if (e.handleEvent( - eventMessage.event, eventMessage.data, eventMessage.buffers)) { - handled = true; + InitilizationResult? initilizationResult; + _initializeCallOnce ??= AsyncMemoizer(); + await _initializeCallOnce!.runOnce(() async { + _setuponDetachedFromEngineListener(); + + initilizationResult = await _irisMethodChannelInternal.initilize(args); + + _irisMethodChannelInternal.setIrisEventMessageListener((eventMessage) { + bool handled = false; + for (final sub in scopedEventHandlers.values) { + final scopedObjects = sub as DisposableScopedObjects; + for (final es in scopedObjects.values) { + final EventHandlerHolder eh = es as EventHandlerHolder; + // We need the event handlers with the same _EventHandlerHolderKey consume the message. + for (final e in eh.getEventHandlers()) { + if (e.handleEvent(eventMessage.event, eventMessage.data, + eventMessage.buffers)) { + handled = true; + } + } + + // Break the loop after the event handlers in the same EventHandlerHolder + // consume the message. + if (handled) { + break; } } - // Break the loop after the event handlers in the same EventHandlerHolder - // consume the message. + // Break the loop if there is an EventHandlerHolder consume the message. if (handled) { break; } } + }); - // Break the loop if there is an EventHandlerHolder consume the message. - if (handled) { - break; - } - } + _initilized = true; }); - _initilized = true; - return initilizationResult; } @@ -117,6 +123,7 @@ class IrisMethodChannel { _initilized = false; await _irisMethodChannelInternal.dispose(); + _initializeCallOnce = null; } Future registerEventHandler( diff --git a/lib/src/platform/io/iris_method_channel_internal_io.dart b/lib/src/platform/io/iris_method_channel_internal_io.dart index 9134d6b..8aad837 100644 --- a/lib/src/platform/io/iris_method_channel_internal_io.dart +++ b/lib/src/platform/io/iris_method_channel_internal_io.dart @@ -408,6 +408,8 @@ class IrisMethodChannelInternalIO implements IrisMethodChannelInternal { late Isolate workerIsolate; late _HotRestartFinalizer _hotRestartFinalizer; + AsyncMemoizer? _initializeCallOnce; + static Future _execute(_InitilizationArgs args) async { final SendPort mainApiCallSendPort = args.apiCallPortSendPort; final SendPort mainEventSendPort = args.eventPortSendPort; @@ -507,63 +509,60 @@ class IrisMethodChannelInternalIO implements IrisMethodChannelInternal { return null; } - final apiCallPort = ReceivePort(); - final eventPort = ReceivePort(); - - _hotRestartFinalizer = _HotRestartFinalizer(_nativeBindingsProvider); - - workerIsolate = await Isolate.spawn( - _execute, - _InitilizationArgs( - apiCallPort.sendPort, - eventPort.sendPort, - _hotRestartFinalizer.onExitSendPort, - _nativeBindingsProvider, - args, - ), - onExit: _hotRestartFinalizer.onExitSendPort, - ); - - // Convert the ReceivePort into a StreamQueue to receive messages from the - // spawned isolate using a pull-based interface. Events are stored in this - // queue until they are accessed by `events.next`. - // final events = StreamQueue(p); - final responseQueue = StreamQueue(apiCallPort); - - // The first message from the spawned isolate is a SendPort. This port is - // used to communicate with the spawned isolate. - // SendPort sendPort = await events.next; - final msg = await responseQueue.next; - assert(msg is InitilizationResult); - final initilizationResult = msg as InitilizationResultIO; - final requestPort = initilizationResult._apiCallPortSendPort; - _nativeHandle = initilizationResult.irisApiEngineNativeHandle; - - assert(() { - _hotRestartFinalizer.debugIrisApiEngineNativeHandle = - initilizationResult.irisApiEngineNativeHandle; - _hotRestartFinalizer.debugIrisCEventHandlerNativeHandle = - initilizationResult._debugIrisCEventHandlerNativeHandle; - _hotRestartFinalizer.debugIrisEventHandlerNativeHandle = - initilizationResult._debugIrisEventHandlerNativeHandle; - - return true; - }()); + late InitilizationResultIO initilizationResult; + _initializeCallOnce ??= AsyncMemoizer(); + await _initializeCallOnce!.runOnce(() async { + final apiCallPort = ReceivePort(); + final eventPort = ReceivePort(); + + _hotRestartFinalizer = _HotRestartFinalizer(_nativeBindingsProvider); + + workerIsolate = await Isolate.spawn( + _execute, + _InitilizationArgs( + apiCallPort.sendPort, + eventPort.sendPort, + _hotRestartFinalizer.onExitSendPort, + _nativeBindingsProvider, + args, + ), + onExit: _hotRestartFinalizer.onExitSendPort, + ); + + final responseQueue = StreamQueue(apiCallPort); + + final msg = await responseQueue.next; + assert(msg is InitilizationResult); + initilizationResult = msg as InitilizationResultIO; + final requestPort = initilizationResult._apiCallPortSendPort; + _nativeHandle = initilizationResult.irisApiEngineNativeHandle; + + assert(() { + _hotRestartFinalizer.debugIrisApiEngineNativeHandle = + initilizationResult.irisApiEngineNativeHandle; + _hotRestartFinalizer.debugIrisCEventHandlerNativeHandle = + initilizationResult._debugIrisCEventHandlerNativeHandle; + _hotRestartFinalizer.debugIrisEventHandlerNativeHandle = + initilizationResult._debugIrisEventHandlerNativeHandle; + + return true; + }()); + + _messenger = _Messenger(requestPort, responseQueue); + + _evntSubscription = eventPort.listen((message) { + if (!_initilized) { + return; + } - _messenger = _Messenger(requestPort, responseQueue); + final eventMessage = parseMessage(message); - _evntSubscription = eventPort.listen((message) { - if (!_initilized) { - return; - } + _irisEventMessageListener?.call(eventMessage); + }); - final eventMessage = parseMessage(message); - - _irisEventMessageListener?.call(eventMessage); + _initilized = true; }); - _initilized = true; - return initilizationResult; } @@ -576,8 +575,8 @@ class IrisMethodChannelInternalIO implements IrisMethodChannelInternal { _irisEventMessageListener = null; _hotRestartFinalizer.dispose(); await _evntSubscription.cancel(); - await _messenger.dispose(); + _initializeCallOnce = null; } @override diff --git a/test/iris_method_channel_test.dart b/test/iris_method_channel_test.dart index 5a5d0d8..eaedb2c 100644 --- a/test/iris_method_channel_test.dart +++ b/test/iris_method_channel_test.dart @@ -71,6 +71,49 @@ void main() { await irisMethodChannel.dispose(); }); + test('only initialize once', () async { + await irisMethodChannel.initilize([]); + await irisMethodChannel.initilize([]); + await irisMethodChannel.initilize([]); + + final callRecord1 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'createApiEngine'); + expect(callRecord1.length, 1); + + await irisMethodChannel.dispose(); + }); + + test('only initialize once when called simultaneously', () async { + for (int i = 0; i < 5; ++i) { + irisMethodChannel.initilize([]); + } + // Wait for the 5 times calls of `irisMethodChannel.initilize` are completed. + await Future.delayed(const Duration(milliseconds: 1000)); + final callRecord1 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'createApiEngine'); + expect(callRecord1.length, 1); + + await irisMethodChannel.dispose(); + }); + + test('can re-initialize after dispose', () async { + await irisMethodChannel.initilize([]); + await irisMethodChannel.initilize([]); + await irisMethodChannel.initilize([]); + await irisMethodChannel.dispose(); + final callRecord1 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'createApiEngine'); + expect(callRecord1.length, 1); + + await irisMethodChannel.initilize([]); + await irisMethodChannel.initilize([]); + await irisMethodChannel.initilize([]); + final callRecord2 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'createApiEngine'); + expect(callRecord2.length, 2); + await irisMethodChannel.dispose(); + }); + test('invokeMethod', () async { await irisMethodChannel.initilize([]); final callApiResult = await irisMethodChannel diff --git a/test/platform/fake/fake_platform_binding_delegate_io.dart b/test/platform/fake/fake_platform_binding_delegate_io.dart index 1781604..ed1c629 100644 --- a/test/platform/fake/fake_platform_binding_delegate_io.dart +++ b/test/platform/fake/fake_platform_binding_delegate_io.dart @@ -102,6 +102,15 @@ class FakeNativeBindingDelegate extends PlatformBindingsDelegateInterface { ), ); apiCallPortSendPort.send(record); + } else { + final record = CallApiRecord( + const IrisMethodCall('createApiEngine', '{}'), + CallApiRecordApiParam( + 'createApiEngine', + '{}', + ), + ); + apiCallPortSendPort.send(record); } return CreateApiEngineResult( engineHandle,