Search code examples
macoscocoakerneliokit

Enabling Closed-Display Mode w/o Meeting Apple's Requirements


EDIT: I have heavily edited this question after making some significant new discoveries and the question not having any answers yet.

Historically/AFAIK, keeping your Mac awake while in closed-display mode and not meeting Apple's requirements, has only been possible with a kernel extension (kext), or a command run as root. Recently however, I have discovered that there must be another way. I could really use some help figuring out how to get this working for use in a (100% free, no IAP) sandboxed Mac App Store (MAS) compatible app.

I have confirmed that some other MAS apps are able to do this, and it looks like they might be writing YES to a key named clamshellSleepDisabled. Or perhaps there's some other trickery involved that causes the key value to be set to YES? I found the function in IOPMrootDomain.cpp:

void IOPMrootDomain::setDisableClamShellSleep( bool val )
{
    if (gIOPMWorkLoop->inGate() == false) {

       gIOPMWorkLoop->runAction(
               OSMemberFunctionCast(IOWorkLoop::Action, this, &IOPMrootDomain::setDisableClamShellSleep),
               (OSObject *)this,
               (void *)val);

       return;
    }
    else {
       DLOG("setDisableClamShellSleep(%x)\n", (uint32_t) val);
       if ( clamshellSleepDisabled != val )
       {
           clamshellSleepDisabled = val;
           // If clamshellSleepDisabled is reset to 0, reevaluate if
           // system need to go to sleep due to clamshell state
           if ( !clamshellSleepDisabled && clamshellClosed)
              handlePowerNotification(kLocalEvalClamshellCommand);
       }
    }
}

I'd like to give this a try and see if that's all it takes, but I don't really have any idea about how to go about calling this function. It's certainly not a part of the IOPMrootDomain documentation, and I can't seem to find any helpful example code for functions that are in the IOPMrootDomain documentation, such as setAggressiveness or setPMAssertionLevel. Here's some evidence of what's going on behind the scenes according to Console:

Image of message logs from Console.app

I've had a tiny bit of experience working with IOMProotDomain via adapting some of ControlPlane's source for another project, but I'm at a loss for how to get started on this. Any help would be greatly appreciated. Thank you!

EDIT: With @pmdj's contribution/answer, this has been solved!

Full example project: https://github.com/x74353/CDMManager

This ended up being surprisingly simple/straightforward:

1. Import header:

#import <IOKit/pwr_mgt/IOPMLib.h>

2. Add this function in your implementation file:

IOReturn RootDomain_SetDisableClamShellSleep (io_connect_t root_domain_connection, bool disable)
{
    uint32_t num_outputs = 0;
    uint32_t input_count = 1;
    uint64_t input[input_count];
    input[0] = (uint64_t) { disable ? 1 : 0 };

    return IOConnectCallScalarMethod(root_domain_connection, kPMSetClamshellSleepState, input, input_count, NULL, &num_outputs);
}

3. Use the following to call the above function from somewhere else in your implementation:

io_connect_t connection = IO_OBJECT_NULL;
io_service_t pmRootDomain =  IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPMrootDomain"));

IOServiceOpen (pmRootDomain, current_task(), 0, &connection);

// 'enable' is a bool you should assign a YES or NO value to prior to making this call
RootDomain_SetDisableClamShellSleep(connection, enable);

IOServiceClose(connection);

Solution

  • I have no personal experience with the PM root domain, but I do have extensive experience with IOKit, so here goes:

    • You want IOPMrootDomain::setDisableClamShellSleep() to be called.
    • A code search for sites calling setDisableClamShellSleep() quickly reveals a location in RootDomainUserClient::externalMethod(), in the file iokit/Kernel/RootDomainUserClient.cpp. This is certainly promising, as externalMethod() is what gets called in response to user space programs calling the IOConnectCall*() family of functions.

    Let's dig in:

    IOReturn RootDomainUserClient::externalMethod(
        uint32_t selector,
        IOExternalMethodArguments * arguments,
        IOExternalMethodDispatch * dispatch __unused,
        OSObject * target __unused,
        void * reference __unused )
    {
        IOReturn    ret = kIOReturnBadArgument;
    
        switch (selector)
        {
    …
    …
    …
            case kPMSetClamshellSleepState:
                fOwner->setDisableClamShellSleep(arguments->scalarInput[0] ? true : false);
                ret = kIOReturnSuccess;
                break;
    …
    

    So, to invoke setDisableClamShellSleep() you'll need to:

    1. Open a user client connection to IOPMrootDomain. This looks straightforward, because:
      • Upon inspection, IOPMrootDomain has an IOUserClientClass property of RootDomainUserClient, so IOServiceOpen() from user space will by default create an RootDomainUserClient instance.
      • IOPMrootDomain does not override the newUserClient member function, so there are no access controls there.
      • RootDomainUserClient::initWithTask() does not appear to place any restrictions (e.g. root user, code signing) on the connecting user space process.
      • So it should simply be a case of running this code in your program:
        io_connect_t connection = IO_OBJECT_NULL;
        IOReturn ret = IOServiceOpen(
          root_domain_service,
          current_task(),
          0, // user client type, ignored
          &connection);
    
    1. Call the appropriate external method.
      • From the code excerpt earlier on, we know that the selector must be kPMSetClamshellSleepState.
      • arguments->scalarInput[0] being zero will call setDisableClamShellSleep(false), while a nonzero value will call setDisableClamShellSleep(true).
      • This amounts to:
    IOReturn RootDomain_SetDisableClamShellSleep(io_connect_t root_domain_connection, bool disable)
    {
        uint32_t num_outputs = 0;
        uint64_t inputs[] = { disable ? 1 : 0 };
        return IOConnectCallScalarMethod(
            root_domain_connection, kPMSetClamshellSleepState,
            &inputs, 1, // 1 = length of array 'inputs'
            NULL, &num_outputs);
    }
    
    1. When you're done with your io_connect_t handle, don't forget to IOServiceClose() it.

    This should let you toggle clamshell sleep on or off. Note that there does not appear to be any provision for automatically resetting the value to its original state, so if your program crashes or exits without cleaning up after itself, whatever state was last set will remain. This might not be great from a user experience perspective, so perhaps try to defend against it somehow, for example in a crash handler.