diff --git a/docs/TESTING.md b/docs/TESTING.md index c35d997..8733394 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -64,6 +64,59 @@ Expected: `warm_empty_allocations=0 single_packet_allocations=2`, correct creden The same source passed AddressSanitizer/UndefinedBehaviorSanitizer and leak detection. Allocation counts depend on this fixture/compiler/library; preserve the no-wasted-slot property rather than interpreting them as game performance. See [setup](SETUP.md#updating-an-existing-runtime) for server build/install/activation. +## System sleep/wake notifications + +The power monitor uses `IORegisterForSystemPower`, an `IOPMrootDomain` user +client and standard IOKit interest messages. `iokitd` listens to logind's +`PrepareForSleep` signals on the host system bus. A delay inhibitor stays open +until every notified application acknowledges sleep or the deadline expires. +Wake notifications arrive in `SystemWillPowerOn`, `SystemHasPoweredOn` order. +Registration also works without logind, with no host events in that case. + +Build the runtime and standalone probes: + +```sh +cmake --build build/darling-src/build --target darlingserver iokitd --parallel 3 +python3 tools/build-riot-power-notify-check.py +cc -Wall -Wextra -Werror -pthread \ + -I build/darling-src/src/external/iokitd/src \ + tools/riot-logind-sleep-check.c \ + build/darling-src/src/external/iokitd/src/LogindSleep.c \ + $(pkg-config --cflags --libs libsystemd) -o build/riot-logind-sleep-check +build/riot-logind-sleep-check +``` + +The host check uses a private D-Bus daemon and fake logind. It covers absent +services, delay-lock ownership, acknowledgement deadlines, stale +acknowledgements, repeated sleep/wake, a logind restart, a full system-bus +restart and a sleep signal during startup timeout discovery. Expected: +`LOGIND_SLEEP PASS checks=57 failures=0`. It also runs with +`-fsanitize=address,undefined`. + +`IOServiceOpen` sends an empty out-of-line descriptor. Older Darling servers +reject that descriptor with `0x400` before the request reaches `iokitd`. +Install the rebuilt server with Darling stopped, using the server installer in +[setup](SETUP.md#updating-an-existing-runtime), before running: + +```sh +python3 tools/riot-power-integration-check.py +``` + +The integration check creates its own prefix, copies the built `iokitd` into +it and connects it exclusively to fake logind. It never suspends the host, +starts Riot or uses the normal Darling prefix. Expected: `POWER_INTEGRATION +PASS`, covering a full notification queue, two clients, dead destinations, +failed-registration cleanup, the acknowledgement timeout, deregistration during +sleep and sleep with no clients. Fixture logs stay under +`build/power-integration-*`; Darling's overlay work directory may require root +to remove. + +The integration check passed against the installed rebuilt server without an +interposer. Its bootstrap run can also acquire an inhibitor, so readiness +requires a new lock from the persistent launcher plus confirmation that its +shell has started. Lock numbering is relative to that launcher, rather than +assuming that the first recorded lock belongs to the test clients. + ## Host URL hand-off regression LaunchServices' D-Bus hand-off code (`LSHostOpenBus.c`) builds natively against the host's libsystemd. The check starts a private `dbus-daemon` with no service directories and a fake portal/systemd service, so nothing real is activated and no browser opens: diff --git a/patches/IOKitUser.patch b/patches/IOKitUser.patch index 0210a03..a5920f0 100644 --- a/patches/IOKitUser.patch +++ b/patches/IOKitUser.patch @@ -7,10 +7,10 @@ index 81ed8f7..3120abb 100644 Boolean cfReusedPort = false; +#ifdef DARLING -+ /* Fail soft instead of crashing: without iokitd some registry/power -+ * lookups never produce a port, and callers (e.g. Chromium's power -+ * monitor) pass the NULL through unchecked. CFRunLoopAddSource -+ * already ignores NULL sources, so returning NULL here is safe. */ ++ /* A notification port can still be absent when allocation fails or ++ * the IOKit service is unavailable. Keep the accessor tolerant of ++ * that failure; CFRunLoopAddSource already ignores NULL sources. ++ * Normal power registration is implemented by iokitd. */ + if (!notify) + return NULL; +#endif diff --git a/patches/darlingserver.patch b/patches/darlingserver.patch index 2c660fe..93505c0 100644 --- a/patches/darlingserver.patch +++ b/patches/darlingserver.patch @@ -1031,7 +1031,7 @@ index bbaab01..4f01234 100644 }; diff --git a/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c b/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c -index 7c2559b..712ff8f 100644 +index 7c2559b..202d5dc 100644 --- a/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c +++ b/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c @@ -77,6 +77,7 @@ @@ -1053,6 +1053,17 @@ index 7c2559b..712ff8f 100644 mr = MACH_RCV_INVALID_DATA; size = 0; } else { +@@ -3819,7 +3824,9 @@ ipc_kmsg_copyin_body( + goto clean_message; + } + +- if ((size >= MSG_OOL_SIZE_SMALL) && ++ /* Darling has a zero small-copy threshold. Empty OOL regions ++ * require no allocation and must not trip the overflow check. */ ++ if (size != 0 && (size >= MSG_OOL_SIZE_SMALL) && + (daddr->out_of_line.copy == MACH_MSG_PHYSICAL_COPY) && + !(daddr->out_of_line.deallocate)) { + /* diff --git a/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c b/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c index 98ad438..221a1be 100644 --- a/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c diff --git a/patches/iokitd.patch b/patches/iokitd.patch index e12b70b..1df128c 100644 --- a/patches/iokitd.patch +++ b/patches/iokitd.patch @@ -1,14 +1,26 @@ diff --git a/CMakeLists.txt b/CMakeLists.txt -index 84161fd..1eafdc0 100644 +index 84161fd..3f7fd2b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt -@@ -36,7 +36,9 @@ set(iokitd_sources +@@ -17,6 +17,8 @@ include_directories( + ${X11_Xrandr_INCLUDE_PATH} + ) + include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}) ++# IOPMrootDomain.mm loads the host's libsystemd through the ELF loader. ++include_directories(${CMAKE_SOURCE_DIR}/src/startup/mldr/elfcalls) + + mig(iokitmig.defs) + mig(powermanagement.defs) +@@ -36,7 +38,12 @@ set(iokitd_sources src/IODisplayConnect.mm src/IODisplayConnectX11.mm src/PowerAssertions.mm + src/PowerSources.mm src/PowerAssertionsX11.mm + src/IOPlatformExpertDevice.mm ++ src/IOPMrootDomain.mm ++ src/IOInterestNotification.mm ++ src/LogindSleep.c src/IOSurfaceRoot.mm src/IOSurface.mm src/IOUserClient.mm @@ -407,23 +419,34 @@ index aaed046..7a190c3 100644 + } +} diff --git a/src/main.mm b/src/main.mm -index 37ce9b8..d29f01d 100644 +index 37ce9b8..e23aea0 100644 --- a/src/main.mm +++ b/src/main.mm -@@ -32,6 +32,7 @@ +@@ -32,6 +32,8 @@ #include "IODisplayConnectX11.h" #include "PowerAssertions.h" #include "IOSurfaceRoot.h" +#include "IOPlatformExpertDevice.h" ++#include "IOPMrootDomain.h" extern "C" { #include "iokitmigServer.h" -@@ -146,5 +147,6 @@ static void discoverAllDevices() +@@ -127,6 +129,8 @@ int main(int argc, const char** argv) + dispatch_resume(deathSource); + dispatch_resume(pwrMgmtPortSource); + ++ IOPMrootDomain::instance()->startHostSleepMonitor(); ++ + os_log(OS_LOG_DEFAULT, "iokitd up and running."); + + dispatch_main(); +@@ -146,5 +150,7 @@ static void discoverAllDevices() // Trick to make sure the root object gets instantiated first and gets the lowest ID IORegistryEntry::root(); IODisplayConnectX11::discoverDevices(registry); + IOPlatformExpertDevice::registerSelf(registry); IOSurfaceRoot::registerSelf(registry); ++ IOPMrootDomain::registerSelf(registry); } diff --git a/src/stubs-powermanagement.c b/src/stubs-powermanagement.c index a568ecc..929746c 100644 @@ -453,10 +476,91 @@ index a568ecc..929746c 100644 /* Routine io_ps_copy_chargelog */ diff --git a/src/stubs.c b/src/stubs.c -index 91d11a1..a19d0d6 100644 +index 91d11a1..37551e1 100644 --- a/src/stubs.c +++ b/src/stubs.c -@@ -642,18 +642,6 @@ kern_return_t is_io_server_version +@@ -45,15 +45,6 @@ kern_return_t is_io_registry_entry_get_property_bytes + return KERN_NOT_SUPPORTED; + } + +-kern_return_t is_io_service_close +-( +- mach_port_t connection +-) +-{ +- STUB(); +- return KERN_NOT_SUPPORTED; +-} +- + kern_return_t is_io_connect_get_service + ( + mach_port_t connection, +@@ -325,20 +316,6 @@ kern_return_t is_io_service_add_notification + return KERN_NOT_SUPPORTED; + } + +-kern_return_t is_io_service_add_interest_notification +-( +- mach_port_t service, +- io_name_t type_of_interest, +- mach_port_t wake_port, +- io_async_ref_t reference, +- mach_msg_type_number_t referenceCnt, +- mach_port_t *notification +-) +-{ +- STUB(); +- return KERN_NOT_SUPPORTED; +-} +- + kern_return_t is_io_service_acknowledge_notification + ( + mach_port_t service, +@@ -439,22 +416,6 @@ kern_return_t is_io_object_get_bundle_identifier + return KERN_NOT_SUPPORTED; + } + +-kern_return_t is_io_service_open_extended +-( +- mach_port_t service, +- task_t owningTask, +- uint32_t connect_type, +- NDR_record_t ndr, +- io_buf_ptr_t properties, +- mach_msg_type_number_t propertiesCnt, +- kern_return_t *result, +- mach_port_t *connection +-) +-{ +- STUB(); +- return KERN_NOT_SUPPORTED; +-} +- + kern_return_t is_io_connect_map_memory_into_task + ( + mach_port_t connection, +@@ -533,20 +494,6 @@ kern_return_t is_io_service_add_notification_64 + return KERN_NOT_SUPPORTED; + } + +-kern_return_t is_io_service_add_interest_notification_64 +-( +- mach_port_t service, +- io_name_t type_of_interest, +- mach_port_t wake_port, +- io_async_ref64_t reference, +- mach_msg_type_number_t referenceCnt, +- mach_port_t *notification +-) +-{ +- STUB(); +- return KERN_NOT_SUPPORTED; +-} +- + kern_return_t is_io_service_add_notification_ool_64 + ( + mach_port_t master_port, +@@ -642,18 +589,6 @@ kern_return_t is_io_server_version } @@ -475,7 +579,7 @@ index 91d11a1..a19d0d6 100644 kern_return_t is_io_service_match_property_table_bin ( mach_port_t service, -@@ -682,22 +670,6 @@ kern_return_t is_io_service_add_notification_bin +@@ -682,22 +617,6 @@ kern_return_t is_io_service_add_notification_bin return KERN_NOT_SUPPORTED; } @@ -498,7 +602,7 @@ index 91d11a1..a19d0d6 100644 kern_return_t is_io_registry_entry_from_path_ool ( mach_port_t master_port, -@@ -736,31 +708,3 @@ kern_return_t is_io_device_tree_entry_exists_with_name +@@ -736,31 +655,3 @@ kern_return_t is_io_device_tree_entry_exists_with_name return KERN_NOT_SUPPORTED; } @@ -615,3 +719,1657 @@ new file mode 100644 + targetServiceRegistry->registerService(device); + device->registerInPlane(kIOServicePlane, "Platform", IORegistryEntry::root()); +} +diff --git a/src/IOInterestNotification.h b/src/IOInterestNotification.h +new file mode 100644 +index 0000000..62be712 +--- /dev/null ++++ b/src/IOInterestNotification.h +@@ -0,0 +1,66 @@ ++/* ++ This file is part of Darling. ++ ++ Copyright (C) 2026 Darling developers ++ ++ Darling is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ Darling is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with Darling. If not, see . ++*/ ++ ++#ifndef IOKITD_IOINTERESTNOTIFICATION_H ++#define IOKITD_IOINTERESTNOTIFICATION_H ++#include "IOObject.h" ++#include ++#include ++#include ++ ++class IOService; ++ ++// The object returned by IOServiceAddInterestNotification() (the kernel's ++// IOServiceMessageUserNotification). It lives until the client releases it, ++// and sends service messages to the client's notification port in the same ++// layout as the kernel, which IODispatchCalloutFromMessage() decodes. ++class IOInterestNotification : public IOObject ++{ ++public: ++ // Takes over the send right to wakePort. reference holds referenceSize bytes ++ // of the client's async reference (io_async_ref64_t or io_async_ref_t). ++ IOInterestNotification(IOService* service, const char* interestType, mach_port_t wakePort, ++ const void* reference, size_t referenceSize, bool client64); ++ ~IOInterestNotification(); ++ ++ const char* className() const override; ++ ++ const std::string& interestType() const { return m_interestType; } ++ IOService* service() const { return m_service; } ++ ++ // Queues messageType without blocking; retries in order when the port is full. ++ kern_return_t deliver(uint32_t messageType, uint64_t argument); ++private: ++ kern_return_t sendMessage(uint32_t messageType, uint64_t argument); ++ kern_return_t flushMessages(); ++ void scheduleRetry(); ++ bool wakePortIsAlive() const; ++ struct Message { uint32_t type; uint64_t argument; }; ++ std::deque m_messages; ++ bool m_retryScheduled = false; ++ bool m_failed = false; ++ IOService* m_service; ++ std::string m_interestType; ++ mach_port_t m_wakePort; ++ OSAsyncReference64 m_reference; ++ size_t m_referenceSize; ++ bool m_client64; ++}; ++ ++#endif +diff --git a/src/IOInterestNotification.mm b/src/IOInterestNotification.mm +new file mode 100644 +index 0000000..1aa41a9 +--- /dev/null ++++ b/src/IOInterestNotification.mm +@@ -0,0 +1,286 @@ ++/* ++ This file is part of Darling. ++ ++ Copyright (C) 2026 Darling developers ++ ++ Darling is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ Darling is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with Darling. If not, see . ++*/ ++ ++#include "IOInterestNotification.h" ++#include "IOService.h" ++#include "iokitd.h" ++#include ++#include ++#include ++#include ++#include ++#include ++ ++extern "C" { ++#include "iokitmigServer.h" ++} ++ ++// Mirrors xnu's IOServiceMessageUserNotification::PingMsg: the provider is ++// passed as a port descriptor, followed by the notification header, the ++// client's async reference and the message content. ++struct InterestMessageHead ++{ ++ mach_msg_header_t header; ++ mach_msg_body_t body; ++ mach_msg_port_descriptor_t provider; ++ mach_msg_size_t size; ++ natural_t type; ++}; ++ ++// The kernel's kIOUserNotifyMaxMessageSize, reported in the header's size field. ++static const mach_msg_size_t kMaxMessageArgumentSize = 64; ++ ++// IODispatchCalloutFromMessage() finds the notification header right after the descriptors. ++static_assert(offsetof(InterestMessageHead, size) == sizeof(mach_msg_header_t) + sizeof(mach_msg_body_t) ++ + sizeof(mach_msg_port_descriptor_t), "notification header must follow the port descriptor"); ++static_assert(offsetof(OSNotificationHeader64, reference) == 2 * sizeof(natural_t), "unexpected header layout"); ++static_assert(sizeof(IOServiceInterestContent64) == sizeof(natural_t) + sizeof(io_user_reference_t), ++ "unexpected interest content layout"); ++ ++IOInterestNotification::IOInterestNotification(IOService* service, const char* interestType, ++ mach_port_t wakePort, const void* reference, size_t referenceSize, bool client64) ++: m_service(service), m_interestType(interestType), m_wakePort(wakePort), ++ m_referenceSize(std::min(referenceSize, sizeof(m_reference))), m_client64(client64) ++{ ++ std::memset(m_reference, 0, sizeof(m_reference)); ++ std::memcpy(m_reference, reference, m_referenceSize); ++ m_service->addInterest(this); ++} ++ ++IOInterestNotification::~IOInterestNotification() ++{ ++ m_service->removeInterest(this); ++ if (MACH_PORT_VALID(m_wakePort)) ++ mach_port_deallocate(mach_task_self(), m_wakePort); ++} ++ ++const char* IOInterestNotification::className() const ++{ ++ return "IOServiceMessageUserNotification"; ++} ++ ++kern_return_t IOInterestNotification::sendMessage(uint32_t messageType, uint64_t argument) ++{ ++ // Header, reference (at most OSAsyncReference64) and IOServiceInterestContent64 ++ alignas(8) uint8_t buffer[sizeof(InterestMessageHead) + sizeof(OSAsyncReference64) ++ + sizeof(IOServiceInterestContent64)]; ++ std::memset(buffer, 0, sizeof(buffer)); ++ ++ InterestMessageHead* head = reinterpret_cast(buffer); ++ head->header.msgh_bits = MACH_MSGH_BITS_COMPLEX ++ | MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND); ++ head->header.msgh_remote_port = m_wakePort; ++ // Received as msgh_remote_port: the client checks that it still holds this notification. ++ head->header.msgh_local_port = port(); ++ head->header.msgh_id = kOSNotificationMessageID; ++ ++ head->body.msgh_descriptor_count = 1; ++ head->provider.name = m_service->port(); ++ head->provider.disposition = MACH_MSG_TYPE_MAKE_SEND; ++ head->provider.type = MACH_MSG_PORT_DESCRIPTOR; ++ ++ // A pointer-sized argument: 8 bytes for 64-bit clients, 4 otherwise. ++ // The low bits of the type would carry (size & 3), which is 0 for both. ++ size_t argumentSize = m_client64 ? sizeof(io_user_reference_t) : sizeof(uint32_t); ++ head->size = kMaxMessageArgumentSize + sizeof(IOServiceInterestContent64); ++ head->type = kIOServiceMessageNotificationType; ++ ++ uint8_t* p = buffer + sizeof(InterestMessageHead); ++ std::memcpy(p, m_reference, m_referenceSize); ++ p += m_referenceSize; ++ ++ natural_t type = messageType; ++ std::memcpy(p, &type, sizeof(type)); ++ p += sizeof(type); ++ if (m_client64) ++ std::memcpy(p, &argument, sizeof(argument)); ++ else ++ { ++ uint32_t argument32 = (uint32_t) argument; ++ std::memcpy(p, &argument32, sizeof(argument32)); ++ } ++ p += argumentSize; ++ ++ head->header.msgh_size = (mach_msg_size_t) (p - buffer); ++ ++ // Never block iokitd on a client that does not drain its port. ++ kern_return_t kr = mach_msg(&head->header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, head->header.msgh_size, ++ 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); ++ ++ if (kr != KERN_SUCCESS) ++ { ++ // A pseudo-receive on failed delivery returns newly copied rights. ++ // mach_msg_destroy handles the remote right/descriptors, but not local. ++ mach_msg_type_name_t local = MACH_MSGH_BITS_LOCAL(head->header.msgh_bits); ++ if (local == MACH_MSG_TYPE_MOVE_SEND || local == MACH_MSG_TYPE_MOVE_SEND_ONCE) ++ mach_port_deallocate(mach_task_self(), head->header.msgh_local_port); ++ mach_msg_destroy(&head->header); ++ } ++ return kr; ++} ++ ++bool IOInterestNotification::wakePortIsAlive() const ++{ ++ mach_port_type_t type = 0; ++ return mach_port_type(mach_task_self(), m_wakePort, &type) == KERN_SUCCESS ++ && (type & MACH_PORT_TYPE_SEND); ++} ++ ++void IOInterestNotification::scheduleRetry() ++{ ++ if (m_retryScheduled) ++ return; ++ m_retryScheduled = true; ++ mach_port_t notificationPort = port(); ++ // Do not retain a send right while waiting: deregistration must still ++ // destroy this object. Look it up again on the same serial queue. ++ IOInterestNotification* expected = this; ++ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ ++ auto current = dynamic_cast(IOObject::lookup(notificationPort)); ++ if (current != expected) ++ return; ++ current->m_retryScheduled = false; ++ current->flushMessages(); ++ }); ++} ++ ++kern_return_t IOInterestNotification::flushMessages() ++{ ++ while (!m_messages.empty()) ++ { ++ kern_return_t kr = MACH_SEND_INVALID_DEST; ++ if (wakePortIsAlive()) ++ { ++ const auto& message = m_messages.front(); ++ kr = sendMessage(message.type, message.argument); ++ // Mach can report success if the destination died during the send. ++ if (!wakePortIsAlive()) ++ kr = MACH_SEND_INVALID_DEST; ++ } ++ if (kr == MACH_SEND_TIMED_OUT || kr == MACH_SEND_INTERRUPTED || kr == MACH_SEND_NO_BUFFER) ++ { ++ scheduleRetry(); ++ return KERN_SUCCESS; // accepted; preserve message order until the port drains ++ } ++ if (kr != KERN_SUCCESS) ++ { ++ m_failed = true; ++ m_messages.clear(); ++ os_log_error(OS_LOG_DEFAULT, "iokitd: notification destination unavailable: 0x%x", kr); ++ mach_port_t notificationPort = port(); ++ IOInterestNotification* expected = this; ++ // Defer removal until tellApps has finished building its pending set. ++ dispatch_async(dispatch_get_main_queue(), ^{ ++ auto current = dynamic_cast(IOObject::lookup(notificationPort)); ++ if (current == expected) ++ current->m_service->removeInterest(current); ++ }); ++ return kr; ++ } ++ m_messages.pop_front(); ++ } ++ return KERN_SUCCESS; ++} ++ ++kern_return_t IOInterestNotification::deliver(uint32_t messageType, uint64_t argument) ++{ ++ if (m_failed) ++ return MACH_SEND_INVALID_DEST; ++ m_messages.push_back({messageType, argument}); ++ return flushMessages(); ++} ++ ++static kern_return_t addInterestNotification(mach_port_t service, const char* interestType, ++ mach_port_t wakePort, const void* reference, size_t referenceSize, bool client64, ++ mach_port_t* notification) ++{ ++ *notification = MACH_PORT_NULL; ++ ++ // On failure dispatch_mig_server destroys the request, including wakePort. ++ // Only a successful registration takes ownership of that send right. ++ IOService* s = dynamic_cast(IOObject::lookup(service)); ++ if (!s) ++ return kIOReturnBadArgument; ++ if (!MACH_PORT_VALID(wakePort)) ++ return kIOReturnBadArgument; ++ if (!s->acceptsInterest(interestType)) ++ return kIOReturnUnsupported; ++ ++ IOInterestNotification* n; ++ try ++ { ++ n = new IOInterestNotification(s, interestType, wakePort, reference, referenceSize, client64); ++ } ++ catch (const std::exception& e) ++ { ++ os_log_error(OS_LOG_DEFAULT, "iokitd: cannot create interest notification: %s", e.what()); ++ return kIOReturnNoResources; ++ } ++ ++ *notification = n->port(); ++ // The reply carries a copy of the send right; drop ours afterwards. ++ n->releaseLater(); ++ return kIOReturnSuccess; ++} ++ ++kern_return_t is_io_service_add_interest_notification_64 ++( ++ mach_port_t service, ++ io_name_t type_of_interest, ++ mach_port_t wake_port, ++ io_async_ref64_t reference, ++ mach_msg_type_number_t referenceCnt, ++ mach_port_t *notification ++) ++{ ++ if (referenceCnt > kOSAsyncRef64Count) ++ { ++ *notification = MACH_PORT_NULL; ++ return kIOReturnBadArgument; ++ } ++ ++ OSAsyncReference64 zreference = {}; ++ std::memcpy(zreference, reference, referenceCnt * sizeof(zreference[0])); ++ ++ return addInterestNotification(service, type_of_interest, wake_port, zreference, ++ sizeof(zreference), true, notification); ++} ++ ++kern_return_t is_io_service_add_interest_notification ++( ++ mach_port_t service, ++ io_name_t type_of_interest, ++ mach_port_t wake_port, ++ io_async_ref_t reference, ++ mach_msg_type_number_t referenceCnt, ++ mach_port_t *notification ++) ++{ ++ if (referenceCnt > kOSAsyncRefCount) ++ { ++ *notification = MACH_PORT_NULL; ++ return kIOReturnBadArgument; ++ } ++ ++ io_async_ref_t zreference = {}; ++ std::memcpy(zreference, reference, referenceCnt * sizeof(zreference[0])); ++ ++ return addInterestNotification(service, type_of_interest, wake_port, zreference, ++ sizeof(zreference), false, notification); ++} +diff --git a/src/IOObject.cpp b/src/IOObject.cpp +index 1a40da6..232f150 100644 +--- a/src/IOObject.cpp ++++ b/src/IOObject.cpp +@@ -50,12 +50,17 @@ IOObject::IOObject() + throw std::runtime_error("Failed to create dispatch source"); + + dispatch_source_set_event_handler(m_dispatchSource, ^{ +- dispatch_mig_server(m_dispatchSource, is_iokit_subsystem.maxsize, iokit_server); ++ dispatch_mig_server(m_dispatchSource, is_iokit_subsystem.maxsize, ++ [](mach_msg_header_t* request, mach_msg_header_t* reply) -> boolean_t { ++ if (request->msgh_id == MACH_NOTIFY_NO_SENDERS) ++ return IOObject::deathNotify(request, reply); ++ return iokit_server(request, reply); ++ }); + }); + dispatch_resume(m_dispatchSource); + + mach_port_t oldTargetOfNotification = MACH_PORT_NULL; +- kr = mach_port_request_notification(task, m_port, MACH_NOTIFY_NO_SENDERS, 1, g_deathPort, MACH_MSG_TYPE_MAKE_SEND_ONCE, &oldTargetOfNotification); ++ kr = mach_port_request_notification(task, m_port, MACH_NOTIFY_NO_SENDERS, 1, m_port, MACH_MSG_TYPE_MAKE_SEND_ONCE, &oldTargetOfNotification); + if (kr != KERN_SUCCESS) + throw std::runtime_error("Failed to setup MACH_NOTIFY_NO_SENDERS"); + +@@ -64,12 +69,15 @@ IOObject::IOObject() + + IOObject::~IOObject() + { +- if (m_dispatchSource) +- dispatch_release(m_dispatchSource); +- + m_objects.erase(m_port); +- +- mach_port_deallocate(mach_task_self(), m_port); ++ // A receive source must be cancelled before its receive right is destroyed. ++ // The cancellation handler captures the port value, never the dying object. ++ mach_port_t port = m_port; ++ dispatch_source_set_cancel_handler(m_dispatchSource, ^{ ++ mach_port_destroy(mach_task_self(), port); ++ }); ++ dispatch_source_cancel(m_dispatchSource); ++ dispatch_release(m_dispatchSource); + } + + void IOObject::retain() +@@ -113,12 +121,30 @@ boolean_t IOObject::deathNotify(mach_msg_header_t *request, mach_msg_header_t *r + + if (Request->not_header.msgh_id == MACH_NOTIFY_NO_SENDERS) + { +- IOObject* object = lookup(request->msgh_remote_port); +- +- if (object != nullptr) ++ // No-senders messages carry no object name in their body. They are ++ // delivered to the object's own receive right, identified by local_port. ++ mach_port_t port = request->msgh_local_port; ++ dispatch_async(dispatch_get_main_queue(), ^{ ++ IOObject* object = lookup(port); ++ if (!object) ++ return; ++ mach_port_status_t status; ++ mach_msg_type_number_t count = MACH_PORT_RECEIVE_STATUS_COUNT; ++ if (mach_port_get_attributes(mach_task_self(), port, MACH_PORT_RECEIVE_STATUS, ++ (mach_port_info_t) &status, &count) != KERN_SUCCESS) ++ return; ++ if (status.mps_srights) ++ { ++ // A new send right was made after this notification was queued. ++ mach_port_t old = MACH_PORT_NULL; ++ mach_port_request_notification(mach_task_self(), port, MACH_NOTIFY_NO_SENDERS, ++ status.mps_mscount, port, MACH_MSG_TYPE_MAKE_SEND_ONCE, &old); ++ if (MACH_PORT_VALID(old)) ++ mach_port_deallocate(mach_task_self(), old); ++ return; ++ } + delete object; +- else +- os_log_error(OS_LOG_DEFAULT, "Received MACH_NOTIFY_NO_SENDERS for port %d, which matches no objects", request->msgh_remote_port); ++ }); + + Reply->Head.msgh_bits = 0; + Reply->Head.msgh_remote_port = MACH_PORT_NULL; +diff --git a/src/IOPMrootDomain.h b/src/IOPMrootDomain.h +new file mode 100644 +index 0000000..9f9fc95 +--- /dev/null ++++ b/src/IOPMrootDomain.h +@@ -0,0 +1,89 @@ ++/* ++ This file is part of Darling. ++ ++ Copyright (C) 2026 Darling developers ++ ++ Darling is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ Darling is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with Darling. If not, see . ++*/ ++ ++#ifndef IOKITD_IOPMROOTDOMAIN_H ++#define IOKITD_IOPMROOTDOMAIN_H ++ ++#include "IOUserClient.h" ++#include "ServiceRegistry.h" ++#include ++ ++struct iokitd_logind; ++ ++// The power management root (IOPower:/IOPowerConnection/IOPMrootDomain). ++// IORegisterForSystemPower() opens it and registers for ++// kIOAppPowerStateInterest; host suspend and resume, as announced by ++// systemd-logind, are passed on as kIOMessageSystemWillSleep (which clients ++// acknowledge with IOAllowPowerChange()), kIOMessageSystemWillPowerOn and ++// kIOMessageSystemHasPoweredOn. ++class IOPMrootDomain : public IOService ++{ ++public: ++ static IOPMrootDomain* instance(); ++ static void registerSelf(ServiceRegistry* targetServiceRegistry); ++ ++ const char* className() const override; ++ NSDictionary* matchingDictionary() override; ++ NSDictionary* getProperties() override; ++ bool conformsTo(const char* className) override; ++ IOReturn newUserClient(uint32_t type, IOUserClient** client) override; ++ bool acceptsInterest(const char* type) override; ++ ++ // IOAllowPowerChange() / IOCancelPowerChange() ++ IOReturn allowPowerChange(uint64_t notificationID); ++ IOReturn cancelPowerChange(uint64_t notificationID); ++ ++ // Follows host sleep through logind, without blocking the caller. ++ void startHostSleepMonitor(); ++ ++ // Main queue ++ void systemWillSleep(uint64_t cycle); ++ void systemDidWake(uint64_t cycle); ++protected: ++ void interestRemoved(IOInterestNotification* notification) override; ++private: ++ IOPMrootDomain() = default; ++ uint32_t nextNotificationID(uint16_t index); ++ void tellApps(uint32_t messageType); ++ void checkSleepReady(); ++ ++ struct iokitd_logind* m_logind = nullptr; ++ bool m_sleeping = false; ++ bool m_sleepReadySent = false; ++ uint64_t m_cycle = 0; ++ uint16_t m_serial = 0; ++ // Outstanding kIOMessageSystemWillSleep acknowledgements by notification ID ++ std::map m_pendingAcks; ++}; ++ ++// The connection returned by IOServiceOpen() on the root domain ++class RootDomainUserClient : public IOUserClient ++{ ++public: ++ RootDomainUserClient(IOPMrootDomain* owner); ++ ++ const char* className() const override; ++ NSDictionary* matchingDictionary() override; ++ IOExternalMethod* getExternalMethodForIndex(UInt32 index) override; ++ IOReturn externalMethod(uint32_t selector, IOExternalMethodArguments* arguments) override; ++private: ++ IOPMrootDomain* m_owner; ++}; ++ ++#endif +diff --git a/src/IOPMrootDomain.mm b/src/IOPMrootDomain.mm +new file mode 100644 +index 0000000..268e2f5 +--- /dev/null ++++ b/src/IOPMrootDomain.mm +@@ -0,0 +1,333 @@ ++/* ++ This file is part of Darling. ++ ++ Copyright (C) 2026 Darling developers ++ ++ Darling is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ Darling is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with Darling. If not, see . ++*/ ++ ++#include "IOPMrootDomain.h" ++#include "IOInterestNotification.h" ++#include "LogindSleep.h" ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#import ++ ++extern "C" struct elf_calls* _elfcalls; ++ ++// The IOPower plane parent of the root domain, as on macOS ++class IOPowerConnection : public IORegistryEntry ++{ ++public: ++ const char* className() const override { return "IOPowerConnection"; } ++}; ++ ++IOPMrootDomain* IOPMrootDomain::instance() ++{ ++ static IOPMrootDomain* domain = new IOPMrootDomain; ++ return domain; ++} ++ ++void IOPMrootDomain::registerSelf(ServiceRegistry* targetServiceRegistry) ++{ ++ IOPMrootDomain* domain = instance(); ++ static IOPowerConnection* connection = new IOPowerConnection; ++ ++ targetServiceRegistry->registerService(domain); ++ domain->registerInPlane(kIOServicePlane, "IOPMrootDomain", IORegistryEntry::root()); ++ connection->registerInPlane(kIOPowerPlane, "IOPowerConnection", IORegistryEntry::root()); ++ domain->registerInPlane(kIOPowerPlane, "IOPMrootDomain", connection); ++} ++ ++const char* IOPMrootDomain::className() const ++{ ++ return "IOPMrootDomain"; ++} ++ ++NSDictionary* IOPMrootDomain::matchingDictionary() ++{ ++ return @{ ++ @"IOProviderClass": @"IOPMrootDomain", ++ @"IOClass": @"IOPMrootDomain", ++ }; ++} ++ ++NSDictionary* IOPMrootDomain::getProperties() ++{ ++ return @{ ++ @"IOClass": @"IOPMrootDomain", ++ @"IOUserClientClass": @"RootDomainUserClient", ++ }; ++} ++ ++bool IOPMrootDomain::conformsTo(const char* className) ++{ ++ if (std::strcmp(className, "IOPMrootDomain") == 0) ++ return true; ++ return IOService::conformsTo(className); ++} ++ ++IOReturn IOPMrootDomain::newUserClient(uint32_t type, IOUserClient** client) ++{ ++ try ++ { ++ *client = new RootDomainUserClient(this); ++ return kIOReturnSuccess; ++ } ++ catch (const std::exception& e) ++ { ++ os_log_error(OS_LOG_DEFAULT, "iokitd: cannot open IOPMrootDomain: %s", e.what()); ++ *client = nullptr; ++ return kIOReturnNoResources; ++ } ++} ++ ++bool IOPMrootDomain::acceptsInterest(const char* type) ++{ ++ // General interest messages (clamshell, thermal...) are never sent here, ++ // but registering for them is valid. ++ return std::strcmp(type, kIOAppPowerStateInterest) == 0 ++ || std::strcmp(type, kIOGeneralInterest) == 0; ++} ++ ++// Like xnu: the change serial number in the upper half, the client in the lower one. ++uint32_t IOPMrootDomain::nextNotificationID(uint16_t index) ++{ ++ return (uint32_t(m_serial) << 16) | index; ++} ++ ++void IOPMrootDomain::tellApps(uint32_t messageType) ++{ ++ uint16_t index = 0; ++ ++ m_serial++; ++ for (IOInterestNotification* n : interests(kIOAppPowerStateInterest)) ++ { ++ uint32_t id = messageType == kIOMessageSystemWillSleep ? nextNotificationID(++index) : 0; ++ ++ if (n->deliver(messageType, id) == KERN_SUCCESS && messageType == kIOMessageSystemWillSleep) ++ m_pendingAcks[id] = n; ++ } ++} ++ ++void IOPMrootDomain::systemWillSleep(uint64_t cycle) ++{ ++ os_log(OS_LOG_DEFAULT, "iokitd: host is going to sleep, notifying applications"); ++ ++ m_cycle = cycle; ++ m_sleeping = true; ++ m_sleepReadySent = false; ++ m_pendingAcks.clear(); ++ ++ // kIOMessageCanSystemSleep is not sent: logind only announces a suspend ++ // that has already been decided, so there is nothing applications could veto. ++ tellApps(kIOMessageSystemWillSleep); ++ checkSleepReady(); ++} ++ ++void IOPMrootDomain::systemDidWake(uint64_t cycle) ++{ ++ os_log(OS_LOG_DEFAULT, "iokitd: host has woken up, notifying applications"); ++ ++ m_sleeping = false; ++ m_pendingAcks.clear(); ++ ++ tellApps(kIOMessageSystemWillPowerOn); ++ tellApps(kIOMessageSystemHasPoweredOn); ++} ++ ++void IOPMrootDomain::checkSleepReady() ++{ ++ if (!m_sleeping || m_sleepReadySent || !m_pendingAcks.empty() || !m_logind) ++ return; ++ ++ m_sleepReadySent = true; ++ iokitd_logind_sleep_ready(m_logind, m_cycle); ++} ++ ++IOReturn IOPMrootDomain::allowPowerChange(uint64_t notificationID) ++{ ++ // Unknown or late IDs are ignored, as on macOS. ++ m_pendingAcks.erase(uint32_t(notificationID)); ++ checkSleepReady(); ++ return kIOReturnSuccess; ++} ++ ++IOReturn IOPMrootDomain::cancelPowerChange(uint64_t notificationID) ++{ ++ // Only an idle sleep query (kIOMessageCanSystemSleep) can be cancelled, ++ // and those are never sent. Treat it as an acknowledgement, like macOS ++ // does for kIOMessageSystemWillSleep. ++ return allowPowerChange(notificationID); ++} ++ ++void IOPMrootDomain::interestRemoved(IOInterestNotification* notification) ++{ ++ for (auto it = m_pendingAcks.begin(); it != m_pendingAcks.end(); ) ++ { ++ if (it->second == notification) ++ it = m_pendingAcks.erase(it); ++ else ++ ++it; ++ } ++ checkSleepReady(); ++} ++ ++// Host libsystemd, loaded through the ELF loader. iokitd shares the host's ++// user and network, so the system bus socket is reachable directly. ++ ++static void logindLog(const char* message) ++{ ++ os_log(OS_LOG_DEFAULT, "iokitd: %{public}s", message); ++} ++ ++static bool loadLogindApi(struct iokitd_logind_api* api) ++{ ++ void *systemd, *libc; ++ ++ if (_elfcalls == nullptr) ++ return false; ++ systemd = _elfcalls->dlopen("libsystemd.so.0"); ++ if (systemd == nullptr) ++ return false; ++ libc = _elfcalls->dlopen("libc.so.6"); ++ if (libc == nullptr) ++ { ++ _elfcalls->dlclose(systemd); ++ return false; ++ } ++ ++ std::memset(api, 0, sizeof(*api)); ++#define BIND(lib, field) \ ++ if ((*(void**) &api->field = _elfcalls->dlsym(lib, #field)) == nullptr) \ ++ goto fail; ++ BIND(systemd, sd_bus_new); ++ BIND(systemd, sd_bus_set_address); ++ BIND(systemd, sd_bus_set_bus_client); ++ BIND(systemd, sd_bus_start); ++ BIND(systemd, sd_bus_set_method_call_timeout); ++ BIND(systemd, sd_bus_flush_close_unref); ++ BIND(systemd, sd_bus_match_signal); ++ BIND(systemd, sd_bus_call_method); ++ BIND(systemd, sd_bus_get_property_trivial); ++ BIND(systemd, sd_bus_message_read); ++ BIND(systemd, sd_bus_message_unref); ++ BIND(systemd, sd_bus_slot_unref); ++ BIND(systemd, sd_bus_error_free); ++ BIND(systemd, sd_bus_process); ++ BIND(systemd, sd_bus_wait); ++ // The inhibitor is a host descriptor; use the host's calls on it. ++ BIND(libc, fcntl); ++ BIND(libc, close); ++#undef BIND ++ api->getenv = getenv; ++ api->log = logindLog; ++ // The monitor and both libraries live until process exit, which closes ++ // the host bus and inhibitor descriptors. stop() is used by native tests. ++ return true; ++ ++fail: ++ _elfcalls->dlclose(libc); ++ _elfcalls->dlclose(systemd); ++ return false; ++} ++ ++static void logindWillSleep(void* context, uint64_t cycle) ++{ ++ dispatch_async(dispatch_get_main_queue(), ^{ ++ IOPMrootDomain::instance()->systemWillSleep(cycle); ++ }); ++} ++ ++static void logindDidWake(void* context, uint64_t cycle) ++{ ++ dispatch_async(dispatch_get_main_queue(), ^{ ++ IOPMrootDomain::instance()->systemDidWake(cycle); ++ }); ++} ++ ++void IOPMrootDomain::startHostSleepMonitor() ++{ ++ // Connecting and taking the inhibitor are blocking D-Bus calls. ++ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ ++ static struct iokitd_logind_api api; ++ static const struct iokitd_logind_callbacks callbacks = { logindWillSleep, logindDidWake }; ++ struct iokitd_logind* logind = nullptr; ++ ++ if (!loadLogindApi(&api)) ++ { ++ os_log(OS_LOG_DEFAULT, "iokitd: host libsystemd not available, no sleep/wake notifications"); ++ return; ++ } ++ ++ int r = iokitd_logind_start(&api, &callbacks, nullptr, &logind); ++ if (r < 0) ++ { ++ os_log(OS_LOG_DEFAULT, "iokitd: logind sleep monitor not started (%d), no sleep/wake notifications", r); ++ return; ++ } ++ ++ dispatch_async(dispatch_get_main_queue(), ^{ ++ IOPMrootDomain* domain = IOPMrootDomain::instance(); ++ domain->m_logind = logind; ++ // A suspend may have been announced before we got here. ++ domain->checkSleepReady(); ++ }); ++ }); ++} ++ ++RootDomainUserClient::RootDomainUserClient(IOPMrootDomain* owner) ++: m_owner(owner) ++{ ++} ++ ++const char* RootDomainUserClient::className() const ++{ ++ return "RootDomainUserClient"; ++} ++ ++NSDictionary* RootDomainUserClient::matchingDictionary() ++{ ++ return @{}; ++} ++ ++IOExternalMethod* RootDomainUserClient::getExternalMethodForIndex(UInt32 index) ++{ ++ return nullptr; ++} ++ ++IOReturn RootDomainUserClient::externalMethod(uint32_t selector, IOExternalMethodArguments* arguments) ++{ ++ arguments->scalarOutputCount = 0; ++ arguments->structureOutputSize = 0; ++ ++ switch (selector) ++ { ++ case kPMAllowPowerChange: ++ case kPMCancelPowerChange: ++ if (arguments->scalarInputCount != 1) ++ return kIOReturnBadArgument; ++ if (selector == kPMAllowPowerChange) ++ return m_owner->allowPowerChange(arguments->scalarInput[0]); ++ return m_owner->cancelPowerChange(arguments->scalarInput[0]); ++ default: ++ return kIOReturnUnsupported; ++ } ++} +diff --git a/src/IOService.h b/src/IOService.h +index ab8f5c3..08d34e7 100644 +--- a/src/IOService.h ++++ b/src/IOService.h +@@ -23,6 +23,11 @@ + #include "IORegistryEntry.h" + #include + #include ++#include ++#include ++ ++class IOInterestNotification; ++class IOUserClient; + + class IOService : public IORegistryEntry + { +@@ -32,10 +37,23 @@ public: + bool matches(NSDictionary* dict); + bool conformsTo(const char* className) override; + ++ // IOServiceOpen(). On success *client holds the reference handed to the caller. ++ virtual IOReturn newUserClient(uint32_t type, IOUserClient** client); ++ ++ // IOServiceAddInterestNotification(). Services opt in per interest type. ++ virtual bool acceptsInterest(const char* type); ++ void addInterest(IOInterestNotification* notification); ++ void removeInterest(IOInterestNotification* notification); ++ std::vector interests(const char* type) const; ++ + uint32_t busyState() const { return m_busyState; } + uint64_t accumulatedBusyTime() const { return m_accumulatedBusyTime; } + uint64_t state() const { return m_state; } + protected: ++ // Called after a client released its interest notification ++ virtual void interestRemoved(IOInterestNotification* notification) {} ++ ++ std::list m_interests; + uint32_t m_busyState = 0; + uint64_t m_accumulatedBusyTime = 0; + uint64_t m_state = kIOServiceRegisteredState | kIOServiceMatchedState; +diff --git a/src/IOService.mm b/src/IOService.mm +index 0316b30..25904fa 100644 +--- a/src/IOService.mm ++++ b/src/IOService.mm +@@ -18,6 +18,11 @@ + */ + + #include "IOService.h" ++#include "IOInterestNotification.h" ++#include "IOUserClient.h" ++#include ++#include ++#include + #import + + extern "C" { +@@ -71,6 +76,92 @@ bool IOService::conformsTo(const char* className) + return IOObject::conformsTo(className); + } + ++IOReturn IOService::newUserClient(uint32_t type, IOUserClient** client) ++{ ++ *client = nullptr; ++ return kIOReturnUnsupported; ++} ++ ++bool IOService::acceptsInterest(const char* type) ++{ ++ return false; ++} ++ ++void IOService::addInterest(IOInterestNotification* notification) ++{ ++ m_interests.push_back(notification); ++} ++ ++void IOService::removeInterest(IOInterestNotification* notification) ++{ ++ m_interests.remove(notification); ++ interestRemoved(notification); ++} ++ ++std::vector IOService::interests(const char* type) const ++{ ++ std::vector result; ++ for (IOInterestNotification* notification : m_interests) ++ { ++ if (notification->interestType() == type) ++ result.push_back(notification); ++ } ++ return result; ++} ++ ++kern_return_t is_io_service_open_extended ++( ++ mach_port_t service, ++ task_t owningTask, ++ uint32_t connect_type, ++ NDR_record_t ndr, ++ io_buf_ptr_t properties, ++ mach_msg_type_number_t propertiesCnt, ++ kern_return_t *result, ++ mach_port_t *connection ++) ++{ ++ *connection = MACH_PORT_NULL; ++ ++ // The rights and memory in the request are ours once we return success. ++ if (properties) ++ vm_deallocate(mach_task_self(), (vm_address_t) properties, propertiesCnt); ++ if (MACH_PORT_VALID(owningTask)) ++ mach_port_deallocate(mach_task_self(), owningTask); ++ ++ IOService* s = dynamic_cast(IOObject::lookup(service)); ++ if (!s) ++ { ++ *result = kIOReturnBadArgument; ++ return KERN_SUCCESS; ++ } ++ ++ IOUserClient* client = nullptr; ++ *result = s->newUserClient(connect_type, &client); ++ ++ if (*result == kIOReturnSuccess && client) ++ { ++ *connection = client->port(); ++ // The reply carries a copy of the send right; drop ours afterwards. ++ client->releaseLater(); ++ } ++ else if (*result == kIOReturnSuccess) ++ *result = kIOReturnUnsupported; ++ ++ return KERN_SUCCESS; ++} ++ ++kern_return_t is_io_service_close ++( ++ mach_port_t connection ++) ++{ ++ // The connection goes away once the caller releases its last reference. ++ if (!dynamic_cast(IOObject::lookup(connection))) ++ return kIOReturnBadArgument; ++ return kIOReturnSuccess; ++} ++ + kern_return_t is_io_service_get_state + ( + mach_port_t service, +diff --git a/src/IOUserClient.h b/src/IOUserClient.h +index 82c10db..adeb966 100644 +--- a/src/IOUserClient.h ++++ b/src/IOUserClient.h +@@ -28,7 +28,8 @@ + class IOUserClient : public IOService + { + public: +- IOReturn externalMethod(uint32_t selector, IOExternalMethodArguments *arguments); ++ // The default dispatches through getExternalMethodForIndex() ++ virtual IOReturn externalMethod(uint32_t selector, IOExternalMethodArguments *arguments); + IOExternalMethod *getTargetAndMethodForIndex(IOService **targetP, UInt32 index); + virtual IOExternalMethod *getExternalMethodForIndex(UInt32 index) = 0; + }; +diff --git a/src/LogindSleep.c b/src/LogindSleep.c +new file mode 100644 +index 0000000..1d6aa79 +--- /dev/null ++++ b/src/LogindSleep.c +@@ -0,0 +1,455 @@ ++/* ++ This file is part of Darling. ++ ++ Copyright (C) 2026 Darling developers ++ ++ Darling is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ Darling is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with Darling. If not, see . ++*/ ++ ++#include "LogindSleep.h" ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#define LOGIND_NAME "org.freedesktop.login1" ++#define LOGIND_PATH "/org/freedesktop/login1" ++#define LOGIND_MANAGER "org.freedesktop.login1.Manager" ++ ++#define USEC_PER_SEC 1000000ULL ++/* logind's default InhibitDelayMaxSec */ ++#define DEFAULT_LOGIND_DELAY_USEC (5 * USEC_PER_SEC) ++/* What macOS gives applications to answer kIOMessageSystemWillSleep */ ++#define MAX_ACK_TIMEOUT_USEC (30 * USEC_PER_SEC) ++#define MIN_ACK_TIMEOUT_USEC (USEC_PER_SEC / 10) ++/* Upper bound on how long the bus thread sleeps before checking deadlines and stop */ ++#define POLL_INTERVAL_USEC (200 * 1000ULL) ++#define CALL_TIMEOUT_USEC (5 * USEC_PER_SEC) ++ ++struct iokitd_logind ++{ ++ struct iokitd_logind_api api; ++ struct iokitd_logind_callbacks callbacks; ++ void* context; ++ ++ void* bus; ++ void* slot; ++ void* owner_slot; ++ char* address; ++ pthread_t thread; ++ int thread_started; ++ uint64_t ack_timeout_usec; ++ ++ /* Shared with iokitd_logind_sleep_ready() */ ++ pthread_mutex_t lock; ++ int inhibitor_fd; ++ int sleeping; ++ uint64_t cycle; ++ uint64_t deadline_usec; ++ int stop; ++ ++ /* Bus thread only */ ++ int want_inhibitor; ++}; ++ ++static uint64_t now_usec(void) ++{ ++ struct timespec ts; ++ clock_gettime(CLOCK_MONOTONIC, &ts); ++ return (uint64_t) ts.tv_sec * USEC_PER_SEC + (uint64_t) ts.tv_nsec / 1000; ++} ++ ++static void log_message(struct iokitd_logind* l, const char* format, ...) ++ __attribute__((format(printf, 2, 3))); ++ ++static void log_message(struct iokitd_logind* l, const char* format, ...) ++{ ++ char buffer[256]; ++ va_list ap; ++ ++ if (l->api.log == NULL) ++ return; ++ va_start(ap, format); ++ vsnprintf(buffer, sizeof(buffer), format, ap); ++ va_end(ap); ++ l->api.log(buffer); ++} ++ ++static int api_complete(const struct iokitd_logind_api* api) ++{ ++ return api != NULL && api->sd_bus_new && api->sd_bus_set_address && api->sd_bus_set_bus_client ++ && api->sd_bus_start && api->sd_bus_set_method_call_timeout && api->sd_bus_flush_close_unref && api->sd_bus_match_signal ++ && api->sd_bus_call_method && api->sd_bus_get_property_trivial && api->sd_bus_message_read ++ && api->sd_bus_message_unref && api->sd_bus_slot_unref && api->sd_bus_error_free ++ && api->sd_bus_process && api->sd_bus_wait && api->fcntl && api->close && api->getenv; ++} ++ ++/* Must hold l->lock. */ ++static void close_inhibitor_locked(struct iokitd_logind* l) ++{ ++ if (l->inhibitor_fd >= 0) ++ { ++ l->api.close(l->inhibitor_fd); ++ l->inhibitor_fd = -1; ++ } ++} ++ ++/* Bus thread (or start, before the thread exists). */ ++static void take_inhibitor(struct iokitd_logind* l) ++{ ++ struct iokitd_bus_error error = { NULL, NULL, 0 }; ++ void* reply = NULL; ++ int fd = -1, own = -1, r; ++ ++ r = l->api.sd_bus_call_method(l->bus, LOGIND_NAME, LOGIND_PATH, LOGIND_MANAGER, "Inhibit", ++ &error, &reply, "ssss", "sleep", "Darling", ++ "Notify macOS applications before the system sleeps", "delay"); ++ if (r >= 0) ++ r = l->api.sd_bus_message_read(reply, "h", &fd); ++ /* The descriptor belongs to the reply message; keep our own copy. */ ++ if (r >= 0) ++ { ++ own = l->api.fcntl(fd, IOKITD_LINUX_F_DUPFD_CLOEXEC, 3); ++ if (own < 0) ++ r = -IOKITD_LINUX_ENOMEM; ++ } ++ if (r < 0) ++ log_message(l, "logind: cannot take a sleep delay lock: %s (%d)", ++ error.message ? error.message : "no reply", r); ++ ++ if (reply != NULL) ++ l->api.sd_bus_message_unref(reply); ++ l->api.sd_bus_error_free(&error); ++ ++ if (own >= 0) ++ { ++ pthread_mutex_lock(&l->lock); ++ close_inhibitor_locked(l); ++ l->inhibitor_fd = own; ++ pthread_mutex_unlock(&l->lock); ++ } ++} ++ ++static int on_prepare_for_sleep(void* m, void* userdata, struct iokitd_bus_error* ret_error) ++{ ++ struct iokitd_logind* l = userdata; ++ int start = 0; ++ uint64_t cycle; ++ ++ (void) ret_error; ++ if (l->api.sd_bus_message_read(m, "b", &start) < 0) ++ return 0; ++ ++ pthread_mutex_lock(&l->lock); ++ if (start) ++ { ++ l->cycle++; ++ l->sleeping = 1; ++ l->deadline_usec = now_usec() + l->ack_timeout_usec; ++ } ++ else ++ { ++ l->sleeping = 0; ++ /* If logind suspended without waiting for us, the old lock is still valid. */ ++ l->want_inhibitor = l->inhibitor_fd < 0; ++ } ++ cycle = l->cycle; ++ pthread_mutex_unlock(&l->lock); ++ ++ if (start) ++ { ++ if (l->callbacks.will_sleep) ++ l->callbacks.will_sleep(l->context, cycle); ++ } ++ else if (l->callbacks.did_wake) ++ l->callbacks.did_wake(l->context, cycle); ++ return 0; ++} ++ ++/* logind may appear after iokitd starts, or restart while we are awake. */ ++static int on_logind_owner_changed(void* m, void* userdata, struct iokitd_bus_error* error) ++{ ++ struct iokitd_logind* l = userdata; ++ const char *name, *old_owner, *new_owner; ++ (void) error; ++ if (l->api.sd_bus_message_read(m, "sss", &name, &old_owner, &new_owner) < 0 ++ || strcmp(name, LOGIND_NAME) != 0) ++ return 0; ++ pthread_mutex_lock(&l->lock); ++ close_inhibitor_locked(l); ++ l->sleeping = 0; ++ l->want_inhibitor = new_owner[0] != '\0'; ++ pthread_mutex_unlock(&l->lock); ++ return 0; ++} ++ ++static void close_bus(struct iokitd_logind* l) ++{ ++ if (l->slot) l->api.sd_bus_slot_unref(l->slot); ++ if (l->owner_slot) l->api.sd_bus_slot_unref(l->owner_slot); ++ if (l->bus) l->api.sd_bus_flush_close_unref(l->bus); ++ l->slot = l->owner_slot = l->bus = NULL; ++} ++ ++static int connect_bus(struct iokitd_logind* l) ++{ ++ int r; ++ r = l->api.sd_bus_new(&l->bus); ++ if (r >= 0) ++ r = l->api.sd_bus_set_address(l->bus, l->address); ++ if (r >= 0) ++ r = l->api.sd_bus_set_bus_client(l->bus, 1); ++ if (r >= 0) ++ r = l->api.sd_bus_set_method_call_timeout(l->bus, CALL_TIMEOUT_USEC); ++ if (r >= 0) ++ r = l->api.sd_bus_start(l->bus); ++ /* Subscribe before taking the lock so no announcement is missed. */ ++ if (r >= 0) ++ r = l->api.sd_bus_match_signal(l->bus, &l->slot, LOGIND_NAME, LOGIND_PATH, LOGIND_MANAGER, ++ "PrepareForSleep", on_prepare_for_sleep, l); ++ if (r >= 0) ++ r = l->api.sd_bus_match_signal(l->bus, &l->owner_slot, "org.freedesktop.DBus", ++ "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameOwnerChanged", ++ on_logind_owner_changed, l); ++ if (r < 0) ++ close_bus(l); ++ return r; ++} ++ ++static void connection_lost(struct iokitd_logind* l) ++{ ++ pthread_mutex_lock(&l->lock); ++ close_inhibitor_locked(l); ++ l->sleeping = 0; ++ l->want_inhibitor = 0; ++ pthread_mutex_unlock(&l->lock); ++ close_bus(l); ++} ++ ++static void* bus_thread(void* arg) ++{ ++ struct iokitd_logind* l = arg; ++ uint64_t next_reconnect = 0, backoff = USEC_PER_SEC; ++ ++ for (;;) ++ { ++ uint64_t wait = POLL_INTERVAL_USEC, now; ++ int r; ++ ++ pthread_mutex_lock(&l->lock); ++ if (l->stop) ++ { ++ pthread_mutex_unlock(&l->lock); ++ break; ++ } ++ pthread_mutex_unlock(&l->lock); ++ ++ if (!l->bus) ++ { ++ now = now_usec(); ++ if (now < next_reconnect) ++ { ++ struct timespec pause = {0, POLL_INTERVAL_USEC * 1000}; ++ nanosleep(&pause, NULL); ++ continue; ++ } ++ r = connect_bus(l); ++ if (r < 0) ++ { ++ log_message(l, "logind: reconnect failed (%d), retrying", r); ++ next_reconnect = now_usec() + backoff; ++ backoff = backoff < 15 * USEC_PER_SEC ? backoff * 2 : 30 * USEC_PER_SEC; ++ continue; ++ } ++ backoff = USEC_PER_SEC; ++ take_inhibitor(l); ++ log_message(l, "logind: system bus reconnected"); ++ } ++ ++ r = l->api.sd_bus_process(l->bus, NULL); ++ if (r < 0) ++ { ++ log_message(l, "logind: system bus connection lost (%d), reconnecting", r); ++ connection_lost(l); ++ next_reconnect = 0; ++ continue; ++ } ++ if (r > 0) ++ continue; ++ ++ if (l->want_inhibitor) ++ { ++ l->want_inhibitor = 0; ++ take_inhibitor(l); ++ } ++ ++ now = now_usec(); ++ pthread_mutex_lock(&l->lock); ++ if (l->sleeping && l->inhibitor_fd >= 0) ++ { ++ if (now >= l->deadline_usec) ++ { ++ log_message(l, "logind: applications did not acknowledge sleep in time"); ++ close_inhibitor_locked(l); ++ } ++ else if (l->deadline_usec - now < wait) ++ wait = l->deadline_usec - now; ++ } ++ pthread_mutex_unlock(&l->lock); ++ ++ r = l->api.sd_bus_wait(l->bus, wait); ++ if (r < 0 && r != -IOKITD_LINUX_EINTR) ++ { ++ log_message(l, "logind: waiting on the system bus failed (%d), reconnecting", r); ++ connection_lost(l); ++ next_reconnect = 0; ++ } ++ } ++ ++ /* Never hold up a suspend once nobody is listening. */ ++ pthread_mutex_lock(&l->lock); ++ close_inhibitor_locked(l); ++ pthread_mutex_unlock(&l->lock); ++ return NULL; ++} ++ ++static uint64_t derive_ack_timeout(struct iokitd_logind* l) ++{ ++ struct iokitd_bus_error error = { NULL, NULL, 0 }; ++ uint64_t max_delay = 0, timeout; ++ int r; ++ ++ if (l->api.ack_timeout_usec) ++ return l->api.ack_timeout_usec; ++ ++ r = l->api.sd_bus_get_property_trivial(l->bus, LOGIND_NAME, LOGIND_PATH, LOGIND_MANAGER, ++ "InhibitDelayMaxUSec", &error, 't', &max_delay); ++ l->api.sd_bus_error_free(&error); ++ if (r < 0 || max_delay == 0) ++ max_delay = DEFAULT_LOGIND_DELAY_USEC; ++ ++ timeout = max_delay > 2 * USEC_PER_SEC ? max_delay - USEC_PER_SEC : max_delay / 2; ++ if (timeout > MAX_ACK_TIMEOUT_USEC) ++ timeout = MAX_ACK_TIMEOUT_USEC; ++ if (timeout < MIN_ACK_TIMEOUT_USEC) ++ timeout = MIN_ACK_TIMEOUT_USEC; ++ return timeout; ++} ++ ++static void destroy(struct iokitd_logind* l) ++{ ++ close_bus(l); ++ free(l->address); ++ // No bus thread exists here (startup failure or stop has joined it). ++ close_inhibitor_locked(l); ++ pthread_mutex_destroy(&l->lock); ++ free(l); ++} ++ ++int iokitd_logind_start(const struct iokitd_logind_api* api, ++ const struct iokitd_logind_callbacks* callbacks, void* context, ++ struct iokitd_logind** ret) ++{ ++ struct iokitd_logind* l; ++ const char* address; ++ int r; ++ ++ *ret = NULL; ++ if (!api_complete(api) || callbacks == NULL) ++ return -IOKITD_LINUX_ENOSYS; ++ ++ l = calloc(1, sizeof(*l)); ++ if (l == NULL) ++ return -IOKITD_LINUX_ENOMEM; ++ l->api = *api; ++ l->callbacks = *callbacks; ++ l->context = context; ++ l->inhibitor_fd = -1; ++ // Signal callbacks can run while synchronous startup calls pump the bus. ++ l->ack_timeout_usec = api->ack_timeout_usec ? api->ack_timeout_usec ++ : DEFAULT_LOGIND_DELAY_USEC - USEC_PER_SEC; ++ pthread_mutex_init(&l->lock, NULL); ++ ++ address = api->getenv("DBUS_SYSTEM_BUS_ADDRESS"); ++ if (address == NULL || address[0] == '\0') ++ address = IOKITD_LOGIND_DEFAULT_SYSTEM_BUS; ++ ++ l->address = strdup(address); ++ if (!l->address) ++ { ++ destroy(l); ++ return -IOKITD_LINUX_ENOMEM; ++ } ++ r = connect_bus(l); ++ if (r < 0) ++ { ++ log_message(l, "logind: cannot use the system bus at %s (%d)", address, r); ++ destroy(l); ++ return r; ++ } ++ ++ l->ack_timeout_usec = derive_ack_timeout(l); ++ take_inhibitor(l); ++ ++ r = pthread_create(&l->thread, NULL, bus_thread, l); ++ if (r != 0) ++ { ++ destroy(l); ++ return -IOKITD_LINUX_ENOMEM; ++ } ++ l->thread_started = 1; ++ *ret = l; ++ return 0; ++} ++ ++void iokitd_logind_sleep_ready(struct iokitd_logind* l, uint64_t cycle) ++{ ++ if (l == NULL) ++ return; ++ pthread_mutex_lock(&l->lock); ++ if (l->sleeping && l->cycle == cycle) ++ close_inhibitor_locked(l); ++ pthread_mutex_unlock(&l->lock); ++} ++ ++uint64_t iokitd_logind_ack_timeout_usec(const struct iokitd_logind* l) ++{ ++ return l ? l->ack_timeout_usec : 0; ++} ++ ++int iokitd_logind_holds_inhibitor(struct iokitd_logind* l) ++{ ++ int held; ++ ++ if (l == NULL) ++ return 0; ++ pthread_mutex_lock(&l->lock); ++ held = l->inhibitor_fd >= 0; ++ pthread_mutex_unlock(&l->lock); ++ return held; ++} ++ ++void iokitd_logind_stop(struct iokitd_logind* l) ++{ ++ if (l == NULL) ++ return; ++ pthread_mutex_lock(&l->lock); ++ l->stop = 1; ++ pthread_mutex_unlock(&l->lock); ++ if (l->thread_started) ++ pthread_join(l->thread, NULL); ++ destroy(l); ++} +diff --git a/src/LogindSleep.h b/src/LogindSleep.h +new file mode 100644 +index 0000000..49dac3c +--- /dev/null ++++ b/src/LogindSleep.h +@@ -0,0 +1,145 @@ ++/* ++ This file is part of Darling. ++ ++ Copyright (C) 2026 Darling developers ++ ++ Darling is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ Darling is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with Darling. If not, see . ++*/ ++ ++/* ++ * Follows host suspend/resume through systemd-logind on the system bus. ++ * ++ * At start a "delay" sleep inhibitor is taken. When logind announces a ++ * suspend (PrepareForSleep(true)), will_sleep() is called and the inhibitor ++ * is kept until iokitd_logind_sleep_ready() is called for that cycle or the ++ * acknowledgement timeout expires, whichever comes first; then it is closed ++ * so the suspend can proceed. After resume (PrepareForSleep(false)), ++ * did_wake() is called and a new inhibitor is taken. ++ * ++ * The bus is owned by one background thread; the callbacks run on it. ++ * iokitd_logind_sleep_ready() may be called from any thread. ++ * ++ * This part has no Darling dependencies: every host function comes from the ++ * table below, so it can also be built natively on Linux against libsystemd ++ * for testing. Errors are negative Linux errno values as returned by sd-bus. ++ */ ++ ++#ifndef IOKITD_LOGIND_SLEEP_H_ ++#define IOKITD_LOGIND_SLEEP_H_ ++ ++#include ++ ++#ifdef __cplusplus ++extern "C" { ++#endif ++ ++/* Linux values; the guest's numbering differs. */ ++#define IOKITD_LINUX_EINVAL 22 ++#define IOKITD_LINUX_ENOMEM 12 ++#define IOKITD_LINUX_ENOSYS 38 ++#define IOKITD_LINUX_EINTR 4 ++#define IOKITD_LINUX_F_DUPFD_CLOEXEC 1030 ++ ++#define IOKITD_LOGIND_DEFAULT_SYSTEM_BUS "unix:path=/run/dbus/system_bus_socket" ++ ++/* Layout of sd_bus_error. */ ++struct iokitd_bus_error ++{ ++ const char* name; ++ const char* message; ++ int _need_free; ++}; ++ ++typedef int (*iokitd_bus_handler)(void* m, void* userdata, struct iokitd_bus_error* ret_error); ++ ++struct iokitd_logind_api ++{ ++ /* libsystemd (sd-bus); bus, message and slot objects are opaque */ ++ int (*sd_bus_new)(void** ret); ++ int (*sd_bus_set_address)(void* bus, const char* address); ++ int (*sd_bus_set_bus_client)(void* bus, int b); ++ int (*sd_bus_start)(void* bus); ++ int (*sd_bus_set_method_call_timeout)(void* bus, uint64_t timeout); ++ void* (*sd_bus_flush_close_unref)(void* bus); ++ int (*sd_bus_match_signal)(void* bus, void** ret_slot, const char* sender, const char* path, ++ const char* interface, const char* member, iokitd_bus_handler callback, void* userdata); ++ int (*sd_bus_call_method)(void* bus, const char* destination, const char* path, ++ const char* interface, const char* member, struct iokitd_bus_error* ret_error, ++ void** reply, const char* types, ...); ++ int (*sd_bus_get_property_trivial)(void* bus, const char* destination, const char* path, ++ const char* interface, const char* member, struct iokitd_bus_error* ret_error, ++ char type, void* ret_ptr); ++ int (*sd_bus_message_read)(void* m, const char* types, ...); ++ void* (*sd_bus_message_unref)(void* m); ++ void* (*sd_bus_slot_unref)(void* slot); ++ void (*sd_bus_error_free)(struct iokitd_bus_error* e); ++ int (*sd_bus_process)(void* bus, void** ret); ++ int (*sd_bus_wait)(void* bus, uint64_t timeout_usec); ++ ++ /* Host file descriptors (the inhibitor lock) */ ++ int (*fcntl)(int fd, int cmd, ...); ++ int (*close)(int fd); ++ ++ /* Process environment (DBUS_SYSTEM_BUS_ADDRESS) */ ++ char* (*getenv)(const char* name); ++ ++ /* Optional diagnostics */ ++ void (*log)(const char* message); ++ ++ /* ++ * How long to wait for applications to acknowledge a suspend before ++ * releasing the inhibitor anyway; 0 derives it from logind's ++ * InhibitDelayMaxUSec (one second less, so the release always comes ++ * before logind gives up on us). ++ */ ++ uint64_t ack_timeout_usec; ++}; ++ ++struct iokitd_logind_callbacks ++{ ++ /* Suspend is imminent. Call iokitd_logind_sleep_ready(cycle) once clients agreed. */ ++ void (*will_sleep)(void* context, uint64_t cycle); ++ /* The system has resumed (or the suspend failed). */ ++ void (*did_wake)(void* context, uint64_t cycle); ++}; ++ ++struct iokitd_logind; ++ ++/* ++ * Connects to the system bus (DBUS_SYSTEM_BUS_ADDRESS or the standard ++ * socket), subscribes to PrepareForSleep, takes the delay inhibitor and ++ * starts the bus thread. A missing logind is not an error: no events arrive ++ * then. Returns 0 or a negative errno (no bus, incomplete api). ++ */ ++int iokitd_logind_start(const struct iokitd_logind_api* api, ++ const struct iokitd_logind_callbacks* callbacks, void* context, ++ struct iokitd_logind** ret); ++ ++/* Releases the inhibitor if it is still held for this suspend cycle. */ ++void iokitd_logind_sleep_ready(struct iokitd_logind* logind, uint64_t cycle); ++ ++/* Acknowledgement timeout in use, in microseconds. */ ++uint64_t iokitd_logind_ack_timeout_usec(const struct iokitd_logind* logind); ++ ++/* 1 if a delay inhibitor is currently held. */ ++int iokitd_logind_holds_inhibitor(struct iokitd_logind* logind); ++ ++/* Stops the bus thread, closes the inhibitor and the bus. */ ++void iokitd_logind_stop(struct iokitd_logind* logind); ++ ++#ifdef __cplusplus ++} ++#endif ++ ++#endif diff --git a/tools/build-riot-power-notify-check.py b/tools/build-riot-power-notify-check.py new file mode 100644 index 0000000..be04407 --- /dev/null +++ b/tools/build-riot-power-notify-check.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Build the system sleep/wake notification guest probe (IORegisterForSystemPower).""" +from pathlib import Path +import json +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +config = root / "build/config.json" +sdk = json.loads(config.read_text())["sdk"] if config.exists() else "/usr/local/libexec/darling" +appkit = root / "build/darling-src/build/src/external/cocotron/AppKit" +flags = {} +for line in (appkit / "CMakeFiles/AppKit.dir/flags.make").read_text().splitlines(): + if line.startswith("C_") and " = " in line: + key, value = line.split(" = ", 1) + flags[key] = shlex.split(value) + +source = root / "tools/riot-power-notify-check.c" +obj = root / "build/riot-power-notify-check.o" +exe = root / "build/riot-power-notify-check" +subprocess.run(["clang", "-x", "objective-c", *flags["C_DEFINES"], *flags["C_INCLUDES"], + *flags["C_FLAGS"], "-fblocks", "-c", str(source), + "-o", str(obj)], check=True) +subprocess.run(["clang", "-target", "x86_64-apple-macosx11.0", + "-fuse-ld=lld", "-nostdlib", "-isysroot", + sdk, str(obj), "-lSystem", + "-framework", "IOKit", "-framework", "CoreFoundation", + "-o", str(exe)], + check=True) diff --git a/tools/riot-logind-sleep-check.c b/tools/riot-logind-sleep-check.c new file mode 100644 index 0000000..ac728ef --- /dev/null +++ b/tools/riot-logind-sleep-check.c @@ -0,0 +1,608 @@ +/* Host-side regression for iokitd's logind sleep bridge (LogindSleep.c). + * + * Runs a private dbus-daemon as the "system bus" (DBUS_SYSTEM_BUS_ADDRESS) + * and a fake org.freedesktop.login1 forked from this process. The fake hands + * out a pipe for every Inhibit call and records when its write end is closed, + * i.e. when the lock is released. The bridge is linked natively against + * libsystemd. The real system bus and logind are never used. + * + * cc -std=gnu11 -O2 -Wall -Wextra -pthread \ + * -I build/darling-src/src/external/iokitd/src \ + * tools/riot-logind-sleep-check.c \ + * build/darling-src/src/external/iokitd/src/LogindSleep.c \ + * $(pkg-config --cflags --libs libsystemd) -o build/riot-logind-sleep-check + * build/riot-logind-sleep-check + * + * With --fake-logind DIR it only runs the private bus and the fake logind for + * a guest test: the bus address is written to DIR/address, commands are read + * from the FIFO DIR/control ("S" PrepareForSleep(true), "W" PrepareForSleep(false), + * "Q" quit) and events are appended to DIR/records. + */ +#define _GNU_SOURCE +#include "LogindSleep.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static char tmpdir[256], records[300], bus_address[512]; +static int failures, checks; +static pid_t daemon_pid, fake_pid, main_pid; + +#define CHECK(cond, ...) do { checks++; if (!(cond)) { failures++; \ + fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); fprintf(stderr, __VA_ARGS__); \ + fputc('\n', stderr); } } while (0) + +static uint64_t now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t) ts.tv_sec * 1000 + (uint64_t) ts.tv_nsec / 1000000; +} + +static void sleep_ms(unsigned ms) { + struct timespec ts = { ms / 1000, (long) (ms % 1000) * 1000000L }; + while (nanosleep(&ts, &ts) < 0 && errno == EINTR) {} +} + +/* ---- fake logind (child process) ---- */ + +#define MAX_LOCKS 64 +static FILE *record_file; +static int lock_fds[MAX_LOCKS], lock_ids[MAX_LOCKS], lock_count, next_lock_id = 1; +static uint64_t sleep_started_ms; +static int fake_sleeping; + +static void record(const char *format, ...) __attribute__((format(printf, 1, 2))); +static void record(const char *format, ...) { + va_list ap; + va_start(ap, format); + vfprintf(record_file, format, ap); + va_end(ap); + fputc('\n', record_file); + fflush(record_file); +} + +static int logind_handler(sd_bus_message *m, void *userdata, sd_bus_error *error) { + (void) userdata; (void) error; + if (sd_bus_message_is_method_call(m, "org.freedesktop.login1.Manager", "Inhibit")) { + const char *what, *who, *why, *mode; + int p[2]; + if (sd_bus_message_read(m, "ssss", &what, &who, &why, &mode) < 0) + return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "bad Inhibit arguments"); + if (lock_count == MAX_LOCKS || pipe2(p, O_CLOEXEC) < 0) + return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_NO_MEMORY, "no more locks"); + int id = next_lock_id++; + lock_fds[lock_count] = p[0]; + lock_ids[lock_count++] = id; + record("Inhibit\t%d\t%s\t%s\t%s\t%s", id, what, who, why, mode); + int r = sd_bus_reply_method_return(m, "h", p[1]); + close(p[1]); /* the reply carried its own copy */ + return r; + } + if (sd_bus_message_is_method_call(m, "org.freedesktop.DBus.Properties", "Get")) { + const char *iface, *name; + if (sd_bus_message_read(m, "ss", &iface, &name) < 0) + return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "bad Get arguments"); + record("Get\t%s\t%s", iface, name); + if (!strcmp(iface, "org.freedesktop.login1.Manager") && !strcmp(name, "InhibitDelayMaxUSec")) + return sd_bus_reply_method_return(m, "v", "t", (uint64_t) 5000000); + return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_UNKNOWN_PROPERTY, "unknown property"); + } + return 0; +} + +static void emit_prepare(sd_bus *bus, int start) { + if (start) { + sleep_started_ms = now_ms(); + fake_sleeping = 1; + } else { + fake_sleeping = 0; + } + record(start ? "Sleep" : "Wake"); + sd_bus_emit_signal(bus, "/org/freedesktop/login1", "org.freedesktop.login1.Manager", + "PrepareForSleep", "b", start); + sd_bus_flush(bus); +} + +/* Serves until "Q" arrives on control_fd or the control pipe closes. */ +static void run_fake(int control_fd, int ready_fd) { + sd_bus *bus = NULL; + if (sd_bus_new(&bus) < 0 || sd_bus_set_address(bus, bus_address) < 0 || + sd_bus_set_bus_client(bus, 1) < 0 || sd_bus_start(bus) < 0) + _exit(3); + sd_bus_add_object(bus, NULL, "/org/freedesktop/login1", logind_handler, NULL); + if (sd_bus_request_name(bus, "org.freedesktop.login1", 0) < 0) + _exit(4); + if (write(ready_fd, "R", 1) != 1) + _exit(5); + close(ready_fd); + + for (;;) { + while (sd_bus_process(bus, NULL) > 0) {} + struct pollfd fds[2 + MAX_LOCKS]; + int n = 0; + fds[n].fd = sd_bus_get_fd(bus); + fds[n++].events = (short) sd_bus_get_events(bus); + fds[n].fd = control_fd; + fds[n++].events = POLLIN; + for (int i = 0; i < lock_count; i++) { + fds[n].fd = lock_fds[i]; + fds[n++].events = POLLIN; + } + if (poll(fds, n, 1000) < 0 && errno != EINTR) + _exit(6); + if (fds[1].revents & (POLLIN | POLLHUP)) { + char c; + ssize_t got = read(control_fd, &c, 1); + if (got <= 0 || c == 'Q') + break; + if (c == 'S' || c == 'W') + emit_prepare(bus, c == 'S'); + } + for (int i = 0; i < lock_count; i++) { + if (!(fds[2 + i].revents & (POLLHUP | POLLERR))) + continue; + long delay = fake_sleeping ? (long) (now_ms() - sleep_started_ms) : -1; + record("Released\t%d\t%ld", lock_ids[i], delay); + close(lock_fds[i]); + lock_fds[i] = -1; + } + int kept = 0; + for (int i = 0; i < lock_count; i++) { + if (lock_fds[i] >= 0) { + lock_fds[kept] = lock_fds[i]; + lock_ids[kept++] = lock_ids[i]; + } + } + lock_count = kept; + } + sd_bus_flush_close_unref(bus); + _exit(0); +} + +static pid_t start_fake(int control_fd) { + int ready[2]; + if (pipe(ready)) { perror("pipe"); exit(2); } + pid_t pid = fork(); + if (pid < 0) { perror("fork"); exit(2); } + if (pid == 0) { + close(ready[0]); + record_file = fopen(records, "a"); + if (!record_file) _exit(2); + run_fake(control_fd, ready[1]); + } + close(ready[1]); + char c = 0; + if (read(ready[0], &c, 1) != 1 || c != 'R') { fprintf(stderr, "fake logind failed\n"); exit(2); } + close(ready[0]); + return pid; +} + +static pid_t start_daemon(void) { + char config[300], conf[2048]; + snprintf(config, sizeof(config), "%s/bus.conf", tmpdir); + snprintf(conf, sizeof(conf), + "\n" + "systemunix:path=%s/system_bus_socket" + "EXTERNAL" + "\n", tmpdir); + FILE *f = fopen(config, "w"); + if (!f || fputs(conf, f) < 0 || fclose(f)) { perror(config); exit(2); } + + int out[2]; + if (pipe(out)) { perror("pipe"); exit(2); } + pid_t pid = fork(); + if (pid == 0) { + char arg_config[320], arg_fd[32]; + close(out[0]); + snprintf(arg_config, sizeof(arg_config), "--config-file=%s", config); + snprintf(arg_fd, sizeof(arg_fd), "--print-address=%d", out[1]); + execlp("dbus-daemon", "dbus-daemon", arg_config, "--nofork", "--nosyslog", arg_fd, (char *) NULL); + _exit(127); + } + close(out[1]); + ssize_t n = read(out[0], bus_address, sizeof(bus_address) - 1); + close(out[0]); + if (n <= 0) { fprintf(stderr, "dbus-daemon did not start\n"); exit(2); } + bus_address[n] = '\0'; + bus_address[strcspn(bus_address, "\n")] = '\0'; + return pid; +} + +static void stop_children(void) { + if (getpid() != main_pid) + return; + if (fake_pid > 0) { kill(fake_pid, SIGTERM); waitpid(fake_pid, NULL, 0); fake_pid = 0; } + if (daemon_pid > 0) { kill(daemon_pid, SIGTERM); waitpid(daemon_pid, NULL, 0); daemon_pid = 0; } +} + +/* ---- client side ---- */ + +static atomic_int will_sleep_calls, did_wake_calls; +static atomic_ullong last_sleep_cycle, last_wake_cycle; + +static void on_will_sleep(void *context, uint64_t cycle) { + (void) context; + last_sleep_cycle = cycle; + will_sleep_calls++; +} + +static void on_did_wake(void *context, uint64_t cycle) { + (void) context; + last_wake_cycle = cycle; + did_wake_calls++; +} + +static const struct iokitd_logind_callbacks callbacks = { on_will_sleep, on_did_wake }; + +/* Force an already-subscribed sleep signal to dispatch inside the property + * lookup, before startup has assigned the timeout returned by that lookup. */ +static iokitd_bus_handler startup_handler; +static void *startup_context; +static int capture_sleep_match(void *bus, void **slot, const char *sender, const char *path, + const char *interface, const char *member, + iokitd_bus_handler handler, void *context) { + if (!strcmp(member, "PrepareForSleep")) { + startup_handler = handler; + startup_context = context; + } + return sd_bus_match_signal(bus, (sd_bus_slot **)slot, sender, path, interface, member, + (sd_bus_message_handler_t)handler, context); +} +static int interleaved_property(void *bus, const char *destination, const char *path, + const char *interface, const char *member, + struct iokitd_bus_error *error, char type, void *result) { + int r = sd_bus_get_property_trivial(bus, destination, path, interface, member, + (sd_bus_error *)error, type, result); + sd_bus_message *message = NULL; + if (sd_bus_message_new_signal(bus, &message, "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", "PrepareForSleep") >= 0 && + sd_bus_message_append(message, "b", 1) >= 0 && + sd_bus_message_seal(message, 1, 0) >= 0 && sd_bus_message_rewind(message, 1) >= 0) + startup_handler(message, startup_context, NULL); + sd_bus_message_unref(message); + return r; +} + +static struct iokitd_logind_api native_api(uint64_t ack_timeout_usec) { + struct iokitd_logind_api api; + memset(&api, 0, sizeof(api)); +#define SET(f) api.f = (__typeof__(api.f)) f + SET(sd_bus_new); SET(sd_bus_set_address); SET(sd_bus_set_bus_client); SET(sd_bus_start); SET(sd_bus_set_method_call_timeout); + SET(sd_bus_flush_close_unref); SET(sd_bus_match_signal); SET(sd_bus_call_method); + SET(sd_bus_get_property_trivial); SET(sd_bus_message_read); SET(sd_bus_message_unref); + SET(sd_bus_slot_unref); SET(sd_bus_error_free); SET(sd_bus_process); SET(sd_bus_wait); + SET(fcntl); SET(close); SET(getenv); +#undef SET + api.ack_timeout_usec = ack_timeout_usec; + return api; +} + +static char *read_records(void) { + static char buffer[65536]; + FILE *f = fopen(records, "r"); + size_t n = f ? fread(buffer, 1, sizeof(buffer) - 1, f) : 0; + if (f) fclose(f); + buffer[n] = '\0'; + return buffer; +} + +static int count_prefix(const char *prefix) { + int n = 0; + size_t len = strlen(prefix); + for (const char *line = read_records(); *line; ) { + if (!strncmp(line, prefix, len)) n++; + const char *nl = strchr(line, '\n'); + if (!nl) break; + line = nl + 1; + } + return n; +} + +/* Delay recorded for the release of lock `id`, -2 if not released. */ +static long released_delay(int id) { + char prefix[32]; + snprintf(prefix, sizeof(prefix), "Released\t%d\t", id); + const char *p = strstr(read_records(), prefix); + return p ? strtol(p + strlen(prefix), NULL, 10) : -2; +} + +static int wait_until(int (*cond)(void *), void *arg, unsigned timeout_ms) { + uint64_t end = now_ms() + timeout_ms; + while (!cond(arg)) { + if (now_ms() > end) return 0; + sleep_ms(10); + } + return 1; +} + +struct prefix_count { const char *prefix; int count; }; +static int prefix_reached(void *arg) { + struct prefix_count *pc = arg; + return count_prefix(pc->prefix) >= pc->count; +} +static int not_holding(void *arg) { return !iokitd_logind_holds_inhibitor(arg); } +static int released(void *arg) { return released_delay(*(int *) arg) != -2; } +static int counter_reached(void *arg) { + struct { atomic_int *counter; int value; } *c = arg; + return atomic_load(c->counter) >= c->value; +} + +static int wait_prefix(const char *prefix, int count, unsigned timeout_ms) { + struct prefix_count pc = { prefix, count }; + return wait_until(prefix_reached, &pc, timeout_ms); +} + +static int wait_counter(atomic_int *counter, int value, unsigned timeout_ms) { + struct { atomic_int *counter; int value; } c = { counter, value }; + return wait_until(counter_reached, &c, timeout_ms); +} + +static int wait_released(int id, unsigned timeout_ms) { + return wait_until(released, &id, timeout_ms); +} + +static void command(int control_fd, char c) { + if (write(control_fd, &c, 1) != 1) { perror("control"); exit(2); } +} + +static int serve(const char *dir) { + char path[300]; + snprintf(tmpdir, sizeof(tmpdir), "%s", dir); + snprintf(records, sizeof(records), "%s/records", dir); + fclose(fopen(records, "w")); + main_pid = getpid(); + atexit(stop_children); + signal(SIGTERM, exit); + signal(SIGINT, exit); + daemon_pid = start_daemon(); + + snprintf(path, sizeof(path), "%s/control", dir); + unlink(path); + if (mkfifo(path, 0600) < 0) { perror(path); return 2; } + /* O_RDWR keeps the FIFO open between writers. */ + int control = open(path, O_RDWR); + if (control < 0) { perror(path); return 2; } + fake_pid = start_fake(control); + + snprintf(path, sizeof(path), "%s/address", dir); + FILE *f = fopen(path, "w"); + if (!f || fprintf(f, "%s\n", bus_address) < 0 || fclose(f)) { perror(path); return 2; } + printf("FAKE_LOGIND address=%s pid=%d daemon=%d\n", bus_address, (int) getpid(), (int) daemon_pid); + fflush(stdout); + + int status = 0; + waitpid(fake_pid, &status, 0); + fake_pid = 0; + return 0; +} + +int main(int argc, char **argv) { + if (argc == 3 && !strcmp(argv[1], "--fake-logind")) + return serve(argv[2]); + + snprintf(tmpdir, sizeof(tmpdir), "/tmp/riot-logind-sleep-XXXXXX"); + if (!mkdtemp(tmpdir)) { perror("mkdtemp"); return 2; } + snprintf(records, sizeof(records), "%s/records", tmpdir); + fclose(fopen(records, "w")); + + main_pid = getpid(); + atexit(stop_children); + daemon_pid = start_daemon(); + struct iokitd_logind *l = NULL; + int r; + + /* Incomplete host api: nothing is attempted. */ + { + struct iokitd_logind_api api = native_api(0); + api.sd_bus_wait = NULL; + r = iokitd_logind_start(&api, &callbacks, NULL, &l); + CHECK(r == -ENOSYS && l == NULL, "incomplete api: r=%d", r); + } + + /* No system bus: an error, no crash. */ + { + char missing[400]; + snprintf(missing, sizeof(missing), "unix:path=%s/no-such-socket", tmpdir); + setenv("DBUS_SYSTEM_BUS_ADDRESS", missing, 1); + struct iokitd_logind_api api = native_api(0); + r = iokitd_logind_start(&api, &callbacks, NULL, &l); + CHECK(r < 0 && l == NULL, "missing bus: r=%d", r); + } + + setenv("DBUS_SYSTEM_BUS_ADDRESS", bus_address, 1); + + /* Bus without logind: starts, holds no lock, no events. */ + { + struct iokitd_logind_api api = native_api(0); + r = iokitd_logind_start(&api, &callbacks, NULL, &l); + CHECK(r == 0 && l != NULL, "bus without logind: r=%d", r); + CHECK(!iokitd_logind_holds_inhibitor(l), "bus without logind: holds a lock"); + CHECK(iokitd_logind_ack_timeout_usec(l) == 4000000, + "default ack timeout without logind: %llu", (unsigned long long) iokitd_logind_ack_timeout_usec(l)); + iokitd_logind_stop(l); + l = NULL; + } + + /* The fake is started before any lock exists, so it inherits none. */ + int control[2]; + if (pipe(control)) { perror("pipe"); return 2; } + fake_pid = start_fake(control[0]); + close(control[0]); + + /* Start: one delay lock, timeout derived from InhibitDelayMaxUSec (5 s) minus 1 s. */ + struct iokitd_logind_api api = native_api(0); + r = iokitd_logind_start(&api, &callbacks, NULL, &l); + CHECK(r == 0 && l != NULL, "start: r=%d", r); + CHECK(wait_prefix("Inhibit\t", 1, 2000), "start: no Inhibit call:\n%s", read_records()); + CHECK(strstr(read_records(), "Inhibit\t1\tsleep\tDarling\t") != NULL && + strstr(read_records(), "\tdelay\n") != NULL, "start: wrong Inhibit arguments:\n%s", read_records()); + CHECK(strstr(read_records(), "Get\torg.freedesktop.login1.Manager\tInhibitDelayMaxUSec") != NULL, + "start: InhibitDelayMaxUSec not read"); + CHECK(iokitd_logind_ack_timeout_usec(l) == 4000000, "derived ack timeout %llu", + (unsigned long long) iokitd_logind_ack_timeout_usec(l)); + CHECK(iokitd_logind_holds_inhibitor(l), "start: lock not held"); + CHECK(released_delay(1) == -2, "start: lock 1 released early"); + + /* Sleep, then acknowledge: the lock is held until the acknowledgement. */ + command(control[1], 'S'); + CHECK(wait_counter(&will_sleep_calls, 1, 2000) && last_sleep_cycle == 1, "sleep 1: will_sleep %d cycle %llu", + atomic_load(&will_sleep_calls), (unsigned long long) last_sleep_cycle); + sleep_ms(500); + CHECK(released_delay(1) == -2 && iokitd_logind_holds_inhibitor(l), "sleep 1: released before acknowledgement"); + iokitd_logind_sleep_ready(l, 1); + CHECK(wait_released(1, 2000), "sleep 1: not released after acknowledgement"); + long delay = released_delay(1); + CHECK(delay >= 450 && delay < 2000, "sleep 1: released %ld ms after PrepareForSleep(true)", delay); + CHECK(!iokitd_logind_holds_inhibitor(l), "sleep 1: still holds lock"); + + /* Resume: did_wake, a new lock; a stale acknowledgement does not release it. */ + command(control[1], 'W'); + CHECK(wait_counter(&did_wake_calls, 1, 2000) && last_wake_cycle == 1, "wake 1: did_wake %d", + atomic_load(&did_wake_calls)); + CHECK(wait_prefix("Inhibit\t", 2, 2000), "wake 1: lock not re-taken:\n%s", read_records()); + CHECK(wait_until((int (*)(void *)) iokitd_logind_holds_inhibitor, l, 1000), "wake 1: lock not held"); + iokitd_logind_sleep_ready(l, 1); + sleep_ms(300); + CHECK(released_delay(2) == -2 && iokitd_logind_holds_inhibitor(l), "stale acknowledgement released lock 2"); + + /* logind suspended without waiting (no acknowledgement before resume): keep the same lock. */ + command(control[1], 'S'); + CHECK(wait_counter(&will_sleep_calls, 2, 2000) && last_sleep_cycle == 2, "sleep 2: will_sleep"); + command(control[1], 'W'); + CHECK(wait_counter(&did_wake_calls, 2, 2000) && last_wake_cycle == 2, "wake 2: did_wake"); + sleep_ms(300); + CHECK(count_prefix("Inhibit\t") == 2 && released_delay(2) == -2 && iokitd_logind_holds_inhibitor(l), + "wake 2: lock replaced or released:\n%s", read_records()); + iokitd_logind_sleep_ready(l, 2); + sleep_ms(200); + CHECK(released_delay(2) == -2, "acknowledgement after resume released the lock"); + + /* Stop releases the lock. */ + iokitd_logind_stop(l); + l = NULL; + CHECK(wait_released(2, 2000) && released_delay(2) == -1, "stop: lock 2 not released"); + + /* Nobody acknowledges: the lock is released at the timeout, not before. */ + api = native_api(700000); + r = iokitd_logind_start(&api, &callbacks, NULL, &l); + CHECK(r == 0 && iokitd_logind_ack_timeout_usec(l) == 700000, "timeout bridge: r=%d", r); + CHECK(wait_prefix("Inhibit\t", 3, 2000), "timeout bridge: no lock"); + int sleeps = atomic_load(&will_sleep_calls); + command(control[1], 'S'); + CHECK(wait_counter(&will_sleep_calls, sleeps + 1, 2000) && last_sleep_cycle == 1, "timeout: will_sleep"); + sleep_ms(350); + CHECK(released_delay(3) == -2, "timeout: released before the timeout"); + CHECK(wait_released(3, 3000), "timeout: never released"); + delay = released_delay(3); + CHECK(delay >= 650 && delay < 1500, "timeout: released %ld ms after PrepareForSleep(true)", delay); + iokitd_logind_sleep_ready(l, 1); /* late acknowledgement is harmless */ + int wakes = atomic_load(&did_wake_calls); + command(control[1], 'W'); + CHECK(wait_counter(&did_wake_calls, wakes + 1, 2000), "timeout: did_wake"); + CHECK(wait_prefix("Inhibit\t", 4, 2000), "timeout: lock not re-taken after resume"); + iokitd_logind_stop(l); + l = NULL; + CHECK(wait_released(4, 2000), "timeout: lock 4 not released at stop"); + + /* Restart logind while a bridge is active: discard the old lock and re-take it. */ + api = native_api(700000); + r = iokitd_logind_start(&api, &callbacks, NULL, &l); + CHECK(r == 0 && wait_prefix("Inhibit\t", 5, 2000), "restart: initial lock"); + command(control[1], 'Q'); + close(control[1]); + waitpid(fake_pid, NULL, 0); + fake_pid = 0; + CHECK(wait_until(not_holding, l, 2000), "restart: stale inhibitor still held"); + CHECK(pipe(control) == 0, "restart: control pipe"); + fake_pid = start_fake(control[0]); + close(control[0]); + CHECK(wait_prefix("Inhibit\t", 6, 2000), "restart: inhibitor not re-taken"); + CHECK(iokitd_logind_holds_inhibitor(l), "restart: no new inhibitor"); + sleeps = atomic_load(&will_sleep_calls); + command(control[1], 'S'); + CHECK(wait_counter(&will_sleep_calls, sleeps + 1, 2000), "restart: sleep not received"); + iokitd_logind_sleep_ready(l, last_sleep_cycle); + CHECK(wait_until(not_holding, l, 1000), "restart: ack did not release inhibitor"); + wakes = atomic_load(&did_wake_calls); + command(control[1], 'W'); + CHECK(wait_counter(&did_wake_calls, wakes + 1, 2000), "restart: wake not received"); + CHECK(wait_prefix("Inhibit\t", 7, 2000), "restart: inhibitor missing after wake"); + iokitd_logind_stop(l); + l = NULL; + /* Startup signal during timeout discovery must not create an expired deadline. */ + api = native_api(0); + api.sd_bus_match_signal = capture_sleep_match; + api.sd_bus_get_property_trivial = interleaved_property; + sleeps = atomic_load(&will_sleep_calls); + r = iokitd_logind_start(&api, &callbacks, NULL, &l); + CHECK(r == 0 && l != NULL, "startup interleave: start failed %d", r); + CHECK(atomic_load(&will_sleep_calls) == sleeps + 1, "startup interleave: signal not dispatched"); + sleep_ms(300); + CHECK(iokitd_logind_holds_inhibitor(l), "startup interleave: inhibitor released before ack"); + iokitd_logind_sleep_ready(l, last_sleep_cycle); + CHECK(wait_until(not_holding, l, 1000), "startup interleave: ack did not release inhibitor"); + iokitd_logind_stop(l); + l = NULL; + + /* A complete system-bus outage must reconnect, not disable monitoring forever. */ + char stable_address[400]; + snprintf(stable_address, sizeof(stable_address), "unix:path=%s/system_bus_socket", tmpdir); + setenv("DBUS_SYSTEM_BUS_ADDRESS", stable_address, 1); + api = native_api(700000); + int expected_locks = count_prefix("Inhibit\t") + 1; + r = iokitd_logind_start(&api, &callbacks, NULL, &l); + CHECK(r == 0 && wait_prefix("Inhibit\t", expected_locks, 2000), "bus restart: initial lock"); + command(control[1], 'Q'); + close(control[1]); + waitpid(fake_pid, NULL, 0); + fake_pid = 0; + kill(daemon_pid, SIGTERM); + waitpid(daemon_pid, NULL, 0); + daemon_pid = 0; + CHECK(wait_until(not_holding, l, 2000), "bus restart: stale inhibitor"); + sleep_ms(300); + daemon_pid = start_daemon(); + CHECK(pipe(control) == 0, "bus restart: control pipe"); + fake_pid = start_fake(control[0]); + close(control[0]); + CHECK(wait_prefix("Inhibit\t", expected_locks + 1, 7000), "bus restart: no new inhibitor"); + CHECK(iokitd_logind_holds_inhibitor(l), "bus restart: inhibitor not held"); + sleeps = atomic_load(&will_sleep_calls); + command(control[1], 'S'); + CHECK(wait_counter(&will_sleep_calls, sleeps + 1, 2000), "bus restart: sleep not delivered"); + iokitd_logind_sleep_ready(l, last_sleep_cycle); + CHECK(wait_until(not_holding, l, 1000), "bus restart: acknowledgement failed"); + wakes = atomic_load(&did_wake_calls); + command(control[1], 'W'); + CHECK(wait_counter(&did_wake_calls, wakes + 1, 2000), "bus restart: wake not delivered"); + CHECK(wait_prefix("Inhibit\t", expected_locks + 2, 2000), "bus restart: no lock after wake"); + iokitd_logind_stop(l); + l = NULL; + + command(control[1], 'Q'); + close(control[1]); + waitpid(fake_pid, NULL, 0); + fake_pid = 0; + stop_children(); + + char path[400]; + snprintf(path, sizeof(path), "%s/bus.conf", tmpdir); unlink(path); + snprintf(path, sizeof(path), "%s/system_bus_socket", tmpdir); unlink(path); + unlink(records); + rmdir(tmpdir); + + printf("LOGIND_SLEEP %s checks=%d failures=%d\n", failures ? "FAIL" : "PASS", checks, failures); + return failures ? 1 : 0; +} diff --git a/tools/riot-power-integration-check.py b/tools/riot-power-integration-check.py new file mode 100644 index 0000000..46da5e6 --- /dev/null +++ b/tools/riot-power-integration-check.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +"""Exercise IOKit sleep notifications in a disposable prefix against fake logind. + +Requires a built iokitd, both power probes and an installed Darling server +with the empty OOL descriptor fix. Never contacts the real system bus. +""" +import argparse +import errno +import fcntl +import os +from pathlib import Path +import pty +import re +import select +import shutil +import subprocess +import tempfile +import termios +import time + +ROOT = Path(__file__).resolve().parent.parent +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("--open-workaround", type=Path, + help="development-only guest interposer for testing with an older server") +args = parser.parse_args() + + +def wait_for(predicate, description, timeout=15): + end = time.monotonic() + timeout + while time.monotonic() < end: + if predicate(): + return + time.sleep(0.02) + raise RuntimeError("timed out: " + description) + + +class Guest: + def __init__(self, env, delay): + master, slave = pty.openpty() + self.master = master + self.anchor = delay is None + self.output = "" + self.log_path = Path(env["DPREFIX"]).parent / f"guest-{time.monotonic_ns()}.log" + command = ["darling", "shell"] + if args.open_workaround: + command += ["env", "DYLD_INSERT_LIBRARIES=/Volumes/SystemRoot" + + str(args.open_workaround.resolve())] + if delay is None: + command += ["sh", "-c", "printf 'ANCHOR_READY\\n'; exec sleep 120"] + else: + command += ["/Volumes/SystemRoot" + str(ROOT / "build/riot-power-notify-check"), str(delay), "30"] + def terminal(): + os.setsid() + fcntl.ioctl(0, termios.TIOCSCTTY, 0) + self.process = subprocess.Popen(command, cwd="/tmp", env=env, stdin=slave, + stdout=slave, stderr=slave, preexec_fn=terminal) + os.close(slave) + + def read(self): + while select.select([self.master], [], [], 0)[0]: + try: + data = os.read(self.master, 65536) + except OSError as error: + if error.errno == errno.EIO: + break + raise + if not data: + break + self.output += data.decode(errors="replace") + self.log_path.write_text(self.output) + return self.output + + def ready(self): + output = self.read() + if self.anchor: + if self.process.poll() is not None: + raise RuntimeError("anchor launcher exited:\n" + output[-2500:]) + return re.search(r"(?m)^ANCHOR_READY\r?$", output) is not None + if "POWER_NOTIFY FAIL" in output or (self.process.poll() is not None and "READY " not in output): + raise RuntimeError("guest registration failed:\n" + output[-2500:]) + return "READY " in output + + def finish(self): + def finished(): + self.read() + return self.process.poll() is not None + wait_for(finished, "guest exit") + output = self.read() + if self.process.returncode or "POWER_NOTIFY PASS" not in output: + raise RuntimeError("guest failed:\n" + output[-2500:]) + + def close(self): + # Closing the controlling terminal also releases a privileged launcher. + os.close(self.master) + try: + self.process.wait(timeout=3) + except subprocess.TimeoutExpired: + try: + self.process.terminate() + self.process.wait(timeout=2) + except (PermissionError, subprocess.TimeoutExpired): + print("Launcher did not exit; fixture retained:", self.log_path, flush=True) + + +# Darling creates a root-owned overlay workdir. Keep fixture files as diagnostics; +# recursive deletion without root would mask the actual test result. +def main(): + directory = Path(tempfile.mkdtemp(prefix="power-integration-", dir=ROOT / "build")) + temp = str(directory) + print("Fixture:", directory, flush=True) + prefix = directory / "prefix" + fake = subprocess.Popen([str(ROOT / "build/riot-logind-sleep-check"), "--fake-logind", temp], + stdout=subprocess.DEVNULL) + guests = [] + env = dict(os.environ, DPREFIX=str(prefix), EGL_PLATFORM="x11") + # Do not pass caller-injected libraries or frameworks into the fixture. + for name in ("DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH", "DYLD_FRAMEWORK_PATH"): + env.pop(name, None) + def command(value): + descriptor = os.open(directory / "control", os.O_WRONLY | os.O_NONBLOCK) + try: + os.write(descriptor, value.encode()) + finally: + os.close(descriptor) + + try: + wait_for(lambda: (directory / "address").exists(), "fake logind startup") + env["DBUS_SYSTEM_BUS_ADDRESS"] = (directory / "address").read_text().strip() + + # Let the launcher initialize the empty prefix before adding overrides. + subprocess.run(["darling", "shell", "true"], cwd="/tmp", env=env, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=45, check=True) + subprocess.run(["darling", "shutdown"], env=env, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, timeout=10, check=True) + (prefix / "usr/sbin").mkdir(parents=True, exist_ok=True) + shutil.copy2(ROOT / "build/darling-src/build/src/external/iokitd/iokitd", prefix / "usr/sbin/iokitd") + + def records(): + return (directory / "records").read_text() + + def new_guest(delay): + guest = Guest(env, delay) + guests.append(guest) + try: + wait_for(guest.ready, "IORegisterForSystemPower", timeout=40) + except RuntimeError: + print(guest.read()[-3000:], flush=True) + raise + return guest + + def current_lock(): + matches = re.findall(r"^Inhibit\t(\d+)\t", records(), re.M) + return int(matches[-1]) if matches else 0 + + def released(lock): + match = re.search(rf"^Released\t{lock}\t(-?\d+)$", records(), re.M) + return int(match[1]) if match else None + + # The bootstrap run can itself have taken and released an inhibitor. + # Wait for a NEW lock from the anchor; an old record is not readiness. + bootstrap_lock = current_lock() + anchor = Guest(env, None) + guests.append(anchor) + wait_for(anchor.ready, "anchor shell startup", timeout=40) + wait_for(lambda: current_lock() > bootstrap_lock, "anchor delay inhibitor") + initial_lock = current_lock() + + # Two independent clients: wait for the slower acknowledgement. + first, second = new_guest(-3), new_guest(0) + wait_for(lambda: current_lock() > 0, "initial delay inhibitor") + lock = current_lock() + command("S") + time.sleep(0.4) + assert released(lock) is None, "inhibitor released before both clients acknowledged" + wait_for(lambda: released(lock) is not None, "both acknowledgements") + assert 1100 <= released(lock) < 3500, records() + command("W") + second.finish() + wait_for(lambda: current_lock() > lock, "new inhibitor after resume") + print("POWER_INTEGRATION multiple_clients=PASS", flush=True) + + # A client that does not acknowledge cannot hold up sleep indefinitely. + lock = current_lock() + command("S") + time.sleep(0.5) + assert released(lock) is None, "inhibitor deadline was bypassed" + wait_for(lambda: released(lock) is not None, "acknowledgement timeout", timeout=7) + assert 3500 <= released(lock) < 5500, records() + command("W") + wait_for(lambda: current_lock() > lock, "inhibitor after timeout/resume") + print("POWER_INTEGRATION timeout=PASS", flush=True) + + # Dropping the last notification reference releases its pending ack. + lock = current_lock() + command("S") + first.finish() + wait_for(lambda: released(lock) is not None, "deregistered client's acknowledgement") + assert 0 <= released(lock) < 2500, records() + command("W") + wait_for(lambda: current_lock() > lock, "inhibitor after deregistration") + print("POWER_INTEGRATION deregister=PASS", flush=True) + + # No clients remain: stale registrations must not force a timeout. + lock = current_lock() + command("S") + wait_for(lambda: released(lock) is not None, "sleep with no clients") + assert 0 <= released(lock) < 1000, records() + command("W") + wait_for(lambda: current_lock() > lock, "inhibitor after empty cycle") + assert current_lock() == initial_lock + 4, "iokitd restarted or acquired duplicate inhibitors:\n" + records() + print("POWER_INTEGRATION no_clients=PASS", flush=True) + print("POWER_INTEGRATION PASS" + (" (empty-OOL test workaround enabled)" if args.open_workaround else "")) + finally: + subprocess.run(["darling", "shutdown"], env=env, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, timeout=10) + for guest in guests: + guest.close() + if (directory / "control").exists(): + try: + command("Q") + except OSError: + pass + try: + fake.wait(timeout=5) + except subprocess.TimeoutExpired: + fake.terminate() + fake.wait(timeout=5) + + +if __name__ == "__main__": + main() diff --git a/tools/riot-power-notify-check.c b/tools/riot-power-notify-check.c new file mode 100644 index 0000000..64a3d17 --- /dev/null +++ b/tools/riot-power-notify-check.c @@ -0,0 +1,208 @@ +/* Guest probe for system sleep/wake notifications (iokitd IOPMrootDomain). + * + * Registers the way Chromium's power monitor does (IORegisterForSystemPower, + * then the notification port's run loop source), acknowledges + * kIOMessageSystemWillSleep with IOAllowPowerChange after ACK_DELAY_MS and + * waits until kIOMessageSystemHasPoweredOn or TIMEOUT_S. Sleep and wake are + * triggered from the host (a fake logind, see tools/riot-logind-sleep-check.c). + * + * riot-power-notify-check [ACK_DELAY_MS [TIMEOUT_S]] + * ACK_DELAY_MS: >=0 acknowledges after that delay; -1 never acknowledges; + * -2 deregisters on sleep; -3 runs the integration suite's three cycles + * (delayed acknowledgement, timeout, deregistration). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static io_connect_t root_port; +static io_object_t notifier; +static int deregistered, suite, completed_cycles; +static int ack_delay_ms, failures, step; +static int got_will_sleep, got_will_power_on, got_has_powered_on; +static IOReturn ack_result = -1; + +#define CHECK(cond, ...) do { if (!(cond)) { failures++; printf("FAIL: "); printf(__VA_ARGS__); \ + printf("\n"); } } while (0) + +static double now_s(void) { + struct timeval tv; + gettimeofday(&tv, NULL); + return tv.tv_sec + tv.tv_usec / 1e6; +} + +static const char *message_name(natural_t type) { + switch (type) { + case kIOMessageCanSystemSleep: return "CanSystemSleep"; + case kIOMessageSystemWillSleep: return "SystemWillSleep"; + case kIOMessageSystemWillNotSleep: return "SystemWillNotSleep"; + case kIOMessageSystemWillPowerOn: return "SystemWillPowerOn"; + case kIOMessageSystemHasPoweredOn: return "SystemHasPoweredOn"; + default: return "other"; + } +} + +static void callback(void *refcon, io_service_t service, natural_t type, void *argument) { + CHECK(refcon == &root_port, "refcon %p", refcon); + printf("MESSAGE %.3f %s (0x%x) argument=0x%lx service=%s\n", now_s(), message_name(type), type, + (unsigned long) (uintptr_t) argument, service ? "set" : "null"); + fflush(stdout); + switch (type) { + case kIOMessageCanSystemSleep: + IOAllowPowerChange(root_port, (intptr_t) argument); + break; + case kIOMessageSystemWillSleep: + CHECK(step == 0, "WillSleep out of order (step %d)", step); + step = 1; + got_will_sleep++; + if (ack_delay_ms == -2 || (suite && completed_cycles == 2)) { + CHECK(IODeregisterForSystemPower(¬ifier) == kIOReturnSuccess, "deregister during sleep"); + deregistered = 1; + CFRunLoopStop(CFRunLoopGetCurrent()); + break; + } + if ((ack_delay_ms < 0 && !suite) || (suite && completed_cycles == 1)) break; + int delay = suite ? 1200 : ack_delay_ms; + if (delay) + usleep(delay * 1000); + ack_result = IOAllowPowerChange(root_port, (intptr_t) argument); + printf("ACK %.3f IOAllowPowerChange=0x%x\n", now_s(), ack_result); + fflush(stdout); + break; + case kIOMessageSystemWillPowerOn: + CHECK(argument == NULL, "WillPowerOn argument must be NULL"); + CHECK(step == 1, "WillPowerOn out of order (step %d)", step); + step = 2; + got_will_power_on++; + break; + case kIOMessageSystemHasPoweredOn: + CHECK(argument == NULL, "HasPoweredOn argument must be NULL"); + CHECK(step == 2, "HasPoweredOn out of order (step %d)", step); + completed_cycles++; + step = suite ? 0 : 3; + got_has_powered_on++; + CFRunLoopStop(CFRunLoopGetCurrent()); + break; + } +} + +int main(int argc, char **argv) { + IONotificationPortRef port = NULL; + io_object_t dead_notifier = IO_OBJECT_NULL; + double timeout = 60; + + if (argc > 1) ack_delay_ms = atoi(argv[1]); + if (argc > 2) timeout = atof(argv[2]); + suite = ack_delay_ms == -3; + + /* The registry entries IORegisterForSystemPower relies on. */ + io_registry_entry_t entry = IORegistryEntryFromPath(kIOMasterPortDefault, + kIOPowerPlane ":/IOPowerConnection/IOPMrootDomain"); + io_service_t matched = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPMrootDomain")); + io_name_t class_name = ""; + if (entry) IOObjectGetClass(entry, class_name); + CHECK(entry != IO_OBJECT_NULL && !strcmp(class_name, "IOPMrootDomain"), "IOPower path: entry=%u class=%s", + entry, class_name); + CHECK(matched != IO_OBJECT_NULL, "IOServiceMatching(IOPMrootDomain) found nothing"); + if (entry) { + // Failed MIG requests must release the wake send right exactly once. + IONotificationPortRef rejected = IONotificationPortCreate(kIOMasterPortDefault); + io_object_t unused = IO_OBJECT_NULL; + kern_return_t error = IOServiceAddInterestNotification(rejected, entry, "UnsupportedInterest", + callback, &root_port, &unused); + CHECK(error != KERN_SUCCESS && unused == IO_OBJECT_NULL, "unsupported interest succeeded"); + mach_port_status_t status; + mach_msg_type_number_t count = MACH_PORT_RECEIVE_STATUS_COUNT; + CHECK(mach_port_get_attributes(mach_task_self(), IONotificationPortGetMachPort(rejected), + MACH_PORT_RECEIVE_STATUS, (mach_port_info_t)&status, &count) == KERN_SUCCESS && + !status.mps_srights, "failed registration leaked a wake send right"); + IONotificationPortDestroy(rejected); + if (suite) { + // A retained notifier with a destroyed destination must not become + // an outstanding sleep acknowledgement. + IONotificationPortRef dead = IONotificationPortCreate(kIOMasterPortDefault); + CHECK(IOServiceAddInterestNotification(dead, entry, kIOAppPowerStateInterest, + callback, &root_port, &dead_notifier) == KERN_SUCCESS, "dead-port fixture registration"); + IONotificationPortDestroy(dead); + } + } + if (entry) IOObjectRelease(entry); + if (matched) IOObjectRelease(matched); + + root_port = IORegisterForSystemPower(&root_port, &port, callback, ¬ifier); + printf("REGISTER root_port=%u port=%s notifier=%u\n", root_port, port ? "set" : "null", notifier); + CHECK(root_port != IO_OBJECT_NULL && port != NULL && notifier != IO_OBJECT_NULL, "IORegisterForSystemPower failed"); + if (root_port == IO_OBJECT_NULL || port == NULL) { + /* Repeat the steps of IORegisterForSystemPower to show which one fails. */ + io_service_t domain = IORegistryEntryFromPath(kIOMasterPortDefault, + kIOPowerPlane ":/IOPowerConnection/IOPMrootDomain"); + io_connect_t connect = IO_OBJECT_NULL; + IONotificationPortRef diag_port = IONotificationPortCreate(kIOMasterPortDefault); + kern_return_t open_kr = IOServiceOpen(domain, mach_task_self(), 0, &connect); + kern_return_t interest_kr = IOServiceAddInterestNotification(diag_port, domain, kIOAppPowerStateInterest, + callback, &root_port, ¬ifier); + printf("DIAG domain=%u IOServiceOpen=0x%x connect=%u IOServiceAddInterestNotification=0x%x\n", + domain, open_kr, connect, interest_kr); + printf("POWER_NOTIFY FAIL failures=%d\n", failures); + return 1; + } + + /* Selectors other than allow/cancel are refused cleanly. */ + uint64_t scalar = 0; + kern_return_t kr = IOConnectCallScalarMethod(root_port, 99, &scalar, 1, NULL, NULL); + CHECK(kr != KERN_SUCCESS, "unknown root domain selector succeeded"); + /* A stale acknowledgement is accepted and ignored. */ + CHECK(IOAllowPowerChange(root_port, 0x12345) == kIOReturnSuccess, "stale IOAllowPowerChange failed"); + + CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(port), kCFRunLoopCommonModes); + if (suite) { + // Fill the notification queue, then keep it full through the host's first + // PrepareForSleep. The power event must be retried without losing order. + int filled = 0; + for (int i = 0; i < 64; i++) { + mach_msg_header_t filler = {0}; + filler.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0); + filler.msgh_remote_port = IONotificationPortGetMachPort(port); + filler.msgh_size = sizeof(filler); + filler.msgh_id = 42; + kern_return_t sent = mach_msg(&filler, MACH_SEND_MSG | MACH_SEND_TIMEOUT, + sizeof(filler), 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); + if (sent == MACH_SEND_TIMED_OUT) break; + CHECK(sent == KERN_SUCCESS, "cannot fill notification queue: 0x%x", sent); + if (sent != KERN_SUCCESS) break; + filled++; + } + CHECK(filled > 0, "notification queue not filled"); + printf("CONGESTED queued=%d\n", filled); + } + printf("READY %.3f ack_delay_ms=%d\n", now_s(), ack_delay_ms); + fflush(stdout); + + if (suite) usleep(500000); + double end = now_s() + timeout; + while (step < 3 && !deregistered && now_s() < end) + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, false); + + CHECK(got_will_sleep == (suite ? 3 : 1), "WillSleep received %d times", got_will_sleep); + if (ack_delay_ms >= 0 || suite) + CHECK(ack_result == kIOReturnSuccess, "IOAllowPowerChange returned 0x%x", ack_result); + if (!deregistered || suite) { + CHECK(got_will_power_on == (suite ? 2 : 1), "WillPowerOn received %d times", got_will_power_on); + CHECK(got_has_powered_on == (suite ? 2 : 1), "HasPoweredOn received %d times", got_has_powered_on); + } + + CHECK(IODeregisterForSystemPower(¬ifier) == kIOReturnSuccess && notifier == IO_OBJECT_NULL, + "IODeregisterForSystemPower"); + CHECK(IOServiceClose(root_port) == KERN_SUCCESS, "IOServiceClose"); + IONotificationPortDestroy(port); + if (dead_notifier) IOObjectRelease(dead_notifier); + + printf("POWER_NOTIFY %s failures=%d\n", failures ? "FAIL" : "PASS", failures); + return failures ? 1 : 0; +} diff --git a/upstream/manifest.json b/upstream/manifest.json index d3df87a..b4dfd50 100644 --- a/upstream/manifest.json +++ b/upstream/manifest.json @@ -124,11 +124,17 @@ "revision": "5549ac4cd2db923c256f016a1381b3bfb716730a", "repository": "https://github.com/darlinghq/darling-iokitd.git", "patch": "patches/iokitd.patch", - "modified_files": 8, + "modified_files": 12, "new_files": [ - "src/PowerSources.mm", + "src/IOInterestNotification.h", + "src/IOInterestNotification.mm", + "src/IOPMrootDomain.h", + "src/IOPMrootDomain.mm", "src/IOPlatformExpertDevice.h", - "src/IOPlatformExpertDevice.mm" + "src/IOPlatformExpertDevice.mm", + "src/LogindSleep.c", + "src/LogindSleep.h", + "src/PowerSources.mm" ] }, {