mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-27 19:54:38 +00:00
Every install reported the same IOPlatformUUID and an Apple-format serial (C02ZL0GVMD6N) that may belong to a real MacBook Pro. Derive both from a SHA-256 of the host machine-id and a fixed app string: stable per host, distinct between hosts, and the raw machine-id is not exposed. The serial is DARLING-<8 hex>, a form no Mac uses. iokitd is built without ARC, so the derived strings are owned for the daemon's lifetime. Fix the header comment that said the values came from DMI. Refs #24 Co-Authored-By: Claude Opus 5.5 <[email protected]>
3118 lines
95 KiB
Diff
3118 lines
95 KiB
Diff
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
|
index 84161fd..5b23944 100644
|
|
--- a/CMakeLists.txt
|
|
+++ b/CMakeLists.txt
|
|
@@ -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,13 @@ 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/HostPowerInhibit.c
|
|
src/IOSurfaceRoot.mm
|
|
src/IOSurface.mm
|
|
src/IOUserClient.mm
|
|
diff --git a/src/IORegistryEntry.mm b/src/IORegistryEntry.mm
|
|
index 50c10fe..bd501cf 100644
|
|
--- a/src/IORegistryEntry.mm
|
|
+++ b/src/IORegistryEntry.mm
|
|
@@ -513,3 +513,86 @@ kern_return_t is_io_registry_entry_get_property_bin
|
|
return kr;
|
|
}
|
|
|
|
+
|
|
+kern_return_t is_io_registry_entry_get_property_bin_buf
|
|
+(
|
|
+ mach_port_t registry_entry,
|
|
+ io_name_t plane,
|
|
+ io_name_t property_name,
|
|
+ uint32_t options,
|
|
+ mach_vm_address_t buf,
|
|
+ mach_vm_size_t *bufsize,
|
|
+ io_buf_ptr_t *properties,
|
|
+ mach_msg_type_number_t *propertiesCnt
|
|
+)
|
|
+{
|
|
+ IORegistryEntry* e = dynamic_cast<IORegistryEntry*>(IOObject::lookup(registry_entry));
|
|
+ if (!e)
|
|
+ return kIOReturnBadArgument;
|
|
+
|
|
+ *propertiesCnt = 0;
|
|
+ *properties = nullptr;
|
|
+
|
|
+ NSString* propName = [NSString stringWithUTF8String: property_name];
|
|
+ id value = nil;
|
|
+
|
|
+ if ((options & kIORegistryIterateRecursively) && plane[0])
|
|
+ value = [findKey(e, plane, propName) objectForKey: propName];
|
|
+ else
|
|
+ value = [e->getProperties() objectForKey: propName];
|
|
+
|
|
+ if (!value)
|
|
+ return kIOReturnNotFound;
|
|
+
|
|
+ // Unlike io_registry_entry_get_property_bin, the reply carries the value itself.
|
|
+ // The caller's buffer lives in another task, so always answer out-of-line, which the
|
|
+ // protocol allows when the result does not go into 'buf'.
|
|
+ CFDataRef data = IOCFSerialize((CFTypeRef) value, kIOCFSerializeToBinary);
|
|
+ if (!data)
|
|
+ return kIOReturnNoMemory;
|
|
+
|
|
+ *propertiesCnt = CFDataGetLength(data);
|
|
+ kern_return_t kr = vm_allocate(mach_task_self(), (vm_address_t*) properties, *propertiesCnt, true);
|
|
+
|
|
+ if (kr == kIOReturnSuccess)
|
|
+ memcpy(*properties, CFDataGetBytePtr(data), *propertiesCnt);
|
|
+ else
|
|
+ *propertiesCnt = 0;
|
|
+
|
|
+ CFRelease(data);
|
|
+ return kr;
|
|
+}
|
|
+
|
|
+kern_return_t is_io_registry_entry_get_properties_bin_buf
|
|
+(
|
|
+ mach_port_t registry_entry,
|
|
+ mach_vm_address_t buf,
|
|
+ mach_vm_size_t *bufsize,
|
|
+ io_buf_ptr_t *properties,
|
|
+ mach_msg_type_number_t *propertiesCnt
|
|
+)
|
|
+{
|
|
+ IORegistryEntry* e = dynamic_cast<IORegistryEntry*>(IOObject::lookup(registry_entry));
|
|
+ if (!e)
|
|
+ return kIOReturnBadArgument;
|
|
+
|
|
+ *propertiesCnt = 0;
|
|
+ *properties = nullptr;
|
|
+
|
|
+ // The caller's buffer lives in another task, so always answer out-of-line, which the
|
|
+ // protocol allows when the result does not go into 'buf'.
|
|
+ CFDataRef data = IOCFSerialize((CFTypeRef) e->getProperties(), kIOCFSerializeToBinary);
|
|
+ if (!data)
|
|
+ return kIOReturnNoMemory;
|
|
+
|
|
+ *propertiesCnt = CFDataGetLength(data);
|
|
+ kern_return_t kr = vm_allocate(mach_task_self(), (vm_address_t*) properties, *propertiesCnt, true);
|
|
+
|
|
+ if (kr == kIOReturnSuccess)
|
|
+ memcpy(*properties, CFDataGetBytePtr(data), *propertiesCnt);
|
|
+ else
|
|
+ *propertiesCnt = 0;
|
|
+
|
|
+ CFRelease(data);
|
|
+ return kr;
|
|
+}
|
|
diff --git a/src/IOSurfaceRoot.mm b/src/IOSurfaceRoot.mm
|
|
index d7e6fab..d860582 100644
|
|
--- a/src/IOSurfaceRoot.mm
|
|
+++ b/src/IOSurfaceRoot.mm
|
|
@@ -21,13 +21,17 @@
|
|
#include "iokitd.h"
|
|
#include "IOSurface.h"
|
|
#include "IODisplayConnectX11.h"
|
|
+#include <EGL/eglext.h>
|
|
#include <IOKit/IOCFUnserialize.h>
|
|
#include <stdexcept>
|
|
#include <iostream>
|
|
|
|
IOSurfaceRoot::IOSurfaceRoot()
|
|
{
|
|
- m_display = eglGetDisplay(IODisplayConnectX11::getDisplay());
|
|
+ // The native display is an Xlib Display. eglGetDisplay() would let Mesa
|
|
+ // guess the platform from EGL_PLATFORM, which Wayland sessions often set
|
|
+ // to "wayland"; Mesa then treats the Display as a wl_display and crashes.
|
|
+ m_display = eglGetPlatformDisplay(EGL_PLATFORM_X11_KHR, IODisplayConnectX11::getDisplay(), NULL);
|
|
|
|
if (m_display == EGL_NO_DISPLAY)
|
|
return;
|
|
diff --git a/src/PowerSources.mm b/src/PowerSources.mm
|
|
new file mode 100644
|
|
index 0000000..70ca9bd
|
|
--- /dev/null
|
|
+++ b/src/PowerSources.mm
|
|
@@ -0,0 +1,139 @@
|
|
+extern "C" {
|
|
+#include "powermanagementServer.h"
|
|
+}
|
|
+#import <Foundation/NSArray.h>
|
|
+#import <Foundation/NSDictionary.h>
|
|
+#import <Foundation/NSNumber.h>
|
|
+#import <Foundation/NSString.h>
|
|
+#import <Foundation/NSData.h>
|
|
+#import <Foundation/NSPropertyList.h>
|
|
+#include <fstream>
|
|
+#include <string>
|
|
+#include <cstring>
|
|
+#include <dirent.h>
|
|
+#include <mach/mach.h>
|
|
+#include <vector>
|
|
+
|
|
+// The guest has no /sys; the host's is exposed under the system root
|
|
+static const char* const kPowerSupplyDir = "/Volumes/SystemRoot/sys/class/power_supply";
|
|
+
|
|
+static bool readSysfs(const std::string& dir, const char* name, std::string& value)
|
|
+{
|
|
+ std::ifstream file(dir + "/" + name);
|
|
+ if (!file || !std::getline(file, value))
|
|
+ return false;
|
|
+ return true;
|
|
+}
|
|
+
|
|
+static bool readSysfsLong(const std::string& dir, const char* name, long long& value)
|
|
+{
|
|
+ std::string text;
|
|
+ if (!readSysfs(dir, name, text))
|
|
+ return false;
|
|
+ char* end = nullptr;
|
|
+ value = strtoll(text.c_str(), &end, 10);
|
|
+ return end != text.c_str();
|
|
+}
|
|
+
|
|
+static std::vector<std::string> powerSupplies(const char* wantedType)
|
|
+{
|
|
+ std::vector<std::string> result;
|
|
+ DIR* d = opendir(kPowerSupplyDir);
|
|
+ if (!d)
|
|
+ return result;
|
|
+
|
|
+ while (struct dirent* e = readdir(d))
|
|
+ {
|
|
+ if (e->d_name[0] == '.')
|
|
+ continue;
|
|
+ std::string dir = std::string(kPowerSupplyDir) + "/" + e->d_name;
|
|
+ std::string type;
|
|
+ if (readSysfs(dir, "type", type) && type == wantedType)
|
|
+ result.push_back(dir);
|
|
+ }
|
|
+ closedir(d);
|
|
+ return result;
|
|
+}
|
|
+
|
|
+static NSArray* copyInternalBatteries()
|
|
+{
|
|
+ bool onAC = false;
|
|
+ for (const std::string& ac : powerSupplies("Mains"))
|
|
+ {
|
|
+ long long online;
|
|
+ if (readSysfsLong(ac, "online", online) && online)
|
|
+ onAC = true;
|
|
+ }
|
|
+
|
|
+ NSMutableArray* sources = [NSMutableArray array];
|
|
+ for (const std::string& dir : powerSupplies("Battery"))
|
|
+ {
|
|
+ long long present = 1, capacity, energyNow, energyFull, powerNow, voltage;
|
|
+ std::string status;
|
|
+ readSysfsLong(dir, "present", present);
|
|
+ if (!present || !readSysfs(dir, "status", status) || !readSysfsLong(dir, "capacity", capacity))
|
|
+ continue;
|
|
+
|
|
+ NSMutableDictionary* ps = [NSMutableDictionary dictionary];
|
|
+ ps[@"Name"] = [NSString stringWithFormat: @"InternalBattery-%lu", (unsigned long) [sources count]];
|
|
+ ps[@"Type"] = @"InternalBattery";
|
|
+ ps[@"Transport Type"] = @"Internal";
|
|
+ ps[@"Power Source ID"] = @((int) [sources count]);
|
|
+ ps[@"Is Present"] = @YES;
|
|
+ ps[@"Current Capacity"] = @((int) capacity);
|
|
+ ps[@"Max Capacity"] = @100;
|
|
+ ps[@"Power Source State"] = onAC ? @"AC Power" : @"Battery Power";
|
|
+ ps[@"Is Charging"] = @(status == "Charging");
|
|
+ ps[@"Is Charged"] = @(status == "Full");
|
|
+
|
|
+ if (readSysfsLong(dir, "voltage_now", voltage))
|
|
+ ps[@"Voltage"] = @((int) (voltage / 1000)); // uV -> mV
|
|
+
|
|
+ // Remaining time in minutes from the energy and power draw reported by the host; -1 when unknown
|
|
+ long long minutes = -1;
|
|
+ if (readSysfsLong(dir, "power_now", powerNow) && powerNow > 0)
|
|
+ {
|
|
+ if (status == "Discharging" && readSysfsLong(dir, "energy_now", energyNow))
|
|
+ minutes = energyNow * 60 / powerNow;
|
|
+ else if (status == "Charging" && readSysfsLong(dir, "energy_now", energyNow) && readSysfsLong(dir, "energy_full", energyFull))
|
|
+ minutes = (energyFull - energyNow) * 60 / powerNow;
|
|
+ }
|
|
+ if (status == "Charging")
|
|
+ ps[@"Time to Full Charge"] = @((int) minutes);
|
|
+ else
|
|
+ ps[@"Time to Empty"] = @((int) minutes);
|
|
+
|
|
+ [sources addObject: ps];
|
|
+ }
|
|
+ return [sources copy];
|
|
+}
|
|
+
|
|
+kern_return_t _io_ps_copy_powersources_info
|
|
+(
|
|
+ mach_port_t server,
|
|
+ audit_token_t token,
|
|
+ int pstype,
|
|
+ vm_offset_t *powersources,
|
|
+ mach_msg_type_number_t *powersourcesCnt,
|
|
+ int *return_code
|
|
+)
|
|
+{
|
|
+ *powersources = 0;
|
|
+ *powersourcesCnt = 0;
|
|
+ *return_code = 0;
|
|
+
|
|
+ // The host only exposes internal batteries; there is no UPS data to report
|
|
+ NSArray* sources = copyInternalBatteries();
|
|
+ NSData* data = [NSPropertyListSerialization dataWithPropertyList: sources
|
|
+ format: NSPropertyListBinaryFormat_v1_0 options: 0 error: nil];
|
|
+ if (!data)
|
|
+ return KERN_FAILURE;
|
|
+
|
|
+ kern_return_t kr = vm_allocate(mach_task_self(), powersources, [data length], VM_FLAGS_ANYWHERE);
|
|
+ if (kr != KERN_SUCCESS)
|
|
+ return kr;
|
|
+
|
|
+ memcpy((void*) *powersources, [data bytes], [data length]);
|
|
+ *powersourcesCnt = [data length];
|
|
+ return KERN_SUCCESS;
|
|
+}
|
|
diff --git a/src/ServiceRegistry.h b/src/ServiceRegistry.h
|
|
index 34e74ad..0d0740b 100644
|
|
--- a/src/ServiceRegistry.h
|
|
+++ b/src/ServiceRegistry.h
|
|
@@ -32,6 +32,7 @@ public:
|
|
static ServiceRegistry* instance();
|
|
void registerService(IOService* service);
|
|
IOIterator* iteratorForMatchingServices(NSDictionary* criteria) const;
|
|
+ IOObject* firstMatchingService(NSDictionary* criteria) const;
|
|
private:
|
|
std::list<IOService*> m_registeredServices;
|
|
};
|
|
diff --git a/src/ServiceRegistry.mm b/src/ServiceRegistry.mm
|
|
index aaed046..7a190c3 100644
|
|
--- a/src/ServiceRegistry.mm
|
|
+++ b/src/ServiceRegistry.mm
|
|
@@ -24,6 +24,9 @@
|
|
#include <IOKit/IOReturn.h>
|
|
#include <os/log.h>
|
|
#include <stdexcept>
|
|
+#include <string>
|
|
+#include <cstring>
|
|
+#include <vector>
|
|
#include "IOIterator.h"
|
|
|
|
extern "C" {
|
|
@@ -49,6 +52,16 @@ IOIterator* ServiceRegistry::iteratorForMatchingServices(NSDictionary* criteria)
|
|
return new IOIterator(matching);
|
|
}
|
|
|
|
+IOObject* ServiceRegistry::firstMatchingService(NSDictionary* criteria) const
|
|
+{
|
|
+ for (IOService* service : m_registeredServices)
|
|
+ {
|
|
+ if (service->matches(criteria))
|
|
+ return service;
|
|
+ }
|
|
+ return nullptr;
|
|
+}
|
|
+
|
|
void ServiceRegistry::registerService(IOService* service)
|
|
{
|
|
m_registeredServices.push_back(service);
|
|
@@ -118,3 +131,91 @@ kern_return_t is_io_service_get_matching_services
|
|
// Old unsupported API
|
|
return kIOReturnUnsupported;
|
|
}
|
|
+
|
|
+static NSDictionary* unserializeMatching(const char* caller, io_struct_inband_t matching, mach_msg_type_number_t matchingCnt)
|
|
+{
|
|
+ CFStringRef errorString = nullptr;
|
|
+ CFTypeRef criteria = IOCFUnserializeBinary(matching, matchingCnt, nullptr, 0, &errorString);
|
|
+
|
|
+ if (!criteria)
|
|
+ throwCFStringException(CFSTR("cannot parse 'matching': %@"), errorString);
|
|
+
|
|
+ if (CFGetTypeID(criteria) != CFDictionaryGetTypeID())
|
|
+ {
|
|
+ CFRelease(criteria);
|
|
+ throw std::runtime_error(std::string(caller) + "(): dictionary expected");
|
|
+ }
|
|
+
|
|
+ return (NSDictionary*) criteria;
|
|
+}
|
|
+
|
|
+kern_return_t is_io_service_get_matching_service_bin
|
|
+(
|
|
+ mach_port_t master_port,
|
|
+ io_struct_inband_t matching,
|
|
+ mach_msg_type_number_t matchingCnt,
|
|
+ mach_port_t *service
|
|
+)
|
|
+{
|
|
+ *service = MACH_PORT_NULL;
|
|
+
|
|
+ try
|
|
+ {
|
|
+ NSDictionary* criteria = unserializeMatching("io_service_get_matching_service_bin", matching, matchingCnt);
|
|
+ IOObject* match = ServiceRegistry::instance()->firstMatchingService(criteria);
|
|
+ CFRelease(criteria);
|
|
+
|
|
+ if (!match)
|
|
+ return kIOReturnNotFound;
|
|
+
|
|
+ *service = match->port();
|
|
+ return kIOReturnSuccess;
|
|
+ }
|
|
+ catch (const std::exception& e)
|
|
+ {
|
|
+ os_log_error(OS_LOG_DEFAULT, "is_io_service_get_matching_service_bin: %s", e.what());
|
|
+ return kIOReturnBadArgument;
|
|
+ }
|
|
+}
|
|
+
|
|
+kern_return_t is_io_service_add_notification_bin_64
|
|
+(
|
|
+ mach_port_t master_port,
|
|
+ io_name_t notification_type,
|
|
+ io_struct_inband_t matching,
|
|
+ mach_msg_type_number_t matchingCnt,
|
|
+ mach_port_t wake_port,
|
|
+ io_async_ref64_t reference,
|
|
+ mach_msg_type_number_t referenceCnt,
|
|
+ mach_port_t *notification
|
|
+)
|
|
+{
|
|
+ *notification = MACH_PORT_NULL;
|
|
+
|
|
+ try
|
|
+ {
|
|
+ NSDictionary* criteria = unserializeMatching("io_service_add_notification_bin_64", matching, matchingCnt);
|
|
+ IOIterator* iterator;
|
|
+
|
|
+ // The registry is fixed once iokitd has started, so no service is ever published or
|
|
+ // terminated later. The notification object is the iterator of the services that match
|
|
+ // now (as in IOKit for the publish/matched types); termination types start out empty.
|
|
+ if (!strcmp(notification_type, "IOServicePublish") ||
|
|
+ !strcmp(notification_type, "IOServiceFirstMatch") ||
|
|
+ !strcmp(notification_type, "IOServiceMatched"))
|
|
+ iterator = ServiceRegistry::instance()->iteratorForMatchingServices(criteria);
|
|
+ else
|
|
+ iterator = new IOIterator(std::vector<IOObject*>());
|
|
+ CFRelease(criteria);
|
|
+
|
|
+ *notification = iterator->port();
|
|
+ iterator->releaseLater();
|
|
+
|
|
+ return kIOReturnSuccess;
|
|
+ }
|
|
+ catch (const std::exception& e)
|
|
+ {
|
|
+ os_log_error(OS_LOG_DEFAULT, "is_io_service_add_notification_bin_64: %s", e.what());
|
|
+ return kIOReturnBadArgument;
|
|
+ }
|
|
+}
|
|
diff --git a/src/main.mm b/src/main.mm
|
|
index 37ce9b8..e23aea0 100644
|
|
--- a/src/main.mm
|
|
+++ b/src/main.mm
|
|
@@ -32,6 +32,8 @@
|
|
#include "IODisplayConnectX11.h"
|
|
#include "PowerAssertions.h"
|
|
#include "IOSurfaceRoot.h"
|
|
+#include "IOPlatformExpertDevice.h"
|
|
+#include "IOPMrootDomain.h"
|
|
|
|
extern "C" {
|
|
#include "iokitmigServer.h"
|
|
@@ -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..c2d4063 100644
|
|
--- a/src/stubs-powermanagement.c
|
|
+++ b/src/stubs-powermanagement.c
|
|
@@ -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
|
|
}
|
|
|
|
|
|
-/* Routine io_ps_copy_powersources_info */
|
|
-
|
|
-kern_return_t _io_ps_copy_powersources_info
|
|
-(
|
|
- mach_port_t server,
|
|
- audit_token_t token,
|
|
- int pstype,
|
|
- vm_offset_t *powersources,
|
|
- mach_msg_type_number_t *powersourcesCnt,
|
|
- int *return_code
|
|
-)
|
|
-{
|
|
- STUB();
|
|
- return KERN_NOT_SUPPORTED;
|
|
-}
|
|
-
|
|
|
|
/* Routine io_ps_copy_chargelog */
|
|
|
|
diff --git a/src/stubs.c b/src/stubs.c
|
|
index 91d11a1..37551e1 100644
|
|
--- a/src/stubs.c
|
|
+++ b/src/stubs.c
|
|
@@ -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
|
|
}
|
|
|
|
|
|
-kern_return_t is_io_service_get_matching_service_bin
|
|
-(
|
|
- mach_port_t master_port,
|
|
- io_struct_inband_t matching,
|
|
- mach_msg_type_number_t matchingCnt,
|
|
- mach_port_t *service
|
|
-)
|
|
-{
|
|
- STUB();
|
|
- return KERN_NOT_SUPPORTED;
|
|
-}
|
|
-
|
|
kern_return_t is_io_service_match_property_table_bin
|
|
(
|
|
mach_port_t service,
|
|
@@ -682,22 +617,6 @@ kern_return_t is_io_service_add_notification_bin
|
|
return KERN_NOT_SUPPORTED;
|
|
}
|
|
|
|
-kern_return_t is_io_service_add_notification_bin_64
|
|
-(
|
|
- mach_port_t master_port,
|
|
- io_name_t notification_type,
|
|
- io_struct_inband_t matching,
|
|
- mach_msg_type_number_t matchingCnt,
|
|
- 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_registry_entry_from_path_ool
|
|
(
|
|
mach_port_t master_port,
|
|
@@ -736,31 +655,3 @@ kern_return_t is_io_device_tree_entry_exists_with_name
|
|
return KERN_NOT_SUPPORTED;
|
|
}
|
|
|
|
-kern_return_t is_io_registry_entry_get_properties_bin_buf
|
|
-(
|
|
- mach_port_t registry_entry,
|
|
- mach_vm_address_t buf,
|
|
- mach_vm_size_t *bufsize,
|
|
- io_buf_ptr_t *properties,
|
|
- mach_msg_type_number_t *propertiesCnt
|
|
-)
|
|
-{
|
|
- STUB();
|
|
- return KERN_NOT_SUPPORTED;
|
|
-}
|
|
-
|
|
-kern_return_t is_io_registry_entry_get_property_bin_buf
|
|
-(
|
|
- mach_port_t registry_entry,
|
|
- io_name_t plane,
|
|
- io_name_t property_name,
|
|
- uint32_t options,
|
|
- mach_vm_address_t buf,
|
|
- mach_vm_size_t *bufsize,
|
|
- io_buf_ptr_t *properties,
|
|
- mach_msg_type_number_t *propertiesCnt
|
|
-)
|
|
-{
|
|
- STUB();
|
|
- return KERN_NOT_SUPPORTED;
|
|
-}
|
|
diff --git a/src/IOPlatformExpertDevice.h b/src/IOPlatformExpertDevice.h
|
|
new file mode 100644
|
|
index 0000000..38ad635
|
|
--- /dev/null
|
|
+++ b/src/IOPlatformExpertDevice.h
|
|
@@ -0,0 +1,21 @@
|
|
+#ifndef IOKITD_IOPLATFORMEXPERTDEVICE_H
|
|
+#define IOKITD_IOPLATFORMEXPERTDEVICE_H
|
|
+
|
|
+#include "IOService.h"
|
|
+#include "ServiceRegistry.h"
|
|
+
|
|
+// The root platform device (what IOServiceMatching("IOPlatformExpertDevice") finds on macOS).
|
|
+// Its model strings name a fixed MacBookPro16,1. IOPlatformUUID and IOPlatformSerialNumber are
|
|
+// derived from the host machine-id, so they differ per host and stay stable across reinstalls.
|
|
+class IOPlatformExpertDevice : public IOService
|
|
+{
|
|
+public:
|
|
+ const char* className() const override;
|
|
+ NSDictionary* matchingDictionary() override;
|
|
+ NSDictionary* getProperties() override;
|
|
+ bool conformsTo(const char* className) override;
|
|
+
|
|
+ static void registerSelf(ServiceRegistry* targetServiceRegistry);
|
|
+};
|
|
+
|
|
+#endif
|
|
diff --git a/src/IOPlatformExpertDevice.mm b/src/IOPlatformExpertDevice.mm
|
|
new file mode 100644
|
|
index 0000000..e330569
|
|
--- /dev/null
|
|
+++ b/src/IOPlatformExpertDevice.mm
|
|
@@ -0,0 +1,93 @@
|
|
+#include "IOPlatformExpertDevice.h"
|
|
+#import <Foundation/NSString.h>
|
|
+#import <Foundation/NSData.h>
|
|
+#import <Foundation/NSArray.h>
|
|
+#import <Foundation/NSCharacterSet.h>
|
|
+#import <Foundation/NSDictionary.h>
|
|
+#include <CommonCrypto/CommonDigest.h>
|
|
+#include <uuid/uuid.h>
|
|
+#include <unistd.h>
|
|
+#include <cstring>
|
|
+
|
|
+// Platform expert strings are NUL-terminated data
|
|
+static NSData* dataForString(NSString* string)
|
|
+{
|
|
+ const char* utf8 = [string UTF8String];
|
|
+ return [NSData dataWithBytes: utf8 length: strlen(utf8) + 1];
|
|
+}
|
|
+
|
|
+const char* IOPlatformExpertDevice::className() const
|
|
+{
|
|
+ return "IOPlatformExpertDevice";
|
|
+}
|
|
+
|
|
+NSDictionary* IOPlatformExpertDevice::matchingDictionary()
|
|
+{
|
|
+ return @{
|
|
+ @"IOProviderClass": @"IOPlatformExpertDevice",
|
|
+ @"IOClass": @"IOPlatformExpertDevice",
|
|
+ };
|
|
+}
|
|
+
|
|
+// Per-host identifiers, stable like a real Mac's: a SHA-256 of the host machine-id and a fixed
|
|
+// string, so the raw machine-id (which machine-id(5) says not to expose) never reaches apps.
|
|
+// The serial's "DARLING-" form is longer than any Apple serial and cannot match a real Mac.
|
|
+static bool hostIdentifiers(NSString** uuid, NSString** serial)
|
|
+{
|
|
+ static const char app[] = "darling-lol IOPlatformExpertDevice";
|
|
+ uuid_t machineID;
|
|
+ struct timespec wait = { 0, 0 };
|
|
+ if (gethostuuid(machineID, &wait) != 0)
|
|
+ return false;
|
|
+
|
|
+ unsigned char input[sizeof(machineID) + sizeof(app)];
|
|
+ std::memcpy(input, machineID, sizeof(machineID));
|
|
+ std::memcpy(input + sizeof(machineID), app, sizeof(app));
|
|
+ unsigned char hash[CC_SHA256_DIGEST_LENGTH];
|
|
+ CC_SHA256(input, sizeof(input), hash);
|
|
+
|
|
+ hash[6] = (hash[6] & 0x0f) | 0x40; // version 4
|
|
+ hash[8] = (hash[8] & 0x3f) | 0x80; // RFC 4122 variant
|
|
+ uuid_string_t text;
|
|
+ uuid_unparse_upper(hash, text);
|
|
+ // Owned (not autoreleased): the caller keeps them in statics for the daemon's lifetime
|
|
+ *uuid = [[NSString alloc] initWithUTF8String: text];
|
|
+ *serial = [[NSString alloc] initWithFormat: @"DARLING-%02X%02X%02X%02X", hash[16], hash[17], hash[18], hash[19]];
|
|
+ return true;
|
|
+}
|
|
+
|
|
+NSDictionary* IOPlatformExpertDevice::getProperties()
|
|
+{
|
|
+ // MacBookPro16,1 model strings; the Linux DMI values describe the host hardware, not the Mac we emulate
|
|
+ NSMutableDictionary* properties = [NSMutableDictionary dictionaryWithDictionary: @{
|
|
+ @"manufacturer": dataForString(@"Apple Inc."),
|
|
+ @"model": dataForString(@"MacBookPro16,1"),
|
|
+ @"product-name": dataForString(@"MacBookPro16,1"),
|
|
+ @"board-id": dataForString(@"Mac-E1008331FDC96864"),
|
|
+ }];
|
|
+
|
|
+ static NSString* uuid;
|
|
+ static NSString* serial;
|
|
+ static bool known = hostIdentifiers(&uuid, &serial);
|
|
+ if (known)
|
|
+ {
|
|
+ properties[@"IOPlatformUUID"] = uuid;
|
|
+ properties[@"IOPlatformSerialNumber"] = serial;
|
|
+ }
|
|
+ return properties;
|
|
+}
|
|
+
|
|
+bool IOPlatformExpertDevice::conformsTo(const char* className)
|
|
+{
|
|
+ if (std::strcmp(className, "IOPlatformDevice") == 0)
|
|
+ return true;
|
|
+ return IOService::conformsTo(className);
|
|
+}
|
|
+
|
|
+void IOPlatformExpertDevice::registerSelf(ServiceRegistry* targetServiceRegistry)
|
|
+{
|
|
+ static IOPlatformExpertDevice* device = new IOPlatformExpertDevice;
|
|
+
|
|
+ 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 <http://www.gnu.org/licenses/>.
|
|
+*/
|
|
+
|
|
+#ifndef IOKITD_IOINTERESTNOTIFICATION_H
|
|
+#define IOKITD_IOINTERESTNOTIFICATION_H
|
|
+#include "IOObject.h"
|
|
+#include <IOKit/OSMessageNotification.h>
|
|
+#include <string>
|
|
+#include <deque>
|
|
+
|
|
+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<Message> 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 <http://www.gnu.org/licenses/>.
|
|
+*/
|
|
+
|
|
+#include "IOInterestNotification.h"
|
|
+#include "IOService.h"
|
|
+#include "iokitd.h"
|
|
+#include <IOKit/IOReturn.h>
|
|
+#include <os/log.h>
|
|
+#include <algorithm>
|
|
+#include <cstddef>
|
|
+#include <cstring>
|
|
+#include <new>
|
|
+
|
|
+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<InterestMessageHead*>(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<IOInterestNotification*>(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<IOInterestNotification*>(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<IOService*>(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 <http://www.gnu.org/licenses/>.
|
|
+*/
|
|
+
|
|
+#ifndef IOKITD_IOPMROOTDOMAIN_H
|
|
+#define IOKITD_IOPMROOTDOMAIN_H
|
|
+
|
|
+#include "IOUserClient.h"
|
|
+#include "ServiceRegistry.h"
|
|
+#include <map>
|
|
+
|
|
+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<uint32_t, IOInterestNotification*> 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..9b6d30b
|
|
--- /dev/null
|
|
+++ b/src/IOPMrootDomain.mm
|
|
@@ -0,0 +1,342 @@
|
|
+/*
|
|
+ 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 <http://www.gnu.org/licenses/>.
|
|
+*/
|
|
+
|
|
+#include "IOPMrootDomain.h"
|
|
+#include "IOInterestNotification.h"
|
|
+#include "LogindSleep.h"
|
|
+#include <IOKit/IOKitKeys.h>
|
|
+#include <IOKit/IOMessage.h>
|
|
+#include <IOKit/pwr_mgt/IOPMLibDefs.h>
|
|
+#include <dispatch/dispatch.h>
|
|
+#include <elfcalls.h>
|
|
+#include <os/log.h>
|
|
+#include <cstdlib>
|
|
+#include <cstring>
|
|
+#import <Foundation/NSString.h>
|
|
+
|
|
+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;
|
|
+}
|
|
+
|
|
+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(), ^{
|
|
+ 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), ^{
|
|
+ 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 (!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 <IOKit/IOKitLib.h>
|
|
#include <IOKit/IOKitLibPrivate.h>
|
|
+#include <list>
|
|
+#include <vector>
|
|
+
|
|
+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<IOInterestNotification*> 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<IOInterestNotification*> 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 <algorithm>
|
|
+#include <cstring>
|
|
+#include <os/log.h>
|
|
#import <Foundation/NSArray.h>
|
|
|
|
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<IOInterestNotification*> IOService::interests(const char* type) const
|
|
+{
|
|
+ std::vector<IOInterestNotification*> 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<IOService*>(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<IOUserClient*>(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 <http://www.gnu.org/licenses/>.
|
|
+*/
|
|
+
|
|
+#include "LogindSleep.h"
|
|
+#include <pthread.h>
|
|
+#include <stdarg.h>
|
|
+#include <stdio.h>
|
|
+#include <stdlib.h>
|
|
+#include <string.h>
|
|
+#include <time.h>
|
|
+
|
|
+#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..a161a68
|
|
--- /dev/null
|
|
+++ b/src/LogindSleep.h
|
|
@@ -0,0 +1,148 @@
|
|
+/*
|
|
+ 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 <http://www.gnu.org/licenses/>.
|
|
+*/
|
|
+
|
|
+/*
|
|
+ * 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 <stdint.h>
|
|
+
|
|
+#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;
|
|
+};
|
|
+
|
|
+/* 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. */
|
|
+ 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/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 <stdio.h>
|
|
+#include <stdlib.h>
|
|
+#include <string.h>
|
|
+#include <unistd.h>
|
|
+
|
|
+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<int, HeldAssertion> 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 <stdexcept>
|
|
#include <iostream>
|
|
#include <string>
|
|
+#include <climits>
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#include <bsm/libbsm.h>
|
|
#import <Foundation/NSString.h>
|
|
@@ -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<int, AppAssertions*> AppAssertions::m_instances;
|
|
static std::unordered_map<std::string, PowerAssertion*> 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 <X11/extensions/dpms.h>
|
|
#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;
|
|
}
|
|
}
|