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
58 changes: 41 additions & 17 deletions lib/src/iris_method_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import 'dart:async';

import 'package:async/async.dart' show AsyncMemoizer;
import 'package:flutter/foundation.dart'
show VoidCallback, debugPrint, visibleForTesting;
show
VoidCallback,
debugPrint,
visibleForTesting,
FlutterError,
FlutterErrorDetails,
ErrorDescription,
kDebugMode;
import 'package:flutter/services.dart' show MethodChannel;
import 'package:iris_method_channel/iris_method_channel.dart';
import 'package:iris_method_channel/src/platform/iris_method_channel_internal.dart';
Expand Down Expand Up @@ -64,29 +71,46 @@ class IrisMethodChannel {
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;
try {
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.
final handlersSnapshot = eh.getEventHandlers();

for (final e in handlersSnapshot.toList()) {
if (!eh.getEventHandlers().contains(e)) {
continue;
}
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;
} catch (e, s) {
FlutterError.reportError(FlutterErrorDetails(
exception: e,
stack: s,
library: 'iris_method_channel',
context: ErrorDescription('IrisMethodChannel handleEvent'),
));
if (kDebugMode) {
rethrow;
}
}
Comment thread
ZGaopeng marked this conversation as resolved.
Comment thread
ZGaopeng marked this conversation as resolved.
});
Expand Down
3 changes: 2 additions & 1 deletion lib/src/platform/iris_method_channel_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,9 @@ class EventHandlerHolder
_eventHandlers.add(eventHandler);
}

Future<void> removeEventHandler(EventLoopEventHandler eventHandler) async {
Future<void> removeEventHandler(EventLoopEventHandler eventHandler) {
_eventHandlers.remove(eventHandler);
return SynchronousFuture(null);
}

Set<EventLoopEventHandler> getEventHandlers() => _eventHandlers;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/scoped_objects.dart
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ class ScopedObjects {
/// [DisposableObject.dispose]
Future<void> clear() async {
_isClearing = true;
final values = pool.values;
final values = pool.values.toList();
for (final v in values) {
await v?._disposeOnParentClear();
}
Expand Down
123 changes: 123 additions & 0 deletions test/repro_concurrent_modification_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import 'dart:typed_data';

import 'package:flutter_test/flutter_test.dart';
import 'package:iris_method_channel/iris_method_channel.dart';

class _FakePlatformBindingsDelegate extends PlatformBindingsDelegateInterface {
@override
int callApi(IrisMethodCall methodCall, IrisApiEngineHandle apiEnginePtr, IrisApiParamHandle param) => 0;

@override
Future<CallApiResult> callApiAsync(IrisMethodCall methodCall, IrisApiEngineHandle apiEnginePtr, IrisApiParamHandle param) async => CallApiResult(data: {}, irisReturnCode: 0);

@override
CreateApiEngineResult createApiEngine(List<InitilizationArgProvider> args) => CreateApiEngineResult(IrisApiEngineHandle(0));

@override
IrisEventHandlerHandle createIrisEventHandler(IrisCEventHandlerHandle eventHandler) => IrisEventHandlerHandle(0);

@override
void destroyIrisEventHandler(IrisEventHandlerHandle handler) {}

@override
void destroyNativeApiEngine(IrisApiEngineHandle apiEnginePtr) {}

@override
void initialize() {}
}

class _FakePlatformBindingsProvider extends PlatformBindingsProvider {
@override
PlatformBindingsDelegateInterface provideNativeBindingDelegate() {
return _FakePlatformBindingsDelegate();
}
}

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

test('ConcurrentModificationError should not be thrown when adding/removing handlers during event dispatch', () async {
final provider = _FakePlatformBindingsProvider();
final irisMethodChannel = IrisMethodChannel(provider);

// Logic Tester that mimics the FIXED IrisMethodChannel event loop
void simulateEventLoop(IrisMethodChannel channel, IrisEventMessage message) {
bool handled = false;
// We use the same snapshotting logic as in the fixed IrisMethodChannel
for (final sub in channel.scopedEventHandlers.values) {
final scopedObjects = sub as DisposableScopedObjects;
for (final es in scopedObjects.values) {
final EventHandlerHolder eh = es as EventHandlerHolder;
final handlersSnapshot = eh.getEventHandlers();

for (final e in handlersSnapshot.toList()) {
if (!eh.getEventHandlers().contains(e)) {
continue;
}
if (e.handleEvent(message.event, message.data, message.buffers)) {
handled = true;
}
}

if (handled) {
break;
}
}
if (handled) {
break;
}
}
}

const key = TypedScopedKey(Object);
final subScopedObjects = irisMethodChannel.scopedEventHandlers.putIfAbsent(key, () => DisposableScopedObjects()) as DisposableScopedObjects;
final eventKey = EventHandlerHolderKey(registerName: 'test_event', unregisterName: 'test_event');
final holder = subScopedObjects.putIfAbsent(eventKey, () => EventHandlerHolder(key: eventKey)) as EventHandlerHolder;

// 1. Test removing self during event handling
late EventLoopEventHandler handlerToRemove;
bool handlerCalled = false;

handlerToRemove = _TestEventHandler((eventName, eventData, buffers) {
handlerCalled = true;
holder.removeEventHandler(handlerToRemove);
return true;
Comment thread
ZGaopeng marked this conversation as resolved.
});

holder.addEventHandler(handlerToRemove);

// This should NOT throw error
simulateEventLoop(irisMethodChannel, const IrisEventMessage('test_event', '{}', []));
expect(handlerCalled, true);
Comment thread
ZGaopeng marked this conversation as resolved.
expect(holder.getEventHandlers().length, 0);

// 2. Test adding another handler during event handling
bool firstHandlerCalled = false;
final firstHandler = _TestEventHandler((eventName, eventData, buffers) {
firstHandlerCalled = true;
holder.addEventHandler(_TestEventHandler((_, __, ___) => true));
return false;
});

holder.addEventHandler(firstHandler);

simulateEventLoop(irisMethodChannel, const IrisEventMessage('test_event', '{}', []));
expect(firstHandlerCalled, true);
Comment thread
ZGaopeng marked this conversation as resolved.
expect(holder.getEventHandlers().length, 2); // 1 (firstHandler) + 1 (added handler)
});
}

typedef HandleEventCallback = bool Function(String eventName, String eventData, List<Uint8List> buffers);

class _TestEventHandler extends EventLoopEventHandler with ScopedDisposableObjectMixin implements DisposableObject {
_TestEventHandler(this.callback);
final HandleEventCallback callback;

@override
bool handleEventInternal(String eventName, String eventData, List<Uint8List> buffers) {
return callback(eventName, eventData, buffers);
}

@override
Future<void> dispose() async {}
}
Loading