From 06bd68c3cc3ef9c5b8d3350ff2be1d5a85d352c0 Mon Sep 17 00:00:00 2001 From: littleGnAl Date: Thu, 18 Apr 2024 15:54:18 +0800 Subject: [PATCH 1/3] fix: Prevent creation of multiple isolates when IrisMethodChannel.initialize is called multiple times simultaneously --- lib/src/iris_method_channel.dart | 73 +++++++----- .../io/iris_method_channel_internal_io.dart | 105 +++++++++--------- test/iris_method_channel_test.dart | 30 +++++ .../fake_platform_binding_delegate_io.dart | 9 ++ 4 files changed, 139 insertions(+), 78 deletions(-) diff --git a/lib/src/iris_method_channel.dart b/lib/src/iris_method_channel.dart index 2567dc4..4244754 100644 --- a/lib/src/iris_method_channel.dart +++ b/lib/src/iris_method_channel.dart @@ -8,6 +8,23 @@ import 'package:iris_method_channel/src/platform/iris_method_channel_internal.da // ignore_for_file: public_member_api_docs +class CallOnce { + Completer _completer = Completer(); + + Future callOnce(Future Function() func) async { + if (!_completer.isCompleted) { + try { + await func(); + } catch (e) { + _completer = Completer(); + rethrow; + } + _completer.complete(); + } + return _completer.future; + } +} + class IrisMethodChannel { IrisMethodChannel(this._nativeBindingsProvider) { _irisMethodChannelInternal = @@ -24,6 +41,8 @@ class IrisMethodChannel { @visibleForTesting final ScopedObjects scopedEventHandlers = ScopedObjects(); + CallOnce? _initializeCallOnce; + void _setuponDetachedFromEngineListener() { _channel.setMethodCallHandler((call) async { if (call.method == 'onDetachedFromEngine_fromPlatform') { @@ -43,41 +62,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 ??= CallOnce(); + await _initializeCallOnce!.callOnce(() 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 +139,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..4278306 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; + CallOnce? _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 ??= CallOnce(); + await _initializeCallOnce!.callOnce(() 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..21379ff 100644 --- a/test/iris_method_channel_test.dart +++ b/test/iris_method_channel_test.dart @@ -71,6 +71,36 @@ 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('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, From 907ccbc00448d519f0237c355867a48b14624e83 Mon Sep 17 00:00:00 2001 From: littleGnAl Date: Thu, 18 Apr 2024 16:52:52 +0800 Subject: [PATCH 2/3] fix: Prevent creation of multiple isolates when IrisMethodChannel.initialize is called multiple times simultaneously --- lib/src/iris_method_channel.dart | 6 +++++- test/iris_method_channel_test.dart | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/src/iris_method_channel.dart b/lib/src/iris_method_channel.dart index 4244754..6e0e462 100644 --- a/lib/src/iris_method_channel.dart +++ b/lib/src/iris_method_channel.dart @@ -10,16 +10,20 @@ import 'package:iris_method_channel/src/platform/iris_method_channel_internal.da class CallOnce { Completer _completer = Completer(); + bool _isRunning = false; Future callOnce(Future Function() func) async { - if (!_completer.isCompleted) { + if (!_completer.isCompleted && !_isRunning) { try { + _isRunning = true; await func(); } catch (e) { _completer = Completer(); + _isRunning = false; rethrow; } _completer.complete(); + _isRunning = false; } return _completer.future; } diff --git a/test/iris_method_channel_test.dart b/test/iris_method_channel_test.dart index 21379ff..eaedb2c 100644 --- a/test/iris_method_channel_test.dart +++ b/test/iris_method_channel_test.dart @@ -83,6 +83,19 @@ void main() { 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([]); From d5c700383384f9397d7a8c4d7fdba85491c2dc52 Mon Sep 17 00:00:00 2001 From: littleGnAl Date: Thu, 18 Apr 2024 18:18:43 +0800 Subject: [PATCH 3/3] fix: Prevent creation of multiple isolates when IrisMethodChannel.initialize is called multiple times simultaneously --- lib/src/iris_method_channel.dart | 28 +++---------------- .../io/iris_method_channel_internal_io.dart | 6 ++-- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/lib/src/iris_method_channel.dart b/lib/src/iris_method_channel.dart index 6e0e462..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; @@ -8,27 +9,6 @@ import 'package:iris_method_channel/src/platform/iris_method_channel_internal.da // ignore_for_file: public_member_api_docs -class CallOnce { - Completer _completer = Completer(); - bool _isRunning = false; - - Future callOnce(Future Function() func) async { - if (!_completer.isCompleted && !_isRunning) { - try { - _isRunning = true; - await func(); - } catch (e) { - _completer = Completer(); - _isRunning = false; - rethrow; - } - _completer.complete(); - _isRunning = false; - } - return _completer.future; - } -} - class IrisMethodChannel { IrisMethodChannel(this._nativeBindingsProvider) { _irisMethodChannelInternal = @@ -45,7 +25,7 @@ class IrisMethodChannel { @visibleForTesting final ScopedObjects scopedEventHandlers = ScopedObjects(); - CallOnce? _initializeCallOnce; + AsyncMemoizer? _initializeCallOnce; void _setuponDetachedFromEngineListener() { _channel.setMethodCallHandler((call) async { @@ -67,8 +47,8 @@ class IrisMethodChannel { } InitilizationResult? initilizationResult; - _initializeCallOnce ??= CallOnce(); - await _initializeCallOnce!.callOnce(() async { + _initializeCallOnce ??= AsyncMemoizer(); + await _initializeCallOnce!.runOnce(() async { _setuponDetachedFromEngineListener(); initilizationResult = await _irisMethodChannelInternal.initilize(args); 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 4278306..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,7 +408,7 @@ class IrisMethodChannelInternalIO implements IrisMethodChannelInternal { late Isolate workerIsolate; late _HotRestartFinalizer _hotRestartFinalizer; - CallOnce? _initializeCallOnce; + AsyncMemoizer? _initializeCallOnce; static Future _execute(_InitilizationArgs args) async { final SendPort mainApiCallSendPort = args.apiCallPortSendPort; @@ -510,8 +510,8 @@ class IrisMethodChannelInternalIO implements IrisMethodChannelInternal { } late InitilizationResultIO initilizationResult; - _initializeCallOnce ??= CallOnce(); - await _initializeCallOnce!.callOnce(() async { + _initializeCallOnce ??= AsyncMemoizer(); + await _initializeCallOnce!.runOnce(() async { final apiCallPort = ReceivePort(); final eventPort = ReceivePort();