Report exceptions from key event handlers and listeners (#91792)
This PR ensures that all 3 ways of handling key events are wrapped with exception handling logic, so that user exceptions do not cause unstable states and are correctly reported.
This commit is contained in:
parent
f84a2dc7bb
commit
5f4ac10d16
@ -514,18 +514,19 @@ class HardwareKeyboard {
|
|||||||
final bool thisResult = handler(event);
|
final bool thisResult = handler(event);
|
||||||
handled = handled || thisResult;
|
handled = handled || thisResult;
|
||||||
} catch (exception, stack) {
|
} catch (exception, stack) {
|
||||||
|
InformationCollector? collector;
|
||||||
|
assert(() {
|
||||||
|
collector = () sync* {
|
||||||
|
yield DiagnosticsProperty<KeyEvent>('Event', event);
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
FlutterError.reportError(FlutterErrorDetails(
|
FlutterError.reportError(FlutterErrorDetails(
|
||||||
exception: exception,
|
exception: exception,
|
||||||
stack: stack,
|
stack: stack,
|
||||||
library: 'services library',
|
library: 'services library',
|
||||||
context: ErrorDescription('while dispatching notifications for $runtimeType'),
|
context: ErrorDescription('while processing a key handler'),
|
||||||
informationCollector: () sync* {
|
informationCollector: collector,
|
||||||
yield DiagnosticsProperty<HardwareKeyboard>(
|
|
||||||
'The $runtimeType sending notification was',
|
|
||||||
this,
|
|
||||||
style: DiagnosticsTreeStyle.errorProperty,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -718,15 +719,20 @@ class KeyEventManager {
|
|||||||
/// Key messages received from the platform are first sent to [RawKeyboard]'s
|
/// Key messages received from the platform are first sent to [RawKeyboard]'s
|
||||||
/// listeners and [HardwareKeyboard]'s handlers, then sent to
|
/// listeners and [HardwareKeyboard]'s handlers, then sent to
|
||||||
/// [keyMessageHandler], regardless of the results of [HardwareKeyboard]'s
|
/// [keyMessageHandler], regardless of the results of [HardwareKeyboard]'s
|
||||||
/// handlers. The result from the handlers and [keyMessageHandler] are
|
/// handlers. The event results from the handlers and [keyMessageHandler] are
|
||||||
/// combined and returned to the platform. The handler result is explained below.
|
/// combined and returned to the platform. The event result is explained
|
||||||
|
/// below.
|
||||||
///
|
///
|
||||||
/// This handler is normally set by the [FocusManager] so that it can control
|
/// For most common applications, which use [WidgetsBinding], this field
|
||||||
/// the key event propagation to focused widgets. Applications that use the
|
/// is set by the focus system (see `FocusManger`) on startup and should not
|
||||||
/// focus system (see [Focus] and [FocusManager]) to receive key events
|
/// be change explicitly.
|
||||||
/// do not need to set this field.
|
|
||||||
///
|
///
|
||||||
/// ## Handler result
|
/// If you are not using the focus system to manage focus, set this
|
||||||
|
/// attribute to a [KeyMessageHandler] that returns true if the propagation
|
||||||
|
/// on the platform should not be continued. If this field is null, key events
|
||||||
|
/// will be assumed to not have been handled by Flutter.
|
||||||
|
///
|
||||||
|
/// ## Event result
|
||||||
///
|
///
|
||||||
/// Key messages on the platform are given to Flutter to be handled by the
|
/// Key messages on the platform are given to Flutter to be handled by the
|
||||||
/// engine. If they are not handled, then the platform will continue to
|
/// engine. If they are not handled, then the platform will continue to
|
||||||
@ -738,20 +744,14 @@ class KeyEventManager {
|
|||||||
/// is not handled by other controls either (such as the "bonk" noise on
|
/// is not handled by other controls either (such as the "bonk" noise on
|
||||||
/// macOS).
|
/// macOS).
|
||||||
///
|
///
|
||||||
/// If you are not using the [FocusManager] to manage focus, set this
|
/// The result from [keyMessageHandler] and [HardwareKeyboard]'s handlers
|
||||||
/// attribute to a [KeyMessageHandler] that returns true if the propagation
|
/// are combined. If any of the handlers claim to handle the event,
|
||||||
/// on the platform should not be continued. Otherwise, key events will be
|
/// the overall result will be "event handled".
|
||||||
/// assumed to not have been handled by Flutter, and will also be sent to
|
|
||||||
/// other (possibly non-Flutter) controls in the application.
|
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [Focus.onKeyEvent], a [Focus] callback attribute that will be given
|
/// * [HardwareKeyboard.addHandler], which accepts multiple global handlers
|
||||||
/// key events distributed by the [FocusManager] based on the current
|
/// to process [KeyEvent]s
|
||||||
/// primary focus.
|
|
||||||
/// * [HardwareKeyboard.addHandler], which accepts multiple handlers to
|
|
||||||
/// control the handler result but only accepts [KeyEvent] instead of
|
|
||||||
/// [KeyMessage].
|
|
||||||
KeyMessageHandler? keyMessageHandler;
|
KeyMessageHandler? keyMessageHandler;
|
||||||
|
|
||||||
final HardwareKeyboard _hardwareKeyboard;
|
final HardwareKeyboard _hardwareKeyboard;
|
||||||
@ -827,7 +827,25 @@ class KeyEventManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (keyMessageHandler != null) {
|
if (keyMessageHandler != null) {
|
||||||
handled = keyMessageHandler!(KeyMessage(_keyEventsSinceLastMessage, rawEvent)) || handled;
|
final KeyMessage message = KeyMessage(_keyEventsSinceLastMessage, rawEvent);
|
||||||
|
try {
|
||||||
|
handled = keyMessageHandler!(message) || handled;
|
||||||
|
} catch (exception, stack) {
|
||||||
|
InformationCollector? collector;
|
||||||
|
assert(() {
|
||||||
|
collector = () sync* {
|
||||||
|
yield DiagnosticsProperty<KeyMessage>('KeyMessage', message);
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
FlutterError.reportError(FlutterErrorDetails(
|
||||||
|
exception: exception,
|
||||||
|
stack: stack,
|
||||||
|
library: 'services library',
|
||||||
|
context: ErrorDescription('while processing the key message handler'),
|
||||||
|
informationCollector: collector,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_keyEventsSinceLastMessage.clear();
|
_keyEventsSinceLastMessage.clear();
|
||||||
|
|
||||||
|
@ -673,9 +673,26 @@ class RawKeyboard {
|
|||||||
);
|
);
|
||||||
// Send the event to passive listeners.
|
// Send the event to passive listeners.
|
||||||
for (final ValueChanged<RawKeyEvent> listener in List<ValueChanged<RawKeyEvent>>.from(_listeners)) {
|
for (final ValueChanged<RawKeyEvent> listener in List<ValueChanged<RawKeyEvent>>.from(_listeners)) {
|
||||||
|
try {
|
||||||
if (_listeners.contains(listener)) {
|
if (_listeners.contains(listener)) {
|
||||||
listener(event);
|
listener(event);
|
||||||
}
|
}
|
||||||
|
} catch (exception, stack) {
|
||||||
|
InformationCollector? collector;
|
||||||
|
assert(() {
|
||||||
|
collector = () sync* {
|
||||||
|
yield DiagnosticsProperty<RawKeyEvent>('Event', event);
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
FlutterError.reportError(FlutterErrorDetails(
|
||||||
|
exception: exception,
|
||||||
|
stack: stack,
|
||||||
|
library: 'services library',
|
||||||
|
context: ErrorDescription('while processing a raw key listener'),
|
||||||
|
informationCollector: collector,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -250,4 +250,107 @@ void main() {
|
|||||||
expect(events.length, 1);
|
expect(events.length, 1);
|
||||||
expect(rawEvents.length, 2);
|
expect(rawEvents.length, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Exceptions from keyMessageHandler are caught and reported', (WidgetTester tester) async {
|
||||||
|
final KeyMessageHandler? oldKeyMessageHandler = tester.binding.keyEventManager.keyMessageHandler;
|
||||||
|
addTearDown(() {
|
||||||
|
tester.binding.keyEventManager.keyMessageHandler = oldKeyMessageHandler;
|
||||||
|
});
|
||||||
|
|
||||||
|
// When keyMessageHandler throws an error...
|
||||||
|
tester.binding.keyEventManager.keyMessageHandler = (KeyMessage message) {
|
||||||
|
throw 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simulate a key down event.
|
||||||
|
FlutterErrorDetails? record;
|
||||||
|
await _runWhileOverridingOnError(
|
||||||
|
() => simulateKeyDownEvent(LogicalKeyboardKey.keyA),
|
||||||
|
onError: (FlutterErrorDetails details) {
|
||||||
|
record = details;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ... the error should be caught.
|
||||||
|
expect(record, isNotNull);
|
||||||
|
expect(record!.exception, 1);
|
||||||
|
final Map<String, DiagnosticsNode> infos = _groupDiagnosticsByName(record!.informationCollector!());
|
||||||
|
expect(infos['KeyMessage'], isA<DiagnosticsProperty<KeyMessage>>());
|
||||||
|
|
||||||
|
// But the exception should not interrupt recording the state.
|
||||||
|
// Now the keyMessageHandler no longer throws an error.
|
||||||
|
tester.binding.keyEventManager.keyMessageHandler = null;
|
||||||
|
record = null;
|
||||||
|
|
||||||
|
// Simulate a key up event.
|
||||||
|
await _runWhileOverridingOnError(
|
||||||
|
() => simulateKeyUpEvent(LogicalKeyboardKey.keyA),
|
||||||
|
onError: (FlutterErrorDetails details) {
|
||||||
|
record = details;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// If the previous state (key down) wasn't recorded, this key up event will
|
||||||
|
// trigger assertions.
|
||||||
|
expect(record, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Exceptions from HardwareKeyboard handlers are caught and reported', (WidgetTester tester) async {
|
||||||
|
bool throwingCallback(KeyEvent event) {
|
||||||
|
throw 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the handler throws an error...
|
||||||
|
HardwareKeyboard.instance.addHandler(throwingCallback);
|
||||||
|
|
||||||
|
// Simulate a key down event.
|
||||||
|
FlutterErrorDetails? record;
|
||||||
|
await _runWhileOverridingOnError(
|
||||||
|
() => simulateKeyDownEvent(LogicalKeyboardKey.keyA),
|
||||||
|
onError: (FlutterErrorDetails details) {
|
||||||
|
record = details;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ... the error should be caught.
|
||||||
|
expect(record, isNotNull);
|
||||||
|
expect(record!.exception, 1);
|
||||||
|
final Map<String, DiagnosticsNode> infos = _groupDiagnosticsByName(record!.informationCollector!());
|
||||||
|
expect(infos['Event'], isA<DiagnosticsProperty<KeyEvent>>());
|
||||||
|
|
||||||
|
// But the exception should not interrupt recording the state.
|
||||||
|
// Now the key handler no longer throws an error.
|
||||||
|
HardwareKeyboard.instance.removeHandler(throwingCallback);
|
||||||
|
record = null;
|
||||||
|
|
||||||
|
// Simulate a key up event.
|
||||||
|
await _runWhileOverridingOnError(
|
||||||
|
() => simulateKeyUpEvent(LogicalKeyboardKey.keyA),
|
||||||
|
onError: (FlutterErrorDetails details) {
|
||||||
|
record = details;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// If the previous state (key down) wasn't recorded, this key up event will
|
||||||
|
// trigger assertions.
|
||||||
|
expect(record, isNull);
|
||||||
|
}, variant: KeySimulatorTransitModeVariant.all());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Future<void> _runWhileOverridingOnError(AsyncCallback body, {required FlutterExceptionHandler onError}) async {
|
||||||
|
final FlutterExceptionHandler? oldFlutterErrorOnError = FlutterError.onError;
|
||||||
|
FlutterError.onError = onError;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await body();
|
||||||
|
} finally {
|
||||||
|
FlutterError.onError = oldFlutterErrorOnError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, DiagnosticsNode> _groupDiagnosticsByName(Iterable<DiagnosticsNode> infos) {
|
||||||
|
return Map<String, DiagnosticsNode>.fromIterable(
|
||||||
|
infos,
|
||||||
|
key: (dynamic node) => (node as DiagnosticsNode).name ?? '',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -753,6 +753,46 @@ void main() {
|
|||||||
expect(logs, <int>[1, 3, 2]);
|
expect(logs, <int>[1, 3, 2]);
|
||||||
logs.clear();
|
logs.clear();
|
||||||
}, variant: KeySimulatorTransitModeVariant.all());
|
}, variant: KeySimulatorTransitModeVariant.all());
|
||||||
|
|
||||||
|
testWidgets('Exceptions from RawKeyboard listeners are caught and reported', (WidgetTester tester) async {
|
||||||
|
void throwingListener(RawKeyEvent event) {
|
||||||
|
throw 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the listener throws an error...
|
||||||
|
RawKeyboard.instance.addListener(throwingListener);
|
||||||
|
|
||||||
|
// Simulate a key down event.
|
||||||
|
FlutterErrorDetails? record;
|
||||||
|
await _runWhileOverridingOnError(
|
||||||
|
() => simulateKeyDownEvent(LogicalKeyboardKey.keyA),
|
||||||
|
onError: (FlutterErrorDetails details) {
|
||||||
|
record = details;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ... the error should be caught.
|
||||||
|
expect(record, isNotNull);
|
||||||
|
expect(record!.exception, 1);
|
||||||
|
final Map<String, DiagnosticsNode> infos = _groupDiagnosticsByName(record!.informationCollector!());
|
||||||
|
expect(infos['Event'], isA<DiagnosticsProperty<RawKeyEvent>>());
|
||||||
|
|
||||||
|
// But the exception should not interrupt recording the state.
|
||||||
|
// Now the key handler no longer throws an error.
|
||||||
|
RawKeyboard.instance.removeListener(throwingListener);
|
||||||
|
record = null;
|
||||||
|
|
||||||
|
// Simulate a key up event.
|
||||||
|
await _runWhileOverridingOnError(
|
||||||
|
() => simulateKeyUpEvent(LogicalKeyboardKey.keyA),
|
||||||
|
onError: (FlutterErrorDetails details) {
|
||||||
|
record = details;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// If the previous state (key down) wasn't recorded, this key up event will
|
||||||
|
// trigger assertions.
|
||||||
|
expect(record, isNull);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('RawKeyEventDataAndroid', () {
|
group('RawKeyEventDataAndroid', () {
|
||||||
@ -2504,3 +2544,21 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _runWhileOverridingOnError(AsyncCallback body, {required FlutterExceptionHandler onError}) async {
|
||||||
|
final FlutterExceptionHandler? oldFlutterErrorOnError = FlutterError.onError;
|
||||||
|
FlutterError.onError = onError;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await body();
|
||||||
|
} finally {
|
||||||
|
FlutterError.onError = oldFlutterErrorOnError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, DiagnosticsNode> _groupDiagnosticsByName(Iterable<DiagnosticsNode> infos) {
|
||||||
|
return Map<String, DiagnosticsNode>.fromIterable(
|
||||||
|
infos,
|
||||||
|
key: (dynamic node) => (node as DiagnosticsNode).name ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user