Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Conversation

@yaakovschectman
Copy link
Contributor

@yaakovschectman yaakovschectman commented Mar 21, 2023

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_CLOSE is 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

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide and the C++, Objective-C, Java style guides.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. See testing the engine for instructions on writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the CLA.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@yaakovschectman
Copy link
Contributor Author

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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@gspencergoog
Copy link
Contributor

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.

In the test, could you intercept the WM_QUIT message in the message loop so that when PostQuitMessage is called, it has no effect until you want to exit the test app?

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);
Copy link
Member

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);
Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

@loic-sharma loic-sharma Mar 21, 2023

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:

  1. Launch engine
  2. Use plugin API to register a top-level window message handler for testing purposes:
  3. Send a WM_CLOSE message. Verify that your plugin's handler was called instead of the Windows embedder's default WM_CLOSE handler

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 WM_CLOSE messages so that it can persist its data to disk when the app shuts down. For example, I could have a plugin that creates "Are you sure you really want to close the app?" popups.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@yaakovschectman
Copy link
Contributor Author

@gspencergoog The issue I see is that the window that receives WM_CLOSE in a flutter application is not part of the engine; it's part of the runner application from the framework side. So there is the question of how/where to pass the message in the first place.

@loic-sharma
Copy link
Member

If you are aware of how to test the behavior of closing the top-level window from within the engine, please inform me.

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:

std::optional<LRESULT> delegate_result =
controller->view->GetEngine()
->window_proc_delegate_manager()
->OnTopLevelWindowProc(hwnd, message, wparam, lparam);

}

bool WindowTopLevelMessageHandler::IsLastWindowOfProcess() {
// TODO(schectman): This currently is not working. Test apps have 3 "windows" to their process.
Copy link
Contributor Author

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).

Copy link
Contributor

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.

@yaakovschectman yaakovschectman marked this pull request as ready for review March 21, 2023 21:40
return false;
}

static BOOL CALLBACK WindowEnumCallback(HWND hwnd, LPARAM user_data) {
Copy link
Contributor

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);
Copy link
Member

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

Copy link
Member

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.

Copy link
Contributor Author

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 {
Copy link
Member

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";
Copy link
Member

@loic-sharma loic-sharma Mar 22, 2023

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"}]';
Copy link
Member

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}}';
Copy link
Member

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);
Copy link
Member

@cbracken cbracken Mar 22, 2023

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));
Copy link
Member

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.

Copy link
Member

@loic-sharma loic-sharma left a 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 :)

@yaakovschectman yaakovschectman merged commit ebc8b04 into flutter:main Mar 24, 2023
@yaakovschectman yaakovschectman deleted the windows_exit branch March 24, 2023 16:44
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Mar 24, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Mar 24, 2023
eyebrowsoffire pushed a commit to eyebrowsoffire/engine that referenced this pull request Mar 27, 2023
…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]>
cbracken added a commit that referenced this pull request Mar 29, 2023
… cancel exit (#40493)" (#40739)

This caused regressions for two internal customers.

Issue: flutter/flutter#123654
Internal issue: b/275565297.

This reverts commit ebc8b04.
yaakovschectman added a commit that referenced this pull request Mar 29, 2023
yaakovschectman added a commit to yaakovschectman/flutter-engine that referenced this pull request Mar 30, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants