summaryrefslogtreecommitdiffstats
path: root/widget/cocoa/nsAppShell.mm
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /widget/cocoa/nsAppShell.mm
parentInitial commit. (diff)
downloadfirefox-esr-upstream/115.8.0esr.tar.xz
firefox-esr-upstream/115.8.0esr.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/cocoa/nsAppShell.mm')
-rw-r--r--widget/cocoa/nsAppShell.mm1120
1 files changed, 1120 insertions, 0 deletions
diff --git a/widget/cocoa/nsAppShell.mm b/widget/cocoa/nsAppShell.mm
new file mode 100644
index 0000000000..9ba512ce49
--- /dev/null
+++ b/widget/cocoa/nsAppShell.mm
@@ -0,0 +1,1120 @@
+/* -*- tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Runs the main native Cocoa run loop, interrupting it as needed to process
+ * Gecko events.
+ */
+
+#import <Cocoa/Cocoa.h>
+
+#include <dlfcn.h>
+
+#include "mozilla/AvailableMemoryWatcher.h"
+#include "CustomCocoaEvents.h"
+#include "mozilla/WidgetTraceEvent.h"
+#include "nsAppShell.h"
+#include "gfxPlatform.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsString.h"
+#include "nsIRollupListener.h"
+#include "nsIWidget.h"
+#include "nsMemoryPressure.h"
+#include "nsThreadUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsCocoaUtils.h"
+#include "nsCocoaFeatures.h"
+#include "nsChildView.h"
+#include "nsToolkit.h"
+#include "TextInputHandler.h"
+#include "mozilla/BackgroundHangMonitor.h"
+#include "ScreenHelperCocoa.h"
+#include "mozilla/Hal.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerThreadSleep.h"
+#include "mozilla/widget/ScreenManager.h"
+#include "HeadlessScreenHelper.h"
+#include "MOZMenuOpeningCoordinator.h"
+#include "pratom.h"
+#if !defined(RELEASE_OR_BETA) || defined(DEBUG)
+# include "nsSandboxViolationSink.h"
+#endif
+
+#include <IOKit/pwr_mgt/IOPMLib.h>
+#include "nsIDOMWakeLockListener.h"
+#include "nsIPowerManagerService.h"
+
+#include "nsIObserverService.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_widget.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+#define WAKE_LOCK_LOG(...) MOZ_LOG(gMacWakeLockLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+static mozilla::LazyLogModule gMacWakeLockLog("MacWakeLock");
+
+// A wake lock listener that disables screen saver when requested by
+// Gecko. For example when we're playing video in a foreground tab we
+// don't want the screen saver to turn on.
+
+class MacWakeLockListener final : public nsIDOMMozWakeLockListener {
+ public:
+ NS_DECL_ISUPPORTS;
+
+ private:
+ ~MacWakeLockListener() {}
+
+ IOPMAssertionID mAssertionNoDisplaySleepID = kIOPMNullAssertionID;
+ IOPMAssertionID mAssertionNoIdleSleepID = kIOPMNullAssertionID;
+
+ NS_IMETHOD Callback(const nsAString& aTopic, const nsAString& aState) override {
+ if (!aTopic.EqualsASCII("screen") && !aTopic.EqualsASCII("audio-playing") &&
+ !aTopic.EqualsASCII("video-playing")) {
+ return NS_OK;
+ }
+
+ // we should still hold the lock for background audio.
+ if (aTopic.EqualsASCII("audio-playing") && aState.EqualsASCII("locked-background")) {
+ WAKE_LOCK_LOG("keep audio playing even in background");
+ return NS_OK;
+ }
+
+ bool shouldKeepDisplayOn = aTopic.EqualsASCII("screen") || aTopic.EqualsASCII("video-playing");
+ CFStringRef assertionType =
+ shouldKeepDisplayOn ? kIOPMAssertionTypeNoDisplaySleep : kIOPMAssertionTypeNoIdleSleep;
+ IOPMAssertionID& assertionId =
+ shouldKeepDisplayOn ? mAssertionNoDisplaySleepID : mAssertionNoIdleSleepID;
+ WAKE_LOCK_LOG("topic=%s, state=%s, shouldKeepDisplayOn=%d", NS_ConvertUTF16toUTF8(aTopic).get(),
+ NS_ConvertUTF16toUTF8(aState).get(), shouldKeepDisplayOn);
+
+ // Note the wake lock code ensures that we're not sent duplicate
+ // "locked-foreground" notifications when multiple wake locks are held.
+ if (aState.EqualsASCII("locked-foreground")) {
+ if (assertionId != kIOPMNullAssertionID) {
+ WAKE_LOCK_LOG("already has a lock");
+ return NS_OK;
+ }
+ // Prevent screen saver.
+ CFStringRef cf_topic = ::CFStringCreateWithCharacters(
+ kCFAllocatorDefault, reinterpret_cast<const UniChar*>(aTopic.Data()), aTopic.Length());
+ IOReturn success = ::IOPMAssertionCreateWithName(assertionType, kIOPMAssertionLevelOn,
+ cf_topic, &assertionId);
+ CFRelease(cf_topic);
+ if (success != kIOReturnSuccess) {
+ WAKE_LOCK_LOG("failed to disable screensaver");
+ }
+ WAKE_LOCK_LOG("create screensaver");
+ } else {
+ // Re-enable screen saver.
+ if (assertionId != kIOPMNullAssertionID) {
+ IOReturn result = ::IOPMAssertionRelease(assertionId);
+ if (result != kIOReturnSuccess) {
+ WAKE_LOCK_LOG("failed to release screensaver");
+ }
+ WAKE_LOCK_LOG("Release screensaver");
+ assertionId = kIOPMNullAssertionID;
+ }
+ }
+ return NS_OK;
+ }
+}; // MacWakeLockListener
+
+// defined in nsCocoaWindow.mm
+extern int32_t gXULModalLevel;
+
+static bool gAppShellMethodsSwizzled = false;
+
+void OnUncaughtException(NSException* aException) {
+ nsObjCExceptionLog(aException);
+ MOZ_CRASH("Uncaught Objective C exception from NSSetUncaughtExceptionHandler");
+}
+
+@implementation GeckoNSApplication
+
+// Load is called very early during startup, when the Objective C runtime loads this class.
++ (void)load {
+ NSSetUncaughtExceptionHandler(OnUncaughtException);
+}
+
+// This method is called from NSDefaultTopLevelErrorHandler, which is invoked when an Objective C
+// exception propagates up into the native event loop. It is possible that it is also called in
+// other cases.
+- (void)reportException:(NSException*)aException {
+ if (ShouldIgnoreObjCException(aException)) {
+ return;
+ }
+
+ nsObjCExceptionLog(aException);
+
+#ifdef NIGHTLY_BUILD
+ MOZ_CRASH("Uncaught Objective C exception from -[GeckoNSApplication reportException:]");
+#endif
+}
+
+- (void)sendEvent:(NSEvent*)anEvent {
+ mozilla::BackgroundHangMonitor().NotifyActivity();
+
+ if ([anEvent type] == NSEventTypeApplicationDefined && [anEvent subtype] == kEventSubtypeTrace) {
+ mozilla::SignalTracerThread();
+ return;
+ }
+ [super sendEvent:anEvent];
+}
+
+- (NSEvent*)nextEventMatchingMask:(NSEventMask)mask
+ untilDate:(NSDate*)expiration
+ inMode:(NSString*)mode
+ dequeue:(BOOL)flag {
+ if (expiration) {
+ mozilla::BackgroundHangMonitor().NotifyWait();
+ }
+ NSEvent* nextEvent = [super nextEventMatchingMask:mask
+ untilDate:expiration
+ inMode:mode
+ dequeue:flag];
+ if (expiration) {
+ mozilla::BackgroundHangMonitor().NotifyActivity();
+ }
+ return nextEvent;
+}
+
+@end
+
+// AppShellDelegate
+//
+// Cocoa bridge class. An object of this class is registered to receive
+// notifications.
+//
+@interface AppShellDelegate : NSObject {
+ @private
+ nsAppShell* mAppShell;
+}
+
+- (id)initWithAppShell:(nsAppShell*)aAppShell;
+- (void)applicationWillTerminate:(NSNotification*)aNotification;
+- (BOOL)shouldSaveApplicationState:(NSCoder*)coder;
+- (BOOL)shouldRestoreApplicationState:(NSCoder*)coder;
+@end
+
+// nsAppShell implementation
+
+NS_IMETHODIMP
+nsAppShell::ResumeNative(void) {
+ nsresult retval = nsBaseAppShell::ResumeNative();
+ if (NS_SUCCEEDED(retval) && (mSuspendNativeCount == 0) && mSkippedNativeCallback) {
+ mSkippedNativeCallback = false;
+ ScheduleNativeEventCallback();
+ }
+ return retval;
+}
+
+nsAppShell::nsAppShell()
+ : mAutoreleasePools(nullptr),
+ mDelegate(nullptr),
+ mCFRunLoop(NULL),
+ mCFRunLoopSource(NULL),
+ mRunningEventLoop(false),
+ mStarted(false),
+ mTerminated(false),
+ mSkippedNativeCallback(false),
+ mNativeEventCallbackDepth(0),
+ mNativeEventScheduledDepth(0) {
+ // A Cocoa event loop is running here if (and only if) we've been embedded
+ // by a Cocoa app.
+ mRunningCocoaEmbedded = [NSApp isRunning] ? true : false;
+}
+
+nsAppShell::~nsAppShell() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ hal::Shutdown();
+
+ if (mMemoryPressureSource) {
+ dispatch_release(mMemoryPressureSource);
+ mMemoryPressureSource = nullptr;
+ }
+
+ if (mCFRunLoop) {
+ if (mCFRunLoopSource) {
+ ::CFRunLoopRemoveSource(mCFRunLoop, mCFRunLoopSource, kCFRunLoopCommonModes);
+ ::CFRelease(mCFRunLoopSource);
+ }
+ if (mCFRunLoopObserver) {
+ ::CFRunLoopRemoveObserver(mCFRunLoop, mCFRunLoopObserver, kCFRunLoopCommonModes);
+ ::CFRelease(mCFRunLoopObserver);
+ }
+ ::CFRelease(mCFRunLoop);
+ }
+
+ if (mAutoreleasePools) {
+ NS_ASSERTION(::CFArrayGetCount(mAutoreleasePools) == 0,
+ "nsAppShell destroyed without popping all autorelease pools");
+ ::CFRelease(mAutoreleasePools);
+ }
+
+ [mDelegate release];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+}
+
+NS_IMPL_ISUPPORTS(MacWakeLockListener, nsIDOMMozWakeLockListener)
+mozilla::StaticRefPtr<MacWakeLockListener> sWakeLockListener;
+
+static void AddScreenWakeLockListener() {
+ nsCOMPtr<nsIPowerManagerService> sPowerManagerService =
+ do_GetService(POWERMANAGERSERVICE_CONTRACTID);
+ if (sPowerManagerService) {
+ sWakeLockListener = new MacWakeLockListener();
+ sPowerManagerService->AddWakeLockListener(sWakeLockListener);
+ } else {
+ NS_WARNING("Failed to retrieve PowerManagerService, wakelocks will be broken!");
+ }
+}
+
+static void RemoveScreenWakeLockListener() {
+ nsCOMPtr<nsIPowerManagerService> sPowerManagerService =
+ do_GetService(POWERMANAGERSERVICE_CONTRACTID);
+ if (sPowerManagerService) {
+ sPowerManagerService->RemoveWakeLockListener(sWakeLockListener);
+ sPowerManagerService = nullptr;
+ sWakeLockListener = nullptr;
+ }
+}
+
+void RunLoopObserverCallback(CFRunLoopObserverRef aObserver, CFRunLoopActivity aActivity,
+ void* aInfo) {
+ static_cast<nsAppShell*>(aInfo)->OnRunLoopActivityChanged(aActivity);
+}
+
+void nsAppShell::OnRunLoopActivityChanged(CFRunLoopActivity aActivity) {
+ if (aActivity == kCFRunLoopBeforeWaiting) {
+ mozilla::BackgroundHangMonitor().NotifyWait();
+ }
+
+ // When the event loop is in its waiting state, we would like the profiler to know that the thread
+ // is idle. The usual way to notify the profiler of idleness would be to place a profiler label
+ // frame with the IDLE category on the stack, for the duration of the function that does the
+ // waiting. However, since macOS uses an event loop model where "the event loop calls you", we do
+ // not control the function that does the waiting; the waiting happens inside CFRunLoop code.
+ // Instead, the run loop notifies us when it enters and exits the waiting state, by calling this
+ // function.
+ // So we do not have a function under our control that stays on the stack for the duration of the
+ // wait. So, rather than putting an AutoProfilerLabel on the stack, we will manually push and pop
+ // the label frame here.
+ // The location in the stack where this label frame is inserted is somewhat arbitrary. In
+ // practice, the label frame will be at the very tip of the stack, looking like it's "inside" the
+ // mach_msg_trap wait function.
+ if (aActivity == kCFRunLoopBeforeWaiting) {
+ using ThreadRegistration = mozilla::profiler::ThreadRegistration;
+ ThreadRegistration::WithOnThreadRef([&](ThreadRegistration::OnThreadRef aOnThreadRef) {
+ ProfilingStack& profilingStack =
+ aOnThreadRef.UnlockedConstReaderAndAtomicRWRef().ProfilingStackRef();
+ mProfilingStackWhileWaiting = &profilingStack;
+ uint8_t variableOnStack = 0;
+ profilingStack.pushLabelFrame("Native event loop idle", nullptr, &variableOnStack,
+ JS::ProfilingCategoryPair::IDLE, 0);
+ profiler_thread_sleep();
+ });
+ } else {
+ if (mProfilingStackWhileWaiting) {
+ mProfilingStackWhileWaiting->pop();
+ mProfilingStackWhileWaiting = nullptr;
+ profiler_thread_wake();
+ }
+ }
+}
+
+// Init
+//
+// Loads the nib (see bug 316076c21) and sets up the CFRunLoopSource used to
+// interrupt the main native run loop.
+//
+// public
+nsresult nsAppShell::Init() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // No event loop is running yet (unless an embedding app that uses
+ // NSApplicationMain() is running).
+ NSAutoreleasePool* localPool = [[NSAutoreleasePool alloc] init];
+
+ char* mozAppNoDock = PR_GetEnv("MOZ_APP_NO_DOCK");
+ if (mozAppNoDock && strcmp(mozAppNoDock, "") != 0) {
+ [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
+ }
+
+ // mAutoreleasePools is used as a stack of NSAutoreleasePool objects created
+ // by |this|. CFArray is used instead of NSArray because NSArray wants to
+ // retain each object you add to it, and you can't retain an
+ // NSAutoreleasePool.
+ mAutoreleasePools = ::CFArrayCreateMutable(nullptr, 0, nullptr);
+ NS_ENSURE_STATE(mAutoreleasePools);
+
+ bool isNSApplicationProcessType = (XRE_GetProcessType() != GeckoProcessType_RDD) &&
+ (XRE_GetProcessType() != GeckoProcessType_Socket);
+
+ if (isNSApplicationProcessType) {
+ // This call initializes NSApplication unless:
+ // 1) we're using xre -- NSApp's already been initialized by
+ // MacApplicationDelegate.mm's EnsureUseCocoaDockAPI().
+ // 2) an embedding app that uses NSApplicationMain() is running -- NSApp's
+ // already been initialized and its main run loop is already running.
+ [[NSBundle mainBundle] loadNibNamed:@"res/MainMenu"
+ owner:[GeckoNSApplication sharedApplication]
+ topLevelObjects:nil];
+ }
+
+ mDelegate = [[AppShellDelegate alloc] initWithAppShell:this];
+ NS_ENSURE_STATE(mDelegate);
+
+ // Add a CFRunLoopSource to the main native run loop. The source is
+ // responsible for interrupting the run loop when Gecko events are ready.
+
+ mCFRunLoop = [[NSRunLoop currentRunLoop] getCFRunLoop];
+ NS_ENSURE_STATE(mCFRunLoop);
+ ::CFRetain(mCFRunLoop);
+
+ CFRunLoopSourceContext context;
+ bzero(&context, sizeof(context));
+ // context.version = 0;
+ context.info = this;
+ context.perform = ProcessGeckoEvents;
+
+ mCFRunLoopSource = ::CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
+ NS_ENSURE_STATE(mCFRunLoopSource);
+
+ ::CFRunLoopAddSource(mCFRunLoop, mCFRunLoopSource, kCFRunLoopCommonModes);
+
+ // Add a CFRunLoopObserver so that the profiler can be notified when we enter and exit the waiting
+ // state.
+ CFRunLoopObserverContext observerContext;
+ PodZero(&observerContext);
+ observerContext.info = this;
+
+ mCFRunLoopObserver = ::CFRunLoopObserverCreate(
+ kCFAllocatorDefault, kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting | kCFRunLoopExit, true,
+ 0, RunLoopObserverCallback, &observerContext);
+ NS_ENSURE_STATE(mCFRunLoopObserver);
+
+ ::CFRunLoopAddObserver(mCFRunLoop, mCFRunLoopObserver, kCFRunLoopCommonModes);
+
+ hal::Init();
+
+ if (XRE_IsParentProcess()) {
+ ScreenManager& screenManager = ScreenManager::GetSingleton();
+
+ if (gfxPlatform::IsHeadless()) {
+ screenManager.SetHelper(mozilla::MakeUnique<HeadlessScreenHelper>());
+ } else {
+ screenManager.SetHelper(mozilla::MakeUnique<ScreenHelperCocoa>());
+ }
+
+ InitMemoryPressureObserver();
+ }
+
+ nsresult rv = nsBaseAppShell::Init();
+
+ if (isNSApplicationProcessType && !gAppShellMethodsSwizzled) {
+ // We should only replace the original terminate: method if we're not
+ // running in a Cocoa embedder. See bug 604901.
+ if (!mRunningCocoaEmbedded) {
+ nsToolkit::SwizzleMethods([NSApplication class], @selector(terminate:),
+ @selector(nsAppShell_NSApplication_terminate:));
+ }
+ gAppShellMethodsSwizzled = true;
+ }
+
+#if !defined(RELEASE_OR_BETA) || defined(DEBUG)
+ if (Preferences::GetBool("security.sandbox.mac.track.violations", false)) {
+ nsSandboxViolationSink::Start();
+ }
+#endif
+
+ [localPool release];
+
+ return rv;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// ProcessGeckoEvents
+//
+// The "perform" target of mCFRunLoop, called when mCFRunLoopSource is
+// signalled from ScheduleNativeEventCallback.
+//
+// Arrange for Gecko events to be processed on demand (in response to a call
+// to ScheduleNativeEventCallback(), if processing of Gecko events via "native
+// methods" hasn't been suspended). This happens in NativeEventCallback().
+//
+// protected static
+void nsAppShell::ProcessGeckoEvents(void* aInfo) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ AUTO_PROFILER_LABEL("nsAppShell::ProcessGeckoEvents", OTHER);
+
+ nsAppShell* self = static_cast<nsAppShell*>(aInfo);
+
+ if (self->mRunningEventLoop) {
+ self->mRunningEventLoop = false;
+
+ // The run loop may be sleeping -- [NSRunLoop runMode:...]
+ // won't return until it's given a reason to wake up. Awaken it by
+ // posting a bogus event. There's no need to make the event
+ // presentable.
+ //
+ // But _don't_ set windowNumber to '-1' -- that can lead to nasty
+ // weirdness like bmo bug 397039 (a crash in [NSApp sendEvent:] on one of
+ // these fake events, because the -1 has gotten changed into the number
+ // of an actual NSWindow object, and that NSWindow object has just been
+ // destroyed). Setting windowNumber to '0' seems to work fine -- this
+ // seems to prevent the OS from ever trying to associate our bogus event
+ // with a particular NSWindow object.
+ [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined
+ location:NSMakePoint(0, 0)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:NULL
+ subtype:kEventSubtypeNone
+ data1:0
+ data2:0]
+ atStart:NO];
+ // Previously we used to send this second event regardless of
+ // self->mRunningEventLoop. However, that was removed in bug 1690687 for
+ // performance reasons. It is still needed for the mRunningEventLoop case
+ // otherwise we'll get in a cycle of sending postEvent followed by the
+ // DummyEvent inserted by nsBaseAppShell::OnProcessNextEvent. This second
+ // event will cause the second call to AcquireFirstMatchingEventInQueue in
+ // nsAppShell::ProcessNextNativeEvent to return true. Which makes
+ // nsBaseAppShell::OnProcessNextEvent call nsAppShell::ProcessNextNativeEvent
+ // again during which it will loop until it sleeps because ProcessGeckoEvents()
+ // won't be called for the DummyEvent.
+ //
+ // This is not a good approach and we should fix things up so that only
+ // one postEvent is needed.
+ [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined
+ location:NSMakePoint(0, 0)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:NULL
+ subtype:kEventSubtypeNone
+ data1:0
+ data2:0]
+ atStart:NO];
+ }
+
+ if (self->mSuspendNativeCount <= 0) {
+ ++self->mNativeEventCallbackDepth;
+ self->NativeEventCallback();
+ --self->mNativeEventCallbackDepth;
+ } else {
+ self->mSkippedNativeCallback = true;
+ }
+
+ if (self->mTerminated) {
+ // Still needed to avoid crashes on quit in most Mochitests.
+ [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined
+ location:NSMakePoint(0, 0)
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:NULL
+ subtype:kEventSubtypeNone
+ data1:0
+ data2:0]
+ atStart:NO];
+ }
+ // Normally every call to ScheduleNativeEventCallback() results in
+ // exactly one call to ProcessGeckoEvents(). So each Release() here
+ // normally balances exactly one AddRef() in ScheduleNativeEventCallback().
+ // But if Exit() is called just after ScheduleNativeEventCallback(), the
+ // corresponding call to ProcessGeckoEvents() will never happen. We check
+ // for this possibility in two different places -- here and in Exit()
+ // itself. If we find here that Exit() has been called (that mTerminated
+ // is true), it's because we've been called recursively, that Exit() was
+ // called from self->NativeEventCallback() above, and that we're unwinding
+ // the recursion. In this case we'll never be called again, and we balance
+ // here any extra calls to ScheduleNativeEventCallback().
+ //
+ // When ProcessGeckoEvents() is called recursively, it's because of a
+ // call to ScheduleNativeEventCallback() from NativeEventCallback(). We
+ // balance the "extra" AddRefs here (rather than always in Exit()) in order
+ // to ensure that 'self' stays alive until the end of this method. We also
+ // make sure not to finish the balancing until all the recursion has been
+ // unwound.
+ if (self->mTerminated) {
+ int32_t releaseCount = 0;
+ if (self->mNativeEventScheduledDepth > self->mNativeEventCallbackDepth) {
+ releaseCount =
+ PR_ATOMIC_SET(&self->mNativeEventScheduledDepth, self->mNativeEventCallbackDepth);
+ }
+ while (releaseCount-- > self->mNativeEventCallbackDepth) self->Release();
+ } else {
+ // As best we can tell, every call to ProcessGeckoEvents() is triggered
+ // by a call to ScheduleNativeEventCallback(). But we've seen a few
+ // (non-reproducible) cases of double-frees that *might* have been caused
+ // by spontaneous calls (from the OS) to ProcessGeckoEvents(). So we
+ // deal with that possibility here.
+ if (PR_ATOMIC_DECREMENT(&self->mNativeEventScheduledDepth) < 0) {
+ PR_ATOMIC_SET(&self->mNativeEventScheduledDepth, 0);
+ NS_WARNING("Spontaneous call to ProcessGeckoEvents()!");
+ } else {
+ self->Release();
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// WillTerminate
+//
+// Called by the AppShellDelegate when an NSApplicationWillTerminate
+// notification is posted. After this method is called, native events should
+// no longer be processed. The NSApplicationWillTerminate notification is
+// only posted when [NSApp terminate:] is called, which doesn't happen on a
+// "normal" application quit.
+//
+// public
+void nsAppShell::WillTerminate() {
+ if (mTerminated) return;
+
+ // Make sure that the nsAppExitEvent posted by nsAppStartup::Quit() (called
+ // from [MacApplicationDelegate applicationShouldTerminate:]) gets run.
+ NS_ProcessPendingEvents(NS_GetCurrentThread());
+
+ mTerminated = true;
+}
+
+// ScheduleNativeEventCallback
+//
+// Called (possibly on a non-main thread) when Gecko has an event that
+// needs to be processed. The Gecko event needs to be processed on the
+// main thread, so the native run loop must be interrupted.
+//
+// In nsBaseAppShell.cpp, the mNativeEventPending variable is used to
+// ensure that ScheduleNativeEventCallback() is called no more than once
+// per call to NativeEventCallback(). ProcessGeckoEvents() can skip its
+// call to NativeEventCallback() if processing of Gecko events by native
+// means is suspended (using nsIAppShell::SuspendNative()), which will
+// suspend calls from nsBaseAppShell::OnDispatchedEvent() to
+// ScheduleNativeEventCallback(). But when Gecko event processing by
+// native means is resumed (in ResumeNative()), an extra call is made to
+// ScheduleNativeEventCallback() (from ResumeNative()). This triggers
+// another call to ProcessGeckoEvents(), which calls NativeEventCallback(),
+// and nsBaseAppShell::OnDispatchedEvent() resumes calling
+// ScheduleNativeEventCallback().
+//
+// protected virtual
+void nsAppShell::ScheduleNativeEventCallback() {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (mTerminated) return;
+
+ // Each AddRef() here is normally balanced by exactly one Release() in
+ // ProcessGeckoEvents(). But there are exceptions, for which see
+ // ProcessGeckoEvents() and Exit().
+ NS_ADDREF_THIS();
+ PR_ATOMIC_INCREMENT(&mNativeEventScheduledDepth);
+
+ // This will invoke ProcessGeckoEvents on the main thread.
+ ::CFRunLoopSourceSignal(mCFRunLoopSource);
+ ::CFRunLoopWakeUp(mCFRunLoop);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// Undocumented Cocoa Event Manager function, present in the same form since
+// at least OS X 10.6.
+extern "C" EventAttributes GetEventAttributes(EventRef inEvent);
+
+// ProcessNextNativeEvent
+//
+// If aMayWait is false, process a single native event. If it is true, run
+// the native run loop until stopped by ProcessGeckoEvents.
+//
+// Returns true if more events are waiting in the native event queue.
+//
+// protected virtual
+bool nsAppShell::ProcessNextNativeEvent(bool aMayWait) {
+ bool moreEvents = false;
+
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ bool eventProcessed = false;
+ NSString* currentMode = nil;
+
+ if (mTerminated) return false;
+
+ // Do not call -[NSApplication nextEventMatchingMask:...] when we're trying to close a native
+ // menu. Doing so could confuse the NSMenu's closing mechanism. Instead, we try to unwind the
+ // stack as quickly as possible and return to the parent event loop. At that point, native events
+ // will be processed.
+ if (MOZMenuOpeningCoordinator.needToUnwindForMenuClosing) {
+ return false;
+ }
+
+ bool wasRunningEventLoop = mRunningEventLoop;
+ mRunningEventLoop = aMayWait;
+ NSDate* waitUntil = nil;
+ if (aMayWait) waitUntil = [NSDate distantFuture];
+
+ NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop];
+
+ EventQueueRef currentEventQueue = GetCurrentEventQueue();
+
+ if (aMayWait) {
+ mozilla::BackgroundHangMonitor().NotifyWait();
+ }
+
+ // Only call -[NSApp sendEvent:] (and indirectly send user-input events to
+ // Gecko) if aMayWait is true. Tbis ensures most calls to -[NSApp
+ // sendEvent:] happen under nsAppShell::Run(), at the lowest level of
+ // recursion -- thereby making it less likely Gecko will process user-input
+ // events in the wrong order or skip some of them. It also avoids eating
+ // too much CPU in nsBaseAppShell::OnProcessNextEvent() (which calls
+ // us) -- thereby avoiding the starvation of nsIRunnable events in
+ // nsThread::ProcessNextEvent(). For more information see bug 996848.
+ do {
+ // No autorelease pool is provided here, because OnProcessNextEvent
+ // and AfterProcessNextEvent are responsible for maintaining it.
+ NS_ASSERTION(mAutoreleasePools && ::CFArrayGetCount(mAutoreleasePools),
+ "No autorelease pool for native event");
+
+ if (aMayWait) {
+ currentMode = [currentRunLoop currentMode];
+ if (!currentMode) currentMode = NSDefaultRunLoopMode;
+ NSEvent* nextEvent = [NSApp nextEventMatchingMask:NSEventMaskAny
+ untilDate:waitUntil
+ inMode:currentMode
+ dequeue:YES];
+ if (nextEvent) {
+ mozilla::BackgroundHangMonitor().NotifyActivity();
+ [NSApp sendEvent:nextEvent];
+ eventProcessed = true;
+ }
+ } else {
+ // In at least 10.15, AcquireFirstMatchingEventInQueue will move 1
+ // CGEvent from the CGEvent queue into the Carbon event queue. Unfortunately,
+ // once an event has been moved to the Carbon event queue it's no longer a
+ // candidate for coalescing. This means that even if we don't remove the
+ // event from the queue, just calling AcquireFirstMatchingEventInQueue can
+ // cause behaviour change. Prior to bug 1690687 landing, the event that we got
+ // from AcquireFirstMatchingEventInQueue was often our own ApplicationDefined
+ // event. However, once we stopped posting that event on every Gecko
+ // event we're much more likely to get a CGEvent. When we have a high
+ // amount of load on the main thread, we end up alternating between Gecko
+ // events and native events. Without CGEvent coalescing, the native
+ // event events can accumulate in the Carbon event queue which will
+ // manifest as laggy scrolling.
+#if 1
+ eventProcessed = false;
+ break;
+#else
+ // AcquireFirstMatchingEventInQueue() doesn't spin the (native) event
+ // loop, though it does queue up any newly available events from the
+ // window server.
+ EventRef currentEvent =
+ AcquireFirstMatchingEventInQueue(currentEventQueue, 0, NULL, kEventQueueOptionsNone);
+ if (!currentEvent) {
+ continue;
+ }
+ EventAttributes attrs = GetEventAttributes(currentEvent);
+ UInt32 eventKind = GetEventKind(currentEvent);
+ UInt32 eventClass = GetEventClass(currentEvent);
+ bool osCocoaEvent =
+ ((eventClass == 'appl') || (eventClass == kEventClassAppleEvent) ||
+ ((eventClass == 'cgs ') && (eventKind != NSEventTypeApplicationDefined)));
+ // If attrs is kEventAttributeUserEvent or kEventAttributeMonitored
+ // (i.e. a user input event), we shouldn't process it here while
+ // aMayWait is false. Likewise if currentEvent will eventually be
+ // turned into an OS-defined Cocoa event, or otherwise needs AppKit
+ // processing. Doing otherwise risks doing too much work here, and
+ // preventing the event from being properly processed by the AppKit
+ // framework.
+ if ((attrs != kEventAttributeNone) || osCocoaEvent) {
+ // Since we can't process the next event here (while aMayWait is false),
+ // we want moreEvents to be false on return.
+ eventProcessed = false;
+ // This call to ReleaseEvent() matches a call to RetainEvent() in
+ // AcquireFirstMatchingEventInQueue() above.
+ ReleaseEvent(currentEvent);
+ break;
+ }
+ // This call to RetainEvent() matches a call to ReleaseEvent() in
+ // RemoveEventFromQueue() below.
+ RetainEvent(currentEvent);
+ RemoveEventFromQueue(currentEventQueue, currentEvent);
+ EventTargetRef eventDispatcherTarget = GetEventDispatcherTarget();
+ SendEventToEventTarget(currentEvent, eventDispatcherTarget);
+ // This call to ReleaseEvent() matches a call to RetainEvent() in
+ // AcquireFirstMatchingEventInQueue() above.
+ ReleaseEvent(currentEvent);
+ eventProcessed = true;
+#endif
+ }
+ } while (mRunningEventLoop);
+
+ if (eventProcessed) {
+ moreEvents = (AcquireFirstMatchingEventInQueue(currentEventQueue, 0, NULL,
+ kEventQueueOptionsNone) != NULL);
+ }
+
+ mRunningEventLoop = wasRunningEventLoop;
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+
+ if (!moreEvents) {
+ nsChildView::UpdateCurrentInputEventCount();
+ }
+
+ return moreEvents;
+}
+
+// Attempt to work around bug 1801419 by loading and initializing the
+// SidecarCore private framework as the app shell starts up. This normally
+// happens on demand, the first time any Cmd-key combination is pressed, and
+// sometimes triggers crashes, caused by an Apple bug. We hope that doing it
+// now, and somewhat more simply, will avoid the crashes. They happen
+// (intermittently) when SidecarCore code tries to access C strings in special
+// sections of its own __TEXT segment, and triggers fatal page faults (which
+// is Apple's bug). Many of the C strings are part of the Objective-C class
+// hierarchy (class names and so forth). We hope that adding them to this
+// hierarchy will "pin" them in place -- so they'll rarely, if ever, be paged
+// out again. Bug 1801419's crashes happen much more often on macOS 13
+// (Ventura) than on other versions of macOS. So we only use this hack on
+// macOS 13 and up.
+static void PinSidecarCoreTextCStringSections() {
+ if (!dlopen("/System/Library/PrivateFrameworks/SidecarCore.framework/SidecarCore", RTLD_LAZY)) {
+ return;
+ }
+
+ // Explicitly run the most basic part of the initialization code that
+ // normally runs automatically on the first Cmd-key combination.
+ Class displayManagerClass = NSClassFromString(@"SidecarDisplayManager");
+ if ([displayManagerClass respondsToSelector:@selector(sharedManager)]) {
+ id sharedManager = [displayManagerClass performSelector:@selector(sharedManager)];
+ if ([sharedManager respondsToSelector:@selector(devices)]) {
+ [sharedManager performSelector:@selector(devices)];
+ }
+ }
+}
+
+// Run
+//
+// Overrides the base class's Run() method to call [NSApp run] (which spins
+// the native run loop until the application quits). Since (unlike the base
+// class's Run() method) we don't process any Gecko events here, they need
+// to be processed elsewhere (in NativeEventCallback(), called from
+// ProcessGeckoEvents()).
+//
+// Camino called [NSApp run] on its own (via NSApplicationMain()), and so
+// didn't call nsAppShell::Run().
+//
+// public
+NS_IMETHODIMP
+nsAppShell::Run(void) {
+ NS_ASSERTION(!mStarted, "nsAppShell::Run() called multiple times");
+ if (mStarted || mTerminated) return NS_OK;
+
+ mStarted = true;
+
+ if (XRE_IsParentProcess()) {
+ if (nsCocoaFeatures::OnVenturaOrLater()) {
+ PinSidecarCoreTextCStringSections();
+ }
+ AddScreenWakeLockListener();
+ }
+
+ // We use the native Gecko event loop in content processes.
+ nsresult rv = NS_OK;
+ if (XRE_UseNativeEventProcessing()) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ [NSApp run];
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ } else {
+ rv = nsBaseAppShell::Run();
+ }
+
+ if (XRE_IsParentProcess()) {
+ RemoveScreenWakeLockListener();
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsAppShell::Exit(void) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // This method is currently called more than once -- from (according to
+ // mento) an nsAppExitEvent dispatched by nsAppStartup::Quit() and from an
+ // XPCOM shutdown notification that nsBaseAppShell has registered to
+ // receive. So we need to ensure that multiple calls won't break anything.
+ // But we should also complain about it (since it isn't quite kosher).
+ if (mTerminated) {
+ NS_WARNING("nsAppShell::Exit() called redundantly");
+ return NS_OK;
+ }
+
+ mTerminated = true;
+
+#if !defined(RELEASE_OR_BETA) || defined(DEBUG)
+ nsSandboxViolationSink::Stop();
+#endif
+
+ // Quoting from Apple's doc on the [NSApplication stop:] method (from their
+ // doc on the NSApplication class): "If this method is invoked during a
+ // modal event loop, it will break that loop but not the main event loop."
+ // nsAppShell::Exit() shouldn't be called from a modal event loop. So if
+ // it is we complain about it (to users of debug builds) and call [NSApp
+ // stop:] one extra time. (I'm not sure if modal event loops can be nested
+ // -- Apple's docs don't say one way or the other. But the return value
+ // of [NSApp _isRunningModal] doesn't change immediately after a call to
+ // [NSApp stop:], so we have to assume that one extra call to [NSApp stop:]
+ // will do the job.)
+ BOOL cocoaModal = [NSApp _isRunningModal];
+ NS_ASSERTION(!cocoaModal, "Don't call nsAppShell::Exit() from a modal event loop!");
+ if (cocoaModal) [NSApp stop:nullptr];
+ [NSApp stop:nullptr];
+
+ // A call to Exit() just after a call to ScheduleNativeEventCallback()
+ // prevents the (normally) matching call to ProcessGeckoEvents() from
+ // happening. If we've been called from ProcessGeckoEvents() (as usually
+ // happens), we take care of it there. But if we have an unbalanced call
+ // to ScheduleNativeEventCallback() and ProcessGeckoEvents() isn't on the
+ // stack, we need to take care of the problem here.
+ if (!mNativeEventCallbackDepth && mNativeEventScheduledDepth) {
+ int32_t releaseCount = PR_ATOMIC_SET(&mNativeEventScheduledDepth, 0);
+ while (releaseCount-- > 0) NS_RELEASE_THIS();
+ }
+
+ return nsBaseAppShell::Exit();
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// OnProcessNextEvent
+//
+// This nsIThreadObserver method is called prior to processing an event.
+// Set up an autorelease pool that will service any autoreleased Cocoa
+// objects during this event. This includes native events processed by
+// ProcessNextNativeEvent. The autorelease pool will be popped by
+// AfterProcessNextEvent, it is important for these two methods to be
+// tightly coupled.
+//
+// public
+NS_IMETHODIMP
+nsAppShell::OnProcessNextEvent(nsIThreadInternal* aThread, bool aMayWait) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NS_ASSERTION(mAutoreleasePools, "No stack on which to store autorelease pool");
+
+ NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+ ::CFArrayAppendValue(mAutoreleasePools, pool);
+
+ return nsBaseAppShell::OnProcessNextEvent(aThread, aMayWait);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// AfterProcessNextEvent
+//
+// This nsIThreadObserver method is called after event processing is complete.
+// The Cocoa implementation cleans up the autorelease pool create by the
+// previous OnProcessNextEvent call.
+//
+// public
+NS_IMETHODIMP
+nsAppShell::AfterProcessNextEvent(nsIThreadInternal* aThread, bool aEventWasProcessed) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ CFIndex count = ::CFArrayGetCount(mAutoreleasePools);
+
+ NS_ASSERTION(mAutoreleasePools && count, "Processed an event, but there's no autorelease pool?");
+
+ const NSAutoreleasePool* pool =
+ static_cast<const NSAutoreleasePool*>(::CFArrayGetValueAtIndex(mAutoreleasePools, count - 1));
+ ::CFArrayRemoveValueAtIndex(mAutoreleasePools, count - 1);
+ [pool release];
+
+ return nsBaseAppShell::AfterProcessNextEvent(aThread, aEventWasProcessed);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void nsAppShell::InitMemoryPressureObserver() {
+ // Testing shows that sometimes the memory pressure event is not fired for
+ // over a minute after the memory pressure change is reflected in sysctl
+ // values. Hence this may need to be augmented with polling of the memory
+ // pressure sysctls for lower latency reactions to OS memory pressure. This
+ // was also observed when using DISPATCH_QUEUE_PRIORITY_HIGH.
+ mMemoryPressureSource =
+ dispatch_source_create(DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, 0,
+ DISPATCH_MEMORYPRESSURE_NORMAL | DISPATCH_MEMORYPRESSURE_WARN |
+ DISPATCH_MEMORYPRESSURE_CRITICAL,
+ dispatch_get_main_queue());
+
+ dispatch_source_set_event_handler(mMemoryPressureSource, ^{
+ dispatch_source_memorypressure_flags_t pressureLevel =
+ dispatch_source_get_data(mMemoryPressureSource);
+ nsAppShell::OnMemoryPressureChanged(pressureLevel);
+ });
+
+ dispatch_resume(mMemoryPressureSource);
+
+ // Initialize the memory watcher.
+ RefPtr<mozilla::nsAvailableMemoryWatcherBase> watcher(
+ nsAvailableMemoryWatcherBase::GetSingleton());
+}
+
+void nsAppShell::OnMemoryPressureChanged(dispatch_source_memorypressure_flags_t aPressureLevel) {
+ // The memory pressure dispatch source is created (above) with
+ // dispatch_get_main_queue() which always fires on the main thread.
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MacMemoryPressureLevel geckoPressureLevel;
+ switch (aPressureLevel) {
+ case DISPATCH_MEMORYPRESSURE_NORMAL:
+ geckoPressureLevel = MacMemoryPressureLevel::Value::eNormal;
+ break;
+ case DISPATCH_MEMORYPRESSURE_WARN:
+ geckoPressureLevel = MacMemoryPressureLevel::Value::eWarning;
+ break;
+ case DISPATCH_MEMORYPRESSURE_CRITICAL:
+ geckoPressureLevel = MacMemoryPressureLevel::Value::eCritical;
+ break;
+ default:
+ geckoPressureLevel = MacMemoryPressureLevel::Value::eUnexpected;
+ }
+
+ RefPtr<mozilla::nsAvailableMemoryWatcherBase> watcher(
+ nsAvailableMemoryWatcherBase::GetSingleton());
+ watcher->OnMemoryPressureChanged(geckoPressureLevel);
+}
+
+// AppShellDelegate implementation
+
+@implementation AppShellDelegate
+// initWithAppShell:
+//
+// Constructs the AppShellDelegate object
+- (id)initWithAppShell:(nsAppShell*)aAppShell {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ((self = [self init])) {
+ mAppShell = aAppShell;
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(applicationWillTerminate:)
+ name:NSApplicationWillTerminateNotification
+ object:NSApp];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(applicationDidBecomeActive:)
+ name:NSApplicationDidBecomeActiveNotification
+ object:NSApp];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(timezoneChanged:)
+ name:NSSystemTimeZoneDidChangeNotification
+ object:nil];
+ }
+
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// applicationWillTerminate:
+//
+// Notify the nsAppShell that native event processing should be discontinued.
+- (void)applicationWillTerminate:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ mAppShell->WillTerminate();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+// applicationDidBecomeActive
+//
+// Make sure TextInputHandler::sLastModifierState is updated when we become
+// active (since we won't have received [ChildView flagsChanged:] messages
+// while inactive).
+- (void)applicationDidBecomeActive:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // [NSEvent modifierFlags] is valid on every kind of event, so we don't need
+ // to worry about getting an NSInternalInconsistencyException here.
+ NSEvent* currentEvent = [NSApp currentEvent];
+ if (currentEvent) {
+ TextInputHandler::sLastModifierState =
+ [currentEvent modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask;
+ }
+
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ if (observerService) {
+ observerService->NotifyObservers(nullptr, NS_WIDGET_MAC_APP_ACTIVATE_OBSERVER_TOPIC, nullptr);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)timezoneChanged:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ nsBaseAppShell::OnSystemTimezoneChange();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (BOOL)shouldSaveApplicationState:(NSCoder*)coder {
+ return YES;
+}
+
+- (BOOL)shouldRestoreApplicationState:(NSCoder*)coder {
+ return YES;
+}
+
+@end
+
+// We hook terminate: in order to make OS-initiated termination work nicely
+// with Gecko's shutdown sequence. (Two ways to trigger OS-initiated
+// termination: 1) Quit from the Dock menu; 2) Log out from (or shut down)
+// your computer while the browser is active.)
+@interface NSApplication (MethodSwizzling)
+- (void)nsAppShell_NSApplication_terminate:(id)sender;
+@end
+
+@implementation NSApplication (MethodSwizzling)
+
+// Called by the OS after [MacApplicationDelegate applicationShouldTerminate:]
+// has returned NSTerminateNow. This method "subclasses" and replaces the
+// OS's original implementation. The only thing the orginal method does which
+// we need is that it posts NSApplicationWillTerminateNotification. Everything
+// else is unneeded (because it's handled elsewhere), or actively interferes
+// with Gecko's shutdown sequence. For example the original terminate: method
+// causes the app to exit() inside [NSApp run] (called from nsAppShell::Run()
+// above), which means that nothing runs after the call to nsAppStartup::Run()
+// in XRE_Main(), which in particular means that ScopedXPCOMStartup's destructor
+// and NS_ShutdownXPCOM() never get called.
+- (void)nsAppShell_NSApplication_terminate:(id)sender {
+ [[NSNotificationCenter defaultCenter] postNotificationName:NSApplicationWillTerminateNotification
+ object:NSApp];
+}
+
+@end