commit f55e7af1f62c05c440be228802d947c15291321f
parent ef13c67d27be90daf9ba6482ab8f66c02684cc72
Author: Nika Layzell <nika@thelayzells.com>
Date: Tue, 16 Dec 2025 04:53:45 +0000
Bug 1908693 - Part 4: Shutdown XPCOM on iOS app exit, r=glandium,geckoview-reviewers,m_kato
Before this change on iOS, when the application exits, UIKit's main loop method
does not return, so we exit before running through normal XPCOM shutdown. This
patch changes the behaviour around applicationWillTerminate to ensure that we
run at least some of XPCOM shutdown before the application is exited.
This fix also ensures that the iOS application will exit when Quit() is called
by Gecko JS code. This won't happen in normal operation on iOS, as the OS
generally controls app lifetimes, but is important for automated testing. As
iOS provides no proper support for gracefully exiting an app, we call
`_exit(0);` after performing as much of the shutdown process as possible.
Differential Revision: https://phabricator.services.mozilla.com/D217134
Diffstat:
5 files changed, 82 insertions(+), 26 deletions(-)
diff --git a/mobile/ios/GeckoTestBrowser/GeckoTestBrowser/UI/RootViewController.swift b/mobile/ios/GeckoTestBrowser/GeckoTestBrowser/UI/RootViewController.swift
@@ -199,7 +199,10 @@ extension RootViewController: ContentDelegate {
func onFocusRequest(session: GeckoSession) {}
- func onCloseRequest(session: GeckoSession) {}
+ func onCloseRequest(session: GeckoSession) {
+ session.close()
+ geckoview.session = nil
+ }
func onFullScreen(session: GeckoSession, fullScreen: Bool) {}
diff --git a/toolkit/xre/nsAppRunner.cpp b/toolkit/xre/nsAppRunner.cpp
@@ -5914,6 +5914,10 @@ nsresult XREMain::XRE_mainRun() {
}
#endif
+ // We're entering the main run loop now, so we don't need to keep holding onto
+ // the `nsICommandLineRunner` anymore.
+ cmdLine = nullptr;
+
{
rv = appStartup->Run();
if (NS_FAILED(rv)) {
diff --git a/widget/uikit/moz.build b/widget/uikit/moz.build
@@ -46,8 +46,10 @@ include("/ipc/chromium/chromium-config.mozbuild")
FINAL_LIBRARY = "xul"
LOCAL_INCLUDES += [
+ "/toolkit/xre",
"/widget",
"/widget/headless",
+ "/xpcom/components",
]
XPCOM_MANIFESTS += [
diff --git a/widget/uikit/nsAppShell.h b/widget/uikit/nsAppShell.h
@@ -34,8 +34,6 @@ class nsAppShell : public nsBaseAppShell {
NS_IMETHOD Run(void) override;
NS_IMETHOD Exit(void) override;
- // Called by the application delegate
- void WillTerminate(void);
static nsAppShell* gAppShell;
@@ -51,9 +49,9 @@ class nsAppShell : public nsBaseAppShell {
CFRunLoopRef mCFRunLoop;
CFRunLoopSourceRef mCFRunLoopSource;
+ bool mUsingNativeEventLoop;
bool mRunningEventLoop;
bool mTerminated;
- bool mNotifiedWillTerminate;
};
#endif // nsAppShell_h_
diff --git a/widget/uikit/nsAppShell.mm b/widget/uikit/nsAppShell.mm
@@ -11,8 +11,10 @@
#include "mozilla/dom/Document.h"
#include "nsIObserverService.h"
#include "gfxPlatform.h"
+#include "nsAppRunner.h"
#include "nsAppShell.h"
#include "nsCOMPtr.h"
+#include "nsComponentManager.h"
#include "nsDirectoryServiceDefs.h"
#include "nsObjCExceptions.h"
#include "nsString.h"
@@ -28,6 +30,7 @@
#include "mozilla/Hal.h"
#include "HeadlessScreenHelper.h"
#include "nsWindow.h"
+#include "nsXREDirProvider.h"
using namespace mozilla;
using namespace mozilla::widget;
@@ -38,6 +41,8 @@ nsAppShell* nsAppShell::gAppShell = NULL;
fprintf(stderr, args); \
fprintf(stderr, "\n")
+static void ApplicationWillTerminate(bool aCallExit);
+
// AppShellDelegate
//
// Acts as a delegate for the UIApplication
@@ -58,7 +63,7 @@ nsAppShell* nsAppShell::gAppShell = NULL;
- (void)applicationWillTerminate:(UIApplication*)application {
ALOG("[AppShellDelegate applicationWillTerminate:]");
- nsAppShell::gAppShell->WillTerminate();
+ ApplicationWillTerminate(/* aCallExit */ false);
}
- (void)applicationDidBecomeActive:(UIApplication*)application {
@@ -85,9 +90,9 @@ nsAppShell::nsAppShell()
mDelegate(NULL),
mCFRunLoop(NULL),
mCFRunLoopSource(NULL),
+ mUsingNativeEventLoop(false),
mRunningEventLoop(false),
- mTerminated(false),
- mNotifiedWillTerminate(false) {
+ mTerminated(false) {
gAppShell = this;
}
@@ -113,6 +118,8 @@ nsAppShell::~nsAppShell() {
//
// public
nsresult nsAppShell::Init() {
+ mUsingNativeEventLoop = XRE_UseNativeEventProcessing();
+
mAutoreleasePool = [[NSAutoreleasePool alloc] init];
// Add a CFRunLoopSource to the main native run loop. The source is
@@ -151,7 +158,10 @@ nsresult nsAppShell::Init() {
mozilla::services::GetObserverService();
if (obsServ) {
obsServ->AddObserver(this, "profile-after-change", false);
- obsServ->AddObserver(this, "chrome-document-loaded", false);
+ obsServ->AddObserver(this, "quit-application-granted", false);
+ if (XRE_IsParentProcess()) {
+ obsServ->AddObserver(this, "chrome-document-loaded", false);
+ }
}
return rv;
@@ -172,6 +182,15 @@ NS_IMETHODIMP nsAppShell::Observe(nsISupports* aSubject, const char* aTopic,
appStartup->EnterLastWindowClosingSurvivalArea();
}
removeObserver = true;
+ } else if (!strcmp(aTopic, "quit-application-granted")) {
+ // We are told explicitly to quit, perhaps due to
+ // nsIAppStartup::Quit being called. We should release our hold on
+ // nsIAppStartup and let it continue to quit.
+ nsCOMPtr<nsIAppStartup> appStartup = components::AppStartup::Service();
+ if (appStartup) {
+ appStartup->ExitLastWindowClosingSurvivalArea();
+ }
+ removeObserver = true;
} else if (!strcmp(aTopic, "chrome-document-loaded")) {
// Set the global ready state and enable the window event dispatcher
// for this particular GeckoView.
@@ -210,27 +229,10 @@ void nsAppShell::ProcessGeckoEvents(void* aInfo) {
self->Release();
}
-// WillTerminate
-//
-// public
-void nsAppShell::WillTerminate() {
- mNotifiedWillTerminate = true;
- if (mTerminated) return;
- mTerminated = true;
- // We won't get another chance to process events
- NS_ProcessPendingEvents(NS_GetCurrentThread());
-
- // Unless we call nsBaseAppShell::Exit() here, it might not get called
- // at all.
- nsBaseAppShell::Exit();
-}
-
// ScheduleNativeEventCallback
//
// protected virtual
void nsAppShell::ScheduleNativeEventCallback() {
- if (mTerminated) return;
-
NS_ADDREF_THIS();
// This will invoke ProcessGeckoEvents on the main thread.
@@ -279,7 +281,7 @@ nsAppShell::Run(void) {
ALOG("nsAppShell::Run");
nsresult rv = NS_OK;
- if (XRE_UseNativeEventProcessing()) {
+ if (mUsingNativeEventLoop) {
char argv[1][4] = {"app"};
UIApplicationMain(1, (char**)argv, nil, @"AppShellDelegate");
// UIApplicationMain doesn't exit. :-(
@@ -295,5 +297,52 @@ nsAppShell::Exit(void) {
if (mTerminated) return NS_OK;
mTerminated = true;
+
+ if (mUsingNativeEventLoop) {
+ // Dispatch a native event to the main queue to terminate. XPCOM doesn't
+ // like being shut down from within an XPCOM runnable, so we cannot use
+ // `NS_DispatchToMainThread` here.
+ dispatch_async(dispatch_get_main_queue(), ^{
+ ApplicationWillTerminate(true);
+ });
+ return NS_OK;
+ }
+
return nsBaseAppShell::Exit();
}
+
+static bool gNotifiedWillTerminate = false;
+
+static void ApplicationWillTerminate(bool aCallExit) {
+ if (std::exchange(gNotifiedWillTerminate, true)) {
+ return;
+ }
+
+ // Perform the steps which would normally happen after nsAppShell::Run, such
+ // as `~ScopedXPCOMStartup`, as UIApplicationMain will never shutdown.
+ if (nsCOMPtr<nsIAppStartup> appStartup = components::AppStartup::Service()) {
+ // Ensure quit notifications have fired to start the shutdown process, and
+ // notify listeners.
+ bool userAllowedQuit;
+ appStartup->Quit(nsIAppStartup::eForceQuit, 0, &userAllowedQuit);
+
+ appStartup->DestroyHiddenWindow();
+ }
+
+ gDirServiceProvider->DoShutdown();
+
+ WriteConsoleLog();
+
+ // Release the final reference to the `nsIServiceManager` which is being held
+ // by `ScopedXPCOMStartup`, as we will never destroy that object.
+ nsIServiceManager* servMgr = nsComponentManagerImpl::gComponentManager;
+ NS_ShutdownXPCOM(servMgr);
+
+ // Matches the ScopedLogger initialized in `XRE_main` which will never be
+ // cleaned up.
+ NS_LogTerm();
+
+ if (aCallExit) {
+ _exit(0);
+ }
+}