Detecting DarkWake and Maintainance Sleep transitions

On a macOS machine running v15.0, I have a daemon run by launchd which subscribes to the sleep and wakeup notifications using the IORegisterForSystemPower method.

void PowerCallBack(void* refCon, io_service_t service, natural_t messageType, void* messageArgument)
{ 
    switch (messageType)
    {
        case kIOMessageSystemWillSleep:
            logger->Debug("Received sleep notification from macOS");
            if (refCon)
            {
                 //Handle Sleep
            }

            IOAllowPowerChange(root_port, (long)messageArgument);

            break;

        case kIOMessageSystemHasPoweredOn:
            logger->Debug("Received wakeup notification from macOS");
            if (refCon)
            {
                // Handle Wakeup
            }

            break;

        default:
            break;
    }
}

void MacOSNotification::RegisterNotifications()
{
    logger->Debug("Registering for notifications from macOS");
    powerNotificationThread = [[NSThread alloc] initWithBlock:^{

        // Notifier object, used to deregister later
        root_port = IORegisterForSystemPower(this, &notifyPortRef, PowerCallBack, &notifierObject);
        if (root_port == 0)
        {
            return;
        }

        logger->Debug("Registered for system power notifications from macOS");

        // Add the notification port to the application runloop
        CFRunLoopAddSource(CFRunLoopGetCurrent(),
                           IONotificationPortGetRunLoopSource(notifyPortRef),
                           kCFRunLoopCommonModes);
        CFRunLoopRun();
    }]; //END OF THREAD BLOCK

    [powerNotificationThread start];
}

Using this mechanism, I am getting notifications for normal sleep and wakeup transitions like closing and opening the lid. I need these notifications to terminate/reconnect my connection to a cloud service when we go to sleep/wakeup respectively.

I have noticed from the power logs at /private/var/log/powermanagement that the after the sleep initiated by lid closing or clicking sleep in the top apple menu (both of which I can detect as they generate power notification), the macOS machine wakes up with the following message from powerd logs:

DarkWake from Deep Idle [CDNP] : due to SMC.OutboxNotEmpty smc.70070000 wifibt/

I do not get any notification for this wakeup and my application threads start running. This happens every 15 to 16 mins from my observation.

After this DarkWake, we go back to 'Maintenance' sleep in under a minute as can be seen by the following powerd log:

Entering Sleep state due to 'Maintenance Sleep':TCPKeepAlive=active

I do not get any notifications for this either.

Is there a way to track and get notified of these DarkWake -> Maintenance sleep cycles? At the very least I would like to log when we go into and come out of these states. Currently I just rely on seeing a 15 min window of no logs to know this must have a DarkWake -> Maintenance sleep cycle.

Also is there a way to make sure my application and its threads are not woken up by DarkWake (like an opt-out)? I would like to make it so that my application only runs when we are properly sleeping and waking.

Is there a way to track and get notified of these DarkWake -> Maintenance sleep cycles? At the very least I would like to log when we go into and come out of these states. Currently I just rely on seeing a 15 min window of no logs to know this must have a DarkWake -> Maintenance sleep cycle.

The answers are "no" and "maybe". On the "no" side, the long standing answer is that dark wake in a interenal mechanism that shouldn't really be visible to the larger system, which is why, for example, IOKit doesn't provide any API that notifies of it. I'd actually encourage you to file a bug on this and post the bug number back here. I don't think this is an area that's likely to change, but this is something the system should handle better.

However, "maybe" is where things get more complicated. Dark Wake isn't exposed through API as such, but it definitely "leaks" into the larger system (with the most obvious example being the fact your app is awake). I haven't tested either of these approach, but I suspect one of or a combination of both of them would work:

  1. You can use IOServiceAddInterestNotification to setup your own interest notifications, giving you access to a broader message set. I haven't tested this, but I believe you'll get messages on this path that you would not get from IORegisterForSystemPower.

  2. That object has it's own set of properties describing the current system state. Again, I haven't tested this but while none of them directly correspond to Dark Wake, it's almost certainly the case that Dark Wake could be reliably inferred based on the combination of factors. Note that the API state of IOKit registry keys is not very well defined. Some of them are formally defined in headers (and are thus API) but many of them are not. If you choose to use a key that isn't formally defined, be aware that the key could (potentially) be removed or renamed at some point in the future.

Coming form the other direction:

Also is there a way to make sure my application and its threads are not woken up by DarkWake (like an opt-out)? I would like to make it so that my application only runs when we are properly sleeping and waking.

You can't opt out of the wake itself but, strictly speaking, the system already told you eaclty that. The reason "kIOMessageSystemHasPoweredOn" wasn't delivered is because Dark Wake isn't considered "On". At a higher level, "NSWorkspaceDidWakeNotification" (LaunchAgent only) basically means exactly what you're asking for- the system is "fully awake" and apps should operate normally.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I have tried the XPC activity system by writing a small test program which I load up using launchd, making it a LaunchDaemon. What I observe is that I get the activity callback to run only once when I first put the device to sleep after loading the daemon. I do not see the callback running for any DarkWakes or even any subsequent wakeup -> sleep cycle (I open and close the macbook lid to do this i.e. clamshell sleep). Here is my test code:

#import <Foundation/Foundation.h>
#import <xpc/xpc.h>

#include <signal.h>

// Define a unique identifier for the activity
const char *activityIdentifier = "com.example.myapp.myactivity";

void CleanupAndExit(void)
{
    NSLog(@"Unregistering activity...");
    xpc_activity_unregister(activityIdentifier);
    NSLog(@"Exiting program.");
    exit(0);
}

void HandleSignal(int signal)
{
    CleanupAndExit();
}

