From e554644d0d8e1c308fae2028badceb7159454af1 Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:05:30 +0200 Subject: [PATCH] Implement legacy IOKit power assertions with host idle inhibitors --- docs/TESTING.md | 44 +- patches/iokitd.patch | 723 +++++++++++++++++- tools/build-riot-power-assertion-check.py | 29 + tools/riot-logind-sleep-check.c | 82 +- tools/riot-power-assertion-check.c | 105 +++ .../riot-power-assertion-integration-check.py | 139 ++++ tools/riot-power-integration-check.py | 44 +- upstream/manifest.json | 6 +- 8 files changed, 1149 insertions(+), 23 deletions(-) create mode 100644 tools/build-riot-power-assertion-check.py create mode 100644 tools/riot-power-assertion-check.c create mode 100644 tools/riot-power-assertion-integration-check.py diff --git a/docs/TESTING.md b/docs/TESTING.md index 8733394..c39f4de 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -82,6 +82,7 @@ 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 \ + build/darling-src/src/external/iokitd/src/HostPowerInhibit.c \ $(pkg-config --cflags --libs libsystemd) -o build/riot-logind-sleep-check build/riot-logind-sleep-check ``` @@ -90,7 +91,7 @@ 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 +`LOGIND_SLEEP PASS checks=73 failures=0`. It also runs with `-fsanitize=address,undefined`. `IOServiceOpen` sends an empty out-of-line descriptor. Older Darling servers @@ -117,6 +118,47 @@ 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. +## Power-save assertions + +Chromium uses the legacy names `NoIdleSleepAssertion` and +`NoDisplaySleepAssertion` with `IOPMAssertionCreateWithName`. These share +backends with the modern `PreventUserIdleSystemSleep` and +`PreventUserIdleDisplaySleep` names. System-idle prevention uses a logind +`idle/block` inhibitor; display prevention additionally holds a desktop +`org.freedesktop.ScreenSaver.Inhibit` cookie. The existing X11 DPMS backend is +a fallback when desktop inhibition is unavailable, and restores the prior +DPMS enabled state. These assertions do not take a logind `sleep/block` lock. + +Create, retain, release, level On/Off and process-exit cleanup are supported. +The last enabled assertion releases the backend; retaining an assertion ID +does not add another backend hold. Unsupported types and nonzero assertion +timeouts return `kIOReturnUnsupported`, rather than silently creating an +indefinite inhibitor. Host-backend failures are returned to the caller. + +After the builds above: + +```sh +python3 tools/build-riot-power-assertion-check.py +python3 tools/riot-power-assertion-integration-check.py +``` + +Expected: `POWER_ASSERTION_INTEGRATION PASS`. The fixture uses private fake +logind and ScreenSaver services for both D-Bus addresses and a separate Darling +prefix. It checks legacy/modern aliases, multiple clients, retain/final release, +level changes, failed requests and cleanup of assertions left held by an exiting +process. It never contacts the real desktop's inhibitor services or launches +Riot. The native check above additionally verifies backend-denial and missing-bus +cleanup, and passes under AddressSanitizer/UndefinedBehaviorSanitizer. + +The assertion integration fails against the previously installed `iokitd` with +the reported `0xe00002c9` create error, and passes with the rebuilt service. +Only `iokitd` needs reinstalling for this follow-up, with Darling stopped: + +```sh +sudo install -m755 build/darling-src/build/src/external/iokitd/iokitd \ + /usr/local/libexec/darling/usr/sbin/iokitd +``` + ## 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/iokitd.patch b/patches/iokitd.patch index 1df128c..78a80bd 100644 --- a/patches/iokitd.patch +++ b/patches/iokitd.patch @@ -1,5 +1,5 @@ diff --git a/CMakeLists.txt b/CMakeLists.txt -index 84161fd..3f7fd2b 100644 +index 84161fd..5b23944 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,8 @@ include_directories( @@ -11,7 +11,7 @@ index 84161fd..3f7fd2b 100644 mig(iokitmig.defs) mig(powermanagement.defs) -@@ -36,7 +38,12 @@ set(iokitd_sources +@@ -36,7 +38,13 @@ set(iokitd_sources src/IODisplayConnect.mm src/IODisplayConnectX11.mm src/PowerAssertions.mm @@ -21,6 +21,7 @@ index 84161fd..3f7fd2b 100644 + src/IOPMrootDomain.mm + src/IOInterestNotification.mm + src/LogindSleep.c ++ src/HostPowerInhibit.c src/IOSurfaceRoot.mm src/IOSurface.mm src/IOUserClient.mm @@ -449,10 +450,36 @@ index 37ce9b8..e23aea0 100644 + IOPMrootDomain::registerSelf(registry); } diff --git a/src/stubs-powermanagement.c b/src/stubs-powermanagement.c -index a568ecc..929746c 100644 +index a568ecc..c2d4063 100644 --- a/src/stubs-powermanagement.c +++ b/src/stubs-powermanagement.c -@@ -368,22 +368,6 @@ kern_return_t _io_ps_release_pspowersource +@@ -131,25 +131,6 @@ kern_return_t _io_pm_last_wake_time + } + + +-/* Routine io_pm_assertion_set_properties */ +- +-kern_return_t _io_pm_assertion_set_properties +-( +- mach_port_t server, +- audit_token_t token, +- int assertion_id, +- vm_offset_t props, +- mach_msg_type_number_t propsCnt, +- int *disableAppSleep, +- int *enableAppSleep, +- int *enTrIntensity, +- int *return_code +-) +-{ +- STUB(); +- return KERN_NOT_SUPPORTED; +-} +- + /* Routine io_pm_assertion_copy_details */ + + kern_return_t _io_pm_assertion_copy_details +@@ -368,22 +349,6 @@ kern_return_t _io_ps_release_pspowersource } @@ -1261,10 +1288,10 @@ index 0000000..9f9fc95 +#endif diff --git a/src/IOPMrootDomain.mm b/src/IOPMrootDomain.mm new file mode 100644 -index 0000000..268e2f5 +index 0000000..9b6d30b --- /dev/null +++ b/src/IOPMrootDomain.mm -@@ -0,0 +1,333 @@ +@@ -0,0 +1,342 @@ +/* + This file is part of Darling. + @@ -1516,6 +1543,15 @@ index 0000000..268e2f5 + return false; +} + ++const struct iokitd_logind_api* iokitd_host_power_api(void) ++{ ++ static struct iokitd_logind_api api; ++ static bool available; ++ static dispatch_once_t once; ++ dispatch_once(&once, ^{ available = loadLogindApi(&api); }); ++ return available ? &api : nullptr; ++} ++ +static void logindWillSleep(void* context, uint64_t cycle) +{ + dispatch_async(dispatch_get_main_queue(), ^{ @@ -1534,17 +1570,17 @@ index 0000000..268e2f5 +{ + // 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; ++ const struct iokitd_logind_api* api = iokitd_host_power_api(); + static const struct iokitd_logind_callbacks callbacks = { logindWillSleep, logindDidWake }; + struct iokitd_logind* logind = nullptr; + -+ if (!loadLogindApi(&api)) ++ if (!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); ++ 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); @@ -2224,10 +2260,10 @@ index 0000000..1d6aa79 +} diff --git a/src/LogindSleep.h b/src/LogindSleep.h new file mode 100644 -index 0000000..49dac3c +index 0000000..a161a68 --- /dev/null +++ b/src/LogindSleep.h -@@ -0,0 +1,145 @@ +@@ -0,0 +1,148 @@ +/* + This file is part of Darling. + @@ -2336,6 +2372,9 @@ index 0000000..49dac3c + uint64_t ack_timeout_usec; +}; + ++/* Shared host symbol table. Only implemented in the guest iokitd bridge. */ ++const struct iokitd_logind_api* iokitd_host_power_api(void); ++ +struct iokitd_logind_callbacks +{ + /* Suspend is imminent. Call iokitd_logind_sleep_ready(cycle) once clients agreed. */ @@ -2373,3 +2412,665 @@ index 0000000..49dac3c +#endif + +#endif +diff --git a/src/HostPowerInhibit.c b/src/HostPowerInhibit.c +new file mode 100644 +index 0000000..cdcb933 +--- /dev/null ++++ b/src/HostPowerInhibit.c +@@ -0,0 +1,118 @@ ++/* Host power-save inhibitors. GPL-3.0-or-later, like iokitd. */ ++#include "HostPowerInhibit.h" ++#include ++#include ++#include ++#include ++ ++struct iokitd_power_inhibit { ++ struct iokitd_logind_api api; ++ int idle_fd; ++ void* screen_bus; ++ uint32_t screen_cookie; ++ int screen_held; ++}; ++ ++static int connect_bus(const struct iokitd_logind_api* api, const char* address, void** bus) ++{ ++ int r = api->sd_bus_new(bus); ++ if (r >= 0) r = api->sd_bus_set_address(*bus, address); ++ if (r >= 0) r = api->sd_bus_set_bus_client(*bus, 1); ++ /* Assertions are synchronous MIG calls; do not stall the IOKit queue for ++ * sd-bus's default 25-second service timeout. */ ++ if (r >= 0) r = api->sd_bus_set_method_call_timeout(*bus, 500000); ++ if (r >= 0) r = api->sd_bus_start(*bus); ++ return r; ++} ++ ++static int take_idle_lock(struct iokitd_power_inhibit* inhibitor) ++{ ++ const struct iokitd_logind_api* api = &inhibitor->api; ++ const char* address = api->getenv("DBUS_SYSTEM_BUS_ADDRESS"); ++ struct iokitd_bus_error error = {0}; ++ void *bus = NULL, *reply = NULL; ++ int fd = -1; ++ if (!address || !address[0]) address = IOKITD_LOGIND_DEFAULT_SYSTEM_BUS; ++ int r = connect_bus(api, address, &bus); ++ if (r >= 0) ++ r = api->sd_bus_call_method(bus, "org.freedesktop.login1", "/org/freedesktop/login1", ++ "org.freedesktop.login1.Manager", "Inhibit", &error, &reply, "ssss", ++ "idle", "Darling", "macOS application prevents automatic idle sleep", "block"); ++ if (r >= 0) r = api->sd_bus_message_read(reply, "h", &fd); ++ if (r >= 0) { ++ inhibitor->idle_fd = api->fcntl(fd, IOKITD_LINUX_F_DUPFD_CLOEXEC, 3); ++ if (inhibitor->idle_fd < 0) r = -IOKITD_LINUX_ENOMEM; ++ } ++ if (reply) api->sd_bus_message_unref(reply); ++ api->sd_bus_error_free(&error); ++ if (bus) api->sd_bus_flush_close_unref(bus); ++ return r; ++} ++ ++static int take_screen_lock(struct iokitd_power_inhibit* inhibitor) ++{ ++ const struct iokitd_logind_api* api = &inhibitor->api; ++ const char* address = api->getenv("DBUS_SESSION_BUS_ADDRESS"); ++ char fallback[512]; ++ if (!address || !address[0]) { ++ const char* runtime = api->getenv("XDG_RUNTIME_DIR"); ++ int length = runtime && runtime[0] ++ ? snprintf(fallback, sizeof(fallback), "unix:path=%s/bus", runtime) ++ : snprintf(fallback, sizeof(fallback), "unix:path=/run/user/%u/bus", (unsigned)getuid()); ++ if (length < 0 || (size_t)length >= sizeof(fallback)) return -IOKITD_LINUX_EINVAL; ++ address = fallback; ++ } ++ struct iokitd_bus_error error = {0}; ++ void* reply = NULL; ++ int r = connect_bus(api, address, &inhibitor->screen_bus); ++ if (r >= 0) ++ r = api->sd_bus_call_method(inhibitor->screen_bus, "org.freedesktop.ScreenSaver", ++ "/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver", "Inhibit", ++ &error, &reply, "ss", "Darling", "macOS application prevents display sleep"); ++ if (r >= 0) r = api->sd_bus_message_read(reply, "u", &inhibitor->screen_cookie); ++ if (r >= 0) inhibitor->screen_held = 1; ++ if (reply) api->sd_bus_message_unref(reply); ++ api->sd_bus_error_free(&error); ++ return r; ++} ++ ++int iokitd_power_inhibit_create(const struct iokitd_logind_api* api, int display, ++ struct iokitd_power_inhibit** result) ++{ ++ *result = NULL; ++ if (!api || !api->sd_bus_new || !api->sd_bus_set_address || !api->sd_bus_set_bus_client || ++ !api->sd_bus_set_method_call_timeout || !api->sd_bus_start || !api->sd_bus_call_method || ++ !api->sd_bus_message_read || !api->sd_bus_message_unref || !api->sd_bus_error_free || ++ !api->sd_bus_flush_close_unref || !api->fcntl || !api->close || !api->getenv) ++ return -IOKITD_LINUX_ENOSYS; ++ struct iokitd_power_inhibit* inhibitor = calloc(1, sizeof(*inhibitor)); ++ if (!inhibitor) return -IOKITD_LINUX_ENOMEM; ++ inhibitor->api = *api; ++ inhibitor->idle_fd = -1; ++ int r = take_idle_lock(inhibitor); ++ if (r >= 0 && display) r = take_screen_lock(inhibitor); ++ if (r < 0) { ++ iokitd_power_inhibit_destroy(inhibitor); ++ return r; ++ } ++ *result = inhibitor; ++ return 0; ++} ++ ++void iokitd_power_inhibit_destroy(struct iokitd_power_inhibit* inhibitor) ++{ ++ if (!inhibitor) return; ++ const struct iokitd_logind_api* api = &inhibitor->api; ++ if (inhibitor->screen_held) { ++ struct iokitd_bus_error error = {0}; ++ void* reply = NULL; ++ api->sd_bus_call_method(inhibitor->screen_bus, "org.freedesktop.ScreenSaver", ++ "/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver", "UnInhibit", ++ &error, &reply, "u", inhibitor->screen_cookie); ++ if (reply) api->sd_bus_message_unref(reply); ++ api->sd_bus_error_free(&error); ++ } ++ if (inhibitor->screen_bus) api->sd_bus_flush_close_unref(inhibitor->screen_bus); ++ if (inhibitor->idle_fd >= 0) api->close(inhibitor->idle_fd); ++ free(inhibitor); ++} +diff --git a/src/HostPowerInhibit.h b/src/HostPowerInhibit.h +new file mode 100644 +index 0000000..fed7fbe +--- /dev/null ++++ b/src/HostPowerInhibit.h +@@ -0,0 +1,18 @@ ++/* Host power-save inhibitors. GPL-3.0-or-later, like iokitd. */ ++#ifndef IOKITD_HOST_POWER_INHIBIT_H ++#define IOKITD_HOST_POWER_INHIBIT_H ++#include "LogindSleep.h" ++#ifdef __cplusplus ++extern "C" { ++#endif ++struct iokitd_power_inhibit; ++/* display=0 prevents automatic system idle sleep through logind. display=1 ++ * additionally inhibits the desktop ScreenSaver; it fails if that service is ++ * unavailable, allowing the caller to try a real X11 display backend. */ ++int iokitd_power_inhibit_create(const struct iokitd_logind_api* api, int display, ++ struct iokitd_power_inhibit** result); ++void iokitd_power_inhibit_destroy(struct iokitd_power_inhibit* inhibitor); ++#ifdef __cplusplus ++} ++#endif ++#endif +diff --git a/src/PowerAssertions.h b/src/PowerAssertions.h +index 1e4a975..3b0d416 100644 +--- a/src/PowerAssertions.h ++++ b/src/PowerAssertions.h +@@ -28,11 +28,11 @@ void initPM(); + class PowerAssertion + { + public: +- // How many AppAssertions instances reference this assertion (*not* a sum of references within all these AppAssertions instances) +- void addRef(); ++ // One backend hold per enabled assertion; retaining an ID does not add holds. ++ IOReturn addRef(); + void delRef(); + protected: +- virtual void activate() = 0; ++ virtual IOReturn activate() = 0; + virtual void deactivate() = 0; + private: + int m_numHoldingApps = 0; +@@ -44,15 +44,17 @@ private: + AppAssertions(int pid); + ~AppAssertions(); + public: +- static AppAssertions* get(int pid); ++ static AppAssertions* get(int pid, bool create = true); + + struct HeldAssertion + { + int refcount; + PowerAssertion* assertion; ++ bool enabled; + }; + HeldAssertion* getAssertion(int assertionId); +- int createAssertion(PowerAssertion* which); ++ IOReturn createAssertion(PowerAssertion* which, bool enabled, int* assertionId); ++ IOReturn setEnabled(HeldAssertion* assertion, bool enabled); + void killAssertion(int assertionId); + private: + std::unordered_map m_heldAssertions; +diff --git a/src/PowerAssertions.mm b/src/PowerAssertions.mm +index fa55d2f..810fe04 100644 +--- a/src/PowerAssertions.mm ++++ b/src/PowerAssertions.mm +@@ -19,9 +19,11 @@ + + #include "PowerAssertions.h" + #include "PowerAssertionsX11.h" ++#include "HostPowerInhibit.h" + #include + #include + #include ++#include + #include + #include + #import +@@ -30,34 +32,52 @@ extern "C" { + #include "powermanagementServer.h" + } + ++class PowerAssertionPreventIdleSleep : public PowerAssertion ++{ ++ struct iokitd_power_inhibit* m_host = nullptr; ++ IOReturn activate() override ++ { ++ const auto* api = iokitd_host_power_api(); ++ return api && iokitd_power_inhibit_create(api, 0, &m_host) == 0 ++ ? kIOReturnSuccess : kIOReturnNotReady; ++ } ++ void deactivate() override ++ { ++ iokitd_power_inhibit_destroy(m_host); ++ m_host = nullptr; ++ } ++}; ++ + std::unordered_map AppAssertions::m_instances; + static std::unordered_map g_assertionKinds; + + void initPM() + { +- NSString* str = (NSString*) kIOPMAssertPreventUserIdleDisplaySleep; +- g_assertionKinds.insert(std::make_pair([str UTF8String], new PowerAssertionPreventDisplaySleep)); ++ // Legacy and modern spellings share one backend and one hold count. ++ static PowerAssertionPreventIdleSleep idle; ++ static PowerAssertionPreventDisplaySleep display; ++ g_assertionKinds = { ++ {[(NSString*)kIOPMAssertionTypeNoIdleSleep UTF8String], &idle}, ++ {[(NSString*)kIOPMAssertPreventUserIdleSystemSleep UTF8String], &idle}, ++ {[(NSString*)kIOPMAssertionTypeNoDisplaySleep UTF8String], &display}, ++ {[(NSString*)kIOPMAssertPreventUserIdleDisplaySleep UTF8String], &display}, ++ }; + } + +-static PowerAssertion* getByType(NSString* type) +-{ +- auto it = g_assertionKinds.find([type UTF8String]); +- if (it != g_assertionKinds.end()) +- return it->second; +- return nullptr; +-} +- +-void PowerAssertion::addRef() ++IOReturn PowerAssertion::addRef() + { + if (m_numHoldingApps == 0) +- activate(); ++ { ++ IOReturn result = activate(); ++ if (result != kIOReturnSuccess) return result; ++ } + m_numHoldingApps++; ++ return kIOReturnSuccess; + } + + void PowerAssertion::delRef() + { +- m_numHoldingApps--; +- if (m_numHoldingApps == 0) ++ if (m_numHoldingApps > 0 && --m_numHoldingApps == 0) + deactivate(); + } + +@@ -66,37 +86,31 @@ AppAssertions::AppAssertions(int pid) + m_processSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, pid, DISPATCH_PROC_EXIT, dispatch_get_main_queue()); + if (!m_processSource) + throw std::runtime_error("dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC) failed"); +- +- if (m_processSource) +- { +- dispatch_source_set_event_handler(m_processSource, ^{ +- m_instances.erase(pid); +- delete this; +- }); +- dispatch_resume(m_processSource); +- } ++ dispatch_source_set_event_handler(m_processSource, ^{ ++ m_instances.erase(pid); ++ delete this; ++ }); ++ dispatch_resume(m_processSource); + } + + AppAssertions::~AppAssertions() + { +- for (auto& [assertionId, ha] : m_heldAssertions) +- ha.assertion->delRef(); +- +- if (m_processSource != nullptr) +- dispatch_release(m_processSource); ++ for (auto& item : m_heldAssertions) ++ if (item.second.enabled) item.second.assertion->delRef(); ++ dispatch_source_cancel(m_processSource); ++ dispatch_release(m_processSource); + } + +-AppAssertions* AppAssertions::get(int pid) ++AppAssertions* AppAssertions::get(int pid, bool create) + { + auto it = m_instances.find(pid); +- if (it != m_instances.end()) +- return it->second; +- ++ if (it != m_instances.end()) return it->second; ++ if (!create) return nullptr; + try + { +- AppAssertions* aa = new AppAssertions(pid); +- m_instances.insert(std::make_pair(pid, aa)); +- return aa; ++ AppAssertions* app = new AppAssertions(pid); ++ m_instances.emplace(pid, app); ++ return app; + } + catch (const std::exception& e) + { +@@ -108,126 +122,168 @@ AppAssertions* AppAssertions::get(int pid) + AppAssertions::HeldAssertion* AppAssertions::getAssertion(int assertionId) + { + auto it = m_heldAssertions.find(assertionId); +- if (it != m_heldAssertions.end()) +- return &it->second; +- return nullptr; ++ return it == m_heldAssertions.end() ? nullptr : &it->second; + } + +-int AppAssertions::createAssertion(PowerAssertion* which) ++IOReturn AppAssertions::createAssertion(PowerAssertion* which, bool enabled, int* assertionId) + { ++ if (m_nextAssertionNumber == INT_MAX) return kIOReturnNoResources; ++ if (enabled) ++ { ++ IOReturn result = which->addRef(); ++ if (result != kIOReturnSuccess) return result; ++ } + int number = m_nextAssertionNumber++; +- HeldAssertion held = { 1, which }; +- m_heldAssertions.insert(std::make_pair(number, held)); +- which->addRef(); +- return number; ++ try { m_heldAssertions.emplace(number, HeldAssertion{1, which, enabled}); } ++ catch (...) ++ { ++ if (enabled) which->delRef(); ++ return kIOReturnNoMemory; ++ } ++ *assertionId = number; ++ return kIOReturnSuccess; ++} ++ ++IOReturn AppAssertions::setEnabled(HeldAssertion* held, bool enabled) ++{ ++ if (held->enabled == enabled) return kIOReturnSuccess; ++ if (enabled) ++ { ++ IOReturn result = held->assertion->addRef(); ++ if (result != kIOReturnSuccess) return result; ++ } ++ else held->assertion->delRef(); ++ held->enabled = enabled; ++ return kIOReturnSuccess; + } + + void AppAssertions::killAssertion(int assertionId) + { + auto it = m_heldAssertions.find(assertionId); +- if (it == m_heldAssertions.end()) +- return; +- +- it->second.assertion->delRef(); ++ if (it == m_heldAssertions.end()) return; ++ if (it->second.enabled) it->second.assertion->delRef(); + m_heldAssertions.erase(it); + } + +-kern_return_t _io_pm_assertion_create +-( +- mach_port_t server, +- audit_token_t token, +- vm_offset_t props, +- mach_msg_type_number_t propsCnt, +- int *assertion_id, +- int *disableAppSleep, +- int *enTrIntensity, +- int *return_code +-) ++// These MIG routines return their IOReturn in return_code and KERN_SUCCESS for ++// transport. Consequently they own (and consume) the request's OOL data even ++// when validation fails; dispatch_mig_server must not destroy it a second time. ++static CFDictionaryRef copyProperties(vm_offset_t props, mach_msg_type_number_t count) + { +- if (!props) +- return KERN_INVALID_ARGUMENT; +- +- CFDataRef data = CFDataCreateWithBytesNoCopy(nullptr, (const UInt8*) props, propsCnt, kCFAllocatorNull); +- CFDictionaryRef properties = (CFDictionaryRef) CFPropertyListCreateWithData(nullptr, data, kCFPropertyListImmutable, nullptr, nullptr); +- +- CFRelease(data); +- +- vm_deallocate(mach_task_self(), props, propsCnt); +- +- if (!properties) +- return KERN_INVALID_ARGUMENT; +- +- if (CFGetTypeID(properties) != CFDictionaryGetTypeID()) ++ CFDictionaryRef result = nullptr; ++ if (props && count) + { +- std::cerr << "Unexpected properties data type\n"; +- CFRelease(properties); +- return KERN_INVALID_ARGUMENT; ++ CFDataRef data = CFDataCreateWithBytesNoCopy(nullptr, (const UInt8*)props, count, kCFAllocatorNull); ++ if (data) ++ { ++ result = (CFDictionaryRef)CFPropertyListCreateWithData(nullptr, data, kCFPropertyListImmutable, nullptr, nullptr); ++ CFRelease(data); ++ } + } +- +- CFStringRef cftype = (CFStringRef) CFDictionaryGetValue(properties, kIOPMAssertionTypeKey); +- +- if (!cftype || CFStringGetTypeID() != CFGetTypeID(cftype)) ++ if (props) vm_deallocate(mach_task_self(), props, count); ++ if (result && CFGetTypeID(result) != CFDictionaryGetTypeID()) + { +- std::cerr << "kIOPMAssertionTypeKey is not a string\n"; +- CFRelease(properties); +- return KERN_INVALID_ARGUMENT; ++ CFRelease(result); ++ result = nullptr; + } ++ return result; ++} ++ ++static bool assertionLevel(CFDictionaryRef properties, bool* enabled) ++{ ++ CFTypeRef number = CFDictionaryGetValue(properties, kIOPMAssertionLevelKey); ++ if (!number) return true; ++ int level; ++ if (CFGetTypeID(number) != CFNumberGetTypeID() || ++ !CFNumberGetValue((CFNumberRef)number, kCFNumberIntType, &level) || ++ (level != kIOPMAssertionLevelOff && level != kIOPMAssertionLevelOn)) return false; ++ *enabled = level == kIOPMAssertionLevelOn; ++ return true; ++} + +- PowerAssertion* pa = getByType((NSString*) cftype); +- if (pa == nullptr) ++kern_return_t _io_pm_assertion_create(mach_port_t server, audit_token_t token, ++ vm_offset_t props, mach_msg_type_number_t propsCnt, int* assertion_id, ++ int* disableAppSleep, int* enTrIntensity, int* return_code) ++{ ++ *assertion_id = kIOPMNullAssertionID; ++ *disableAppSleep = 0; ++ *enTrIntensity = -1; ++ *return_code = kIOReturnBadArgument; ++ CFDictionaryRef properties = copyProperties(props, propsCnt); ++ if (!properties) return KERN_SUCCESS; ++ CFTypeRef type = CFDictionaryGetValue(properties, kIOPMAssertionTypeKey); ++ bool enabled = true; ++ CFTypeRef timeout = CFDictionaryGetValue(properties, kIOPMAssertionTimeoutKey); ++ double seconds = 0; ++ if (timeout && (CFGetTypeID(timeout) != CFNumberGetTypeID() || ++ !CFNumberGetValue((CFNumberRef)timeout, kCFNumberDoubleType, &seconds))) ++ *return_code = kIOReturnBadArgument; ++ else if (seconds != 0) ++ // Timed assertions need expiration scheduling. Do not silently turn ++ // a timed request into an indefinite host inhibition. ++ *return_code = kIOReturnUnsupported; ++ else if (type && CFGetTypeID(type) == CFStringGetTypeID() && assertionLevel(properties, &enabled)) + { +- std::cerr << "Unsupported assertion type\n"; +- CFRelease(properties); +- return KERN_INVALID_ARGUMENT; ++ auto it = g_assertionKinds.find([(NSString*)type UTF8String]); ++ if (it == g_assertionKinds.end()) *return_code = kIOReturnUnsupported; ++ else ++ { ++ pid_t callerPid; ++ audit_token_to_au32(token, nullptr, nullptr, nullptr, nullptr, nullptr, &callerPid, nullptr, nullptr); ++ AppAssertions* app = AppAssertions::get(callerPid); ++ *return_code = app ? app->createAssertion(it->second, enabled, assertion_id) : kIOReturnNoResources; ++ } + } ++ CFRelease(properties); ++ return KERN_SUCCESS; ++} + +- pid_t callerPid; +- audit_token_to_au32(token, nullptr, nullptr, nullptr, nullptr, nullptr, &callerPid, nullptr, nullptr); +- +- // std::cout << "Creating assertion for PID " << callerPid << std::endl; +- *assertion_id = AppAssertions::get(callerPid)->createAssertion(pa); +- *return_code = 0; +- +- return KERN_SUCCESS; +-} +- +-kern_return_t _io_pm_assertion_retain_release +-( +- mach_port_t server, +- audit_token_t token, +- int assertion_id, +- int action, +- int *retainCnt, +- int *disableAppSleep, +- int *enableAppSleep, +- int *return_code +-) ++kern_return_t _io_pm_assertion_retain_release(mach_port_t server, audit_token_t token, ++ int assertion_id, int action, int* retainCnt, int* disableAppSleep, ++ int* enableAppSleep, int* return_code) + { ++ *retainCnt = *disableAppSleep = *enableAppSleep = 0; ++ *return_code = kIOReturnNotFound; + pid_t callerPid; + audit_token_to_au32(token, nullptr, nullptr, nullptr, nullptr, nullptr, &callerPid, nullptr, nullptr); +- +- AppAssertions* appAssertions = AppAssertions::get(callerPid); +- AppAssertions::HeldAssertion* ha = appAssertions->getAssertion(assertion_id); +- +- if (!ha) +- { +- std::cerr << "_io_pm_assertion_retain_release() cannot find assertion " << assertion_id << std::endl; +- return KERN_INVALID_ARGUMENT; +- } +- +- *return_code = 0; ++ AppAssertions* app = AppAssertions::get(callerPid, false); ++ auto* held = app ? app->getAssertion(assertion_id) : nullptr; ++ if (!held) return KERN_SUCCESS; + if (action == kIOPMAssertionMIGDoRetain) + { +- *retainCnt = ++ha->refcount; ++ if (held->refcount == INT_MAX) { *return_code = kIOReturnNoResources; return KERN_SUCCESS; } ++ *retainCnt = ++held->refcount; + } + else if (action == kIOPMAssertionMIGDoRelease) + { +- *retainCnt = --ha->refcount; +- if (ha->refcount == 0) +- appAssertions->killAssertion(assertion_id); ++ *retainCnt = --held->refcount; ++ if (!held->refcount) app->killAssertion(assertion_id); + } +- else +- return KERN_INVALID_ARGUMENT; ++ else { *return_code = kIOReturnBadArgument; return KERN_SUCCESS; } ++ *return_code = kIOReturnSuccess; ++ return KERN_SUCCESS; ++} + +- return KERN_NOT_SUPPORTED; ++kern_return_t _io_pm_assertion_set_properties(mach_port_t server, audit_token_t token, ++ int assertion_id, vm_offset_t props, mach_msg_type_number_t propsCnt, ++ int* disableAppSleep, int* enableAppSleep, int* enTrIntensity, int* return_code) ++{ ++ *disableAppSleep = *enableAppSleep = 0; ++ *enTrIntensity = -1; ++ *return_code = kIOReturnBadArgument; ++ CFDictionaryRef properties = copyProperties(props, propsCnt); ++ if (!properties) return KERN_SUCCESS; ++ bool enabled = true; ++ if (CFDictionaryGetCount(properties) != 1 || !CFDictionaryContainsKey(properties, kIOPMAssertionLevelKey)) ++ *return_code = kIOReturnUnsupported; ++ else if (assertionLevel(properties, &enabled)) ++ { ++ pid_t callerPid; ++ audit_token_to_au32(token, nullptr, nullptr, nullptr, nullptr, nullptr, &callerPid, nullptr, nullptr); ++ AppAssertions* app = AppAssertions::get(callerPid, false); ++ auto* held = app ? app->getAssertion(assertion_id) : nullptr; ++ *return_code = held ? app->setEnabled(held, enabled) : kIOReturnNotFound; ++ } ++ CFRelease(properties); ++ return KERN_SUCCESS; + } +diff --git a/src/PowerAssertionsX11.h b/src/PowerAssertionsX11.h +index 2905edd..3b4ea10 100644 +--- a/src/PowerAssertionsX11.h ++++ b/src/PowerAssertionsX11.h +@@ -20,12 +20,16 @@ + #ifndef POWERASSERTIONSX11_H + #define POWERASSERTIONSX11_H + #include "PowerAssertions.h" ++#include "HostPowerInhibit.h" + + class PowerAssertionPreventDisplaySleep : public PowerAssertion + { + protected: +- void activate() override; ++ IOReturn activate() override; + void deactivate() override; ++private: ++ struct iokitd_power_inhibit* m_host = nullptr; ++ bool m_changedDPMS = false; + }; + + #endif +diff --git a/src/PowerAssertionsX11.mm b/src/PowerAssertionsX11.mm +index 7a6fcfa..26a7d68 100644 +--- a/src/PowerAssertionsX11.mm ++++ b/src/PowerAssertionsX11.mm +@@ -26,28 +26,37 @@ + #include + #undef BOOL + +-void PowerAssertionPreventDisplaySleep::activate() ++IOReturn PowerAssertionPreventDisplaySleep::activate() + { +- Display* display = IODisplayConnectX11::getDisplay(); +- if (!display) +- return; ++ const auto* api = iokitd_host_power_api(); ++ if (api && iokitd_power_inhibit_create(api, 1, &m_host) == 0) ++ return kIOReturnSuccess; + ++ // Desktop inhibition is preferred on Wayland; retain the real X11 ++ // fallback when no ScreenSaver service is available. ++ Display* display = IODisplayConnectX11::getDisplay(); + int eventBase, errorBase; +- if (DPMSQueryExtension(display, &eventBase, &errorBase)) +- { +- DPMSDisable(display); +- } ++ CARD16 power; ++ unsigned char enabled; ++ if (!display || !DPMSQueryExtension(display, &eventBase, &errorBase) || ++ !DPMSCapable(display) || !DPMSInfo(display, &power, &enabled)) ++ return kIOReturnNotReady; ++ if (api) ++ iokitd_power_inhibit_create(api, 0, &m_host); ++ m_changedDPMS = enabled; ++ if (m_changedDPMS) DPMSDisable(display); ++ XFlush(display); ++ return kIOReturnSuccess; + } + + void PowerAssertionPreventDisplaySleep::deactivate() + { +- Display* display = IODisplayConnectX11::getDisplay(); +- if (!display) +- return; +- +- int eventBase, errorBase; +- if (DPMSQueryExtension(display, &eventBase, &errorBase)) ++ iokitd_power_inhibit_destroy(m_host); ++ m_host = nullptr; ++ if (m_changedDPMS) + { +- DPMSEnable(display); ++ Display* display = IODisplayConnectX11::getDisplay(); ++ if (display) { DPMSEnable(display); XFlush(display); } ++ m_changedDPMS = false; + } + } diff --git a/tools/build-riot-power-assertion-check.py b/tools/build-riot-power-assertion-check.py new file mode 100644 index 0000000..360ea62 --- /dev/null +++ b/tools/build-riot-power-assertion-check.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Build the power-save assertion guest probe (IOPMAssertionCreateWithName).""" +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-assertion-check.c" +obj = root / "build/riot-power-assertion-check.o" +exe = root / "build/riot-power-assertion-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 index ac728ef..fb5b5e8 100644 --- a/tools/riot-logind-sleep-check.c +++ b/tools/riot-logind-sleep-check.c @@ -20,6 +20,7 @@ */ #define _GNU_SOURCE #include "LogindSleep.h" +#include "HostPowerInhibit.h" #include #include #include @@ -61,6 +62,8 @@ 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 int next_screen_cookie = 1, screen_denied; +static int screen_cookies[MAX_LOCKS]; static void record(const char *format, ...) __attribute__((format(printf, 1, 2))); static void record(const char *format, ...) { @@ -89,6 +92,27 @@ static int logind_handler(sd_bus_message *m, void *userdata, sd_bus_error *error close(p[1]); /* the reply carried its own copy */ return r; } + if (sd_bus_message_is_method_call(m, "org.freedesktop.ScreenSaver", "Inhibit")) { + const char *app, *reason; + if (screen_denied) { + record("ScreenDenied"); + return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_ACCESS_DENIED, "inhibition denied"); + } + if (sd_bus_message_read(m, "ss", &app, &reason) < 0 || next_screen_cookie >= MAX_LOCKS) + return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "bad screen inhibit"); + int cookie = next_screen_cookie++; + screen_cookies[cookie] = 1; + record("ScreenInhibit\t%d\t%s\t%s", cookie, app, reason); + return sd_bus_reply_method_return(m, "u", (uint32_t)cookie); + } + if (sd_bus_message_is_method_call(m, "org.freedesktop.ScreenSaver", "UnInhibit")) { + uint32_t cookie; + if (sd_bus_message_read(m, "u", &cookie) < 0 || cookie >= MAX_LOCKS || !screen_cookies[cookie]) + return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "unknown screen cookie"); + screen_cookies[cookie] = 0; + record("ScreenRelease\t%u", cookie); + return sd_bus_reply_method_return(m, ""); + } 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) @@ -121,6 +145,9 @@ static void run_fake(int control_fd, int ready_fd) { 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); + sd_bus_add_object(bus, NULL, "/org/freedesktop/ScreenSaver", logind_handler, NULL); + if (sd_bus_request_name(bus, "org.freedesktop.ScreenSaver", 0) < 0) + _exit(4); if (sd_bus_request_name(bus, "org.freedesktop.login1", 0) < 0) _exit(4); if (write(ready_fd, "R", 1) != 1) @@ -148,6 +175,7 @@ static void run_fake(int control_fd, int ready_fd) { break; if (c == 'S' || c == 'W') emit_prepare(bus, c == 'S'); + if (c == 'D' || c == 'A') { screen_denied = c == 'D'; record("ScreenDeny\t%d", screen_denied); } } for (int i = 0; i < lock_count; i++) { if (!(fds[2 + i].revents & (POLLHUP | POLLERR))) @@ -392,7 +420,8 @@ 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"); + const char *temporary = getenv("TMPDIR"); + snprintf(tmpdir, sizeof(tmpdir), "%s/riot-logind-sleep-XXXXXX", temporary ? temporary : "/tmp"); if (!mkdtemp(tmpdir)) { perror("mkdtemp"); return 2; } snprintf(records, sizeof(records), "%s/records", tmpdir); fclose(fopen(records, "w")); @@ -591,6 +620,57 @@ int main(int argc, char **argv) { iokitd_logind_stop(l); l = NULL; + /* Blocking assertions use idle/block, independently from sleep/delay. */ + setenv("DBUS_SYSTEM_BUS_ADDRESS", bus_address, 1); + setenv("DBUS_SESSION_BUS_ADDRESS", bus_address, 1); + api = native_api(0); + struct iokitd_power_inhibit *inhibit = NULL; + int lock_count_before = count_prefix("Inhibit\t"); + int release_count_before = count_prefix("Released\t"); + r = iokitd_power_inhibit_create(&api, 0, &inhibit); + CHECK(r == 0 && inhibit, "idle inhibit: create %d", r); + CHECK(wait_prefix("Inhibit\t", lock_count_before + 1, 1000), "idle inhibit: not called"); + CHECK(strstr(read_records(), "\tidle\tDarling\tmacOS application prevents automatic idle sleep\tblock") != NULL, + "idle inhibit: wrong arguments"); + iokitd_power_inhibit_destroy(inhibit); + inhibit = NULL; + CHECK(wait_prefix("Released\t", release_count_before + 1, 1000), "idle inhibit: not released"); + + lock_count_before = count_prefix("Inhibit\t"); + release_count_before = count_prefix("Released\t"); + r = iokitd_power_inhibit_create(&api, 1, &inhibit); + CHECK(r == 0 && inhibit, "display inhibit: create %d", r); + CHECK(wait_prefix("ScreenInhibit\t", 1, 1000), "display inhibit: screen not inhibited"); + CHECK(wait_prefix("Inhibit\t", lock_count_before + 1, 1000), "display inhibit: idle not inhibited"); + iokitd_power_inhibit_destroy(inhibit); + inhibit = NULL; + CHECK(wait_prefix("ScreenRelease\t", 1, 1000), "display inhibit: cookie not released"); + CHECK(wait_prefix("Released\t", release_count_before + 1, 1000), "display inhibit: idle not released"); + + command(control[1], 'D'); + CHECK(wait_prefix("ScreenDeny\t1", 1, 1000), "display denial: fake not ready"); + release_count_before = count_prefix("Released\t"); + r = iokitd_power_inhibit_create(&api, 1, &inhibit); + CHECK(r < 0 && !inhibit, "display denial: returned success"); + CHECK(wait_prefix("Released\t", release_count_before + 1, 1000), "display denial: leaked idle lock"); + command(control[1], 'A'); + + char missing_bus[400]; + snprintf(missing_bus, sizeof(missing_bus), "unix:path=%s/missing", tmpdir); + setenv("DBUS_SYSTEM_BUS_ADDRESS", missing_bus, 1); + r = iokitd_power_inhibit_create(&api, 0, &inhibit); + CHECK(r < 0 && !inhibit, "missing system bus: returned success"); + setenv("DBUS_SYSTEM_BUS_ADDRESS", bus_address, 1); + setenv("DBUS_SESSION_BUS_ADDRESS", missing_bus, 1); + release_count_before = count_prefix("Released\t"); + r = iokitd_power_inhibit_create(&api, 1, &inhibit); + CHECK(r < 0 && !inhibit, "missing session bus: returned success"); + CHECK(wait_prefix("Released\t", release_count_before + 1, 1000), "missing session bus: leaked idle lock"); + api.close = NULL; + r = iokitd_power_inhibit_create(&api, 0, &inhibit); + CHECK(r == -ENOSYS && !inhibit, "incomplete inhibitor api: %d", r); + iokitd_power_inhibit_destroy(NULL); + command(control[1], 'Q'); close(control[1]); waitpid(fake_pid, NULL, 0); diff --git a/tools/riot-power-assertion-check.c b/tools/riot-power-assertion-check.c new file mode 100644 index 0000000..dac7043 --- /dev/null +++ b/tools/riot-power-assertion-check.c @@ -0,0 +1,105 @@ +/* Standalone guest probe for Chromium's IOPM assertion create/retain/release API. + * The host integration fixture advances checkpoints and inspects fake inhibitors. + */ +#include +#include +#include +#include +#include +#include +#include + +static int failures; +#define CHECK(c, ...) do { if (!(c)) { failures++; printf("FAIL: "); printf(__VA_ARGS__); puts(""); } } while (0) + +static void checkpoint(const char* name) +{ + if (failures) { printf("POWER_ASSERTION FAIL failures=%d\n", failures); exit(1); } + printf("STAGE %s\n", name); + fflush(stdout); + if (getchar() == EOF) exit(2); +} + +static IOPMAssertionID create(CFStringRef type, IOPMAssertionLevel level) +{ + IOPMAssertionID id = kIOPMNullAssertionID; + IOReturn result = IOPMAssertionCreateWithName(type, level, CFSTR("Darling assertion regression"), &id); + CHECK(result == kIOReturnSuccess && id != kIOPMNullAssertionID, "create assertion: 0x%x id=%u", result, id); + return id; +} + +static void release(IOPMAssertionID id) +{ + IOReturn result = IOPMAssertionRelease(id); + CHECK(result == kIOReturnSuccess, "release assertion %u: 0x%x", id, result); +} + +static void level(IOPMAssertionID id, int value) +{ + CFNumberRef number = CFNumberCreate(NULL, kCFNumberIntType, &value); + IOReturn result = IOPMAssertionSetProperty(id, kIOPMAssertionLevelKey, number); + CFRelease(number); + CHECK(result == kIOReturnSuccess, "set assertion level %u to %d: 0x%x", id, value, result); +} + +int main(int argc, char** argv) +{ + setvbuf(stdout, NULL, _IONBF, 0); + if (argc == 2 && !strcmp(argv[1], "--exit-held")) { + create(kIOPMAssertionTypeNoIdleSleep, kIOPMAssertionLevelOn); + create(kIOPMAssertionTypeNoDisplaySleep, kIOPMAssertionLevelOn); + checkpoint("orphan-held"); + _exit(0); /* Deliberately do not release: iokitd must observe process exit. */ + } + + IOPMAssertionID invalid = kIOPMNullAssertionID; + IOReturn error = IOPMAssertionCreateWithName(CFSTR("UnsupportedRegressionType"), + kIOPMAssertionLevelOn, CFSTR("unsupported"), &invalid); + CHECK(error == kIOReturnUnsupported && invalid == kIOPMNullAssertionID, + "unsupported type: 0x%x id=%u", error, invalid); + + CFMutableDictionaryRef timed = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + int seconds = 5; + CFNumberRef timeout = CFNumberCreate(NULL, kCFNumberIntType, &seconds); + CFDictionarySetValue(timed, kIOPMAssertionTypeKey, kIOPMAssertionTypeNoIdleSleep); + CFDictionarySetValue(timed, kIOPMAssertionNameKey, CFSTR("unsupported timeout")); + CFDictionarySetValue(timed, kIOPMAssertionTimeoutKey, timeout); + invalid = kIOPMNullAssertionID; + error = IOPMAssertionCreateWithProperties(timed, &invalid); + CHECK(error == kIOReturnUnsupported && invalid == kIOPMNullAssertionID, + "unsupported timeout: 0x%x id=%u", error, invalid); + CFRelease(timeout); + CFRelease(timed); + + IOPMAssertionID legacy = create(kIOPMAssertionTypeNoIdleSleep, kIOPMAssertionLevelOn); + IOPMAssertionID modern = create(kIOPMAssertionTypePreventUserIdleSystemSleep, kIOPMAssertionLevelOn); + CHECK(legacy != modern, "assertion IDs must be distinct"); + IOPMAssertionRetain(legacy); + release(legacy); /* One retained reference remains. */ + release(modern); + checkpoint("idle-held"); + release(legacy); + CHECK(IOPMAssertionRelease(legacy) == kIOReturnNotFound, "double release not rejected"); + checkpoint("idle-released"); + + legacy = create(kIOPMAssertionTypeNoDisplaySleep, kIOPMAssertionLevelOn); + modern = create(kIOPMAssertionTypePreventUserIdleDisplaySleep, kIOPMAssertionLevelOn); + release(legacy); + checkpoint("display-held"); + level(modern, kIOPMAssertionLevelOff); + checkpoint("display-off"); + level(modern, kIOPMAssertionLevelOn); + checkpoint("display-on"); + release(modern); + checkpoint("display-released"); + + legacy = create(kIOPMAssertionTypeNoIdleSleep, kIOPMAssertionLevelOff); + checkpoint("idle-off"); + level(legacy, kIOPMAssertionLevelOn); + checkpoint("idle-on"); + release(legacy); + checkpoint("finished"); + printf("POWER_ASSERTION %s failures=%d\n", failures ? "FAIL" : "PASS", failures); + return failures ? 1 : 0; +} diff --git a/tools/riot-power-assertion-integration-check.py b/tools/riot-power-assertion-integration-check.py new file mode 100644 index 0000000..7cf38c3 --- /dev/null +++ b/tools/riot-power-assertion-integration-check.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Test IOPM assertions against fake logind/ScreenSaver in a disposable prefix.""" +import argparse +import importlib.util +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parent.parent +spec = importlib.util.spec_from_file_location("power_fixture", ROOT / "tools/riot-power-integration-check.py") +fixture = importlib.util.module_from_spec(spec) +spec.loader.exec_module(fixture) +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("--iokitd", type=Path, + default=ROOT / "build/darling-src/build/src/external/iokitd/iokitd") +args = parser.parse_args() + + +def main(): + directory = Path(tempfile.mkdtemp(prefix="assertion-integration-", dir=ROOT / "build")) + prefix = directory / "prefix" + print("Fixture:", directory, flush=True) + fake = subprocess.Popen([str(ROOT / "build/riot-logind-sleep-check"), "--fake-logind", str(directory)], + stdout=subprocess.DEVNULL) + env = dict(os.environ, DPREFIX=str(prefix), EGL_PLATFORM="x11") + for name in ("DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH", "DYLD_FRAMEWORK_PATH"): + env.pop(name, None) + guests = [] + + def records(): + return (directory / "records").read_text() + + def state(): + locks, screens = {}, set() + for line in records().splitlines(): + fields = line.split("\t") + if fields[0] == "Inhibit": locks[int(fields[1])] = (fields[2], fields[-1]) + elif fields[0] == "Released": locks.pop(int(fields[1]), None) + elif fields[0] == "ScreenInhibit": screens.add(int(fields[1])) + elif fields[0] == "ScreenRelease": screens.discard(int(fields[1])) + blocking = [what for what, mode in locks.values() if mode == "block"] + assert all(what == "idle" for what in blocking), "assertions must not block manual suspend" + return len(blocking), len(screens) + + def expect_state(idle, screens): + try: + fixture.wait_for(lambda: state() == (idle, screens), f"inhibitors idle={idle} screen={screens}") + except RuntimeError: + print(records(), flush=True) + raise + + def probe(*arguments): + guest = fixture.Guest(env, executable=ROOT / "build/riot-power-assertion-check", arguments=arguments) + guests.append(guest) + return guest + + def stage(guest, label): + def arrived(): + output = guest.read() + if "FAIL:" in output or guest.process.poll() is not None: + raise RuntimeError("assertion guest failed:\n" + output[-1800:]) + return re.search(rf"(?m)^STAGE {re.escape(label)}\r?$", output) is not None + fixture.wait_for(arrived, "assertion stage " + label) + + def advance(guest): + os.write(guest.master, b"\n") + + def finished(guest, marker=None): + def exited(): + guest.read() + return guest.process.poll() is not None + fixture.wait_for(exited, "assertion guest exit") + assert guest.process.returncode == 0, guest.output[-1800:] + if marker: assert marker in guest.output, guest.output[-1800:] + + try: + fixture.wait_for(lambda: (directory / "address").exists(), "fake power services") + address = (directory / "address").read_text().strip() + env["DBUS_SYSTEM_BUS_ADDRESS"] = address + env["DBUS_SESSION_BUS_ADDRESS"] = address + subprocess.run(["darling", "shell", "true"], cwd="/tmp", env=env, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=45, check=True) + fixture.stop_fixture_prefix(env) + (prefix / "usr/sbin").mkdir(parents=True, exist_ok=True) + shutil.copy2(args.iokitd, prefix / "usr/sbin/iokitd") + previous_locks = records().count("Inhibit\t") + anchor = fixture.Guest(env) + guests.append(anchor) + fixture.wait_for(anchor.ready, "anchor shell", timeout=40) + fixture.wait_for(lambda: records().count("Inhibit\t") > previous_locks, "anchor power monitor") + + main_guest = probe() + stage(main_guest, "idle-held") + expect_state(1, 0) + print("POWER_ASSERTION_INTEGRATION aliases_and_retain=PASS", flush=True) + + worker = probe("--exit-held") + stage(worker, "orphan-held") + expect_state(2, 1) # Both applications share the system-idle backend. + advance(worker) + finished(worker) + expect_state(1, 0) # Dead child's display assertion is removed; parent stays held. + print("POWER_ASSERTION_INTEGRATION process_exit=PASS", flush=True) + + advance(main_guest) + stage(main_guest, "idle-released") + expect_state(0, 0) + print("POWER_ASSERTION_INTEGRATION release=PASS", flush=True) + + for label, idle, screens in [("display-held", 1, 1), ("display-off", 0, 0), + ("display-on", 1, 1), ("display-released", 0, 0), + ("idle-off", 0, 0), ("idle-on", 1, 0), ("finished", 0, 0)]: + advance(main_guest) + stage(main_guest, label) + expect_state(idle, screens) + advance(main_guest) + finished(main_guest, "POWER_ASSERTION PASS failures=0") + print("POWER_ASSERTION_INTEGRATION display_and_levels=PASS", flush=True) + print("POWER_ASSERTION_INTEGRATION PASS", flush=True) + finally: + fixture.stop_fixture_prefix(env) + for guest in guests: guest.close() + try: + control = os.open(directory / "control", os.O_WRONLY | os.O_NONBLOCK) + os.write(control, b"Q") + os.close(control) + 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-integration-check.py b/tools/riot-power-integration-check.py index 46da5e6..9240fff 100644 --- a/tools/riot-power-integration-check.py +++ b/tools/riot-power-integration-check.py @@ -22,7 +22,7 @@ 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() +args = argparse.Namespace(open_workaround=None) def wait_for(predicate, description, timeout=15): @@ -34,18 +34,46 @@ def wait_for(predicate, description, timeout=15): raise RuntimeError("timed out: " + description) +def stop_fixture_prefix(env): + """Stop only this fixture's prefix and wait out Darling's asynchronous kill.""" + pid_file = Path(env["DPREFIX"]) / ".init.pid" + try: + pid_text = pid_file.read_text().strip() + pid = int(pid_text) + except (OSError, ValueError): + pid_text, pid = None, None + subprocess.run(["darling", "shutdown"], env=env, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, timeout=10) + if pid is not None: + def exited(): + try: + return Path(f"/proc/{pid}/stat").read_text().rsplit(")", 1)[1].split()[0] == "Z" + except FileNotFoundError: + return True + wait_for(exited, "fixture Darling shutdown", timeout=10) + # getInitProcess treats a zombie PID as live, although its mount namespace + # is already gone. Forget that exited PID before the next fixture launch. + try: + if pid_file.read_text().strip() == pid_text: + pid_file.unlink() + except FileNotFoundError: + pass + + class Guest: - def __init__(self, env, delay): + def __init__(self, env, delay=None, *, executable=None, arguments=()): master, slave = pty.openpty() self.master = master - self.anchor = delay is None + self.anchor = delay is None and executable 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: + if executable is not None: + command += ["/Volumes/SystemRoot" + str(Path(executable).resolve()), *arguments] + elif 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"] @@ -105,6 +133,8 @@ class Guest: # Darling creates a root-owned overlay workdir. Keep fixture files as diagnostics; # recursive deletion without root would mask the actual test result. def main(): + global args + args = parser.parse_args() directory = Path(tempfile.mkdtemp(prefix="power-integration-", dir=ROOT / "build")) temp = str(directory) print("Fixture:", directory, flush=True) @@ -130,8 +160,7 @@ def main(): # 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) + stop_fixture_prefix(env) (prefix / "usr/sbin").mkdir(parents=True, exist_ok=True) shutil.copy2(ROOT / "build/darling-src/build/src/external/iokitd/iokitd", prefix / "usr/sbin/iokitd") @@ -211,8 +240,7 @@ def main(): 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) + stop_fixture_prefix(env) for guest in guests: guest.close() if (directory / "control").exists(): diff --git a/upstream/manifest.json b/upstream/manifest.json index b4dfd50..016154d 100644 --- a/upstream/manifest.json +++ b/upstream/manifest.json @@ -124,7 +124,7 @@ "revision": "5549ac4cd2db923c256f016a1381b3bfb716730a", "repository": "https://github.com/darlinghq/darling-iokitd.git", "patch": "patches/iokitd.patch", - "modified_files": 12, + "modified_files": 16, "new_files": [ "src/IOInterestNotification.h", "src/IOInterestNotification.mm", @@ -134,7 +134,9 @@ "src/IOPlatformExpertDevice.mm", "src/LogindSleep.c", "src/LogindSleep.h", - "src/PowerSources.mm" + "src/PowerSources.mm", + "src/HostPowerInhibit.c", + "src/HostPowerInhibit.h" ] }, {