3

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.

c00000fd
  • 20,994
  • 29
  • 177
  • 400

1 Answers1

3

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:

  • In order for the property to be true, clamshellSleepDisableMask must be false
  • The property can be false for other reasons than clamshellSleepDisableMask being true

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.

pmdj
  • 22,018
  • 3
  • 52
  • 103
  • Thanks. Just from curiosity, the `AppleClamshellState` that [they mention here](https://github.com/apple-opensource/xnu/blob/4f43d4276fc6a87f2461a3ab18287e4a2e5a1cc0/iokit/IOKit/pwr_mgt/IOPM.h#L420) - is it a notification when you connect an outside monitor, a keyboard and a mouse and close the MacBook lid so that you can use the other monitor? And what is `AppleClamshellCausesSleep` then? – c00000fd May 10 '23 at 12:01
  • You'll need to read the code for details, but `kIOPMMessageClamshellStateChange` is posted in a bunch of places whenever there's a state change to the clamshell state or setting of some sort. The `AppleClamshellState` property is supposed to be `true` if the lid is shut, `false` if open, and the property is missing altogether if there's no lid on this Mac. `AppleClamshellCausesSleep`, as I understand, is true if the current configuration would cause the system to go to sleep if its lid were shut. On my MacBook Pro I can flip this between true & false by disconnecting or reconnecting AC power. – pmdj May 10 '23 at 13:57
  • [That link](https://github.com/apple-opensource/xnu) that you posed, is that the current source code for the kernel that they use to build the latest macOS? – c00000fd May 10 '23 at 15:20
  • And, I did some tests too. The `AppleClamshellState` seems to be coinciding with the lid-closed state. But `AppleClamshellCausesSleep` - is it different for AC/battery power configuration? I wonder if there's a higher level function that makes that distinction, as `IOConnectCallScalarMethod` doesn't seem to care if it's an AC or DC power setting. – c00000fd May 10 '23 at 15:20
  • @c00000fd Sorry, looks like I linked to an unofficial repo for the xnu source, this is the official Apple one which is also more up to date: https://github.com/apple-oss-distributions/xnu/tree/xnu-8792.81.2 – pmdj May 12 '23 at 08:02
  • And yes, the external method `kPMSetClamshellSleepState` [calls `IOPMRootDomain::setClamShellSleepDisable`](https://github.com/apple-oss-distributions/xnu/blob/xnu-8792.81.2/iokit/Kernel/RootDomainUserClient.cpp), which in turn [sets or clears the `kClamshellSleepDisablePowerd` bit in `clamshellSleepDisableMask`](https://github.com/apple-oss-distributions/xnu/blob/xnu-8792.81.2/iokit/Kernel/IOPMrootDomain.cpp#L4351). – pmdj May 12 '23 at 08:11
  • The `AppleClamshellCausesSleep` property [takes its value](https://github.com/apple-oss-distributions/xnu/blob/19c3b8c28c31cb8130e034cfb5df6bf9ba342d90/iokit/Kernel/IOPMrootDomain.cpp#L4294) from the [`shouldSleepOnClamshellClosed` function](https://github.com/apple-oss-distributions/xnu/blob/19c3b8c28c31cb8130e034cfb5df6bf9ba342d90/iokit/Kernel/IOPMrootDomain.cpp#L4256), which takes into consideration [a bunch more variables than `clamshellSleepDisableMask`](https://github.com/apple-oss-distributions/xnu/blob/19c3b8c28c31cb8130e034cfb5df6bf9ba342d90/iokit/Kernel/IOPMrootDomain.cpp#L4265). – pmdj May 12 '23 at 08:15
  • I recommend reading the code to understand the functionality, that's all I'm doing here as well. – pmdj May 12 '23 at 08:16
  • Thanks. I've been doing it too, although that code base is very new to me. I'm assuming it's all highly undocumented and can change with the next build of macOS. Is that how Apple deals with it? – c00000fd May 12 '23 at 09:48
  • 1
    Pretty much. None of these are officially supported APIs as far as I'm aware, so there will be no notice if they change. – pmdj May 12 '23 at 09:59