cmacoscocoaiokitlaunch-daemon

How to use IOConnectCallScalarMethod only to read a value?


I'm trying to read the current kPMSetClamshellSleepState setting from my launch daemon for macOS. I tried the following approach:

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

if(pmRootDomain)
{
    kern_return_t res = IOServiceOpen(pmRootDomain, current_task(), 0, &connection);
    if(res == KERN_SUCCESS)
    {
        uint64_t outputs[1] = { };
        uint32_t num_outputs = 1;

        res = IOConnectCallScalarMethod(connection,
                                        kPMSetClamshellSleepState,
                                        NULL,
                                        0,
                                        outputs,
                                        &num_outputs);
        
        if(res == KERN_SUCCESS)
        {
            //Done
        }

        //Close connection
        IOServiceClose(connection);
    }

    IOObjectRelease(pmRootDomain);
}

But in this case the call to IOConnectCallScalarMethod returns 0xE00002C2, or -536870206, or kIOReturnBadArgument.

How do I read a setting? Any advice.


Solution

  • I/O Kit User Client External Methods

    There are no generalised semantics you can infer about an I/O Kit user client's external method API and what combination of scalar and struct inputs or outputs you may pass to it. This always depends on the specific IOUserClient subclass with which you're interfacing, and the specific method selector you're passing.

    In your case, you're dealing with RootDomainUserClient and selector kPMSetClamshellSleepState, whose allowed arguments are defined here:

            [kPMSetClamshellSleepState] = {
                .function                 = &RootDomainUserClient::externalMethodDispatched,
                .checkScalarInputCount    = 1,
                .checkStructureInputSize  = 0,
                .checkScalarOutputCount   = 0,
                .checkStructureOutputSize = 0,
                .allowAsync               = false,
                .checkEntitlement         = NULL,
            },
    

    So, you can only call this selector with precisely 1 scalar input and no other arguments. Moreover, its return value does not depend on previous state. There's no way of hacking this specific interface to do what you want.

    Clamshell state

    If you trace the external method through, you'll find the state stored in the IOPMRootDomain object's clamshellSleepDisableMask field. I don't see a direct user space accessor for this.

    It is referenced in the IOPMrootDomain::shouldSleepOnClamshellClosed() function, whose state is reflected in the kAppleClamshellCausesSleepKey/"AppleClamshellCausesSleep" property. You can query this property using IORegistryEntryCreateCFProperty from user space.

    The exact value of the exposed property also depends on a bunch of other flags:

    So you will not be able to infer clamshellSleepDisableMask with 100% accuracy from this property alone, but perhaps it's enough. You can also pick up the trail through the IOPMRootDomain source code where I left off, perhaps there's another place where you can infer something about the state of this flag. Of note, there's the kIOPMMessageClamshellStateChange message which is apparently delivered to listeners when the above value is updated - perhaps responding to changes like this leads you to a reliable solution.