From 5ef8254cba6803fb3a05e726006f63f7309f493c Mon Sep 17 00:00:00 2001 From: Mykola Baibuz Date: Thu, 3 Apr 2025 22:12:54 +0300 Subject: [PATCH] MacOS suspend mode handler draft --- client/platforms/macos/macosnetworkwatcher.h | 21 +++++ client/platforms/macos/macosnetworkwatcher.mm | 89 +++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/client/platforms/macos/macosnetworkwatcher.h b/client/platforms/macos/macosnetworkwatcher.h index 2ae4cfd7..faddc565 100644 --- a/client/platforms/macos/macosnetworkwatcher.h +++ b/client/platforms/macos/macosnetworkwatcher.h @@ -10,8 +10,28 @@ #include "../ios/iosnetworkwatcher.h" #include "networkwatcherimpl.h" +#include +#include + + class QString; +// Inspired by https://ladydebug.com/blog/2020/05/21/programmatically-capture-energy-saver-event-on-mac/ +class PowerNotificationsListener +{ +public: + void registerForNotifications(); + +private: + static void sleepWakeupCallBack(void *refParam, io_service_t service, natural_t messageType, void *messageArgument); + +private: + IONotificationPortRef notifyPortRef = nullptr; // notification port allocated by IORegisterForSystemPower + io_object_t notifierObj = IO_OBJECT_NULL; // notifier object, used to deregister later + io_connect_t rootPowerDomain = IO_OBJECT_NULL; // a reference to the Root Power Domain IOService +}; + + class MacOSNetworkWatcher final : public IOSNetworkWatcher { public: MacOSNetworkWatcher(QObject* parent); @@ -25,6 +45,7 @@ class MacOSNetworkWatcher final : public IOSNetworkWatcher { private: void* m_delegate = nullptr; + PowerNotificationsListener m_powerlistener; }; #endif // MACOSNETWORKWATCHER_H diff --git a/client/platforms/macos/macosnetworkwatcher.mm b/client/platforms/macos/macosnetworkwatcher.mm index 3b4158a4..d4431941 100644 --- a/client/platforms/macos/macosnetworkwatcher.mm +++ b/client/platforms/macos/macosnetworkwatcher.mm @@ -38,6 +38,93 @@ Logger logger("MacOSNetworkWatcher"); @end +void PowerNotificationsListener::registerForNotifications() +{ + rootPowerDomain = IORegisterForSystemPower(this, ¬ifyPortRef, sleepWakeupCallBack, ¬ifierObj); + if (rootPowerDomain == IO_OBJECT_NULL) { + logger.debug() << "Failed to register for system power notifications!"; + return; + } + + logger.debug() << "IORegisterForSystemPower OK! Root port:" << rootPowerDomain; + + // add the notification port to the application runloop + CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notifyPortRef), kCFRunLoopCommonModes); +} + +static void PowerNotificationsListener::sleepWakeupCallBack(void *refParam, io_service_t service, natural_t messageType, void *messageArgument) +{ + Q_UNUSED(service) + + auto listener = static_cast(refParam); + + switch (messageType) { + case kIOMessageCanSystemSleep: + /* Idle sleep is about to kick in. This message will not be sent for forced sleep. + * Applications have a chance to prevent sleep by calling IOCancelPowerChange. + * Most applications should not prevent idle sleep. Power Management waits up to + * 30 seconds for you to either allow or deny idle sleep. If you don’t acknowledge + * this power change by calling either IOAllowPowerChange or IOCancelPowerChange, + * the system will wait 30 seconds then go to sleep. + */ + + logger.debug() << "System power message: can system sleep?"; + + // Uncomment to cancel idle sleep + // IOCancelPowerChange(thiz->rootPowerDomain, reinterpret_cast(messageArgument)); + + // Allow idle sleep + IOAllowPowerChange(listener->rootPowerDomain, reinterpret_cast(messageArgument)); + break; + + case kIOMessageSystemWillNotSleep: + /* Announces that the system has retracted a previous attempt to sleep; it + * follows `kIOMessageCanSystemSleep`. + */ + logger.debug() << "System power message: system will NOT sleep."; + break; + + case kIOMessageSystemWillSleep: + /* The system WILL go to sleep. If you do not call IOAllowPowerChange or + * IOCancelPowerChange to acknowledge this message, sleep will be delayed by + * 30 seconds. + * + * NOTE: If you call IOCancelPowerChange to deny sleep it returns kIOReturnSuccess, + * however the system WILL still go to sleep. + */ + + logger.debug() << "System power message: system WILL sleep."; + + IOAllowPowerChange(listener->rootPowerDomain, reinterpret_cast(messageArgument)); + break; + + case kIOMessageSystemWillPowerOn: + /* Announces that the system is beginning to power the device tree; most devices + * are still unavailable at this point. + */ + /* From the documentation: + * + * - kIOMessageSystemWillPowerOn is delivered at early wakeup time, before most hardware + * has been powered on. Be aware that any attempts to access disk, network, the display, + * etc. may result in errors or blocking your process until those resources become + * available. + * + * So we do NOT log this event. + */ + break; + + case kIOMessageSystemHasPoweredOn: + /* Announces that the system and its devices have woken up. */ + logger.debug() << "System power message: system has powered on."; + break; + + default: + logger.debug() << "System power message: other event: " << messageType; + /* Not a system sleep and wake notification. */ + break; + } +} + MacOSNetworkWatcher::MacOSNetworkWatcher(QObject* parent) : IOSNetworkWatcher(parent) { MZ_COUNT_CTOR(MacOSNetworkWatcher); } @@ -66,6 +153,8 @@ void MacOSNetworkWatcher::start() { logger.debug() << "Delegate already registered"; return; } + + m_powerlistener.registerForNotifications(); CWWiFiClient* client = CWWiFiClient.sharedWiFiClient; if (!client) {