void PerformTask(void)
{
    // Get the current date
    NSDate *currentDate = [NSDate date];

    // Create a date formatter
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];

    // Set the desired date and time format
    [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];

    // Convert the NSDate to a human-readable string
    NSString *dateString = [dateFormatter stringFromDate:currentDate];
    
    NSLog(@"DarkWake timestamp: %@", dateString);
}

int main(int argc, const char * argv[])
{
    xpc_transaction_begin();
    
    @autoreleasepool
    {
        // Register signal handlers for cleanup
        signal(SIGINT, HandleSignal);  // Handle Ctrl+C
        signal(SIGTERM, HandleSignal); // Handle termination
        
        // Set up criteria for the activity
        xpc_object_t criteria = xpc_dictionary_create(NULL, NULL, 0);
        xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_REQUIRE_SCREEN_SLEEP, true); // Requires screen sleep
        xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_ALLOW_BATTERY, true); // Allow on battery
        xpc_dictionary_set_string(criteria, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_MAINTENANCE); // Priority: Maintainance

        // Register the activity
        xpc_activity_register(activityIdentifier, criteria, ^(xpc_activity_t activity)
        {
            xpc_activity_state_t state = xpc_activity_get_state(activity);

            if (state == XPC_ACTIVITY_STATE_RUN)
            {
                NSLog(@"Activity triggered: %s", activityIdentifier);
                PerformTask();
            }
            else if (state == XPC_ACTIVITY_STATE_WAIT)
            {
                NSLog(@"Activity is waiting to be scheduled.");
            }
            else
            {
                NSLog(@"Activity state: %ld", (long)state);
            }
        });

        // Run the main run loop to keep the program alive
        NSLog(@"Waiting for activities to trigger...");
        [[NSRunLoop currentRunLoop] run];
    }
    
    xpc_transaction_end();
    
    return 0;
}

Let me know if I am not interacting with the XPC system as intended.

Regarding the IOPMFindPowerManagement approach, can you please clarify it a bit more. From the IOPMFindPowerManagement method I get a io_connect_t object. How does this one allow me to get the IOrootDomain IOServiceObject like you say. I am new to this API so could not find a way to do so. Also, I think I need the io_service_t object to pass to the IOServiceAddInterestNotification from the io_connect_t. Can you please point me to the right direction

First off, just to make sure this is clear, the formal situation is that we don't have any API for detecting DarkWake, so you should file a bug asking for us to add one. The informal situation is that even without any specific API the state might be detectable, given some experimentation and testing. That also means that what I'm offering suggestions to try, not guaranteeing anything will work.

I have tried the XPC activity system by writing a small test program which I load up using launchd, making it a LaunchDaemon. What I observe is that I get the activity callback to run only once

Your activity isn't configured to repeat, so it only runs once. Beyond that, you might try immediately responding with "XPC_ACTIVITY_STATE_DEFER" whenever you're run. This will place you task back into "wait" without resetting any time criteria.

Regarding the IOPMFindPowerManagement approach, can you please clarify it a bit more. From the IOPMFindPowerManagement method I get a io_connect_t object. How does this one allow me to get the IOrootDomain IOServiceObject like you say. I am new to this API so could not find a way to do so. Also, I think I need the io_service_t object to pass to the IOServiceAddInterestNotification from the io_connect_t. Can you please point me to the right direction

Sorry about that, I was working fast and didn't look closely enough at the documentation. That code opens the user client, which is not what you want. I've attached some code showing how to get the node you want. Some notes on that code:

  • Using "IORegistryEntryFromPath" is something we usually recommend against, as it assumes the IORegistry's structure will remain constant. This case is an exception, as this particular node is the root of the power plane, which the builds out from that fixed point. Having the node located "somewhere else" would imply such a large scale change that it's unlikely any of this code would work at all. Besides, if it makes you feel better, this is the same way IOPMFindPowerManagement finds the root node.

  • You can also pull the system wide assertion list of the kernel ("IOPMCopyAssertionsStatus") and of user space ("IOPMCopyAssertionsByProcess"). The pid list in particular might be very useful, as I think dark wake is always triggered by the same process.

#import <Foundation/Foundation.h>
#import <IOKit/IOKitLib.h>
#import <IOKit/pwr_mgt/IOPM.h>
#import <IOKit/pwr_mgt/IOPMLib.h>

void KE_PrintIOObject(io_service_t myObj)
{
    if(myObj != 0) {
        NSString* className = CFBridgingRelease(IOObjectCopyClass(myObj));
        NSLog(@"Class Name = %@", className);
        CFMutableDictionaryRef propDictionary = NULL;
        kern_return_t err = IORegistryEntryCreateCFProperties(myObj, &propDictionary, CFAllocatorGetDefault(), 0);
        if(kIOReturnSuccess == err) {
            CFShow(propDictionary);
            CFRelease(propDictionary);
        }
    }
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        io_service_t pmService = IORegistryEntryFromPath( kIOMainPortDefault, kIOPowerPlane ":/IOPowerConnection/IOPMrootDomain");
        if( pmService ) {
            KE_PrintIOObject(pmService);
        }
        CFDictionaryRef assertionDictionary = NULL;
        kern_return_t err = IOPMCopyAssertionsStatus(&assertionDictionary);
        if(kIOReturnSuccess == err) {
            CFShow(assertionDictionary);
            CFRelease(assertionDictionary);
        }
        err = IOPMCopyAssertionsByProcess(&assertionDictionary);
        if(kIOReturnSuccess == err) {
            CFShow(assertionDictionary);
            CFRelease(assertionDictionary);
        }
    }
    return 0;
}

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Detecting DarkWake and Maintainance Sleep transitions
 
 
Q