From d7059df4ebe468913480f84d9d88832e89fc87d2 Mon Sep 17 00:00:00 2001 From: yaakovschectman <109111084+yaakovschectman@users.noreply.github.com> Date: Fri, 24 Mar 2023 16:44:42 +0000 Subject: [PATCH] Listen to `WM_CLOSE` message on Windows to allow framework to cancel exit (flutter/engine#40493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * Update shell/platform/windows/windows_lifecycle_manager.cc Co-authored-by: Chris Bracken * Update shell/platform/windows/windows_lifecycle_manager.cc Co-authored-by: Chris Bracken * Static cast enum to int * Formatting * Update shell/platform/windows/testing/engine_modifier.h Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com> * Update shell/platform/windows/windows_lifecycle_manager.cc Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com> * Update shell/platform/windows/platform_handler.cc Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com> * Update unit tests * PR Feedback * PR Feedback * Constexpr * Formatting * Wparam --------- Co-authored-by: Chris Bracken Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com> --- .../ci/licenses_golden/licenses_flutter | 4 + .../flutter/shell/platform/windows/BUILD.gn | 2 + .../shell/platform/windows/fixtures/main.dart | 61 +++++++++ .../windows/flutter_windows_engine.cc | 23 +++- .../platform/windows/flutter_windows_engine.h | 10 ++ .../flutter_windows_engine_unittests.cc | 87 +++++++++++++ .../platform/windows/platform_handler.cc | 71 ++++++++--- .../shell/platform/windows/platform_handler.h | 27 ++-- .../windows/platform_handler_unittests.cc | 10 +- .../windows/testing/engine_modifier.h | 4 + .../windows/windows_lifecycle_manager.cc | 120 ++++++++++++++++++ .../windows/windows_lifecycle_manager.h | 37 ++++++ 12 files changed, 427 insertions(+), 29 deletions(-) create mode 100644 engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.cc create mode 100644 engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.h diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index bec4b4a6e7e..06a15477f6d 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -3087,6 +3087,8 @@ ORIGIN: ../../../flutter/shell/platform/windows/window_binding_handler_delegate. 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 @@ -5646,6 +5648,8 @@ FILE: ../../../flutter/shell/platform/windows/window_binding_handler_delegate.h 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/engine/src/flutter/shell/platform/windows/BUILD.gn b/engine/src/flutter/shell/platform/windows/BUILD.gn index 1f7a850b071..db9053a34e4 100644 --- a/engine/src/flutter/shell/platform/windows/BUILD.gn +++ b/engine/src/flutter/shell/platform/windows/BUILD.gn @@ -103,6 +103,8 @@ source_set("flutter_windows_source") { "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/engine/src/flutter/shell/platform/windows/fixtures/main.dart b/engine/src/flutter/shell/platform/windows/fixtures/main.dart index 1e8d5a9e278..c9794f68d89 100644 --- a/engine/src/flutter/shell/platform/windows/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/windows/fixtures/main.dart @@ -6,6 +6,7 @@ import 'dart:async'; 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') @@ -86,6 +87,66 @@ void alertPlatformChannel() async { ui.PlatformDispatcher.instance.sendPlatformMessage('flutter/accessibility', byteData, (ByteData? _){}); } +@pragma('vm:entry-point') +void exitTestExit() async { + final Completer closed = Completer(); + ui.channelBuffers.setListener('flutter/platform', (ByteData? data, ui.PlatformMessageResponseCallback callback) async { + final String jsonString = json.encode(>[{'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 exited = Completer(); + final String jsonString = json.encode({ + 'method': 'System.exitApplication', + 'args': { + '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 closed = Completer(); + ui.channelBuffers.setListener('flutter/platform', (ByteData? data, ui.PlatformMessageResponseCallback callback) async { + final String jsonString = json.encode(>[{'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 exited = Completer(); + final String jsonString = json.encode({ + 'method': 'System.exitApplication', + 'args': { + '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() {} diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc index b49e6ece3b6..0a0bbbe94cd 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc @@ -161,7 +161,8 @@ FlutterWindowsEngine::FlutterWindowsEngine( std::unique_ptr registry) : project_(std::make_unique(project)), aot_data_(nullptr, nullptr), - windows_registry_(std::move(registry)) { + windows_registry_(std::move(registry)), + lifecycle_manager_(std::make_unique(this)) { embedder_api_.struct_size = sizeof(FlutterEngineProcTable); FlutterEngineGetProcAddresses(&embedder_api_); @@ -203,6 +204,17 @@ FlutterWindowsEngine::FlutterWindowsEngine( std::make_unique(this, gl_procs_); surface_manager_ = AngleSurfaceManager::Create(); window_proc_delegate_manager_ = std::make_unique(); + 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(user_data); + BASE_DCHECK(that->lifecycle_manager_); + return that->lifecycle_manager_->WindowProc(hwnd, msg, wpar, lpar, + result); + }, + static_cast(this)); // Set up internal channels. // TODO: Replace this with an embedder.h API. See @@ -751,4 +763,13 @@ void FlutterWindowsEngine::HandleAccessibilityMessage( reinterpret_cast(""), 0); } +void FlutterWindowsEngine::RequestApplicationQuit(ExitType exit_type, + UINT exit_code) { + platform_handler_->RequestAppExit(exit_type, exit_code); +} + +void FlutterWindowsEngine::OnQuit(UINT exit_code) { + lifecycle_manager_->Quit(exit_code); +} + } // namespace flutter diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h index 01de0d39f20..e4964041ffb 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h +++ b/engine/src/flutter/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,12 @@ class FlutterWindowsEngine { // 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(UINT exit_code); + + // Called when a WM_CLOSE message is received. + void RequestApplicationQuit(ExitType exit_type, UINT exit_code); + protected: // Creates the keyboard key handler. // @@ -395,6 +402,9 @@ class FlutterWindowsEngine { // Wrapper providing Windows registry access. std::unique_ptr windows_registry_; + // Handler for top level window messages. + std::unique_ptr lifecycle_manager_; + FML_DISALLOW_COPY_AND_ASSIGN(FlutterWindowsEngine); }; diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc index 7a503aae7b3..d007a9f38ed 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc @@ -618,5 +618,92 @@ TEST_F(FlutterWindowsEngineTest, AlertPlatformMessage) { } } +class MockWindowsLifecycleManager : public WindowsLifecycleManager { + public: + MockWindowsLifecycleManager(FlutterWindowsEngine* engine) + : WindowsLifecycleManager(engine) {} + virtual ~MockWindowsLifecycleManager() {} + + MOCK_CONST_METHOD1(Quit, void(UINT)); +}; + +TEST_F(FlutterWindowsEngineTest, TestExit) { + FlutterWindowsEngineBuilder builder{GetContext()}; + builder.SetDartEntrypoint("exitTestExit"); + bool finished = false; + bool did_call = false; + + std::unique_ptr engine = builder.Build(); + + EngineModifier modifier(engine.get()); + modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; }; + auto handler = std::make_unique(engine.get()); + ON_CALL(*handler, Quit).WillByDefault([&finished](UINT exit_code) { + finished = exit_code == 0; + }); + EXPECT_CALL(*handler, Quit).Times(1); + modifier.SetLifecycleManager(std::move(handler)); + + auto binary_messenger = + std::make_unique(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(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 engine = builder.Build(); + + EngineModifier modifier(engine.get()); + modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; }; + auto handler = std::make_unique(engine.get()); + ON_CALL(*handler, Quit).WillByDefault([&finished](int64_t exit_code) { + finished = true; + }); + EXPECT_CALL(*handler, Quit).Times(0); + modifier.SetLifecycleManager(std::move(handler)); + + auto binary_messenger = + std::make_unique(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(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); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/shell/platform/windows/platform_handler.cc b/engine/src/flutter/shell/platform/windows/platform_handler.cc index a33710becbf..cec1daf25a8 100644 --- a/engine/src/flutter/shell/platform/windows/platform_handler.cc +++ b/engine/src/flutter/shell/platform/windows/platform_handler.cc @@ -28,8 +28,6 @@ static constexpr char kPlaySoundMethod[] = "SystemSound.play"; 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 char kValueKey[] = "value"; 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 @@ int ScopedClipboard::SetString(const std::wstring string) { } // namespace +static ExitType StringToExitType(const std::string& string) { + if (string.compare(PlatformHandler::kExitTypeRequired) == 0) { + return ExitType::required; + } else if (string.compare(PlatformHandler::kExitTypeCancelable) == 0) { + return ExitType::cancelable; + } + FML_LOG(ERROR) << string << " is not recognized as a valid exit type."; + return ExitType::required; +} + PlatformHandler::PlatformHandler( BinaryMessenger* messenger, FlutterWindowsEngine* engine, @@ -354,12 +366,12 @@ void PlatformHandler::SystemSoundPlay( } void PlatformHandler::SystemExitApplication( - const std::string& exit_type, - int64_t exit_code, + ExitType exit_type, + UINT exit_code, std::unique_ptr> result) { rapidjson::Document result_doc; result_doc.SetObject(); - if (exit_type.compare(kExitTypeRequired) == 0) { + if (exit_type == ExitType::required) { QuitApplication(exit_code); result_doc.GetObjectW().AddMember(kExitResponseKey, kExitResponseExit, result_doc.GetAllocator()); @@ -372,8 +384,12 @@ void PlatformHandler::SystemExitApplication( } } -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(ExitType exit_type, UINT exit_code) { auto callback = std::make_unique>( [this, exit_code](const rapidjson::Document* response) { RequestAppExitSuccess(response, exit_code); @@ -381,21 +397,31 @@ void PlatformHandler::RequestAppExit(const std::string& exit_type, nullptr, nullptr); auto args = std::make_unique(); args->SetObject(); - args->GetObjectW().AddMember(kExitTypeKey, exit_type, args->GetAllocator()); + args->GetObjectW().AddMember( + kExitTypeKey, std::string(kExitTypeNames[static_cast(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(); + 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); } } -void PlatformHandler::QuitApplication(int64_t exit_code) { - PostQuitMessage(exit_code); +void PlatformHandler::QuitApplication(UINT exit_code) { + engine_->OnQuit(exit_code); } void PlatformHandler::HandleMethodCall( @@ -404,9 +430,24 @@ void PlatformHandler::HandleMethodCall( 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(StringToExitType(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/engine/src/flutter/shell/platform/windows/platform_handler.h b/engine/src/flutter/shell/platform/windows/platform_handler.h index 096378db4f4..f5eb9f2021e 100644 --- a/engine/src/flutter/shell/platform/windows/platform_handler.h +++ b/engine/src/flutter/shell/platform/windows/platform_handler.h @@ -22,6 +22,13 @@ namespace flutter { 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 ExitType { + required, + cancelable, +}; + // Handler for internal system channels. class PlatformHandler { public: @@ -33,6 +40,14 @@ class PlatformHandler { 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. + virtual void RequestAppExit(ExitType 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 +72,17 @@ class PlatformHandler { // Handle a request from the framework to exit the application. virtual void SystemExitApplication( - const std::string& exit_type, - int64_t exit_code, + ExitType exit_type, + UINT exit_code, std::unique_ptr> 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); + virtual void QuitApplication(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); + UINT exit_code); // A error type to use for error responses. static constexpr char kClipboardError[] = "Clipboard error"; diff --git a/engine/src/flutter/shell/platform/windows/platform_handler_unittests.cc b/engine/src/flutter/shell/platform/windows/platform_handler_unittests.cc index 85d4f5a9a97..eb879569b3d 100644 --- a/engine/src/flutter/shell/platform/windows/platform_handler_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/platform_handler_unittests.cc @@ -82,7 +82,7 @@ class MockPlatformHandler : public PlatformHandler { void(const std::string&, std::unique_ptr>)); - MOCK_METHOD1(QuitApplication, void(int64_t exit_code)); + MOCK_METHOD1(QuitApplication, void(UINT exit_code)); private: FML_DISALLOW_COPY_AND_ASSIGN(MockPlatformHandler); @@ -483,7 +483,7 @@ TEST_F(PlatformHandlerTest, PlaySystemSound) { 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 +491,7 @@ TEST_F(PlatformHandlerTest, SystemExitApplicationRequired) { MockPlatformHandler platform_handler(&messenger, engine()); ON_CALL(platform_handler, QuitApplication) - .WillByDefault([&exit_code](int ec) { exit_code = ec; }); + .WillByDefault([&exit_code](UINT ec) { exit_code = ec; }); EXPECT_CALL(platform_handler, QuitApplication).Times(1); std::string result = SimulatePlatformMessage( @@ -524,7 +524,7 @@ TEST_F(PlatformHandlerTest, SystemExitApplicationCancelableCancel) { 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 +536,7 @@ TEST_F(PlatformHandlerTest, SystemExitApplicationCancelableExit) { MockPlatformHandler platform_handler(&messenger, engine()); ON_CALL(platform_handler, QuitApplication) - .WillByDefault([&exit_code](int ec) { exit_code = ec; }); + .WillByDefault([&exit_code](UINT ec) { exit_code = ec; }); EXPECT_CALL(platform_handler, QuitApplication).Times(1); std::string result = SimulatePlatformMessage( diff --git a/engine/src/flutter/shell/platform/windows/testing/engine_modifier.h b/engine/src/flutter/shell/platform/windows/testing/engine_modifier.h index f49362ba6d6..ee355c0b5b9 100644 --- a/engine/src/flutter/shell/platform/windows/testing/engine_modifier.h +++ b/engine/src/flutter/shell/platform/windows/testing/engine_modifier.h @@ -66,6 +66,10 @@ class EngineModifier { // restart. This resets the keyboard's state if it exists. void Restart() { engine_->OnPreEngineRestart(); } + void SetLifecycleManager(std::unique_ptr&& handler) { + engine_->lifecycle_manager_ = std::move(handler); + } + private: FlutterWindowsEngine* engine_; diff --git a/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.cc b/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.cc new file mode 100644 index 00000000000..c8e85487a32 --- /dev/null +++ b/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.cc @@ -0,0 +1,120 @@ +// 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 +#include +#include +#include + +#include "flutter/shell/platform/windows/flutter_windows_engine.h" + +namespace flutter { + +WindowsLifecycleManager::WindowsLifecycleManager(FlutterWindowsEngine* engine) + : engine_(engine) {} + +WindowsLifecycleManager::~WindowsLifecycleManager() {} + +void WindowsLifecycleManager::Quit(UINT exit_code) const { + ::PostQuitMessage(exit_code); +} + +bool WindowsLifecycleManager::WindowProc(HWND hwnd, + UINT msg, + WPARAM wpar, + LPARAM lpar, + LRESULT* result) { + switch (msg) { + case WM_CLOSE: + if (IsLastWindowOfProcess()) { + engine_->RequestApplicationQuit(ExitType::cancelable, 0); + } + return true; + } + return false; +} + +class ThreadSnapshot { + public: + ThreadSnapshot() { + thread_snapshot_ = ::CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + } + ~ThreadSnapshot() { + if (thread_snapshot_ != INVALID_HANDLE_VALUE) { + ::CloseHandle(thread_snapshot_); + } + } + + std::optional 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(lparam); + if (::GetParent(hwnd) == nullptr) { + (*windows_ptr)++; + } + return *windows_ptr <= 1 ? TRUE : FALSE; + }, + reinterpret_cast(&num_windows)); + return num_windows; +} + +bool WindowsLifecycleManager::IsLastWindowOfProcess() { + DWORD pid = ::GetCurrentProcessId(); + ThreadSnapshot thread_snapshot; + std::optional 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/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.h b/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.h new file mode 100644 index 00000000000..7f452775432 --- /dev/null +++ b/engine/src/flutter/shell/platform/windows/windows_lifecycle_manager.h @@ -0,0 +1,37 @@ +// 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 + +#include + +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(); + + virtual void Quit(UINT exit_code) const; + + bool WindowProc(HWND hwnd, UINT msg, WPARAM w, LPARAM l, LRESULT* result); + + private: + bool IsLastWindowOfProcess(); + + FlutterWindowsEngine* engine_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOWS_LIFECYCLE_MANAGER_H_