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:
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);
21 Answer
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 inRootDomainUserClient::externalMethod()
, in the fileiokit/Kernel/RootDomainUserClient.cpp
. This is certainly promising, asexternalMethod()
is what gets called in response to user space programs calling theIOConnectCall*()
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:
IOPMrootDomain
. This looks straightforward, because:- Upon inspection,
IOPMrootDomain
has anIOUserClientClass
property ofRootDomainUserClient
, soIOServiceOpen()
from user space will by default create anRootDomainUserClient
instance. IOPMrootDomain
does not override thenewUserClient
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);
- From the code excerpt earlier on, we know that the selector must be
kPMSetClamshellSleepState
. arguments->scalarInput[0]
being zero will callsetDisableClamShellSleep(false)
, while a nonzero value will callsetDisableClamShellSleep(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); }
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.
2ncG1vNJzZmirpJawrLvVnqmfpJ%2Bse6S7zGiorp2jqbawutJobHJtaWl%2Bc3%2BOnqWampyeu6h5wqWmrJ2UYrGqv8%2BlmLJlnaSxpnnWZqZmpZWawaq6xmaYqaicmsBuvsSqrKKqlaKyr8DS