-
Notifications
You must be signed in to change notification settings - Fork 6k
Listen to WM_CLOSE message on Windows to allow framework to cancel exit
#40493
Conversation
|
This PR has no tests yet. If you are aware of how to test the behavior of closing the top-level window from within the engine, please inform me. |
| // static | ||
| bool PlatformHandler::WindowProcCallback(HWND hwnd, UINT msg, WPARAM wpar, LPARAM lpar, void* user_data, LRESULT* result) { | ||
| PlatformHandler* that = static_cast<PlatformHandler*>(user_data); | ||
| if (msg == WM_CLOSE) { |
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 blocks the window from closing, and assumes that closing the window is meant to close the app. What happens when we eventually have multiple windows, won't we have to check to make sure this is the last window before asking? Also, is there a case where the developer might want the window to actually close, but then keep running the app until they are ready to exit?
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.
It would require a check if it is the last window, but I do not think that check is possible to write yet.
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.
Could you use a Windows API to find how many open windows an app has? I think that even when we can enumerate Flutter-controlled windows in an app, we won't want to actually exit the app unless this is the last window, including windows that aren't Flutter controlled.
In the test, could you intercept the WM_QUIT message in the message loop so that when I think you could also write this as an integration test that logs when it is about to quit, and run the app in a separate process and see if it quits. We might not have the infrastructure set up for that, though, so I'm not sure how big a lift that is. |
| return std::make_unique<ScopedClipboard>(); | ||
| }; | ||
| } | ||
| FlutterDesktopPluginRegistrarRegisterTopLevelWindowProcDelegate(engine_->GetRegistrar(), PlatformHandler::WindowProcCallback, this); |
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.
Instead of using the plugin API, could we instead use WindowProcDelegateManager::RegisterTopLevelWindowProcDelegate directly?
| return std::make_unique<ScopedClipboard>(); | ||
| }; | ||
| } | ||
| FlutterDesktopPluginRegistrarRegisterTopLevelWindowProcDelegate(engine_->GetRegistrar(), PlatformHandler::WindowProcCallback, this); |
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.
Plugins should be able to override the Windows embedder's behavior. In other words, plugins' top-level window handlers should run before the Windows embedder's top-level window handlers. Please add a test to cover this behavior.
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.
How would we go about testing that within the engine? What would we actually be trying to confirm?
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.
An example test could be:
- Launch engine
- Use plugin API to register a top-level window message handler for testing purposes:
- Send a
WM_CLOSEmessage. Verify that your plugin's handler was called instead of the Windows embedder's defaultWM_CLOSEhandler
What would we actually be trying to confirm?
We want to confirm that plugins' top-level window handlers run before the Windows embedder's top-level window handlers. For example, imagine I have a database plugin that buffers updates in memory for performance reasons. This database plugin may want to listen to For example, I could have a plugin that creates "Are you sure you really want to close the app?" popups.WM_CLOSE messages so that it can persist its data to disk when the app shuts down.
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.
That makes sense. Though do we have a way of sending a window message to a top level window for our tests? I'm not aware of anywhere we already do something like this.
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.
It doesn't change your valid point, and I realize that's just an example, but saving data on shutdown is an anti-pattern: there are plenty of ways for an app to close without sending a WM_CLOSE that can be handled, so we don't really want to encourage people to do that (it's the reason we eliminated the exiting state from the app state design doc).
I think a better example would be so that a plugin could abort the close if it wanted to.
|
@gspencergoog The issue I see is that the window that receives |
Can you craft Windows messages, similar to how we craft Windows messages to the Flutter view? You can use this logic to send top-level windows messages to the Windows embedder: engine/shell/platform/windows/flutter_windows.cc Lines 117 to 120 in 87ddd91
|
| } | ||
|
|
||
| bool WindowTopLevelMessageHandler::IsLastWindowOfProcess() { | ||
| // TODO(schectman): This currently is not working. Test apps have 3 "windows" to their process. |
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.
@gspencergoog I am here attempting to use the win32 API's functions to enumerate the windows of each thread of the current process, and only translate a WM_CLOSE message to a request to exit if only 1 window is found. However, when testing with 1-window apps, it reports there being 3 windows. I suspect one of these is the embedder window, one the top level window, and I am unsure about the third. One possibility may be to write a bool FlutterWindowsEngine::IsOwnedWindow(HWND) that returns true when the provided handle is present in a set of one HWND per view of the engine (which would currently only be one).
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.
Oh, I see. Yeah, we'd have to only count top level windows.
| return false; | ||
| } | ||
|
|
||
| static BOOL CALLBACK WindowEnumCallback(HWND hwnd, LPARAM user_data) { |
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.
Could you call GetParent on the window here and only count the ones that return NULL?
|
|
||
| class WindowTopLevelMessageHandler { | ||
| public: | ||
| WindowTopLevelMessageHandler(FlutterWindowsEngine& engine); |
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.
Could this accept a PlatformHandler instead of the engine? That would allow us to remove the RequestApplicationQuit from the engine
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.
Alternatively, I would consider making this class agnostic to Flutter. You could introduce a WindowsLifecycleDelegate with methods like OnExitRequested. The FlutterWindowsEngine could implement this lifecycle delegate and call the PlatformHandler::RequestAppExit.
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.
The former can be done so long as we expose the engine's platform handler, basically replacing RequestApplicationQuit with a getter for it. In the case of the latter, what is the advantage of making it agnostic to Flutter?
|
|
||
| class FlutterWindowsEngine; | ||
|
|
||
| class WindowTopLevelMessageHandler { |
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.
Please document type and methods
|
|
||
| namespace flutter { | ||
|
|
||
| static constexpr char kExitTypeCancelable[] = "cancelable"; |
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.
Could we move this string out of this class? Ideally all strings for Flutter framework messages are contained in the PlatformHandler.
| void exitTestExit() async { | ||
| final Completer<ByteData?> closed = Completer<ByteData?>(); | ||
| ui.channelBuffers.setListener('flutter/platform', (ByteData? data, ui.PlatformMessageResponseCallback callback) async { | ||
| const String jsonString = '[{"response":"exit"}]'; |
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.
Optional: rather than hand-writing JSON, consider doing something like:
final String jsonString = json.encode(<String, dynamic>{"response": "exit"});Mostly because that allows the compiler to catch Dart syntax errors and json.encode to catch JSON syntax errors.
|
|
||
| // From here down, nothing should be called, because the application will have already been closed. | ||
| final Completer<ByteData?> exited = Completer<ByteData?>(); | ||
| const String jsonString = '{"method":"System.exitApplication","args":{"type":"required","exitCode":0}}'; |
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.
Similarly here, using json.encode to encode a map would allow the map to be laid out the way it's structured:
final String jsonString = json.encode(<String, dynamic>{
"method": "System.exitApplication",
"args": <String, dynamic>{
"type": "required",
"exitCode": 0,
}
};|
|
||
| class WindowsLifecycleManager { | ||
| public: | ||
| WindowsLifecycleManager(FlutterWindowsEngine& engine); |
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.
Since we've got a reference cycle between FlutterWindowsEngine and WindowsLifecycleManager this should be a weak pointer (just a raw FlutterWindowsEngine* pointer) to indicate that to the reader.
See notes here: https://google.github.io/styleguide/cppguide.html#Inputs_and_Outputs
| static BOOL CALLBACK WindowEnumCallback(HWND hwnd, LPARAM user_data) { | ||
| HWND parent = GetParent(hwnd); | ||
| if (parent == NULL) { | ||
| int64_t& count = *static_cast<int64_t*>(reinterpret_cast<void*>(user_data)); |
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.
Just reinterpret_cast<int64_t*>(user_data) directly.
Co-authored-by: Chris Bracken <[email protected]>
Co-authored-by: Chris Bracken <[email protected]>
Co-authored-by: Loïc Sharma <[email protected]>
Co-authored-by: Loïc Sharma <[email protected]>
Co-authored-by: Loïc Sharma <[email protected]>
05421e2 to
69138f9
Compare
loic-sharma
left a comment
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.
Great work! Don't forget to address the wParam exit code issue before merging :)
…exit (flutter#40493) * Intercept WM_CLOSE * Messy but framework is in place * Test exit and cancel * Try to test for windows * Check for parent HWND * Move string to PlatformHandler class * Rename lifecycle manageR * Change condition for headless * Move window proc to lambda * Formatting and licenses * Encode JSON dart values * Clean up lifecycle * PR feedback * Update shell/platform/windows/platform_handler.h Co-authored-by: Chris Bracken <[email protected]> * Update shell/platform/windows/windows_lifecycle_manager.cc Co-authored-by: Chris Bracken <[email protected]> * Update shell/platform/windows/windows_lifecycle_manager.cc Co-authored-by: Chris Bracken <[email protected]> * Static cast enum to int * Formatting * Update shell/platform/windows/testing/engine_modifier.h Co-authored-by: Loïc Sharma <[email protected]> * Update shell/platform/windows/windows_lifecycle_manager.cc Co-authored-by: Loïc Sharma <[email protected]> * Update shell/platform/windows/platform_handler.cc Co-authored-by: Loïc Sharma <[email protected]> * Update unit tests * PR Feedback * PR Feedback * Constexpr * Formatting * Wparam --------- Co-authored-by: Chris Bracken <[email protected]> Co-authored-by: Loïc Sharma <[email protected]>
… cancel exit (#40493)" (#40739) This caused regressions for two internal customers. Issue: flutter/flutter#123654 Internal issue: b/275565297. This reverts commit ebc8b04.
…ework to cancel exit (flutter#40493)" (flutter#40739)" This reverts commit 22f27ad.
Register the platform handler to receive top level window messages and allow the framework to cancel an exit. When the user closes the application window,
WM_CLOSEis sent to the top level window, which triggers a request from the engine to the framework, which it can pass to any registered observers before returning the result to the engine.Addresses windows portion of flutter/flutter#30735
Design doc: https://flutter.dev/go/application-lifecycle-events
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.