-
Notifications
You must be signed in to change notification settings - Fork 27.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix: Pressing and releasing a key before the framework has started should not throw #149790
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR looks very promising. I'm eager to see it merged because it is a great improvement for dev experience.
Just spotted two nits.
packages/flutter/test/services/hardware_keyboard_init_test.dart
Outdated
Show resolved
Hide resolved
packages/flutter/test/services/hardware_keyboard_init_test.dart
Outdated
Show resolved
Hide resolved
Co-authored-by: Bruno Leroux <bruno.leroux@gmail.com>
while (true) { | ||
await Future<void>.delayed(const Duration(milliseconds: 20)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should definitely never write code like this :-)
doesn't the framework synchronously add its listeners? (if not, why not?)
can't we just buffer all the events and not discard them? (by having the platform side configure the channel to not discard events, by sending it the message that increases the buffer size) |
(or alternatively, if we do want to just discard all messages, can't the platform side set the buffer size to zero?) |
I've thought about it as well. There are two reasons I don't think it's as good:
|
We can, but isn't this the same process? It's just moving the two lines from the framework to the engine. |
I've just come up with an alternative approach: maybe we just don't recreate Edit: no it doesn't work, since during a hot restart all states are cleared. |
The difference is that if the engine does it, there's never anything buffered, so there's nothing for the framework to worry about. Conceptually it should always be the sending side sending these buffer size messages, not the receiving side. |
We tried doing that on the engine side, but there is a core issue. Here is a part of the summary I wrote in #125975 (comment) " And I think this also applies to
The engine can receive some key events before the Dart isolate has started and at that time the buffering logic is not available because it is written in Dart.
The framework add the listeners asynchronously because it is waiting for the result of a method channel invocation to get the initial pressed state, see #122885. |
Maybe we should move the channel buffers logic up to C++? It sounds like there's a race condition between when the messages can start getting sent, and when the engine side is ready to start buffering them.
We probably should have some sort of callback or Future or binding API to indicate that the keyboard subsystem is ready. This all gets triggered by the binding, so maybe the test should override the binding and hook into the appropriate API to know when it's ready. (This may require some rejiggering of how the keyboard subsystem bootstraps, but that's ok. The key is to do it in a way that doesn't reserve memory in every Flutter app that's for no purpose other than serving this test, of course!) |
Closing it for now since I don't have the bandwidth. Let me revisit it in the future. |
Fixes #125975, which fixes #87391.
Background
Flutter requires that the key event stream conforms "the key event rule". Simply speaking, a pointer repeat event or a pointer up event must occur only when key is considered held down according to Flutter's record, while a pointer down event must occur only when the key is considered released. In debug mode, Flutter throws an assertion on violation.
Problem
The key event rule might be broken during the hot restart, because the event is transmitted and processed asynchronously through the message channel, and at the time the framework reinitializes the keyboard record, the engine might have sent events to the framework.
During hot restart, the Dart VM is recreated but the engine isn't. The engine retains the keyboard state from the last session, meaning there can be keys pressed, but the framework's state starts from zero. Therefore the framework must query the latest keyboard state (i.e. "what keys are being pressed right now?") before assigning the keyboard handler.
However, if any key events occur between when the channelBuffers is created and when getKeyboardState returns, these keys will have been sent out by the engine, unprocessed by any channel handlers and therefore buffered, but are also included in the returned engine state, causing the error.
For example:
When Dart VM hot restarts while key A is pressed, therefore engine.pressedKeys = { 'A' }
While framework is being initialized, key F on the physical keyboard is pressed.
Engine processes the physical event, records engine.pressedKeys = {'A', 'F'} and sends out keyboard event PointerData(down, 'F')
There are currently no handlers for flutter/keyevent, therefore this key event is buffered.
The framework finally initializes the binding and _initKeyboard sends out getKeyboardState and gets the pressed state {'A', 'F'}.
The framework assigns handler to flutter/keyevent, and received PointerData(down, 'F'), and finds that F is already pressed according to its record, and asserts.
An earlier attempt #122885 tried to fix the issue by making the framework query the keyboard state right before it sets the key event handlers. This resolves most, but not all, of the issue, because the message queue can still buffer 1 event, which has been included in the init state sent by the engine and will also be sent to the event handler, causing a crash.
Solution
This PR fixes this issue by clearing the method channels before assigning channel handlers.
Remaining question
Currently there are two problems I'm not sure:
_keyboard.syncKeyboardState().then
and ascheduleMicrotask
, but it's still ugly.ChannelBuffer
's doc it seems the resizing is designed to occur from the engine side. And also, since my intention is to clear the buffer (I don't care about buffer size afterwards though), should we add aclearBuffer
method instead?Pre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.