diff options
Diffstat (limited to '')
-rw-r--r-- | widget/cocoa/nsAppShell.mm | 1120 |
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 |