Skip to content

Commit cf2a5ce

Browse files
committed
fix(macos): preserve Flutter quit approval after AppKit cancellation
Distinguish native NSTerminateLater replies from AppKit acknowledgements of synchronous cancellation while Flutter is still deciding asynchronously. Keep the pending Flutter ticket alive until its channel response and cover the acknowledgement in the real-engine regression. Refs #65.
1 parent 6de59e1 commit cf2a5ce

2 files changed

Lines changed: 14 additions & 3 deletions

File tree

‎src/platform/macos/application_quit_macos.mm‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ void Reset(id object) {
4545
WeakObject host;
4646
std::shared_ptr<nativeapi::EventRequest> request;
4747
bool asking = true, decided = false, approved = false, waiting_host = false;
48+
bool waiting_appkit_reply = false;
4849
explicit Session(id target) : host(target) {}
4950
};
5051
struct FlutterBridge;
@@ -92,6 +93,7 @@ NSApplicationTerminateReply Continue(
9293
const std::function<NSApplicationTerminateReply()>& original) {
9394
owner->ObserveHost(target->host.Get());
9495
target->waiting_host = false;
96+
target->waiting_appkit_reply = false;
9597
++entering;
9698
NSApplicationTerminateReply reply;
9799
@try {
@@ -104,6 +106,7 @@ NSApplicationTerminateReply Continue(
104106
End(target);
105107
} else if (reply == NSTerminateLater) {
106108
target->waiting_host = true;
109+
target->waiting_appkit_reply = true;
107110
} else if (!target->waiting_host) {
108111
End(target); // An ordinary host cancellation, without an async bridge.
109112
}
@@ -281,7 +284,11 @@ void InstallFlutterBridge(const std::shared_ptr<FlutterBridge>& bridge) {
281284
if (!impl_->app)
282285
return;
283286
auto target = impl_->session;
284-
if (impl_->Current(target) && target->waiting_host) {
287+
// AppKit also calls this with NO after a synchronous NSTerminateCancel.
288+
// Flutter returns Cancel while its own channel reply is pending; that
289+
// notification must not invalidate the ticket for its eventual approval.
290+
// Only NSTerminateLater delegates finish through this AppKit reply API.
291+
if (impl_->Current(target) && target->waiting_appkit_reply) {
285292
if (allowed)
286293
impl_->announced_termination = true;
287294
impl_->End(target);

‎tests/application_flutter_engine_macos_test.mm‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ @interface HeadlessQuitApplication : NSApplication
1515
@end
1616
@implementation HeadlessQuitApplication
1717
- (void)terminate:(id)sender {
18-
[self.delegate applicationShouldTerminate:self];
18+
const auto reply = [self.delegate applicationShouldTerminate:self];
19+
// Real AppKit acknowledges NSTerminateCancel this way even when Flutter
20+
// still has its own asynchronous Dart decision outstanding (#65).
21+
if (reply == NSTerminateCancel)
22+
[self replyToApplicationShouldTerminate:NO];
1923
}
2024
@end
2125

@@ -149,7 +153,7 @@ int main(int argc, char** argv) {
149153
std::thread worker([&] { b->Accept(); });
150154
worker.join();
151155
check(WaitFor([&] { return state->requests == 3; }) && pending(),
152-
"worker approval resumes the real Flutter request on UI");
156+
"AppKit cancel acknowledgement preserves the pending Dart request after worker approval");
153157
check([[coordinator valueForKey:@"shouldTerminate"] boolValue] && ask() == NSTerminateCancel,
154158
"Flutter's early shouldTerminate flag cannot authorize a repeated user quit");
155159
app.Quit(23);

0 commit comments

Comments
 (0)