Intro
Continuing on from where we left off in part 1, let's review a few more options of writing launch daemons, or how we can deal with power operations in macOS.
The full source code for the code samples that I used below could be found in my GitHub repo.
Contents
This post needs its own table of contents:
- Shutdown/Reboot/Logoff Notifications
- Sleep/Wake Notifications
- How To Wake macOS From Sleep
- How To Put macOS to Sleep, or to Reboot, Shutdown
- Put macOS to Sleep
- Reboot/Shutdown macOS
Shutdown/Reboot/Logoff Notifications
While writing a regular GUI or a console app, you may never need to know when the macOS had initiated, or about to initiate a shutdown, reboot, or when an interactive user is logging off. But if you are writing a launch daemon, or a service that runs in the background, you may need to receive such notifications so that you can start saving state, or perform some other actions.
Unfortunately such notifications are not well documented by Apple and it took me a while to figure out how to receive them. And if you ask someone else, the go-to response you get on the popular forums is the smarmy, "Why do you need to know this?"
Well, this website is not your popular toxic programming forum, and I can't assume why you need to know this. Thus, let me show you how to get these notifications.
To figure out how Apple does it in their power management components, I had to dig through the source code. Because of that, a usual warning applies to this section.
The following contains functions and features that are not documented by the original manufacturer. By following it, you're doing so at your own risk. The methods presented below may rely on the internal implementation and may not work in the future.
I was able to come up with the following class that encapsulates all the work:
#include <CoreFoundation/CoreFoundation.h>
#include <notify.h>
#include <mach/mach_port.h>
struct Notif_RebootShutdown
{
Notif_RebootShutdown()
{
}
~Notif_RebootShutdown()
{
//Remove callback from a destructor
if(!remove_Notifications(false))
{
//Should not fail here!
assert(false);
}
}
///Set the callback to receive reboot, shutdown & logoff notifications (cannot be called repeatedly)
///'pPortName' = name of the mach port for the notification:
/// - "com.apple.system.loginwindow.shutdownInitiated" for when shut-down warning is shown, may be aborted
/// - "com.apple.system.loginwindow.restartinitiated" for when restart warning is shown, may be aborted
/// - "com.apple.system.loginwindow.logoutInitiated" for when log-out warning is shown, may be aborted
/// - "com.apple.system.loginwindow.logoutcancelled" previous shutdown, restart, logout was aborted
/// - "com.apple.system.loginwindow.logoutNoReturn" previous shutdown, restart, logout is proceeding, can't abort
///'pfn' = callback function that is called when event happens, or null not to call it
///'pParam1' = passed directly into 'pfn' when it's called
///'pParam2' = passed directly into 'pfn' when it's called
///RETURN:
/// - true if success
bool init_Notifications(const char* pPortName,
void (*pfn)(mach_msg_header_t* pHeader,
const char* pPortName,
const void* pParam1,
const void* pParam2) = nullptr,
const void* pParam1 = nullptr,
const void* pParam2 = nullptr)
{
bool bRes = false;
//We must have a port name
if(pPortName &&
pPortName[0])
{
//Act from within a lock
WRITER_LOCK wrl(_lock);
if(!_bCallbackSet)
{
//Remember parameters
_pfnCallback = pfn;
_pParam1 = pParam1;
_pParam2 = pParam2;
//Register for notifications
uint32_t nResShtdn = notify_register_mach_port(pPortName,
&_shutDownMachPort,
(_shutDownMachPort == MACH_PORT_NULL) ? 0 : NOTIFY_REUSE,
&_nShutdownNtf);
if(nResShtdn == NOTIFY_STATUS_OK)
{
//Add to run-loop notifications
CFMachPortContext ctx = {};
ctx.info = this;
_strPortName = pPortName;
Boolean bShouldFree = false;
_shutDownMachPortRef = CFMachPortCreateWithPort(kCFAllocatorDefault,
_shutDownMachPort,
_onCallback,
&ctx,
&bShouldFree);
if(_shutDownMachPortRef)
{
_shutdownRunLoopRef = CFMachPortCreateRunLoopSource(nullptr, _shutDownMachPortRef, 0);
if(_shutdownRunLoopRef)
{
//Add to the run loop
CFRunLoopAddSource(CFRunLoopGetMain(),
_shutdownRunLoopRef,
kCFRunLoopDefaultMode);
//Set flag that we set it
_bCallbackSet = true;
//Done
bRes = true;
}
else
{
//Failed
assert(false);
}
}
else
{
//Failed
assert(false);
}
//We are creating the object here - thus 'bShouldFree' should never be true
assert(!bShouldFree);
if(!bRes)
{
//Clear port name if we failed
_strPortName.clear();
}
}
else
{
//Failed
assert(false);
}
}
else
{
//Can't init again
assert(false);
}
}
else
{
//No port name
assert(false);
}
return bRes;
}
///Checks if the callback to receive notifications was set
///RETURN:
/// - true if yes
bool is_ReceivingNotifications()
{
//Act from within a lock
READER_LOCK rdl(_lock);
return _bCallbackSet;
}
///RETURN:
/// = Port name that is currently used for this class,
/// = "" if it was not initialized
const char* getPortName()
{
//Act from within a lock
READER_LOCK rdl(_lock);
return _strPortName.c_str();
}
///Remove the callback that was set by init_Notifications()
///INFO: It does nothing if the callback wasn't set before.
///'bRebooting' = true if we're calling it when the OS is rebooting
///RETURN:
/// - true if no errors
bool remove_Notifications(bool bRebooting)
{
bool bRes = true;
//Act from within a lock
WRITER_LOCK wrl(_lock);
if(_bCallbackSet)
{
//Unregister from receiving notifications
if(_nShutdownNtf)
{
uint32_t resCancel = notify_cancel(_nShutdownNtf);
if(resCancel != NOTIFY_STATUS_OK)
{
if(bRebooting &&
resCancel == NOTIFY_STATUS_SERVER_NOT_FOUND)
{
//Not an error
}
else
{
//Error
assert(false);
bRes = false;
}
}
_nShutdownNtf = 0;
}
if(_shutdownRunLoopRef)
{
CFRunLoopRemoveSource(CFRunLoopGetMain(),
_shutdownRunLoopRef,
kCFRunLoopDefaultMode);
CFRelease(_shutdownRunLoopRef);
_shutdownRunLoopRef = nullptr;
}
if(_shutDownMachPortRef)
{
CFRelease(_shutDownMachPortRef);
_shutDownMachPortRef = nullptr;
}
//Reset parameters
_bCallbackSet = false;
_strPortName.clear();
_shutDownMachPort = MACH_PORT_NULL;
_pfnCallback = nullptr;
_pParam1 = nullptr;
_pParam2 = nullptr;
}
return bRes;
}
private:
static void _onCallback(CFMachPortRef port,
void *msg,
CFIndex size,
void *info)
{
Notif_RebootShutdown* pThis = (Notif_RebootShutdown*)info;
assert(pThis);
//Act from within a lock
READER_LOCK rdl(pThis->_lock);
if(pThis->_pfnCallback)
{
//Invoke the callback
mach_msg_header_t *header = (mach_msg_header_t *)msg;
pThis->_pfnCallback(header,
pThis->_strPortName.c_str(),
pThis->_pParam1,
pThis->_pParam2);
}
}
private:
///Copy constructor and assignments are NOT available!
Notif_RebootShutdown(const Notif_RebootShutdown& s) = delete;
Notif_RebootShutdown& operator = (const Notif_RebootShutdown& s) = delete;
private:
RDR_WRTR _lock; //Lock for accessing this struct
bool _bCallbackSet = false; //true if we set callback OK
std::string _strPortName; //Port name for Mach port notifications
int _nShutdownNtf = 0;
mach_port_t _shutDownMachPort = MACH_PORT_NULL;
CFMachPortRef _shutDownMachPortRef = nullptr;
CFRunLoopSourceRef _shutdownRunLoopRef = nullptr;
void (*_pfnCallback)(mach_msg_header_t* pHeader,
const char* pPortName,
const void* pParam1,
const void* pParam2) = nullptr;
const void* _pParam1 = nullptr;
const void* _pParam2 = nullptr;
};
To build this class in Xcode you will need to include the CoreFoundation.framework
in the project settings.
There's a lot of code there. So let me explain it a little bit.
First off, you probably noticed that I'm using my RDR_WRTR
struct for synchronization. This is a class that implements a reader/writer lock. (I described it in detail in part 1.) That class is also wrapped in the WRITER_LOCK
/READER_LOCK
RAII constructs, that I also showed in part 1 of this post. The main idea behind using the _lock
is to ensure that my Notif_RebootShutdown
class supports multithreading.
Otherwise, the main work for setting up notifications is done by the Notif_RebootShutdown::init_Notifications
function, that internally calls notify_register_mach_port
, CFMachPortCreateWithPort
and CFMachPortCreateRunLoopSource
system functions, that set up the run-loop for the main thread to receive our needed notifications.
Note that you will need to set up the main run-loop in your process (like I showed in my main
function on GitHub) for the Notif_RebootShutdown
class to broadcast its notifications.
The interesting parameter that Notif_RebootShutdown::init_Notifications
accepts is the pPortName
. It should specify the exact port-name constant that the power management component expects. The following string constants seem to be used:
//User clicked shutdown to show the UI. (It may be aborted later.)
#define kLWShutdownInitiated "com.apple.system.loginwindow.shutdownInitiated"
//User clicked restart to show the UI. (It may be aborted later.)
#define kLWRestartInitiated "com.apple.system.loginwindow.restartinitiated"
//User clicked `logout user` to show the UI. (It may be aborted later.)
#define kLWLogoutInitiated "com.apple.system.loginwindow.logoutInitiated"
//A previously shown UI for shutdown, restart, or logout has been cancelled.
#define kLWLogoutCancelled "com.apple.system.loginwindow.logoutcancelled"
//A previously shown shutdown, restart, or logout was initiated and can no longer be cancelled!
#define kLWPointOfNoReturn "com.apple.system.loginwindow.logoutNoReturn"
The notifications with the kLW*Initiated
constants will be dispatched when a user clicks the Apple logo in the top-left corner of the screen and selects either: Restart, Shut Down, or Log Out user, which may show the following UI:
If the user later cancels this UI, the OS will dispatch the kLWLogoutCancelled
notification. While the kLWPointOfNoReturn
notification will be dispatched if the user confirms the action. It will serve as a point-of-no-return from performing it. If your app receives it, it needs to proceed with a quick run-down as it won't have much time left until its process is killed. (This doesn't apply to launch daemons and the kLWLogoutInitiated
notification.)
Additionally, note that the Notif_RebootShutdown
class uses the CFRunLoopGetMain
function instead of a more traditional CFRunLoopGetCurrent
. This is done because the class assumes that all notifications will be processed by the run-loop on the main UI thread, instead of whatever thread may be calling methods of the Notif_RebootShutdown
class. This is a subtle detail that may be crucial for the correct functioning of the notification class.
I should admit that thekLWPointOfNoReturn
notification seems kinda weird, because it doesn't tell you which operation was initiated. To know which action has been initiated, you need to receive all other notifications, store them somewhere in a persistent variable and then use that info when you receive thekLWPointOfNoReturn
notification. I showed this in mycallback_RebootShutdownLogout
function in the GitHub repo example.
Finally, each notification must be removed with a call to Notif_RebootShutdown::remove_Notifications
.
To see an example how one can use these notifications, check my GitHub repo.
Sleep/Wake Notifications
While on the subject of power notifications, how about a notification when a macOS enters sleep or wakes up from it?
One would think that there will be a similar set of functions like I showed above for this task. But that is not the case. For reasons that are probably lost in time, the approach is somewhat different.
Unlike the reboot/shutdown notifications, the sleep/wake notifications are somewhat documented. The recommended way to get them is with the IORegisterForSystemPower
function. That API is not the most straightforward to use, so I wrapped it in a thread-safe class as well:
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/pwr_mgt/IOPMLib.h>
#include <IOKit/IOMessage.h>
struct Notif_SleepWake
{
Notif_SleepWake()
{
}
~Notif_SleepWake()
{
//Remove callback from a destructor
if(!remove_SleepWakeNotifications())
{
//Should not fail here!
assert(false);
}
}
///Set the callback to receive sleep/wake notifications (cannot be called repeatedly)
///'pfn' = callback function that is called when event happens, or null not to call it
///'pParam1' = passed directly into 'pfn' when it's called
///'pParam2' = passed directly into 'pfn' when it's called
///RETURN:
/// - true if success
bool init_SleepWakeNotifications(void (*pfn)(natural_t msgType,
void *msgArgument,
io_connect_t portSleepWake,
const void* pParam1,
const void* pParam2) = nullptr,
const void* pParam1 = nullptr,
const void* pParam2 = nullptr)
{
bool bRes = false;
//Act from within a lock
WRITER_LOCK wrl(_lock);
if(!_bCallbackSet)
{
//Remember parameters
_pfnCallback = pfn;
_pParam1 = pParam1;
_pParam2 = pParam2;
//Register for sleep/wake notifications
_pwrSleepWakeKernelPort = IORegisterForSystemPower(this,
&_pwrSleepWakeNtfPort,
_pwrSleepWakeCallback,
&_pwrSleepWakeNotifier);
if(_pwrSleepWakeKernelPort != MACH_PORT_NULL)
{
//Add it to the run-loop
CFRunLoopAddSource(CFRunLoopGetMain(),
IONotificationPortGetRunLoopSource(_pwrSleepWakeNtfPort),
kCFRunLoopDefaultMode);
//Set flag that we set it
_bCallbackSet = true;
//Done
bRes = true;
}
else
{
//Failed
assert(false);
}
}
else
{
//Can't init again
assert(false);
}
return bRes;
}
///Checks if the callback to receive wake/sleep notifications was set
///RETURN:
/// - true if yes
bool is_ReceivingSleepWakeNotifications()
{
//Act from within a lock
READER_LOCK rdl(_lock);
return _bCallbackSet;
}
///Remove the callback that was set by init_SleepWakeNotifications()
///INFO: It does nothing if the callback wasn't set before.
///RETURN:
/// - true if no errors
bool remove_SleepWakeNotifications()
{
bool bRes = true;
//Act from within a lock
WRITER_LOCK wrl(_lock);
if(_bCallbackSet)
{
//Unregister sleep/wake notifications
if(_pwrSleepWakeNtfPort != nullptr)
{
//Remove the sleep notification port from the application runloop
CFRunLoopRemoveSource(CFRunLoopGetMain(),
IONotificationPortGetRunLoopSource(_pwrSleepWakeNtfPort),
kCFRunLoopDefaultMode);
//Deregister from system sleep notifications
IOReturn ioRes = IODeregisterForSystemPower(&_pwrSleepWakeNotifier);
if(ioRes != kIOReturnSuccess)
{
//Error
assert(false);
bRes = false;
}
//IORegisterForSystemPower implicitly opens the Root Power Domain,
//so we need to close it here
kern_return_t resKern = IOServiceClose(_pwrSleepWakeKernelPort);
if(resKern != KERN_SUCCESS)
{
//Error
assert(false);
bRes = false;
}
//Destroy the notification port allocated by IORegisterForSystemPower
IONotificationPortDestroy(_pwrSleepWakeNtfPort);
}
else
{
//Should not be here
assert(false);
}
//Reset parameters
_bCallbackSet = false;
_pwrSleepWakeKernelPort = {};
_pwrSleepWakeNtfPort = nullptr;
_pwrSleepWakeNotifier = {};
_pfnCallback = nullptr;
_pParam1 = nullptr;
_pParam2 = nullptr;
}
return bRes;
}
private:
static void _pwrSleepWakeCallback(void* pContext,
io_service_t svc,
natural_t msgType,
void *msgArgument)
{
Notif_SleepWake* pThis = (Notif_SleepWake*)pContext;
assert(pThis);
//Act from within a lock
READER_LOCK rdl(pThis->_lock);
if(pThis->_pfnCallback)
{
//Invoke the callback
pThis->_pfnCallback(msgType,
msgArgument,
pThis->_pwrSleepWakeKernelPort,
pThis->_pParam1,
pThis->_pParam2);
}
}
private:
///Copy constructor and assignments are NOT available!
Notif_SleepWake(const Notif_SleepWake& s) = delete;
Notif_SleepWake& operator = (const Notif_SleepWake& s) = delete;
private:
RDR_WRTR _lock; //Lock for accessing this struct
bool _bCallbackSet = false; //true if set callback OK
io_connect_t _pwrSleepWakeKernelPort = {};
IONotificationPortRef _pwrSleepWakeNtfPort = nullptr;
io_object_t _pwrSleepWakeNotifier = {};
void (*_pfnCallback)(natural_t msgType,
void *msgArgument,
io_connect_t portSleepWake,
const void* pParam1,
const void* pParam2) = nullptr;
const void* _pParam1 = nullptr;
const void* _pParam2 = nullptr;
};
Again, there's a lot to unpack here.
First, notice that I'm again using my RDR_WRTR
reader/writer lock with the READER_LOCK
/WRITER_LOCK
RAII constructs to provide thread safety for my Notif_SleepWake
class. This ensures that its methods can be called from any thread in a process.
Then as before, to set up a callback to receive the sleep/wake notifications, you'd call Notif_SleepWake::init_SleepWakeNotifications
during the process initialization, and then call Notif_SleepWake::remove_SleepWakeNotifications
to unregister it and to release system resources.
Note that you will need to set up the main run-loop in your process (like I showed in my main
function on GitHub) for the Notif_SleepWake
class to broadcast its notifications.
The interesting processing happens in the sleep/wake callback itself. It receives the natural_t msgType
argument as its first parameter, that denotes the type of a notification. There are several options available there:
A much better way to understand the sequence of these notifications is to see them in action if you build and run my test Xcode project on GitHub. Refer to the callback_SleepWake
function for details.
kIOMessageCanSystemSleep
- it is broadcast when the macOS is preparing to enter idle sleep. (This usually happens because of a user inactivity for a certain period of time, that one can specify in the macOS settings.) This notification lets running programs veto the idle sleep if they need to.In a kinda counterintuitive way, a process that receives this notification must always reply with a call to either
IOAllowPowerChange
orIOCancelPowerChange
. You can't just do-nothing in this case.I wonder what would happen if you just ignore to call either of these APIs? I will let the reader to test it out. But don't blame me if it creates a ripple in the space-time continuum of Steve Jobs' reality distortion field. (In all seriousness though, it may create a memory leak because of a bad design of this callback. If you find it out, please leave a comment below.)
Note that there is no guaranteed way to prevent all types of sleep in macOS from the user-land. You need to write a kernel extension to fully control it.
On top of that, there are multiple reasons why macOS may be entering sleep. Some of these ways may be controlled from user-land, some can't. To name just a few:
- User selects "Sleep" from the menu when they click on the Apple logo in the top-left corner of the screen.
- Another running process invokes sleep programmatically.
- User closes the lid on a MacBook laptop.
- A laptop battery is dangerously low.
- User removes external monitor when the MacBook was in the Clamshell mode. (Or when user connected an external keyboard, mouse and a monitor to the MacBook and closed the lid to work with the external periphery.)
- User changes the power source from AC to DC with the lid closed.
If you absolutely need to keep macOS awake from sleep, you may use wake events, as I describe below.
kIOMessageSystemWillNotSleep
- it is broadcast if one of the running processes aborts, or vetoes the idle sleep, as I described above for thekIOMessageCanSystemSleep
notification.kIOMessageSystemWillSleep
- it is broadcast when it is known that the macOS will be entering sleep. It is a so-called point-of-no-return moment when sleep cannot be aborted.This is the moment when your process needs to do its quick preparations before entering sleep.
Also, in the same vein, this notification assumes that you will call
IOAllowPowerChange
to acknowledge it. I would call this API last after you finish all processing in preparation for sleep.kIOMessageSystemWillPowerOn
- it is broadcast early in the sequence of waking up from sleep. At this stage macOS has not yet fully woken up itself, where some network drivers and external devices may not be fully re-connected.I would not be doing any serious processing here at this early stage, unless you are one of those external devices that needs to wake up.
kIOMessageSystemHasPoweredOn
- it is broadcast when the macOS has fully woken up from sleep. Note that there could be a few second delay betweenkIOMessageSystemWillPowerOn
and this notification.This is the moment when most software should be restoring its state from sleep.
Note, that although the following notifications are present in the source code, they are most certainly a legacy of the old-times. They are no longer delivered to the callback and are useless:kIOMessageCanDevicePowerOff
,kIOMessageDeviceWillNotPowerOff
,kIOMessageCanSystemPowerOff
,kIOMessageDeviceWillPowerOn
,kIOMessageDeviceHasPoweredOff
.If you need to receive reboot/shutdown notifications follow my advice above.
Lastly, to build the Notif_SleepWake
class in Xcode you will need to include the CoreFoundation.framework
and IOKit.framework
in the project settings.
You can get a complete source code for my Notif_SleepWake
class and how to use it in my GitHub repo.
How To Wake macOS From Sleep
And since we started talking about sleep/wake notifications, is there a way to wake macOS from sleep programmatically?
Some time ago I've written a small tool that can do it for Windows. And it turns out that one can do it for macOS as well.
To make long story short, you can use IOPMSchedulePowerEvent
function to schedule a wake event. It's a pretty versatile function that can do way more than what is available on Windows.
To showcase its might for waking macOS at a specific time, I wrapped it up in a thread-safe class:
#include <CoreFoundation/CoreFoundation.h>
#include <string>
#include <vector>
struct WakeTimer
{
///'pstrTimerBundleID' = string with unique bundle identifier for this timer. ex: "com.dennisbabkin.wake01"
/// INFO: This ID must remain the same between different instances of this app,
/// as it will be saved in the global scope on disk by the macOS
WakeTimer(const char* pstrTimerBundleID)
{
assert(pstrTimerBundleID && pstrTimerBundleID[0]); //Must be provided
if(pstrTimerBundleID)
{
_strTmrBundleID = pstrTimerBundleID;
}
}
~WakeTimer()
{
//Stop wake event
stopWakeEvent();
}
#define WAKE_TIMER_EVENT_TYPE kIOPMAutoWake
///Set wake event as the relative time from the current moment
///WARNING: When wake timer fires it will also temporarily wake the screen for a few seconds!
///'msFromNow' = number of ms from "now" to wake the system
///'pdtOutWhen' = if not 0, receives UTC date/time when the wake event was set for. Or receives 0 if didn't set it.
///RETURN:
/// = true if set OK
bool setWakeEventRelative(UInt32 msFromNow,
CFAbsoluteTime* pdtOutWhen = nullptr)
{
bool bRes = false;
//Time when to fire the wake event
CFAbsoluteTime dtWhen = CFAbsoluteTimeGetCurrent() + ((CFTimeInterval)msFromNow) / 1000.0;
if(true)
{
//Act from within a lock
WRITER_LOCK wrl(_lock);
bRes = _setWakeEvent(dtWhen,
WAKE_TIMER_EVENT_TYPE,
pdtOutWhen);
}
return bRes;
}
///Set wake event as the absolute time
///WARNING: When wake timer fires it will also temporarily wake the screen for a few seconds!
///'dtWake' = UTC date/time when to set this wake event (use Set_CFAbsoluteTime() function to create it)
///'pdtOutWhen' = if not 0, receives UTC date/time when the wake event was set for. Or receives 0 if didn't set it.
///RETURN:
/// = true if set OK
bool setWakeEventAbsolute(CFAbsoluteTime dtWake,
CFAbsoluteTime* pdtOutWhen = nullptr)
{
bool bRes = false;
if(true)
{
//Act from within a lock
WRITER_LOCK wrl(_lock);
bRes = _setWakeEvent(dtWake,
WAKE_TIMER_EVENT_TYPE,
pdtOutWhen);
}
return bRes;
}
///Stop wake event that was set by setWakeEvent*() functions
///INFO: Does nothing if event was not set.
///RETURN:
/// = true if no errors
bool stopWakeEvent()
{
bool bRes = true;
if(true)
{
//Act from within a lock
WRITER_LOCK wrl(_lock);
//Cancel all events
size_t szCnt;
if(!_cancelEvents(_strTmrBundleID.c_str(), NULL, szCnt))
{
//Failed
bRes = false;
}
//Reset parameters
_dtmWake = 0;
_bWakeEvtSet = false;
}
return bRes;
}
///Get this wake event info
///'pdtOutWhenWake' = if not 0, receives cached UTC date/time when the wake event was supposed to be set for.
///'pstrOutBundleID' = if not 0, receives the bundle ID for this wake event - is always returned, even if return is false
///RETURN:
/// = true if wake event was set
/// = false if wake event was not set - all data returned cannot be used
bool getWakeEventInfo(CFAbsoluteTime* pdtOutWhenWake = nullptr,
std::string* pstrOutBundleID = nullptr)
{
bool bResult = false;
CFAbsoluteTime dtmWake = 0;
std::string strBundle;
if(true)
{
//Act from within a lock
READER_LOCK rdl(_lock);
dtmWake = _dtmWake;
strBundle = _strTmrBundleID;
bResult = _bWakeEvtSet;
}
if(pdtOutWhenWake)
*pdtOutWhenWake = dtmWake;
if(pstrOutBundleID)
*pstrOutBundleID = strBundle;
return bResult;
}
///Cancel specific (wake) event(s)
///'pstrBundleID' = bundle ID to cancel events for, ex: "com.dennisbabkin.wake01", or 0 or "" to cancel all events
///'pstrEventType' = event type to cancel events for, or 0 or "" to cancel for all event types.
/// Ex. kIOPMAutoWake, kIOPMAutoPowerOn, or kIOPMAutoWakeOrPowerOn
///'pszOutCountCanceled' = if not 0, receives the number of events that were canceled
///RETURN:
/// = true if no errors
/// =false if at least one error took place
bool cancelEvents(const char* pstrBundleID,
const char* pstrEventType,
size_t* pszOutCountCanceled = nullptr)
{
bool bRes;
size_t szCnt;
if(true)
{
//Act from within a lock
WRITER_LOCK wrl(_lock);
bRes = _cancelEvents(pstrBundleID, pstrEventType, szCnt);
}
if(pszOutCountCanceled)
*pszOutCountCanceled = szCnt;
return bRes;
}
///Put OS to sleep
///INFO: Any user can call it. Or, in other words, it does not require administrative permissions.
///INFO: This function runs asynchronously, or in other words, it initiates sleep and then returns.
///RETURN:
/// = kIOReturnSuccess if success
/// = Other if error
static IOReturn performSleep()
{
IOReturn iResult;
io_connect_t ioConn = IOPMFindPowerManagement(MACH_PORT_NULL);
if(ioConn != 0)
{
iResult = IOPMSleepSystem(ioConn);
//Free port
IOServiceClose(ioConn);
}
else
{
//Failed
iResult = kIOReturnOffline;
}
return iResult;
}
///RETURN:
/// = true if full-sleep is supported by hardware
/// = false if only doze-sleep is supported
static bool isFullSleepSupported()
{
return IOPMSleepEnabled();
}
#define DIFF_UNIX_EPOCH_AND_MAC_TIME_SEC 978307200 //Difference in seconds between Mac time and Unix Epoch
///Set 'pOutDtm' from an absolute date & time (for the local time zone)
///RETURN:
/// = true if success
static bool Set_CFAbsoluteTime(CFAbsoluteTime* pOutDtm,
int nYear, //4-digit year
int nMonth, //[1-12]
int nDay, //[1-31]
int nHour, //[0-23]
int nMinute, //[0-59]
int nSecond, //[0-59]
int nMillisecond //[0-999]
)
{
bool bRes = false;
CFAbsoluteTime dtm = 0;
struct tm t = {};
t.tm_year = nYear - 1900; //years since 1900
t.tm_mon = nMonth - 1; //months since January [0-11]
t.tm_mday = nDay; //day of the month [1-31]
t.tm_hour = nHour; //hours since midnight [0-23]
t.tm_min = nMinute; //minutes after the hour [0-59]
t.tm_sec = nSecond; //seconds after the minute [0-60]
t.tm_isdst = -1;
//Convert to number of seconds since midnight Jan 1, 1970
time_t time = mktime(&t);
if(time != -1)
{
//CFAbsoluteTime: Time in fractional seconds since midnight of Jan 1, 2001
dtm = time;
//Adjust from Jan 1, 1970 to Jan 1, 2001
dtm -= DIFF_UNIX_EPOCH_AND_MAC_TIME_SEC;
//And apply milliseconds
dtm += (double)nMillisecond / 1000.0;
bRes = true;
}
if(pOutDtm)
*pOutDtm = dtm;
return bRes;
}
private:
///IMPORTANT: Must be called from within a lock!
///'dtWhen' = date/time in UTC
///'pstrEventType' = event type, eg: kIOPMAutoWake, kIOPMAutoPowerOn, or kIOPMAutoWakeOrPowerOn
///'pdtOutWhen' = if not 0, receives when wake timer was set, or 0 if error
///RETURN:
/// = true if success
bool _setWakeEvent(CFAbsoluteTime dtWhen,
const char* pstrEventType,
CFAbsoluteTime* pdtOutWhen = nullptr)
{
bool bRes = false;
CFDateRef refDtm = CFDateCreate(kCFAllocatorDefault, dtWhen);
if(refDtm)
{
CFStringRef refEventType = CFStringCreateWithCString(kCFAllocatorDefault,
pstrEventType,
kCFStringEncodingUTF8);
if(refEventType)
{
CFStringRef refID = CFStringCreateWithCString(kCFAllocatorDefault,
_strTmrBundleID.c_str(),
kCFStringEncodingUTF8);
if(refID)
{
//Cancel previous wake event (if it was set)
size_t szCnt;
if(!_cancelEvents(_strTmrBundleID.c_str(),
pstrEventType,
szCnt))
{
//Failed
assert(false);
}
//Create new wake event
IOReturn ioRes = IOPMSchedulePowerEvent(refDtm,
refID,
refEventType);
if(ioRes == kIOReturnSuccess)
{
//Done
bRes = true;
_bWakeEvtSet = true;
}
else
{
//Error
//eg: kIOReturnNotPrivileged = 0xE00002C1 = -536870207
assert(false);
//Reset parameters
_dtmWake = 0;
_bWakeEvtSet = false;
}
CFRelease(refID);
refID = NULL;
}
else
{
//Error
assert(false);
}
CFRelease(refEventType);
refEventType = NULL;
}
else
{
//Error
assert(false);
}
//Free
CFRelease(refDtm);
}
else
{
//Error
assert(false);
}
//Set time when we set it?
if(pdtOutWhen)
{
//Retrieve when the wake timer was set from the OS itself
if(!_getWakeEventTime(pdtOutWhen))
{
//Error
assert(false);
}
}
return bRes;
}
///IMPORTANT: Must be called from within a lock!
///'pstrBundleID' = bundle ID to cancel events for, ex: "com.dennisbabkin.wake01", or 0 or "" to cancel all events
///'pstrEventType' = event type to cancel events for, or 0 or "" to cancel for all event types.
/// Ex. kIOPMAutoWake, kIOPMAutoPowerOn, or kIOPMAutoWakeOrPowerOn
///'szCntCanceled' = receives the number of events that were canceled
///RETURN:
/// = true if no errors
/// =false if at least one error took place
bool _cancelEvents(const char* pstrBundleID,
const char* pstrEventType,
size_t& szCntCanceled)
{
bool bResult = true;
szCntCanceled = 0;
CFStringRef refID = NULL;
if(pstrBundleID &&
pstrBundleID[0])
{
refID = CFStringCreateWithCString(kCFAllocatorDefault,
pstrBundleID,
kCFStringEncodingUTF8);
if(!refID)
{
//Error
assert(false);
bResult = false;
}
}
CFStringRef refEvtType = NULL;
if(pstrEventType &&
pstrEventType[0])
{
refEvtType = CFStringCreateWithCString(kCFAllocatorDefault,
pstrEventType,
kCFStringEncodingUTF8);
if(!refEvtType)
{
//Error
assert(false);
bResult = false;
}
}
CFArrayRef refArr = NULL;
struct REM_EVT_INFO
{
CFDateRef refDate;
CFStringRef refID;
CFStringRef refType;
void clearIt()
{
refDate = NULL;
refID = NULL;
refType = NULL;
}
};
CFTypeRef resVal;
REM_EVT_INFO rei;
std::vector<REM_EVT_INFO> arrRem;
if(bResult)
{
//Enumerate all wake events in the system
refArr = IOPMCopyScheduledPowerEvents();
if(refArr)
{
CFIndex nCnt = CFArrayGetCount(refArr);
for(CFIndex i = 0; i < nCnt; i++)
{
CFDictionaryRef refDic = (CFDictionaryRef)CFArrayGetValueAtIndex(refArr, i);
if(refDic)
{
CFTypeID type = CFGetTypeID(refDic);
if(type == CFDictionaryGetTypeID())
{
//See if we need to remove this entry
rei.clearIt();
//Get the ID
if(CFDictionaryGetValueIfPresent(refDic,
CFSTR(kIOPMPowerEventAppNameKey),
&resVal))
{
type = CFGetTypeID(resVal);
if(type == CFStringGetTypeID())
{
//See if it's our ID
if(refID == NULL ||
CFStringCompare(refID,
(CFStringRef)resVal,
kCFCompareCaseInsensitive) == kCFCompareEqualTo)
{
//Remove it
rei.refID = (CFStringRef)resVal;
}
}
else
{
//Bad type
assert(false);
bResult = false;
}
}
else
{
//No such key
assert(false);
bResult = false;
}
if(rei.refID)
{
//See if we need to match the type
//Get the event type
if(CFDictionaryGetValueIfPresent(refDic,
CFSTR(kIOPMPowerEventTypeKey),
&resVal))
{
type = CFGetTypeID(resVal);
if(type == CFStringGetTypeID())
{
//See if it's our ID
if(refEvtType == NULL ||
CFStringCompare(refEvtType,
(CFStringRef)resVal,
0) == kCFCompareEqualTo)
{
//Remove it
rei.refType = (CFStringRef)resVal;
}
}
else
{
//Bad type
assert(false);
bResult = false;
}
}
else
{
//Error
assert(false);
bResult = false;
}
if(rei.refType)
{
//Get the date from this event
if(CFDictionaryGetValueIfPresent(refDic,
CFSTR(kIOPMPowerEventTimeKey),
&resVal))
{
type = CFGetTypeID(resVal);
if(type == CFDateGetTypeID())
{
rei.refDate = (CFDateRef)resVal;
//Add it to our list (we'll delete it later)
arrRem.push_back(rei);
}
else
{
//Bad type
assert(false);
bResult = false;
}
}
else
{
//Error
assert(false);
bResult = false;
}
}
}
}
else
{
//Error
assert(false);
bResult = false;
}
}
else
{
//Error
assert(false);
bResult = false;
}
}
}
else
{
//There are no scheduled events
}
size_t szCntRem = arrRem.size();
if(szCntRem > 0)
{
const REM_EVT_INFO* pREI = arrRem.data();
for(size_t i = 0; i < szCntRem; i++, pREI++)
{
assert(pREI->refID);
assert(pREI->refDate);
assert(pREI->refType);
IOReturn ioRes = IOPMCancelScheduledPowerEvent(pREI->refDate,
pREI->refID,
pREI->refType);
if(ioRes == kIOReturnSuccess)
{
//Done
szCntCanceled++;
}
else
{
//Failed - don't report some errors
if(ioRes != kIOReturnNotFound) //This may happen because of a delay of us finding it and then canceling it later
{
assert(false);
bResult = false;
}
}
}
}
}
//Free mem
if(refID)
{
CFRelease(refID);
refID = nullptr;
}
if(refEvtType)
{
CFRelease(refEvtType);
refEvtType = nullptr;
}
if(refArr)
{
CFRelease(refArr);
refArr = nullptr;
}
return bResult;
}
///Retrieves wake date/time from this struct
///'pdtOut' = if not 0, receives wake event date/time in UTC
///RETURN:
/// = true if retrieved OK
bool _getWakeEventTime(CFAbsoluteTime* pdtOut)
{
bool bRes = false;
CFAbsoluteTime dtWake = 0;
CFStringRef refID = CFStringCreateWithCString(kCFAllocatorDefault,
_strTmrBundleID.c_str(),
kCFStringEncodingUTF8);
if(refID)
{
CFTypeRef resVal;
//Enumerate all global wake events
CFArrayRef refArr = IOPMCopyScheduledPowerEvents();
if(refArr)
{
CFIndex nCnt = CFArrayGetCount(refArr);
for(CFIndex i = 0; i < nCnt; i++)
{
CFDictionaryRef refDic = (CFDictionaryRef)CFArrayGetValueAtIndex(refArr, i);
if(refDic &&
CFGetTypeID(refDic) == CFDictionaryGetTypeID())
{
//Find our bundle ID
if(CFDictionaryGetValueIfPresent(refDic,
CFSTR(kIOPMPowerEventAppNameKey),
&resVal) &&
CFGetTypeID(resVal) == CFStringGetTypeID() &&
CFStringCompare(refID,
(CFStringRef)resVal,
kCFCompareCaseInsensitive) == kCFCompareEqualTo)
{
//See if this is the right event type
if(CFDictionaryGetValueIfPresent(refDic,
CFSTR(kIOPMPowerEventTypeKey),
&resVal) &&
CFGetTypeID(resVal) == CFStringGetTypeID() &&
CFStringCompare(CFSTR(WAKE_TIMER_EVENT_TYPE),
(CFStringRef)resVal,
0) == kCFCompareEqualTo)
{
//Get date/time
if(CFDictionaryGetValueIfPresent(refDic,
CFSTR(kIOPMPowerEventTimeKey),
&resVal) &&
CFGetTypeID(resVal) == CFDateGetTypeID())
{
//Got it
dtWake = CFDateGetAbsoluteTime((CFDateRef)resVal);
bRes = true;
}
else
{
assert(false);
}
break;
}
}
}
else
{
assert(false);
}
}
//Free mem
CFRelease(refArr);
}
else
{
assert(false);
}
//Free
CFRelease(refID);
refID = NULL;
}
else
{
assert(false);
}
if(pdtOut)
*pdtOut = dtWake;
return bRes;
}
private:
///Copy constructor and assignments are NOT available!
WakeTimer(const WakeTimer& s) = delete;
WakeTimer& operator = (const WakeTimer& s) = delete;
private:
RDR_WRTR _lock; //Lock for accessing this struct
std::string _strTmrBundleID; //Bundle ID for this timer
bool _bWakeEvtSet = false; //true if we set the wake event
CFAbsoluteTime _dtmWake = 0; //UTC date/time when wake event was scheduled
};
Again, I need to explain a few things in the WakeTimer
class.
As before, I'm using my RDR_WRTR
reader/writer lock to ensure that the WakeTimer
class is thread-safe.
Then, to set the wake event you would call either WakeTimer::setWakeEventRelative
or WakeTimer::setWakeEventAbsolute
functions. The former one sets it as a relative time from the moment it is called, and the former one sets it as an absolute date and time.
To make up an absolute time, I provide a helper function to set the macOS CFAbsoluteTime
type:
///Set 'pOutDtm' from an absolute date & time (for the local time zone)
///RETURN:
/// = true if success
static bool Set_CFAbsoluteTime(CFAbsoluteTime* pOutDtm,
int nYear, //4-digit year
int nMonth, //[1-12]
int nDay, //[1-31]
int nHour, //[0-23]
int nMinute, //[0-59]
int nSecond, //[0-59]
int nMillisecond //[0-999]
)
{
bool bRes = false;
CFAbsoluteTime dtm = 0;
struct tm t = {};
t.tm_year = nYear - 1900; //years since 1900
t.tm_mon = nMonth - 1; //months since January [0-11]
t.tm_mday = nDay; //day of the month [1-31]
t.tm_hour = nHour; //hours since midnight [0-23]
t.tm_min = nMinute; //minutes after the hour [0-59]
t.tm_sec = nSecond; //seconds after the minute [0-60]
t.tm_isdst = -1;
//Convert to number of seconds since midnight Jan 1, 1970
time_t time = mktime(&t);
if(time != -1)
{
//CFAbsoluteTime: Time in fractional seconds since midnight of Jan 1, 2001
dtm = time;
//Adjust from Jan 1, 1970 to Jan 1, 2001
dtm -= DIFF_UNIX_EPOCH_AND_MAC_TIME_SEC;
//And apply milliseconds
dtm += (double)nMillisecond / 1000.0;
bRes = true;
}
if(pOutDtm)
*pOutDtm = dtm;
return bRes;
}
In that case DIFF_UNIX_EPOCH_AND_MAC_TIME_SEC
is defined as a number of seconds between Unix Epoch and the Mac time:
One significant limitation that macOS imposes on the process that wants to set a wake event is that it has to run as a root user for theIOPMSchedulePowerEvent
function to set a wake event. Otherwise it will returnkIOReturnNotPrivileged
error code or-536870207
.
I am not sure I understand such limitation. My only guess is that Apple didn't want any run-of-the-mill program to set up a wake event and undermine how long a MacBook can run on the battery power source. They are pretty diligent after all about the lifespan of batteries in their devices. But that is just a guess. Maybe someone can correct me in the comments?
Another peculiarity of the wake events on macOS is that they are global, or in other words, once set up they will remain in the system even if the process that set them up exits. This unexpected behavior necessitates the need to cancel each wake event before it can be re-set again. To ensure that one can find a wake event, each one comes with its own bundle identifier, which, in most cases, must be some unique string for your program, like a reverse-DNS string or a unique GUID. Example: "com.dennisbabkin.wake01".
Additionally, as you can probably notice in the WakeTimer
class, the wake event does not come with a callback. If you need to do something when the system wakes up, you can use the kIOMessageSystemHasPoweredOn
wake notification. Or, you can use your own timer that will invoke your callback, such as the one from the Grand Central Dispatch (GCD) for the Mac, and set it up using the dispatch_source_set_event_handler_f
and dispatch_source_set_timer
functions. I won't delve into it here, as this post is getting too long. If you want me to show it, let me know in the comments?
Finally, to build the WakeTimer
class in Xcode you will need to include the CoreFoundation.framework
in the project settings.
You can get a complete source code for my WakeTimer
class and how to use it in my GitHub repo.
How To Put macOS to Sleep, or to Reboot, Shutdown
Finally, let me briefly show how to programmatically initiate some power operations that I discussed in this post.
Put macOS to Sleep
The function to put macOS to sleep is pretty straightforward. It uses the IOPMSleepSystem
API:
///Put OS to sleep
///RETURN:
/// = kIOReturnSuccess if success
/// = Other if error
static IOReturn performSleep()
{
IOReturn iResult;
io_connect_t ioConn = IOPMFindPowerManagement(MACH_PORT_NULL);
if(ioConn != 0)
{
iResult = IOPMSleepSystem(ioConn);
//Free port
IOServiceClose(ioConn);
}
else
{
//Failed
iResult = kIOReturnOffline;
}
return iResult;
}
Note that it seems like any process can invoke this function.
Keep in mind that IOPMSleepSystem
function works asynchronously, meaning that it initiates sleep and returns, thus the system may enter sleep at some later time.
Additionally, an interesting function is IOPMSleepEnabled
, that checks if the hardware that the process runs on supports full sleep, or just a doze-sleep:
///RETURN:
/// = true if full-sleep is supported by hardware
/// = false if only doze-sleep is supported
static bool isFullSleepSupported()
{
return IOPMSleepEnabled();
}
It seems like a doze-sleep is an older form of sleep. I could not identify it with any of my available Apple hardware.
Reboot/Shutdown macOS
Unlike sleep, these two power operations exist in two flavors: "soft" and "hard". The distinction comes from what they do when running programs refuse such power action. Since by definition reboot (or restart) and shutdown reset computer RAM, this may lead to a data loss if some running process did not commit user content to persistent storage.
Thus, a soft power action requests permission from all running programs to perform reboot or shutdown, and only if none of them refuse it, the power operation is initiated. This is a safe way to perform these power operations. The obvious downside of it though is that there's no guarantee that reboot or shutdown will happen, since any program may halt it and a user may ultimately cancel it.
A hard power action proceeds without asking any running programs. Thus it is guaranteed to succeed. But this comes with an obvious downside that some unsaved user data in running apps may be lost.
So having outlined the distinction, here's the function to performs "soft" reboot or shutdown. It is also documented by Apple:
#include <Carbon/Carbon.h>
///Perform "soft" reboot or shutdown of the OS
///'bReboot' = true to reboot, false to shut down
///RETURN:
/// - true if successfully started the operation (note that it may be canceled by a user later)
bool RebootShutdownSoft(bool bReboot)
{
bool bRes = false;
AEEventID evt;
if(bReboot)
{
evt = kAERestart;
}
else
{
evt = kAEShutDown;
}
static const ProcessSerialNumber kPSNOfSystemProcess = { 0, kSystemProcess };
AEAddressDesc targetDesc;
OSStatus err = AECreateDesc(typeProcessSerialNumber,
&kPSNOfSystemProcess,
sizeof(kPSNOfSystemProcess),
&targetDesc);
if(err == noErr)
{
AppleEvent appleEventToSend = {typeNull, nullptr};
err = AECreateAppleEvent(kCoreEventClass,
evt,
&targetDesc,
kAutoGenerateReturnID,
kAnyTransactionID,
&appleEventToSend);
if(err == noErr)
{
AppleEvent eventReply = {typeNull, nullptr};
err = AESend(&appleEventToSend,
&eventReply,
kAENoReply,
kAENormalPriority,
kAEDefaultTimeout,
nullptr,
nullptr);
if(err == noErr)
{
//All good
bRes = true;
//Free
AEDisposeDesc(&eventReply);
}
else
{
//Error
assert(false);
}
//Free
AEDisposeDesc(&appleEventToSend);
}
else
{
//Error
assert(false);
}
//Free data
AEDisposeDesc(&targetDesc);
}
else
{
//Error
assert(false);
}
return bRes;
}
To compile it in Xcode you need to include the Carbon.framework
in the project settings.
Note that my RebootShutdownSoft
function, that uses Apple's Carbon framework, does not require a process that calls it to run as a root user to succeed.
And finally, the following function performs a "hard" reboot or shutdown. Note that it uses a low-level API that simply invokes the reboot syscall
to the kernel:
#include <sys/reboot.h>
///Perform "hard" reboot or shutdown of the OS
///'bReboot' = true to reboot, false to shut down
///RETURN:
/// - true if successfully started the operation
/// - false if failed, check errno for details
bool RebootShutdownHard(bool bReboot)
{
int nPwrOpFlgs;
if(bReboot)
{
//Hard reboot
nPwrOpFlgs = RB_AUTOBOOT;
}
else
{
//Hars shutdown
nPwrOpFlgs = RB_HALT;
}
//INFO: As I see in practice this function rarely returns, or
// reboot is executed really fast...
int nErr = reboot(nPwrOpFlgs);
//Set error code before returning
errno = nErr;
return nErr == 0;
}
Thereboot
function that is used by myRebootShutdownHard
requires a process that calls it to run as a root user to succeed.
And as before, you can find a complete source code for my sleep, soft-reboot-shutdown and hard-reboot-shutdown functions in my GitHub repo.
Conclusion
So again we need to make a break. In the next part of this blog post we will review techniques for finding bugs in your native applications developed with Xcode, as well as how to diagnose memory corruption and crashes.