Re-introduce WM_CLOSE listener, new quit protocol posts a second message to next handler (#40802)
With this approach, when the last window of an app is closed, the engine
sends a message to the framework to check if it is allowed to exit. If
it may, the windows lifecycle manager synthesizes a second `WM_CLOSE`
message that it will ignore, and so the next registered top level window
proc delegate, if any, will be able to process the message. If none do
so, the message will be handled by the default window proc, so the app
will be able to close.
I was not able to get a full system tray example application running to
test this, but I could get an application that stays open when its
window is closed and can be seen as a system tray icon as long as it is
running, albeit the icon was non-functional. As this repro app still
exhibited this behavior when using this engine build, I am reasonably
confident in concluding that applications that want to be able to run
headless when their windows close will function properly.
Addresses https://github.com/flutter/flutter/issues/123654.
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide] and the [C++,
Objective-C, Java style guides].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] 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.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [ ] I signed the [CLA].
- [x] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
---------
Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com>
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 387f2de..12e042c 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -3129,6 +3129,8 @@
ORIGIN: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/window_state.h + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/shell/platform/windows/windows_lifecycle_manager.cc + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/shell/platform/windows/windows_lifecycle_manager.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/windows_proc_table.cc + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/windows_proc_table.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/windows/windows_registry.cc + ../../../flutter/LICENSE
@@ -5730,6 +5732,8 @@
FILE: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.cc
FILE: ../../../flutter/shell/platform/windows/window_proc_delegate_manager.h
FILE: ../../../flutter/shell/platform/windows/window_state.h
+FILE: ../../../flutter/shell/platform/windows/windows_lifecycle_manager.cc
+FILE: ../../../flutter/shell/platform/windows/windows_lifecycle_manager.h
FILE: ../../../flutter/shell/platform/windows/windows_proc_table.cc
FILE: ../../../flutter/shell/platform/windows/windows_proc_table.h
FILE: ../../../flutter/shell/platform/windows/windows_registry.cc
diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn
index 1f7a850..db9053a 100644
--- a/shell/platform/windows/BUILD.gn
+++ b/shell/platform/windows/BUILD.gn
@@ -103,6 +103,8 @@
"window_proc_delegate_manager.cc",
"window_proc_delegate_manager.h",
"window_state.h",
+ "windows_lifecycle_manager.cc",
+ "windows_lifecycle_manager.h",
"windows_proc_table.cc",
"windows_proc_table.h",
"windows_registry.cc",
diff --git a/shell/platform/windows/fixtures/main.dart b/shell/platform/windows/fixtures/main.dart
index 1e8d5a9..c9794f6 100644
--- a/shell/platform/windows/fixtures/main.dart
+++ b/shell/platform/windows/fixtures/main.dart
@@ -6,6 +6,7 @@
import 'dart:io' as io;
import 'dart:typed_data' show ByteData, Uint8List;
import 'dart:ui' as ui;
+import 'dart:convert';
// Signals a waiting latch in the native test.
@pragma('vm:external-name', 'Signal')
@@ -87,6 +88,66 @@
}
@pragma('vm:entry-point')
+void exitTestExit() async {
+ final Completer<ByteData?> closed = Completer<ByteData?>();
+ ui.channelBuffers.setListener('flutter/platform', (ByteData? data, ui.PlatformMessageResponseCallback callback) async {
+ final String jsonString = json.encode(<Map<String, String>>[{'response': 'exit'}]);
+ final ByteData responseData = ByteData.sublistView(Uint8List.fromList(utf8.encode(jsonString)));
+ callback(responseData);
+ closed.complete(data);
+ });
+ await closed.future;
+
+ // From here down, nothing should be called, because the application will have already been closed.
+ final Completer<ByteData?> exited = Completer<ByteData?>();
+ final String jsonString = json.encode(<String, dynamic>{
+ 'method': 'System.exitApplication',
+ 'args': <String, dynamic>{
+ 'type': 'required', 'exitCode': 0
+ }
+ });
+ ui.PlatformDispatcher.instance.sendPlatformMessage(
+ 'flutter/platform',
+ ByteData.sublistView(
+ Uint8List.fromList(utf8.encode(jsonString))
+ ),
+ (ByteData? reply) {
+ exited.complete(reply);
+ });
+ await exited.future;
+}
+
+@pragma('vm:entry-point')
+void exitTestCancel() async {
+ final Completer<ByteData?> closed = Completer<ByteData?>();
+ ui.channelBuffers.setListener('flutter/platform', (ByteData? data, ui.PlatformMessageResponseCallback callback) async {
+ final String jsonString = json.encode(<Map<String, String>>[{'response': 'cancel'}]);
+ final ByteData responseData = ByteData.sublistView(Uint8List.fromList(utf8.encode(jsonString)));
+ callback(responseData);
+ closed.complete(data);
+ });
+ await closed.future;
+
+ // Because the request was canceled, the below shall execute.
+ final Completer<ByteData?> exited = Completer<ByteData?>();
+ final String jsonString = json.encode(<String, dynamic>{
+ 'method': 'System.exitApplication',
+ 'args': <String, dynamic>{
+ 'type': 'required', 'exitCode': 0
+ }
+ });
+ ui.PlatformDispatcher.instance.sendPlatformMessage(
+ 'flutter/platform',
+ ByteData.sublistView(
+ Uint8List.fromList(utf8.encode(jsonString))
+ ),
+ (ByteData? reply) {
+ exited.complete(reply);
+ });
+ await exited.future;
+}
+
+@pragma('vm:entry-point')
void customEntrypoint() {}
@pragma('vm:entry-point')
diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc
index d3c9332..8a71913 100644
--- a/shell/platform/windows/flutter_windows_engine.cc
+++ b/shell/platform/windows/flutter_windows_engine.cc
@@ -161,7 +161,8 @@
std::unique_ptr<WindowsRegistry> registry)
: project_(std::make_unique<FlutterProjectBundle>(project)),
aot_data_(nullptr, nullptr),
- windows_registry_(std::move(registry)) {
+ windows_registry_(std::move(registry)),
+ lifecycle_manager_(std::make_unique<WindowsLifecycleManager>(this)) {
embedder_api_.struct_size = sizeof(FlutterEngineProcTable);
FlutterEngineGetProcAddresses(&embedder_api_);
@@ -203,6 +204,17 @@
std::make_unique<FlutterWindowsTextureRegistrar>(this, gl_procs_);
surface_manager_ = AngleSurfaceManager::Create();
window_proc_delegate_manager_ = std::make_unique<WindowProcDelegateManager>();
+ window_proc_delegate_manager_->RegisterTopLevelWindowProcDelegate(
+ [](HWND hwnd, UINT msg, WPARAM wpar, LPARAM lpar, void* user_data,
+ LRESULT* result) {
+ BASE_DCHECK(user_data);
+ FlutterWindowsEngine* that =
+ static_cast<FlutterWindowsEngine*>(user_data);
+ BASE_DCHECK(that->lifecycle_manager_);
+ return that->lifecycle_manager_->WindowProc(hwnd, msg, wpar, lpar,
+ result);
+ },
+ static_cast<void*>(this));
// Set up internal channels.
// TODO: Replace this with an embedder.h API. See
@@ -750,4 +762,18 @@
reinterpret_cast<const uint8_t*>(""), 0);
}
+void FlutterWindowsEngine::RequestApplicationQuit(HWND hwnd,
+ WPARAM wparam,
+ LPARAM lparam,
+ AppExitType exit_type) {
+ platform_handler_->RequestAppExit(hwnd, wparam, lparam, exit_type, 0);
+}
+
+void FlutterWindowsEngine::OnQuit(std::optional<HWND> hwnd,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ UINT exit_code) {
+ lifecycle_manager_->Quit(hwnd, wparam, lparam, exit_code);
+}
+
} // namespace flutter
diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h
index 01de0d3..a3e66d2 100644
--- a/shell/platform/windows/flutter_windows_engine.h
+++ b/shell/platform/windows/flutter_windows_engine.h
@@ -35,6 +35,7 @@
#include "flutter/shell/platform/windows/text_input_plugin.h"
#include "flutter/shell/platform/windows/window_proc_delegate_manager.h"
#include "flutter/shell/platform/windows/window_state.h"
+#include "flutter/shell/platform/windows/windows_lifecycle_manager.h"
#include "flutter/shell/platform/windows/windows_registry.h"
#include "third_party/rapidjson/include/rapidjson/document.h"
@@ -254,6 +255,18 @@
// Updates accessibility, e.g. switch to high contrast mode
void UpdateAccessibilityFeatures(FlutterAccessibilityFeature flags);
+ // Called when the application quits in response to a quit request.
+ void OnQuit(std::optional<HWND> hwnd,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ UINT exit_code);
+
+ // Called when a WM_CLOSE message is received.
+ void RequestApplicationQuit(HWND hwnd,
+ WPARAM wparam,
+ LPARAM lparam,
+ AppExitType exit_type);
+
protected:
// Creates the keyboard key handler.
//
@@ -395,6 +408,9 @@
// Wrapper providing Windows registry access.
std::unique_ptr<WindowsRegistry> windows_registry_;
+ // Handler for top level window messages.
+ std::unique_ptr<WindowsLifecycleManager> lifecycle_manager_;
+
FML_DISALLOW_COPY_AND_ASSIGN(FlutterWindowsEngine);
};
diff --git a/shell/platform/windows/flutter_windows_engine_unittests.cc b/shell/platform/windows/flutter_windows_engine_unittests.cc
index f0d9492..f21e3aa 100644
--- a/shell/platform/windows/flutter_windows_engine_unittests.cc
+++ b/shell/platform/windows/flutter_windows_engine_unittests.cc
@@ -8,6 +8,7 @@
#include "flutter/shell/platform/embedder/embedder.h"
#include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h"
#include "flutter/shell/platform/windows/flutter_windows_view.h"
+#include "flutter/shell/platform/windows/public/flutter_windows.h"
#include "flutter/shell/platform/windows/testing/engine_modifier.h"
#include "flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h"
#include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h"
@@ -619,5 +620,220 @@
}
}
+class MockWindowsLifecycleManager : public WindowsLifecycleManager {
+ public:
+ MockWindowsLifecycleManager(FlutterWindowsEngine* engine)
+ : WindowsLifecycleManager(engine) {}
+ virtual ~MockWindowsLifecycleManager() {}
+
+ MOCK_METHOD4(Quit,
+ void(std::optional<HWND>,
+ std::optional<WPARAM>,
+ std::optional<LPARAM>,
+ UINT));
+ MOCK_METHOD4(DispatchMessage, void(HWND, UINT, WPARAM, LPARAM));
+ MOCK_METHOD0(IsLastWindowOfProcess, bool(void));
+};
+
+TEST_F(FlutterWindowsEngineTest, TestExit) {
+ FlutterWindowsEngineBuilder builder{GetContext()};
+ builder.SetDartEntrypoint("exitTestExit");
+ bool finished = false;
+ bool did_call = false;
+
+ std::unique_ptr<FlutterWindowsEngine> engine = builder.Build();
+
+ EngineModifier modifier(engine.get());
+ modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };
+ auto handler = std::make_unique<MockWindowsLifecycleManager>(engine.get());
+ ON_CALL(*handler, Quit)
+ .WillByDefault(
+ [&finished](std::optional<HWND> hwnd, std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ UINT exit_code) { finished = exit_code == 0; });
+ ON_CALL(*handler, IsLastWindowOfProcess).WillByDefault([]() { return true; });
+ EXPECT_CALL(*handler, Quit).Times(1);
+ modifier.SetLifecycleManager(std::move(handler));
+
+ auto binary_messenger =
+ std::make_unique<BinaryMessengerImpl>(engine->messenger());
+ // If this callback is triggered, the test fails. The exit procedure should be
+ // called without checking with the framework.
+ binary_messenger->SetMessageHandler(
+ "flutter/platform", [&did_call](const uint8_t* message,
+ size_t message_size, BinaryReply reply) {
+ did_call = true;
+ char response[] = "";
+ reply(reinterpret_cast<uint8_t*>(response), 0);
+ });
+
+ engine->Run();
+
+ engine->window_proc_delegate_manager()->OnTopLevelWindowProc(0, WM_CLOSE, 0,
+ 0);
+
+ while (!finished) {
+ engine->task_runner()->ProcessTasks();
+ }
+
+ EXPECT_FALSE(did_call);
+}
+
+TEST_F(FlutterWindowsEngineTest, TestExitCancel) {
+ FlutterWindowsEngineBuilder builder{GetContext()};
+ builder.SetDartEntrypoint("exitTestCancel");
+ bool finished = false;
+ bool did_call = false;
+
+ std::unique_ptr<FlutterWindowsEngine> engine = builder.Build();
+
+ EngineModifier modifier(engine.get());
+ modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };
+ auto handler = std::make_unique<MockWindowsLifecycleManager>(engine.get());
+ ON_CALL(*handler, Quit)
+ .WillByDefault([&finished](std::optional<HWND> hwnd,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ UINT exit_code) { finished = true; });
+ ON_CALL(*handler, IsLastWindowOfProcess).WillByDefault([]() { return true; });
+ EXPECT_CALL(*handler, Quit).Times(0);
+ modifier.SetLifecycleManager(std::move(handler));
+
+ auto binary_messenger =
+ std::make_unique<BinaryMessengerImpl>(engine->messenger());
+ binary_messenger->SetMessageHandler(
+ "flutter/platform", [&did_call](const uint8_t* message,
+ size_t message_size, BinaryReply reply) {
+ std::string contents(message, message + message_size);
+ EXPECT_NE(contents.find("\"method\":\"System.exitApplication\""),
+ std::string::npos);
+ EXPECT_NE(contents.find("\"type\":\"required\""), std::string::npos);
+ EXPECT_NE(contents.find("\"exitCode\":0"), std::string::npos);
+ did_call = true;
+ char response[] = "";
+ reply(reinterpret_cast<uint8_t*>(response), 0);
+ });
+
+ engine->Run();
+
+ engine->window_proc_delegate_manager()->OnTopLevelWindowProc(0, WM_CLOSE, 0,
+ 0);
+
+ while (!did_call) {
+ engine->task_runner()->ProcessTasks();
+ }
+
+ EXPECT_FALSE(finished);
+}
+
+TEST_F(FlutterWindowsEngineTest, TestExitSecondCloseMessage) {
+ FlutterWindowsEngineBuilder builder{GetContext()};
+ builder.SetDartEntrypoint("exitTestExit");
+ bool did_call = false;
+ bool second_close = false;
+
+ std::unique_ptr<FlutterWindowsEngine> engine = builder.Build();
+
+ EngineModifier modifier(engine.get());
+ modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };
+ auto handler = std::make_unique<MockWindowsLifecycleManager>(engine.get());
+ ON_CALL(*handler, IsLastWindowOfProcess).WillByDefault([]() { return true; });
+ ON_CALL(*handler, Quit)
+ .WillByDefault([&handler](std::optional<HWND> hwnd,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam, UINT exit_code) {
+ handler->WindowsLifecycleManager::Quit(hwnd, wparam, lparam, exit_code);
+ });
+ ON_CALL(*handler, DispatchMessage)
+ .WillByDefault(
+ [&engine](HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
+ engine->window_proc_delegate_manager()->OnTopLevelWindowProc(
+ hwnd, msg, wparam, lparam);
+ });
+ modifier.SetLifecycleManager(std::move(handler));
+
+ auto binary_messenger =
+ std::make_unique<BinaryMessengerImpl>(engine->messenger());
+ binary_messenger->SetMessageHandler(
+ "flutter/platform", [&did_call](const uint8_t* message,
+ size_t message_size, BinaryReply reply) {
+ did_call = true;
+ char response[] = "";
+ reply(reinterpret_cast<uint8_t*>(response), 0);
+ });
+
+ engine->Run();
+
+ // This delegate will be registered after the lifecycle manager, so it will be
+ // called only when a message is not consumed by the lifecycle manager. This
+ // should be called on the second, synthesized WM_CLOSE message that the
+ // lifecycle manager posts.
+ engine->window_proc_delegate_manager()->RegisterTopLevelWindowProcDelegate(
+ [](HWND hwnd, UINT message, WPARAM wpar, LPARAM lpar, void* user_data,
+ LRESULT* result) {
+ switch (message) {
+ case WM_CLOSE: {
+ bool* called = reinterpret_cast<bool*>(user_data);
+ *called = true;
+ return true;
+ }
+ }
+ return false;
+ },
+ reinterpret_cast<void*>(&second_close));
+
+ engine->window_proc_delegate_manager()->OnTopLevelWindowProc(0, WM_CLOSE, 0,
+ 0);
+
+ while (!second_close) {
+ engine->task_runner()->ProcessTasks();
+ }
+
+ EXPECT_FALSE(did_call);
+}
+
+TEST_F(FlutterWindowsEngineTest, TestExitCloseMultiWindow) {
+ FlutterWindowsEngineBuilder builder{GetContext()};
+ builder.SetDartEntrypoint("exitTestExit");
+ bool finished = false;
+ bool did_call = false;
+
+ std::unique_ptr<FlutterWindowsEngine> engine = builder.Build();
+
+ EngineModifier modifier(engine.get());
+ modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; };
+ auto handler = std::make_unique<MockWindowsLifecycleManager>(engine.get());
+ ON_CALL(*handler, IsLastWindowOfProcess).WillByDefault([&finished]() {
+ finished = true;
+ return false;
+ });
+ // Quit should not be called when there is more than one window.
+ EXPECT_CALL(*handler, Quit).Times(0);
+ modifier.SetLifecycleManager(std::move(handler));
+
+ auto binary_messenger =
+ std::make_unique<BinaryMessengerImpl>(engine->messenger());
+ // If this callback is triggered, the test fails. The exit procedure should be
+ // called without checking with the framework.
+ binary_messenger->SetMessageHandler(
+ "flutter/platform", [&did_call](const uint8_t* message,
+ size_t message_size, BinaryReply reply) {
+ did_call = true;
+ char response[] = "";
+ reply(reinterpret_cast<uint8_t*>(response), 0);
+ });
+
+ engine->Run();
+
+ engine->window_proc_delegate_manager()->OnTopLevelWindowProc(0, WM_CLOSE, 0,
+ 0);
+
+ while (!finished) {
+ engine->task_runner()->ProcessTasks();
+ }
+
+ EXPECT_FALSE(did_call);
+}
+
} // namespace testing
} // namespace flutter
diff --git a/shell/platform/windows/platform_handler.cc b/shell/platform/windows/platform_handler.cc
index a33710b..ab9a79a 100644
--- a/shell/platform/windows/platform_handler.cc
+++ b/shell/platform/windows/platform_handler.cc
@@ -28,8 +28,6 @@
static constexpr char kExitCodeKey[] = "exitCode";
static constexpr char kExitTypeKey[] = "type";
-static constexpr char kExitTypeCancelable[] = "cancelable";
-static constexpr char kExitTypeRequired[] = "required";
static constexpr char kExitResponseKey[] = "response";
static constexpr char kExitResponseCancel[] = "cancel";
@@ -44,6 +42,10 @@
static constexpr int kAccessDeniedErrorCode = 5;
static constexpr int kErrorSuccess = 0;
+static constexpr char kExitRequestError[] = "ExitApplication error";
+static constexpr char kInvalidExitRequestMessage[] =
+ "Invalid application exit request";
+
namespace flutter {
namespace {
@@ -205,6 +207,16 @@
} // namespace
+static AppExitType StringToAppExitType(const std::string& string) {
+ if (string.compare(PlatformHandler::kExitTypeRequired) == 0) {
+ return AppExitType::required;
+ } else if (string.compare(PlatformHandler::kExitTypeCancelable) == 0) {
+ return AppExitType::cancelable;
+ }
+ FML_LOG(ERROR) << string << " is not recognized as a valid exit type.";
+ return AppExitType::required;
+}
+
PlatformHandler::PlatformHandler(
BinaryMessenger* messenger,
FlutterWindowsEngine* engine,
@@ -354,48 +366,74 @@
}
void PlatformHandler::SystemExitApplication(
- const std::string& exit_type,
- int64_t exit_code,
+ AppExitType exit_type,
+ UINT exit_code,
std::unique_ptr<MethodResult<rapidjson::Document>> result) {
rapidjson::Document result_doc;
result_doc.SetObject();
- if (exit_type.compare(kExitTypeRequired) == 0) {
- QuitApplication(exit_code);
+ if (exit_type == AppExitType::required) {
+ QuitApplication(std::nullopt, std::nullopt, std::nullopt, exit_code);
result_doc.GetObjectW().AddMember(kExitResponseKey, kExitResponseExit,
result_doc.GetAllocator());
result->Success(result_doc);
} else {
- RequestAppExit(exit_type, exit_code);
+ RequestAppExit(std::nullopt, std::nullopt, std::nullopt, exit_type,
+ exit_code);
result_doc.GetObjectW().AddMember(kExitResponseKey, kExitResponseCancel,
result_doc.GetAllocator());
result->Success(result_doc);
}
}
-void PlatformHandler::RequestAppExit(const std::string& exit_type,
- int64_t exit_code) {
+// Indicates whether an exit request may be canceled by the framework.
+// These values must be kept in sync with ExitType in platform_handler.h
+static constexpr const char* kExitTypeNames[] = {
+ PlatformHandler::kExitTypeRequired, PlatformHandler::kExitTypeCancelable};
+
+void PlatformHandler::RequestAppExit(std::optional<HWND> hwnd,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ AppExitType exit_type,
+ UINT exit_code) {
auto callback = std::make_unique<MethodResultFunctions<rapidjson::Document>>(
- [this, exit_code](const rapidjson::Document* response) {
- RequestAppExitSuccess(response, exit_code);
+ [this, exit_code, hwnd, wparam,
+ lparam](const rapidjson::Document* response) {
+ RequestAppExitSuccess(hwnd, wparam, lparam, response, exit_code);
},
nullptr, nullptr);
auto args = std::make_unique<rapidjson::Document>();
args->SetObject();
- args->GetObjectW().AddMember(kExitTypeKey, exit_type, args->GetAllocator());
+ args->GetObjectW().AddMember(
+ kExitTypeKey, std::string(kExitTypeNames[static_cast<int>(exit_type)]),
+ args->GetAllocator());
channel_->InvokeMethod(kRequestAppExitMethod, std::move(args),
std::move(callback));
}
-void PlatformHandler::RequestAppExitSuccess(const rapidjson::Document* result,
- int64_t exit_code) {
- const std::string& exit_type = result[0][kExitResponseKey].GetString();
+void PlatformHandler::RequestAppExitSuccess(std::optional<HWND> hwnd,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ const rapidjson::Document* result,
+ UINT exit_code) {
+ rapidjson::Value::ConstMemberIterator itr =
+ result->FindMember(kExitResponseKey);
+ if (itr == result->MemberEnd() || !itr->value.IsString()) {
+ FML_LOG(ERROR) << "Application request response did not contain a valid "
+ "response value";
+ return;
+ }
+ const std::string& exit_type = itr->value.GetString();
+
if (exit_type.compare(kExitResponseExit) == 0) {
- QuitApplication(exit_code);
+ QuitApplication(hwnd, wparam, lparam, exit_code);
}
}
-void PlatformHandler::QuitApplication(int64_t exit_code) {
- PostQuitMessage(exit_code);
+void PlatformHandler::QuitApplication(std::optional<HWND> hwnd,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ UINT exit_code) {
+ engine_->OnQuit(hwnd, wparam, lparam, exit_code);
}
void PlatformHandler::HandleMethodCall(
@@ -404,9 +442,24 @@
const std::string& method = method_call.method_name();
if (method.compare(kExitApplicationMethod) == 0) {
const rapidjson::Value& arguments = method_call.arguments()[0];
- const std::string& exit_type = arguments[kExitTypeKey].GetString();
- int64_t exit_code = arguments[kExitCodeKey].GetInt64();
- SystemExitApplication(exit_type, exit_code, std::move(result));
+
+ rapidjson::Value::ConstMemberIterator itr =
+ arguments.FindMember(kExitTypeKey);
+ if (itr == arguments.MemberEnd() || !itr->value.IsString()) {
+ result->Error(kExitRequestError, kInvalidExitRequestMessage);
+ return;
+ }
+ const std::string& exit_type = itr->value.GetString();
+
+ itr = arguments.FindMember(kExitCodeKey);
+ if (itr == arguments.MemberEnd() || !itr->value.IsInt()) {
+ result->Error(kExitRequestError, kInvalidExitRequestMessage);
+ return;
+ }
+ UINT exit_code = arguments[kExitCodeKey].GetInt();
+
+ SystemExitApplication(StringToAppExitType(exit_type), exit_code,
+ std::move(result));
} else if (method.compare(kGetClipboardDataMethod) == 0) {
// Only one string argument is expected.
const rapidjson::Value& format = method_call.arguments()[0];
diff --git a/shell/platform/windows/platform_handler.h b/shell/platform/windows/platform_handler.h
index 096378d..8088b16 100644
--- a/shell/platform/windows/platform_handler.h
+++ b/shell/platform/windows/platform_handler.h
@@ -22,6 +22,13 @@
class FlutterWindowsEngine;
class ScopedClipboardInterface;
+// Indicates whether an exit request may be canceled by the framework.
+// These values must be kept in sync with kExitTypeNames in platform_handler.cc
+enum class AppExitType {
+ required,
+ cancelable,
+};
+
// Handler for internal system channels.
class PlatformHandler {
public:
@@ -33,6 +40,20 @@
virtual ~PlatformHandler();
+ // String values used for encoding/decoding exit requests.
+ static constexpr char kExitTypeCancelable[] = "cancelable";
+ static constexpr char kExitTypeRequired[] = "required";
+
+ // Send a request to the framework to test if a cancelable exit request
+ // should be canceled or honored. hwnd is std::nullopt for a request to quit
+ // the process, otherwise it holds the HWND of the window that initiated the
+ // quit request.
+ virtual void RequestAppExit(std::optional<HWND> hwnd,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ AppExitType exit_type,
+ UINT exit_code);
+
protected:
// Gets plain text from the clipboard and provides it to |result| as the
// value in a dictionary with the given |key|.
@@ -57,21 +78,27 @@
// Handle a request from the framework to exit the application.
virtual void SystemExitApplication(
- const std::string& exit_type,
- int64_t exit_code,
+ AppExitType exit_type,
+ UINT exit_code,
std::unique_ptr<MethodResult<rapidjson::Document>> result);
- // Actually quit the application with the provided exit code.
- virtual void QuitApplication(int64_t exit_code);
-
- // Send a request to the framework to test if a cancelable exit request
- // should be canceled or honored.
- virtual void RequestAppExit(const std::string& exit_type, int64_t exit_code);
+ // Actually quit the application with the provided exit code. hwnd is
+ // std::nullopt for a request to quit the process, otherwise it holds the HWND
+ // of the window that initiated the quit request.
+ virtual void QuitApplication(std::optional<HWND> hwnd,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ UINT exit_code);
// Callback from when the cancelable exit request response request is
- // answered by the framework.
- virtual void RequestAppExitSuccess(const rapidjson::Document* result,
- int64_t exit_code);
+ // answered by the framework. hwnd is std::nullopt for a request to quit the
+ // process, otherwise it holds the HWND of the window that initiated the quit
+ // request.
+ virtual void RequestAppExitSuccess(std::optional<HWND> hwnd,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ const rapidjson::Document* result,
+ UINT exit_code);
// A error type to use for error responses.
static constexpr char kClipboardError[] = "Clipboard error";
diff --git a/shell/platform/windows/platform_handler_unittests.cc b/shell/platform/windows/platform_handler_unittests.cc
index 85d4f5a..7de9439 100644
--- a/shell/platform/windows/platform_handler_unittests.cc
+++ b/shell/platform/windows/platform_handler_unittests.cc
@@ -82,7 +82,11 @@
void(const std::string&,
std::unique_ptr<MethodResult<rapidjson::Document>>));
- MOCK_METHOD1(QuitApplication, void(int64_t exit_code));
+ MOCK_METHOD4(QuitApplication,
+ void(std::optional<HWND> hwnd,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ UINT exit_code));
private:
FML_DISALLOW_COPY_AND_ASSIGN(MockPlatformHandler);
@@ -483,7 +487,7 @@
TEST_F(PlatformHandlerTest, SystemExitApplicationRequired) {
use_headless_engine();
- int exit_code = -1;
+ UINT exit_code = 0;
TestBinaryMessenger messenger([](const std::string& channel,
const uint8_t* message, size_t size,
@@ -491,7 +495,10 @@
MockPlatformHandler platform_handler(&messenger, engine());
ON_CALL(platform_handler, QuitApplication)
- .WillByDefault([&exit_code](int ec) { exit_code = ec; });
+ .WillByDefault([&exit_code](std::optional<HWND> hwnd,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ UINT ec) { exit_code = ec; });
EXPECT_CALL(platform_handler, QuitApplication).Times(1);
std::string result = SimulatePlatformMessage(
@@ -524,7 +531,7 @@
TEST_F(PlatformHandlerTest, SystemExitApplicationCancelableExit) {
use_headless_engine();
bool called_cancel = false;
- int exit_code = -1;
+ UINT exit_code = 0;
TestBinaryMessenger messenger(
[&called_cancel](const std::string& channel, const uint8_t* message,
@@ -536,7 +543,10 @@
MockPlatformHandler platform_handler(&messenger, engine());
ON_CALL(platform_handler, QuitApplication)
- .WillByDefault([&exit_code](int ec) { exit_code = ec; });
+ .WillByDefault([&exit_code](std::optional<HWND> hwnd,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ UINT ec) { exit_code = ec; });
EXPECT_CALL(platform_handler, QuitApplication).Times(1);
std::string result = SimulatePlatformMessage(
diff --git a/shell/platform/windows/testing/engine_modifier.h b/shell/platform/windows/testing/engine_modifier.h
index f49362b..ee355c0 100644
--- a/shell/platform/windows/testing/engine_modifier.h
+++ b/shell/platform/windows/testing/engine_modifier.h
@@ -66,6 +66,10 @@
// restart. This resets the keyboard's state if it exists.
void Restart() { engine_->OnPreEngineRestart(); }
+ void SetLifecycleManager(std::unique_ptr<WindowsLifecycleManager>&& handler) {
+ engine_->lifecycle_manager_ = std::move(handler);
+ }
+
private:
FlutterWindowsEngine* engine_;
diff --git a/shell/platform/windows/windows_lifecycle_manager.cc b/shell/platform/windows/windows_lifecycle_manager.cc
new file mode 100644
index 0000000..ecb14d4
--- /dev/null
+++ b/shell/platform/windows/windows_lifecycle_manager.cc
@@ -0,0 +1,152 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "windows_lifecycle_manager.h"
+
+#include <TlHelp32.h>
+#include <WinUser.h>
+#include <Windows.h>
+#include <tchar.h>
+
+#include "flutter/shell/platform/windows/flutter_windows_engine.h"
+
+namespace flutter {
+
+WindowsLifecycleManager::WindowsLifecycleManager(FlutterWindowsEngine* engine)
+ : engine_(engine) {}
+
+WindowsLifecycleManager::~WindowsLifecycleManager() {}
+
+void WindowsLifecycleManager::Quit(std::optional<HWND> hwnd,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ UINT exit_code) {
+ if (!hwnd.has_value()) {
+ ::PostQuitMessage(exit_code);
+ } else {
+ BASE_CHECK(wparam.has_value() && lparam.has_value());
+ sent_close_messages_[std::make_tuple(*hwnd, *wparam, *lparam)]++;
+ DispatchMessage(*hwnd, WM_CLOSE, *wparam, *lparam);
+ }
+}
+
+void WindowsLifecycleManager::DispatchMessage(HWND hwnd,
+ UINT message,
+ WPARAM wparam,
+ LPARAM lparam) {
+ PostMessage(hwnd, message, wparam, lparam);
+}
+
+bool WindowsLifecycleManager::WindowProc(HWND hwnd,
+ UINT msg,
+ WPARAM wpar,
+ LPARAM lpar,
+ LRESULT* result) {
+ switch (msg) {
+ // When WM_CLOSE is received from the final window of an application, we
+ // send a request to the framework to see if the app should exit. If it
+ // is, we re-dispatch a new WM_CLOSE message. In order to allow the new
+ // message to reach other delegates, we ignore it here.
+ case WM_CLOSE:
+ auto key = std::make_tuple(hwnd, wpar, lpar);
+ auto itr = sent_close_messages_.find(key);
+ if (itr != sent_close_messages_.end()) {
+ if (itr->second == 1) {
+ sent_close_messages_.erase(itr);
+ } else {
+ sent_close_messages_[key]--;
+ }
+ return false;
+ }
+ if (IsLastWindowOfProcess()) {
+ engine_->RequestApplicationQuit(hwnd, wpar, lpar,
+ AppExitType::cancelable);
+ return true;
+ }
+ break;
+ }
+ return false;
+}
+
+class ThreadSnapshot {
+ public:
+ ThreadSnapshot() {
+ thread_snapshot_ = ::CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
+ }
+ ~ThreadSnapshot() {
+ if (thread_snapshot_ != INVALID_HANDLE_VALUE) {
+ ::CloseHandle(thread_snapshot_);
+ }
+ }
+
+ std::optional<THREADENTRY32> GetFirstThread() {
+ if (thread_snapshot_ == INVALID_HANDLE_VALUE) {
+ FML_LOG(ERROR) << "Failed to get thread snapshot";
+ return std::nullopt;
+ }
+ THREADENTRY32 thread;
+ thread.dwSize = sizeof(thread);
+ if (!::Thread32First(thread_snapshot_, &thread)) {
+ DWORD error_num = ::GetLastError();
+ char msg[256];
+ ::FormatMessageA(
+ FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL,
+ error_num, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), msg, 256,
+ nullptr);
+ FML_LOG(ERROR) << "Failed to get thread(" << error_num << "): " << msg;
+ return std::nullopt;
+ }
+ return thread;
+ }
+
+ bool GetNextThread(THREADENTRY32& thread) {
+ if (thread_snapshot_ == INVALID_HANDLE_VALUE) {
+ return false;
+ }
+ return ::Thread32Next(thread_snapshot_, &thread);
+ }
+
+ private:
+ HANDLE thread_snapshot_;
+};
+
+static int64_t NumWindowsForThread(const THREADENTRY32& thread) {
+ int64_t num_windows = 0;
+ ::EnumThreadWindows(
+ thread.th32ThreadID,
+ [](HWND hwnd, LPARAM lparam) {
+ int64_t* windows_ptr = reinterpret_cast<int64_t*>(lparam);
+ if (::GetParent(hwnd) == nullptr) {
+ (*windows_ptr)++;
+ }
+ return *windows_ptr <= 1 ? TRUE : FALSE;
+ },
+ reinterpret_cast<LPARAM>(&num_windows));
+ return num_windows;
+}
+
+bool WindowsLifecycleManager::IsLastWindowOfProcess() {
+ DWORD pid = ::GetCurrentProcessId();
+ ThreadSnapshot thread_snapshot;
+ std::optional<THREADENTRY32> first_thread = thread_snapshot.GetFirstThread();
+ if (!first_thread.has_value()) {
+ FML_LOG(ERROR) << "No first thread found";
+ return true;
+ }
+
+ int num_windows = 0;
+ THREADENTRY32 thread = *first_thread;
+ do {
+ if (thread.th32OwnerProcessID == pid) {
+ num_windows += NumWindowsForThread(thread);
+ if (num_windows > 1) {
+ return false;
+ }
+ }
+ } while (thread_snapshot.GetNextThread(thread));
+
+ return num_windows <= 1;
+}
+
+} // namespace flutter
diff --git a/shell/platform/windows/windows_lifecycle_manager.h b/shell/platform/windows/windows_lifecycle_manager.h
new file mode 100644
index 0000000..e2ccfa4
--- /dev/null
+++ b/shell/platform/windows/windows_lifecycle_manager.h
@@ -0,0 +1,57 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWS_LIFECYCLE_MANAGER_H_
+#define FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWS_LIFECYCLE_MANAGER_H_
+
+#include <Windows.h>
+
+#include <cstdint>
+#include <map>
+#include <optional>
+
+namespace flutter {
+
+class FlutterWindowsEngine;
+
+/// A manager for lifecycle events of the top-level window.
+///
+/// Currently handles the following events:
+/// WM_CLOSE
+class WindowsLifecycleManager {
+ public:
+ WindowsLifecycleManager(FlutterWindowsEngine* engine);
+ virtual ~WindowsLifecycleManager();
+
+ // Called when the engine is notified it should quit, e.g. by an application
+ // call to `exitApplication`. When window is std::nullopt, this quits the
+ // application. Otherwise, it holds the HWND of the window that initiated the
+ // request, and exit_code is unused.
+ virtual void Quit(std::optional<HWND> window,
+ std::optional<WPARAM> wparam,
+ std::optional<LPARAM> lparam,
+ UINT exit_code);
+
+ // Intercept top level window messages, only paying attention to WM_CLOSE.
+ bool WindowProc(HWND hwnd, UINT msg, WPARAM w, LPARAM l, LRESULT* result);
+
+ protected:
+ // Check the number of top-level windows associated with this process, and
+ // return true only if there are 1 or fewer.
+ virtual bool IsLastWindowOfProcess();
+
+ virtual void DispatchMessage(HWND window,
+ UINT msg,
+ WPARAM wparam,
+ LPARAM lparam);
+
+ private:
+ FlutterWindowsEngine* engine_;
+
+ std::map<std::tuple<HWND, WPARAM, LPARAM>, int> sent_close_messages_;
+};
+
+} // namespace flutter
+
+#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWS_LIFECYCLE_MANAGER_H_