mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
Implement IOKit system sleep and wake notifications through logind
This commit is contained in:
1 parent
084e003f79
commit
e2f1392369
9 files changed
+2921
-17
No files matched your search
@@ -64,6 +64,59 @@ Expected: `warm_empty_allocations=0 single_packet_allocations=2`, correct creden
|
||||
|
||||
The same source passed AddressSanitizer/UndefinedBehaviorSanitizer and leak detection. Allocation counts depend on this fixture/compiler/library; preserve the no-wasted-slot property rather than interpreting them as game performance. See [setup](SETUP.md#updating-an-existing-runtime) for server build/install/activation.
|
||||
|
||||
## System sleep/wake notifications
|
||||
|
||||
The power monitor uses `IORegisterForSystemPower`, an `IOPMrootDomain` user
|
||||
client and standard IOKit interest messages. `iokitd` listens to logind's
|
||||
`PrepareForSleep` signals on the host system bus. A delay inhibitor stays open
|
||||
until every notified application acknowledges sleep or the deadline expires.
|
||||
Wake notifications arrive in `SystemWillPowerOn`, `SystemHasPoweredOn` order.
|
||||
Registration also works without logind, with no host events in that case.
|
||||
|
||||
Build the runtime and standalone probes:
|
||||
|
||||
```sh
|
||||
cmake --build build/darling-src/build --target darlingserver iokitd --parallel 3
|
||||
python3 tools/build-riot-power-notify-check.py
|
||||
cc -Wall -Wextra -Werror -pthread \
|
||||
-I build/darling-src/src/external/iokitd/src \
|
||||
tools/riot-logind-sleep-check.c \
|
||||
build/darling-src/src/external/iokitd/src/LogindSleep.c \
|
||||
$(pkg-config --cflags --libs libsystemd) -o build/riot-logind-sleep-check
|
||||
build/riot-logind-sleep-check
|
||||
```
|
||||
|
||||
The host check uses a private D-Bus daemon and fake logind. It covers absent
|
||||
services, delay-lock ownership, acknowledgement deadlines, stale
|
||||
acknowledgements, repeated sleep/wake, a logind restart, a full system-bus
|
||||
restart and a sleep signal during startup timeout discovery. Expected:
|
||||
`LOGIND_SLEEP PASS checks=57 failures=0`. It also runs with
|
||||
`-fsanitize=address,undefined`.
|
||||
|
||||
`IOServiceOpen` sends an empty out-of-line descriptor. Older Darling servers
|
||||
reject that descriptor with `0x400` before the request reaches `iokitd`.
|
||||
Install the rebuilt server with Darling stopped, using the server installer in
|
||||
[setup](SETUP.md#updating-an-existing-runtime), before running:
|
||||
|
||||
```sh
|
||||
python3 tools/riot-power-integration-check.py
|
||||
```
|
||||
|
||||
The integration check creates its own prefix, copies the built `iokitd` into
|
||||
it and connects it exclusively to fake logind. It never suspends the host,
|
||||
starts Riot or uses the normal Darling prefix. Expected: `POWER_INTEGRATION
|
||||
PASS`, covering a full notification queue, two clients, dead destinations,
|
||||
failed-registration cleanup, the acknowledgement timeout, deregistration during
|
||||
sleep and sleep with no clients. Fixture logs stay under
|
||||
`build/power-integration-*`; Darling's overlay work directory may require root
|
||||
to remove.
|
||||
|
||||
The integration check passed against the installed rebuilt server without an
|
||||
interposer. Its bootstrap run can also acquire an inhibitor, so readiness
|
||||
requires a new lock from the persistent launcher plus confirmation that its
|
||||
shell has started. Lock numbering is relative to that launcher, rather than
|
||||
assuming that the first recorded lock belongs to the test clients.
|
||||
|
||||
## Host URL hand-off regression
|
||||
|
||||
LaunchServices' D-Bus hand-off code (`LSHostOpenBus.c`) builds natively against the host's libsystemd. The check starts a private `dbus-daemon` with no service directories and a fake portal/systemd service, so nothing real is activated and no browser opens:
|
||||
|
||||
@@ -7,10 +7,10 @@ index 81ed8f7..3120abb 100644
|
||||
Boolean cfReusedPort = false;
|
||||
|
||||
+#ifdef DARLING
|
||||
+ /* Fail soft instead of crashing: without iokitd some registry/power
|
||||
+ * lookups never produce a port, and callers (e.g. Chromium's power
|
||||
+ * monitor) pass the NULL through unchecked. CFRunLoopAddSource
|
||||
+ * already ignores NULL sources, so returning NULL here is safe. */
|
||||
+ /* A notification port can still be absent when allocation fails or
|
||||
+ * the IOKit service is unavailable. Keep the accessor tolerant of
|
||||
+ * that failure; CFRunLoopAddSource already ignores NULL sources.
|
||||
+ * Normal power registration is implemented by iokitd. */
|
||||
+ if (!notify)
|
||||
+ return NULL;
|
||||
+#endif
|
||||
|
||||
@@ -1031,7 +1031,7 @@ index bbaab01..4f01234 100644
|
||||
};
|
||||
|
||||
diff --git a/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c b/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c
|
||||
index 7c2559b..712ff8f 100644
|
||||
index 7c2559b..202d5dc 100644
|
||||
--- a/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c
|
||||
+++ b/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c
|
||||
@@ -77,6 +77,7 @@
|
||||
@@ -1053,6 +1053,17 @@ index 7c2559b..712ff8f 100644
|
||||
mr = MACH_RCV_INVALID_DATA;
|
||||
size = 0;
|
||||
} else {
|
||||
@@ -3819,7 +3824,9 @@ ipc_kmsg_copyin_body(
|
||||
goto clean_message;
|
||||
}
|
||||
|
||||
- if ((size >= MSG_OOL_SIZE_SMALL) &&
|
||||
+ /* Darling has a zero small-copy threshold. Empty OOL regions
|
||||
+ * require no allocation and must not trip the overflow check. */
|
||||
+ if (size != 0 && (size >= MSG_OOL_SIZE_SMALL) &&
|
||||
(daddr->out_of_line.copy == MACH_MSG_PHYSICAL_COPY) &&
|
||||
!(daddr->out_of_line.deallocate)) {
|
||||
/*
|
||||
diff --git a/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c b/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c
|
||||
index 98ad438..221a1be 100644
|
||||
--- a/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c
|
||||
|
||||
+1767
-9
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the system sleep/wake notification guest probe (IORegisterForSystemPower)."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
config = root / "build/config.json"
|
||||
sdk = json.loads(config.read_text())["sdk"] if config.exists() else "/usr/local/libexec/darling"
|
||||
appkit = root / "build/darling-src/build/src/external/cocotron/AppKit"
|
||||
flags = {}
|
||||
for line in (appkit / "CMakeFiles/AppKit.dir/flags.make").read_text().splitlines():
|
||||
if line.startswith("C_") and " = " in line:
|
||||
key, value = line.split(" = ", 1)
|
||||
flags[key] = shlex.split(value)
|
||||
|
||||
source = root / "tools/riot-power-notify-check.c"
|
||||
obj = root / "build/riot-power-notify-check.o"
|
||||
exe = root / "build/riot-power-notify-check"
|
||||
subprocess.run(["clang", "-x", "objective-c", *flags["C_DEFINES"], *flags["C_INCLUDES"],
|
||||
*flags["C_FLAGS"], "-fblocks", "-c", str(source),
|
||||
"-o", str(obj)], check=True)
|
||||
subprocess.run(["clang", "-target", "x86_64-apple-macosx11.0",
|
||||
"-fuse-ld=lld", "-nostdlib", "-isysroot",
|
||||
sdk, str(obj), "-lSystem",
|
||||
"-framework", "IOKit", "-framework", "CoreFoundation",
|
||||
"-o", str(exe)],
|
||||
check=True)
|
||||
@@ -0,0 +1,608 @@
|
||||
/* Host-side regression for iokitd's logind sleep bridge (LogindSleep.c).
|
||||
*
|
||||
* Runs a private dbus-daemon as the "system bus" (DBUS_SYSTEM_BUS_ADDRESS)
|
||||
* and a fake org.freedesktop.login1 forked from this process. The fake hands
|
||||
* out a pipe for every Inhibit call and records when its write end is closed,
|
||||
* i.e. when the lock is released. The bridge is linked natively against
|
||||
* libsystemd. The real system bus and logind are never used.
|
||||
*
|
||||
* cc -std=gnu11 -O2 -Wall -Wextra -pthread \
|
||||
* -I build/darling-src/src/external/iokitd/src \
|
||||
* tools/riot-logind-sleep-check.c \
|
||||
* build/darling-src/src/external/iokitd/src/LogindSleep.c \
|
||||
* $(pkg-config --cflags --libs libsystemd) -o build/riot-logind-sleep-check
|
||||
* build/riot-logind-sleep-check
|
||||
*
|
||||
* With --fake-logind DIR it only runs the private bus and the fake logind for
|
||||
* a guest test: the bus address is written to DIR/address, commands are read
|
||||
* from the FIFO DIR/control ("S" PrepareForSleep(true), "W" PrepareForSleep(false),
|
||||
* "Q" quit) and events are appended to DIR/records.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include "LogindSleep.h"
|
||||
#include <systemd/sd-bus.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <stdarg.h>
|
||||
#include <signal.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static char tmpdir[256], records[300], bus_address[512];
|
||||
static int failures, checks;
|
||||
static pid_t daemon_pid, fake_pid, main_pid;
|
||||
|
||||
#define CHECK(cond, ...) do { checks++; if (!(cond)) { failures++; \
|
||||
fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); fprintf(stderr, __VA_ARGS__); \
|
||||
fputc('\n', stderr); } } while (0)
|
||||
|
||||
static uint64_t now_ms(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t) ts.tv_sec * 1000 + (uint64_t) ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
static void sleep_ms(unsigned ms) {
|
||||
struct timespec ts = { ms / 1000, (long) (ms % 1000) * 1000000L };
|
||||
while (nanosleep(&ts, &ts) < 0 && errno == EINTR) {}
|
||||
}
|
||||
|
||||
/* ---- fake logind (child process) ---- */
|
||||
|
||||
#define MAX_LOCKS 64
|
||||
static FILE *record_file;
|
||||
static int lock_fds[MAX_LOCKS], lock_ids[MAX_LOCKS], lock_count, next_lock_id = 1;
|
||||
static uint64_t sleep_started_ms;
|
||||
static int fake_sleeping;
|
||||
|
||||
static void record(const char *format, ...) __attribute__((format(printf, 1, 2)));
|
||||
static void record(const char *format, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, format);
|
||||
vfprintf(record_file, format, ap);
|
||||
va_end(ap);
|
||||
fputc('\n', record_file);
|
||||
fflush(record_file);
|
||||
}
|
||||
|
||||
static int logind_handler(sd_bus_message *m, void *userdata, sd_bus_error *error) {
|
||||
(void) userdata; (void) error;
|
||||
if (sd_bus_message_is_method_call(m, "org.freedesktop.login1.Manager", "Inhibit")) {
|
||||
const char *what, *who, *why, *mode;
|
||||
int p[2];
|
||||
if (sd_bus_message_read(m, "ssss", &what, &who, &why, &mode) < 0)
|
||||
return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "bad Inhibit arguments");
|
||||
if (lock_count == MAX_LOCKS || pipe2(p, O_CLOEXEC) < 0)
|
||||
return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_NO_MEMORY, "no more locks");
|
||||
int id = next_lock_id++;
|
||||
lock_fds[lock_count] = p[0];
|
||||
lock_ids[lock_count++] = id;
|
||||
record("Inhibit\t%d\t%s\t%s\t%s\t%s", id, what, who, why, mode);
|
||||
int r = sd_bus_reply_method_return(m, "h", p[1]);
|
||||
close(p[1]); /* the reply carried its own copy */
|
||||
return r;
|
||||
}
|
||||
if (sd_bus_message_is_method_call(m, "org.freedesktop.DBus.Properties", "Get")) {
|
||||
const char *iface, *name;
|
||||
if (sd_bus_message_read(m, "ss", &iface, &name) < 0)
|
||||
return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "bad Get arguments");
|
||||
record("Get\t%s\t%s", iface, name);
|
||||
if (!strcmp(iface, "org.freedesktop.login1.Manager") && !strcmp(name, "InhibitDelayMaxUSec"))
|
||||
return sd_bus_reply_method_return(m, "v", "t", (uint64_t) 5000000);
|
||||
return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_UNKNOWN_PROPERTY, "unknown property");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void emit_prepare(sd_bus *bus, int start) {
|
||||
if (start) {
|
||||
sleep_started_ms = now_ms();
|
||||
fake_sleeping = 1;
|
||||
} else {
|
||||
fake_sleeping = 0;
|
||||
}
|
||||
record(start ? "Sleep" : "Wake");
|
||||
sd_bus_emit_signal(bus, "/org/freedesktop/login1", "org.freedesktop.login1.Manager",
|
||||
"PrepareForSleep", "b", start);
|
||||
sd_bus_flush(bus);
|
||||
}
|
||||
|
||||
/* Serves until "Q" arrives on control_fd or the control pipe closes. */
|
||||
static void run_fake(int control_fd, int ready_fd) {
|
||||
sd_bus *bus = NULL;
|
||||
if (sd_bus_new(&bus) < 0 || sd_bus_set_address(bus, bus_address) < 0 ||
|
||||
sd_bus_set_bus_client(bus, 1) < 0 || sd_bus_start(bus) < 0)
|
||||
_exit(3);
|
||||
sd_bus_add_object(bus, NULL, "/org/freedesktop/login1", logind_handler, NULL);
|
||||
if (sd_bus_request_name(bus, "org.freedesktop.login1", 0) < 0)
|
||||
_exit(4);
|
||||
if (write(ready_fd, "R", 1) != 1)
|
||||
_exit(5);
|
||||
close(ready_fd);
|
||||
|
||||
for (;;) {
|
||||
while (sd_bus_process(bus, NULL) > 0) {}
|
||||
struct pollfd fds[2 + MAX_LOCKS];
|
||||
int n = 0;
|
||||
fds[n].fd = sd_bus_get_fd(bus);
|
||||
fds[n++].events = (short) sd_bus_get_events(bus);
|
||||
fds[n].fd = control_fd;
|
||||
fds[n++].events = POLLIN;
|
||||
for (int i = 0; i < lock_count; i++) {
|
||||
fds[n].fd = lock_fds[i];
|
||||
fds[n++].events = POLLIN;
|
||||
}
|
||||
if (poll(fds, n, 1000) < 0 && errno != EINTR)
|
||||
_exit(6);
|
||||
if (fds[1].revents & (POLLIN | POLLHUP)) {
|
||||
char c;
|
||||
ssize_t got = read(control_fd, &c, 1);
|
||||
if (got <= 0 || c == 'Q')
|
||||
break;
|
||||
if (c == 'S' || c == 'W')
|
||||
emit_prepare(bus, c == 'S');
|
||||
}
|
||||
for (int i = 0; i < lock_count; i++) {
|
||||
if (!(fds[2 + i].revents & (POLLHUP | POLLERR)))
|
||||
continue;
|
||||
long delay = fake_sleeping ? (long) (now_ms() - sleep_started_ms) : -1;
|
||||
record("Released\t%d\t%ld", lock_ids[i], delay);
|
||||
close(lock_fds[i]);
|
||||
lock_fds[i] = -1;
|
||||
}
|
||||
int kept = 0;
|
||||
for (int i = 0; i < lock_count; i++) {
|
||||
if (lock_fds[i] >= 0) {
|
||||
lock_fds[kept] = lock_fds[i];
|
||||
lock_ids[kept++] = lock_ids[i];
|
||||
}
|
||||
}
|
||||
lock_count = kept;
|
||||
}
|
||||
sd_bus_flush_close_unref(bus);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
static pid_t start_fake(int control_fd) {
|
||||
int ready[2];
|
||||
if (pipe(ready)) { perror("pipe"); exit(2); }
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) { perror("fork"); exit(2); }
|
||||
if (pid == 0) {
|
||||
close(ready[0]);
|
||||
record_file = fopen(records, "a");
|
||||
if (!record_file) _exit(2);
|
||||
run_fake(control_fd, ready[1]);
|
||||
}
|
||||
close(ready[1]);
|
||||
char c = 0;
|
||||
if (read(ready[0], &c, 1) != 1 || c != 'R') { fprintf(stderr, "fake logind failed\n"); exit(2); }
|
||||
close(ready[0]);
|
||||
return pid;
|
||||
}
|
||||
|
||||
static pid_t start_daemon(void) {
|
||||
char config[300], conf[2048];
|
||||
snprintf(config, sizeof(config), "%s/bus.conf", tmpdir);
|
||||
snprintf(conf, sizeof(conf),
|
||||
"<!DOCTYPE busconfig PUBLIC \"-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN\"\n"
|
||||
" \"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd\">\n"
|
||||
"<busconfig><type>system</type><listen>unix:path=%s/system_bus_socket</listen>"
|
||||
"<auth>EXTERNAL</auth><policy context=\"default\"><allow send_destination=\"*\"/>"
|
||||
"<allow receive_sender=\"*\"/><allow own=\"*\"/></policy></busconfig>\n", tmpdir);
|
||||
FILE *f = fopen(config, "w");
|
||||
if (!f || fputs(conf, f) < 0 || fclose(f)) { perror(config); exit(2); }
|
||||
|
||||
int out[2];
|
||||
if (pipe(out)) { perror("pipe"); exit(2); }
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
char arg_config[320], arg_fd[32];
|
||||
close(out[0]);
|
||||
snprintf(arg_config, sizeof(arg_config), "--config-file=%s", config);
|
||||
snprintf(arg_fd, sizeof(arg_fd), "--print-address=%d", out[1]);
|
||||
execlp("dbus-daemon", "dbus-daemon", arg_config, "--nofork", "--nosyslog", arg_fd, (char *) NULL);
|
||||
_exit(127);
|
||||
}
|
||||
close(out[1]);
|
||||
ssize_t n = read(out[0], bus_address, sizeof(bus_address) - 1);
|
||||
close(out[0]);
|
||||
if (n <= 0) { fprintf(stderr, "dbus-daemon did not start\n"); exit(2); }
|
||||
bus_address[n] = '\0';
|
||||
bus_address[strcspn(bus_address, "\n")] = '\0';
|
||||
return pid;
|
||||
}
|
||||
|
||||
static void stop_children(void) {
|
||||
if (getpid() != main_pid)
|
||||
return;
|
||||
if (fake_pid > 0) { kill(fake_pid, SIGTERM); waitpid(fake_pid, NULL, 0); fake_pid = 0; }
|
||||
if (daemon_pid > 0) { kill(daemon_pid, SIGTERM); waitpid(daemon_pid, NULL, 0); daemon_pid = 0; }
|
||||
}
|
||||
|
||||
/* ---- client side ---- */
|
||||
|
||||
static atomic_int will_sleep_calls, did_wake_calls;
|
||||
static atomic_ullong last_sleep_cycle, last_wake_cycle;
|
||||
|
||||
static void on_will_sleep(void *context, uint64_t cycle) {
|
||||
(void) context;
|
||||
last_sleep_cycle = cycle;
|
||||
will_sleep_calls++;
|
||||
}
|
||||
|
||||
static void on_did_wake(void *context, uint64_t cycle) {
|
||||
(void) context;
|
||||
last_wake_cycle = cycle;
|
||||
did_wake_calls++;
|
||||
}
|
||||
|
||||
static const struct iokitd_logind_callbacks callbacks = { on_will_sleep, on_did_wake };
|
||||
|
||||
/* Force an already-subscribed sleep signal to dispatch inside the property
|
||||
* lookup, before startup has assigned the timeout returned by that lookup. */
|
||||
static iokitd_bus_handler startup_handler;
|
||||
static void *startup_context;
|
||||
static int capture_sleep_match(void *bus, void **slot, const char *sender, const char *path,
|
||||
const char *interface, const char *member,
|
||||
iokitd_bus_handler handler, void *context) {
|
||||
if (!strcmp(member, "PrepareForSleep")) {
|
||||
startup_handler = handler;
|
||||
startup_context = context;
|
||||
}
|
||||
return sd_bus_match_signal(bus, (sd_bus_slot **)slot, sender, path, interface, member,
|
||||
(sd_bus_message_handler_t)handler, context);
|
||||
}
|
||||
static int interleaved_property(void *bus, const char *destination, const char *path,
|
||||
const char *interface, const char *member,
|
||||
struct iokitd_bus_error *error, char type, void *result) {
|
||||
int r = sd_bus_get_property_trivial(bus, destination, path, interface, member,
|
||||
(sd_bus_error *)error, type, result);
|
||||
sd_bus_message *message = NULL;
|
||||
if (sd_bus_message_new_signal(bus, &message, "/org/freedesktop/login1",
|
||||
"org.freedesktop.login1.Manager", "PrepareForSleep") >= 0 &&
|
||||
sd_bus_message_append(message, "b", 1) >= 0 &&
|
||||
sd_bus_message_seal(message, 1, 0) >= 0 && sd_bus_message_rewind(message, 1) >= 0)
|
||||
startup_handler(message, startup_context, NULL);
|
||||
sd_bus_message_unref(message);
|
||||
return r;
|
||||
}
|
||||
|
||||
static struct iokitd_logind_api native_api(uint64_t ack_timeout_usec) {
|
||||
struct iokitd_logind_api api;
|
||||
memset(&api, 0, sizeof(api));
|
||||
#define SET(f) api.f = (__typeof__(api.f)) f
|
||||
SET(sd_bus_new); SET(sd_bus_set_address); SET(sd_bus_set_bus_client); SET(sd_bus_start); SET(sd_bus_set_method_call_timeout);
|
||||
SET(sd_bus_flush_close_unref); SET(sd_bus_match_signal); SET(sd_bus_call_method);
|
||||
SET(sd_bus_get_property_trivial); SET(sd_bus_message_read); SET(sd_bus_message_unref);
|
||||
SET(sd_bus_slot_unref); SET(sd_bus_error_free); SET(sd_bus_process); SET(sd_bus_wait);
|
||||
SET(fcntl); SET(close); SET(getenv);
|
||||
#undef SET
|
||||
api.ack_timeout_usec = ack_timeout_usec;
|
||||
return api;
|
||||
}
|
||||
|
||||
static char *read_records(void) {
|
||||
static char buffer[65536];
|
||||
FILE *f = fopen(records, "r");
|
||||
size_t n = f ? fread(buffer, 1, sizeof(buffer) - 1, f) : 0;
|
||||
if (f) fclose(f);
|
||||
buffer[n] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
static int count_prefix(const char *prefix) {
|
||||
int n = 0;
|
||||
size_t len = strlen(prefix);
|
||||
for (const char *line = read_records(); *line; ) {
|
||||
if (!strncmp(line, prefix, len)) n++;
|
||||
const char *nl = strchr(line, '\n');
|
||||
if (!nl) break;
|
||||
line = nl + 1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Delay recorded for the release of lock `id`, -2 if not released. */
|
||||
static long released_delay(int id) {
|
||||
char prefix[32];
|
||||
snprintf(prefix, sizeof(prefix), "Released\t%d\t", id);
|
||||
const char *p = strstr(read_records(), prefix);
|
||||
return p ? strtol(p + strlen(prefix), NULL, 10) : -2;
|
||||
}
|
||||
|
||||
static int wait_until(int (*cond)(void *), void *arg, unsigned timeout_ms) {
|
||||
uint64_t end = now_ms() + timeout_ms;
|
||||
while (!cond(arg)) {
|
||||
if (now_ms() > end) return 0;
|
||||
sleep_ms(10);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct prefix_count { const char *prefix; int count; };
|
||||
static int prefix_reached(void *arg) {
|
||||
struct prefix_count *pc = arg;
|
||||
return count_prefix(pc->prefix) >= pc->count;
|
||||
}
|
||||
static int not_holding(void *arg) { return !iokitd_logind_holds_inhibitor(arg); }
|
||||
static int released(void *arg) { return released_delay(*(int *) arg) != -2; }
|
||||
static int counter_reached(void *arg) {
|
||||
struct { atomic_int *counter; int value; } *c = arg;
|
||||
return atomic_load(c->counter) >= c->value;
|
||||
}
|
||||
|
||||
static int wait_prefix(const char *prefix, int count, unsigned timeout_ms) {
|
||||
struct prefix_count pc = { prefix, count };
|
||||
return wait_until(prefix_reached, &pc, timeout_ms);
|
||||
}
|
||||
|
||||
static int wait_counter(atomic_int *counter, int value, unsigned timeout_ms) {
|
||||
struct { atomic_int *counter; int value; } c = { counter, value };
|
||||
return wait_until(counter_reached, &c, timeout_ms);
|
||||
}
|
||||
|
||||
static int wait_released(int id, unsigned timeout_ms) {
|
||||
return wait_until(released, &id, timeout_ms);
|
||||
}
|
||||
|
||||
static void command(int control_fd, char c) {
|
||||
if (write(control_fd, &c, 1) != 1) { perror("control"); exit(2); }
|
||||
}
|
||||
|
||||
static int serve(const char *dir) {
|
||||
char path[300];
|
||||
snprintf(tmpdir, sizeof(tmpdir), "%s", dir);
|
||||
snprintf(records, sizeof(records), "%s/records", dir);
|
||||
fclose(fopen(records, "w"));
|
||||
main_pid = getpid();
|
||||
atexit(stop_children);
|
||||
signal(SIGTERM, exit);
|
||||
signal(SIGINT, exit);
|
||||
daemon_pid = start_daemon();
|
||||
|
||||
snprintf(path, sizeof(path), "%s/control", dir);
|
||||
unlink(path);
|
||||
if (mkfifo(path, 0600) < 0) { perror(path); return 2; }
|
||||
/* O_RDWR keeps the FIFO open between writers. */
|
||||
int control = open(path, O_RDWR);
|
||||
if (control < 0) { perror(path); return 2; }
|
||||
fake_pid = start_fake(control);
|
||||
|
||||
snprintf(path, sizeof(path), "%s/address", dir);
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f || fprintf(f, "%s\n", bus_address) < 0 || fclose(f)) { perror(path); return 2; }
|
||||
printf("FAKE_LOGIND address=%s pid=%d daemon=%d\n", bus_address, (int) getpid(), (int) daemon_pid);
|
||||
fflush(stdout);
|
||||
|
||||
int status = 0;
|
||||
waitpid(fake_pid, &status, 0);
|
||||
fake_pid = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc == 3 && !strcmp(argv[1], "--fake-logind"))
|
||||
return serve(argv[2]);
|
||||
|
||||
snprintf(tmpdir, sizeof(tmpdir), "/tmp/riot-logind-sleep-XXXXXX");
|
||||
if (!mkdtemp(tmpdir)) { perror("mkdtemp"); return 2; }
|
||||
snprintf(records, sizeof(records), "%s/records", tmpdir);
|
||||
fclose(fopen(records, "w"));
|
||||
|
||||
main_pid = getpid();
|
||||
atexit(stop_children);
|
||||
daemon_pid = start_daemon();
|
||||
struct iokitd_logind *l = NULL;
|
||||
int r;
|
||||
|
||||
/* Incomplete host api: nothing is attempted. */
|
||||
{
|
||||
struct iokitd_logind_api api = native_api(0);
|
||||
api.sd_bus_wait = NULL;
|
||||
r = iokitd_logind_start(&api, &callbacks, NULL, &l);
|
||||
CHECK(r == -ENOSYS && l == NULL, "incomplete api: r=%d", r);
|
||||
}
|
||||
|
||||
/* No system bus: an error, no crash. */
|
||||
{
|
||||
char missing[400];
|
||||
snprintf(missing, sizeof(missing), "unix:path=%s/no-such-socket", tmpdir);
|
||||
setenv("DBUS_SYSTEM_BUS_ADDRESS", missing, 1);
|
||||
struct iokitd_logind_api api = native_api(0);
|
||||
r = iokitd_logind_start(&api, &callbacks, NULL, &l);
|
||||
CHECK(r < 0 && l == NULL, "missing bus: r=%d", r);
|
||||
}
|
||||
|
||||
setenv("DBUS_SYSTEM_BUS_ADDRESS", bus_address, 1);
|
||||
|
||||
/* Bus without logind: starts, holds no lock, no events. */
|
||||
{
|
||||
struct iokitd_logind_api api = native_api(0);
|
||||
r = iokitd_logind_start(&api, &callbacks, NULL, &l);
|
||||
CHECK(r == 0 && l != NULL, "bus without logind: r=%d", r);
|
||||
CHECK(!iokitd_logind_holds_inhibitor(l), "bus without logind: holds a lock");
|
||||
CHECK(iokitd_logind_ack_timeout_usec(l) == 4000000,
|
||||
"default ack timeout without logind: %llu", (unsigned long long) iokitd_logind_ack_timeout_usec(l));
|
||||
iokitd_logind_stop(l);
|
||||
l = NULL;
|
||||
}
|
||||
|
||||
/* The fake is started before any lock exists, so it inherits none. */
|
||||
int control[2];
|
||||
if (pipe(control)) { perror("pipe"); return 2; }
|
||||
fake_pid = start_fake(control[0]);
|
||||
close(control[0]);
|
||||
|
||||
/* Start: one delay lock, timeout derived from InhibitDelayMaxUSec (5 s) minus 1 s. */
|
||||
struct iokitd_logind_api api = native_api(0);
|
||||
r = iokitd_logind_start(&api, &callbacks, NULL, &l);
|
||||
CHECK(r == 0 && l != NULL, "start: r=%d", r);
|
||||
CHECK(wait_prefix("Inhibit\t", 1, 2000), "start: no Inhibit call:\n%s", read_records());
|
||||
CHECK(strstr(read_records(), "Inhibit\t1\tsleep\tDarling\t") != NULL &&
|
||||
strstr(read_records(), "\tdelay\n") != NULL, "start: wrong Inhibit arguments:\n%s", read_records());
|
||||
CHECK(strstr(read_records(), "Get\torg.freedesktop.login1.Manager\tInhibitDelayMaxUSec") != NULL,
|
||||
"start: InhibitDelayMaxUSec not read");
|
||||
CHECK(iokitd_logind_ack_timeout_usec(l) == 4000000, "derived ack timeout %llu",
|
||||
(unsigned long long) iokitd_logind_ack_timeout_usec(l));
|
||||
CHECK(iokitd_logind_holds_inhibitor(l), "start: lock not held");
|
||||
CHECK(released_delay(1) == -2, "start: lock 1 released early");
|
||||
|
||||
/* Sleep, then acknowledge: the lock is held until the acknowledgement. */
|
||||
command(control[1], 'S');
|
||||
CHECK(wait_counter(&will_sleep_calls, 1, 2000) && last_sleep_cycle == 1, "sleep 1: will_sleep %d cycle %llu",
|
||||
atomic_load(&will_sleep_calls), (unsigned long long) last_sleep_cycle);
|
||||
sleep_ms(500);
|
||||
CHECK(released_delay(1) == -2 && iokitd_logind_holds_inhibitor(l), "sleep 1: released before acknowledgement");
|
||||
iokitd_logind_sleep_ready(l, 1);
|
||||
CHECK(wait_released(1, 2000), "sleep 1: not released after acknowledgement");
|
||||
long delay = released_delay(1);
|
||||
CHECK(delay >= 450 && delay < 2000, "sleep 1: released %ld ms after PrepareForSleep(true)", delay);
|
||||
CHECK(!iokitd_logind_holds_inhibitor(l), "sleep 1: still holds lock");
|
||||
|
||||
/* Resume: did_wake, a new lock; a stale acknowledgement does not release it. */
|
||||
command(control[1], 'W');
|
||||
CHECK(wait_counter(&did_wake_calls, 1, 2000) && last_wake_cycle == 1, "wake 1: did_wake %d",
|
||||
atomic_load(&did_wake_calls));
|
||||
CHECK(wait_prefix("Inhibit\t", 2, 2000), "wake 1: lock not re-taken:\n%s", read_records());
|
||||
CHECK(wait_until((int (*)(void *)) iokitd_logind_holds_inhibitor, l, 1000), "wake 1: lock not held");
|
||||
iokitd_logind_sleep_ready(l, 1);
|
||||
sleep_ms(300);
|
||||
CHECK(released_delay(2) == -2 && iokitd_logind_holds_inhibitor(l), "stale acknowledgement released lock 2");
|
||||
|
||||
/* logind suspended without waiting (no acknowledgement before resume): keep the same lock. */
|
||||
command(control[1], 'S');
|
||||
CHECK(wait_counter(&will_sleep_calls, 2, 2000) && last_sleep_cycle == 2, "sleep 2: will_sleep");
|
||||
command(control[1], 'W');
|
||||
CHECK(wait_counter(&did_wake_calls, 2, 2000) && last_wake_cycle == 2, "wake 2: did_wake");
|
||||
sleep_ms(300);
|
||||
CHECK(count_prefix("Inhibit\t") == 2 && released_delay(2) == -2 && iokitd_logind_holds_inhibitor(l),
|
||||
"wake 2: lock replaced or released:\n%s", read_records());
|
||||
iokitd_logind_sleep_ready(l, 2);
|
||||
sleep_ms(200);
|
||||
CHECK(released_delay(2) == -2, "acknowledgement after resume released the lock");
|
||||
|
||||
/* Stop releases the lock. */
|
||||
iokitd_logind_stop(l);
|
||||
l = NULL;
|
||||
CHECK(wait_released(2, 2000) && released_delay(2) == -1, "stop: lock 2 not released");
|
||||
|
||||
/* Nobody acknowledges: the lock is released at the timeout, not before. */
|
||||
api = native_api(700000);
|
||||
r = iokitd_logind_start(&api, &callbacks, NULL, &l);
|
||||
CHECK(r == 0 && iokitd_logind_ack_timeout_usec(l) == 700000, "timeout bridge: r=%d", r);
|
||||
CHECK(wait_prefix("Inhibit\t", 3, 2000), "timeout bridge: no lock");
|
||||
int sleeps = atomic_load(&will_sleep_calls);
|
||||
command(control[1], 'S');
|
||||
CHECK(wait_counter(&will_sleep_calls, sleeps + 1, 2000) && last_sleep_cycle == 1, "timeout: will_sleep");
|
||||
sleep_ms(350);
|
||||
CHECK(released_delay(3) == -2, "timeout: released before the timeout");
|
||||
CHECK(wait_released(3, 3000), "timeout: never released");
|
||||
delay = released_delay(3);
|
||||
CHECK(delay >= 650 && delay < 1500, "timeout: released %ld ms after PrepareForSleep(true)", delay);
|
||||
iokitd_logind_sleep_ready(l, 1); /* late acknowledgement is harmless */
|
||||
int wakes = atomic_load(&did_wake_calls);
|
||||
command(control[1], 'W');
|
||||
CHECK(wait_counter(&did_wake_calls, wakes + 1, 2000), "timeout: did_wake");
|
||||
CHECK(wait_prefix("Inhibit\t", 4, 2000), "timeout: lock not re-taken after resume");
|
||||
iokitd_logind_stop(l);
|
||||
l = NULL;
|
||||
CHECK(wait_released(4, 2000), "timeout: lock 4 not released at stop");
|
||||
|
||||
/* Restart logind while a bridge is active: discard the old lock and re-take it. */
|
||||
api = native_api(700000);
|
||||
r = iokitd_logind_start(&api, &callbacks, NULL, &l);
|
||||
CHECK(r == 0 && wait_prefix("Inhibit\t", 5, 2000), "restart: initial lock");
|
||||
command(control[1], 'Q');
|
||||
close(control[1]);
|
||||
waitpid(fake_pid, NULL, 0);
|
||||
fake_pid = 0;
|
||||
CHECK(wait_until(not_holding, l, 2000), "restart: stale inhibitor still held");
|
||||
CHECK(pipe(control) == 0, "restart: control pipe");
|
||||
fake_pid = start_fake(control[0]);
|
||||
close(control[0]);
|
||||
CHECK(wait_prefix("Inhibit\t", 6, 2000), "restart: inhibitor not re-taken");
|
||||
CHECK(iokitd_logind_holds_inhibitor(l), "restart: no new inhibitor");
|
||||
sleeps = atomic_load(&will_sleep_calls);
|
||||
command(control[1], 'S');
|
||||
CHECK(wait_counter(&will_sleep_calls, sleeps + 1, 2000), "restart: sleep not received");
|
||||
iokitd_logind_sleep_ready(l, last_sleep_cycle);
|
||||
CHECK(wait_until(not_holding, l, 1000), "restart: ack did not release inhibitor");
|
||||
wakes = atomic_load(&did_wake_calls);
|
||||
command(control[1], 'W');
|
||||
CHECK(wait_counter(&did_wake_calls, wakes + 1, 2000), "restart: wake not received");
|
||||
CHECK(wait_prefix("Inhibit\t", 7, 2000), "restart: inhibitor missing after wake");
|
||||
iokitd_logind_stop(l);
|
||||
l = NULL;
|
||||
/* Startup signal during timeout discovery must not create an expired deadline. */
|
||||
api = native_api(0);
|
||||
api.sd_bus_match_signal = capture_sleep_match;
|
||||
api.sd_bus_get_property_trivial = interleaved_property;
|
||||
sleeps = atomic_load(&will_sleep_calls);
|
||||
r = iokitd_logind_start(&api, &callbacks, NULL, &l);
|
||||
CHECK(r == 0 && l != NULL, "startup interleave: start failed %d", r);
|
||||
CHECK(atomic_load(&will_sleep_calls) == sleeps + 1, "startup interleave: signal not dispatched");
|
||||
sleep_ms(300);
|
||||
CHECK(iokitd_logind_holds_inhibitor(l), "startup interleave: inhibitor released before ack");
|
||||
iokitd_logind_sleep_ready(l, last_sleep_cycle);
|
||||
CHECK(wait_until(not_holding, l, 1000), "startup interleave: ack did not release inhibitor");
|
||||
iokitd_logind_stop(l);
|
||||
l = NULL;
|
||||
|
||||
/* A complete system-bus outage must reconnect, not disable monitoring forever. */
|
||||
char stable_address[400];
|
||||
snprintf(stable_address, sizeof(stable_address), "unix:path=%s/system_bus_socket", tmpdir);
|
||||
setenv("DBUS_SYSTEM_BUS_ADDRESS", stable_address, 1);
|
||||
api = native_api(700000);
|
||||
int expected_locks = count_prefix("Inhibit\t") + 1;
|
||||
r = iokitd_logind_start(&api, &callbacks, NULL, &l);
|
||||
CHECK(r == 0 && wait_prefix("Inhibit\t", expected_locks, 2000), "bus restart: initial lock");
|
||||
command(control[1], 'Q');
|
||||
close(control[1]);
|
||||
waitpid(fake_pid, NULL, 0);
|
||||
fake_pid = 0;
|
||||
kill(daemon_pid, SIGTERM);
|
||||
waitpid(daemon_pid, NULL, 0);
|
||||
daemon_pid = 0;
|
||||
CHECK(wait_until(not_holding, l, 2000), "bus restart: stale inhibitor");
|
||||
sleep_ms(300);
|
||||
daemon_pid = start_daemon();
|
||||
CHECK(pipe(control) == 0, "bus restart: control pipe");
|
||||
fake_pid = start_fake(control[0]);
|
||||
close(control[0]);
|
||||
CHECK(wait_prefix("Inhibit\t", expected_locks + 1, 7000), "bus restart: no new inhibitor");
|
||||
CHECK(iokitd_logind_holds_inhibitor(l), "bus restart: inhibitor not held");
|
||||
sleeps = atomic_load(&will_sleep_calls);
|
||||
command(control[1], 'S');
|
||||
CHECK(wait_counter(&will_sleep_calls, sleeps + 1, 2000), "bus restart: sleep not delivered");
|
||||
iokitd_logind_sleep_ready(l, last_sleep_cycle);
|
||||
CHECK(wait_until(not_holding, l, 1000), "bus restart: acknowledgement failed");
|
||||
wakes = atomic_load(&did_wake_calls);
|
||||
command(control[1], 'W');
|
||||
CHECK(wait_counter(&did_wake_calls, wakes + 1, 2000), "bus restart: wake not delivered");
|
||||
CHECK(wait_prefix("Inhibit\t", expected_locks + 2, 2000), "bus restart: no lock after wake");
|
||||
iokitd_logind_stop(l);
|
||||
l = NULL;
|
||||
|
||||
command(control[1], 'Q');
|
||||
close(control[1]);
|
||||
waitpid(fake_pid, NULL, 0);
|
||||
fake_pid = 0;
|
||||
stop_children();
|
||||
|
||||
char path[400];
|
||||
snprintf(path, sizeof(path), "%s/bus.conf", tmpdir); unlink(path);
|
||||
snprintf(path, sizeof(path), "%s/system_bus_socket", tmpdir); unlink(path);
|
||||
unlink(records);
|
||||
rmdir(tmpdir);
|
||||
|
||||
printf("LOGIND_SLEEP %s checks=%d failures=%d\n", failures ? "FAIL" : "PASS", checks, failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise IOKit sleep notifications in a disposable prefix against fake logind.
|
||||
|
||||
Requires a built iokitd, both power probes and an installed Darling server
|
||||
with the empty OOL descriptor fix. Never contacts the real system bus.
|
||||
"""
|
||||
import argparse
|
||||
import errno
|
||||
import fcntl
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pty
|
||||
import re
|
||||
import select
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import termios
|
||||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--open-workaround", type=Path,
|
||||
help="development-only guest interposer for testing with an older server")
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def wait_for(predicate, description, timeout=15):
|
||||
end = time.monotonic() + timeout
|
||||
while time.monotonic() < end:
|
||||
if predicate():
|
||||
return
|
||||
time.sleep(0.02)
|
||||
raise RuntimeError("timed out: " + description)
|
||||
|
||||
|
||||
class Guest:
|
||||
def __init__(self, env, delay):
|
||||
master, slave = pty.openpty()
|
||||
self.master = master
|
||||
self.anchor = delay is None
|
||||
self.output = ""
|
||||
self.log_path = Path(env["DPREFIX"]).parent / f"guest-{time.monotonic_ns()}.log"
|
||||
command = ["darling", "shell"]
|
||||
if args.open_workaround:
|
||||
command += ["env", "DYLD_INSERT_LIBRARIES=/Volumes/SystemRoot" +
|
||||
str(args.open_workaround.resolve())]
|
||||
if delay is None:
|
||||
command += ["sh", "-c", "printf 'ANCHOR_READY\\n'; exec sleep 120"]
|
||||
else:
|
||||
command += ["/Volumes/SystemRoot" + str(ROOT / "build/riot-power-notify-check"), str(delay), "30"]
|
||||
def terminal():
|
||||
os.setsid()
|
||||
fcntl.ioctl(0, termios.TIOCSCTTY, 0)
|
||||
self.process = subprocess.Popen(command, cwd="/tmp", env=env, stdin=slave,
|
||||
stdout=slave, stderr=slave, preexec_fn=terminal)
|
||||
os.close(slave)
|
||||
|
||||
def read(self):
|
||||
while select.select([self.master], [], [], 0)[0]:
|
||||
try:
|
||||
data = os.read(self.master, 65536)
|
||||
except OSError as error:
|
||||
if error.errno == errno.EIO:
|
||||
break
|
||||
raise
|
||||
if not data:
|
||||
break
|
||||
self.output += data.decode(errors="replace")
|
||||
self.log_path.write_text(self.output)
|
||||
return self.output
|
||||
|
||||
def ready(self):
|
||||
output = self.read()
|
||||
if self.anchor:
|
||||
if self.process.poll() is not None:
|
||||
raise RuntimeError("anchor launcher exited:\n" + output[-2500:])
|
||||
return re.search(r"(?m)^ANCHOR_READY\r?$", output) is not None
|
||||
if "POWER_NOTIFY FAIL" in output or (self.process.poll() is not None and "READY " not in output):
|
||||
raise RuntimeError("guest registration failed:\n" + output[-2500:])
|
||||
return "READY " in output
|
||||
|
||||
def finish(self):
|
||||
def finished():
|
||||
self.read()
|
||||
return self.process.poll() is not None
|
||||
wait_for(finished, "guest exit")
|
||||
output = self.read()
|
||||
if self.process.returncode or "POWER_NOTIFY PASS" not in output:
|
||||
raise RuntimeError("guest failed:\n" + output[-2500:])
|
||||
|
||||
def close(self):
|
||||
# Closing the controlling terminal also releases a privileged launcher.
|
||||
os.close(self.master)
|
||||
try:
|
||||
self.process.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
try:
|
||||
self.process.terminate()
|
||||
self.process.wait(timeout=2)
|
||||
except (PermissionError, subprocess.TimeoutExpired):
|
||||
print("Launcher did not exit; fixture retained:", self.log_path, flush=True)
|
||||
|
||||
|
||||
# Darling creates a root-owned overlay workdir. Keep fixture files as diagnostics;
|
||||
# recursive deletion without root would mask the actual test result.
|
||||
def main():
|
||||
directory = Path(tempfile.mkdtemp(prefix="power-integration-", dir=ROOT / "build"))
|
||||
temp = str(directory)
|
||||
print("Fixture:", directory, flush=True)
|
||||
prefix = directory / "prefix"
|
||||
fake = subprocess.Popen([str(ROOT / "build/riot-logind-sleep-check"), "--fake-logind", temp],
|
||||
stdout=subprocess.DEVNULL)
|
||||
guests = []
|
||||
env = dict(os.environ, DPREFIX=str(prefix), EGL_PLATFORM="x11")
|
||||
# Do not pass caller-injected libraries or frameworks into the fixture.
|
||||
for name in ("DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH", "DYLD_FRAMEWORK_PATH"):
|
||||
env.pop(name, None)
|
||||
def command(value):
|
||||
descriptor = os.open(directory / "control", os.O_WRONLY | os.O_NONBLOCK)
|
||||
try:
|
||||
os.write(descriptor, value.encode())
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
|
||||
try:
|
||||
wait_for(lambda: (directory / "address").exists(), "fake logind startup")
|
||||
env["DBUS_SYSTEM_BUS_ADDRESS"] = (directory / "address").read_text().strip()
|
||||
|
||||
# Let the launcher initialize the empty prefix before adding overrides.
|
||||
subprocess.run(["darling", "shell", "true"], cwd="/tmp", env=env,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=45, check=True)
|
||||
subprocess.run(["darling", "shutdown"], env=env, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, timeout=10, check=True)
|
||||
(prefix / "usr/sbin").mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(ROOT / "build/darling-src/build/src/external/iokitd/iokitd", prefix / "usr/sbin/iokitd")
|
||||
|
||||
def records():
|
||||
return (directory / "records").read_text()
|
||||
|
||||
def new_guest(delay):
|
||||
guest = Guest(env, delay)
|
||||
guests.append(guest)
|
||||
try:
|
||||
wait_for(guest.ready, "IORegisterForSystemPower", timeout=40)
|
||||
except RuntimeError:
|
||||
print(guest.read()[-3000:], flush=True)
|
||||
raise
|
||||
return guest
|
||||
|
||||
def current_lock():
|
||||
matches = re.findall(r"^Inhibit\t(\d+)\t", records(), re.M)
|
||||
return int(matches[-1]) if matches else 0
|
||||
|
||||
def released(lock):
|
||||
match = re.search(rf"^Released\t{lock}\t(-?\d+)$", records(), re.M)
|
||||
return int(match[1]) if match else None
|
||||
|
||||
# The bootstrap run can itself have taken and released an inhibitor.
|
||||
# Wait for a NEW lock from the anchor; an old record is not readiness.
|
||||
bootstrap_lock = current_lock()
|
||||
anchor = Guest(env, None)
|
||||
guests.append(anchor)
|
||||
wait_for(anchor.ready, "anchor shell startup", timeout=40)
|
||||
wait_for(lambda: current_lock() > bootstrap_lock, "anchor delay inhibitor")
|
||||
initial_lock = current_lock()
|
||||
|
||||
# Two independent clients: wait for the slower acknowledgement.
|
||||
first, second = new_guest(-3), new_guest(0)
|
||||
wait_for(lambda: current_lock() > 0, "initial delay inhibitor")
|
||||
lock = current_lock()
|
||||
command("S")
|
||||
time.sleep(0.4)
|
||||
assert released(lock) is None, "inhibitor released before both clients acknowledged"
|
||||
wait_for(lambda: released(lock) is not None, "both acknowledgements")
|
||||
assert 1100 <= released(lock) < 3500, records()
|
||||
command("W")
|
||||
second.finish()
|
||||
wait_for(lambda: current_lock() > lock, "new inhibitor after resume")
|
||||
print("POWER_INTEGRATION multiple_clients=PASS", flush=True)
|
||||
|
||||
# A client that does not acknowledge cannot hold up sleep indefinitely.
|
||||
lock = current_lock()
|
||||
command("S")
|
||||
time.sleep(0.5)
|
||||
assert released(lock) is None, "inhibitor deadline was bypassed"
|
||||
wait_for(lambda: released(lock) is not None, "acknowledgement timeout", timeout=7)
|
||||
assert 3500 <= released(lock) < 5500, records()
|
||||
command("W")
|
||||
wait_for(lambda: current_lock() > lock, "inhibitor after timeout/resume")
|
||||
print("POWER_INTEGRATION timeout=PASS", flush=True)
|
||||
|
||||
# Dropping the last notification reference releases its pending ack.
|
||||
lock = current_lock()
|
||||
command("S")
|
||||
first.finish()
|
||||
wait_for(lambda: released(lock) is not None, "deregistered client's acknowledgement")
|
||||
assert 0 <= released(lock) < 2500, records()
|
||||
command("W")
|
||||
wait_for(lambda: current_lock() > lock, "inhibitor after deregistration")
|
||||
print("POWER_INTEGRATION deregister=PASS", flush=True)
|
||||
|
||||
# No clients remain: stale registrations must not force a timeout.
|
||||
lock = current_lock()
|
||||
command("S")
|
||||
wait_for(lambda: released(lock) is not None, "sleep with no clients")
|
||||
assert 0 <= released(lock) < 1000, records()
|
||||
command("W")
|
||||
wait_for(lambda: current_lock() > lock, "inhibitor after empty cycle")
|
||||
assert current_lock() == initial_lock + 4, "iokitd restarted or acquired duplicate inhibitors:\n" + records()
|
||||
print("POWER_INTEGRATION no_clients=PASS", flush=True)
|
||||
print("POWER_INTEGRATION PASS" + (" (empty-OOL test workaround enabled)" if args.open_workaround else ""))
|
||||
finally:
|
||||
subprocess.run(["darling", "shutdown"], env=env, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, timeout=10)
|
||||
for guest in guests:
|
||||
guest.close()
|
||||
if (directory / "control").exists():
|
||||
try:
|
||||
command("Q")
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
fake.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
fake.terminate()
|
||||
fake.wait(timeout=5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,208 @@
|
||||
/* Guest probe for system sleep/wake notifications (iokitd IOPMrootDomain).
|
||||
*
|
||||
* Registers the way Chromium's power monitor does (IORegisterForSystemPower,
|
||||
* then the notification port's run loop source), acknowledges
|
||||
* kIOMessageSystemWillSleep with IOAllowPowerChange after ACK_DELAY_MS and
|
||||
* waits until kIOMessageSystemHasPoweredOn or TIMEOUT_S. Sleep and wake are
|
||||
* triggered from the host (a fake logind, see tools/riot-logind-sleep-check.c).
|
||||
*
|
||||
* riot-power-notify-check [ACK_DELAY_MS [TIMEOUT_S]]
|
||||
* ACK_DELAY_MS: >=0 acknowledges after that delay; -1 never acknowledges;
|
||||
* -2 deregisters on sleep; -3 runs the integration suite's three cycles
|
||||
* (delayed acknowledgement, timeout, deregistration).
|
||||
*/
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <IOKit/IOKitLib.h>
|
||||
#include <IOKit/IOMessage.h>
|
||||
#include <IOKit/pwr_mgt/IOPMLib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static io_connect_t root_port;
|
||||
static io_object_t notifier;
|
||||
static int deregistered, suite, completed_cycles;
|
||||
static int ack_delay_ms, failures, step;
|
||||
static int got_will_sleep, got_will_power_on, got_has_powered_on;
|
||||
static IOReturn ack_result = -1;
|
||||
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { failures++; printf("FAIL: "); printf(__VA_ARGS__); \
|
||||
printf("\n"); } } while (0)
|
||||
|
||||
static double now_s(void) {
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, NULL);
|
||||
return tv.tv_sec + tv.tv_usec / 1e6;
|
||||
}
|
||||
|
||||
static const char *message_name(natural_t type) {
|
||||
switch (type) {
|
||||
case kIOMessageCanSystemSleep: return "CanSystemSleep";
|
||||
case kIOMessageSystemWillSleep: return "SystemWillSleep";
|
||||
case kIOMessageSystemWillNotSleep: return "SystemWillNotSleep";
|
||||
case kIOMessageSystemWillPowerOn: return "SystemWillPowerOn";
|
||||
case kIOMessageSystemHasPoweredOn: return "SystemHasPoweredOn";
|
||||
default: return "other";
|
||||
}
|
||||
}
|
||||
|
||||
static void callback(void *refcon, io_service_t service, natural_t type, void *argument) {
|
||||
CHECK(refcon == &root_port, "refcon %p", refcon);
|
||||
printf("MESSAGE %.3f %s (0x%x) argument=0x%lx service=%s\n", now_s(), message_name(type), type,
|
||||
(unsigned long) (uintptr_t) argument, service ? "set" : "null");
|
||||
fflush(stdout);
|
||||
switch (type) {
|
||||
case kIOMessageCanSystemSleep:
|
||||
IOAllowPowerChange(root_port, (intptr_t) argument);
|
||||
break;
|
||||
case kIOMessageSystemWillSleep:
|
||||
CHECK(step == 0, "WillSleep out of order (step %d)", step);
|
||||
step = 1;
|
||||
got_will_sleep++;
|
||||
if (ack_delay_ms == -2 || (suite && completed_cycles == 2)) {
|
||||
CHECK(IODeregisterForSystemPower(¬ifier) == kIOReturnSuccess, "deregister during sleep");
|
||||
deregistered = 1;
|
||||
CFRunLoopStop(CFRunLoopGetCurrent());
|
||||
break;
|
||||
}
|
||||
if ((ack_delay_ms < 0 && !suite) || (suite && completed_cycles == 1)) break;
|
||||
int delay = suite ? 1200 : ack_delay_ms;
|
||||
if (delay)
|
||||
usleep(delay * 1000);
|
||||
ack_result = IOAllowPowerChange(root_port, (intptr_t) argument);
|
||||
printf("ACK %.3f IOAllowPowerChange=0x%x\n", now_s(), ack_result);
|
||||
fflush(stdout);
|
||||
break;
|
||||
case kIOMessageSystemWillPowerOn:
|
||||
CHECK(argument == NULL, "WillPowerOn argument must be NULL");
|
||||
CHECK(step == 1, "WillPowerOn out of order (step %d)", step);
|
||||
step = 2;
|
||||
got_will_power_on++;
|
||||
break;
|
||||
case kIOMessageSystemHasPoweredOn:
|
||||
CHECK(argument == NULL, "HasPoweredOn argument must be NULL");
|
||||
CHECK(step == 2, "HasPoweredOn out of order (step %d)", step);
|
||||
completed_cycles++;
|
||||
step = suite ? 0 : 3;
|
||||
got_has_powered_on++;
|
||||
CFRunLoopStop(CFRunLoopGetCurrent());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
IONotificationPortRef port = NULL;
|
||||
io_object_t dead_notifier = IO_OBJECT_NULL;
|
||||
double timeout = 60;
|
||||
|
||||
if (argc > 1) ack_delay_ms = atoi(argv[1]);
|
||||
if (argc > 2) timeout = atof(argv[2]);
|
||||
suite = ack_delay_ms == -3;
|
||||
|
||||
/* The registry entries IORegisterForSystemPower relies on. */
|
||||
io_registry_entry_t entry = IORegistryEntryFromPath(kIOMasterPortDefault,
|
||||
kIOPowerPlane ":/IOPowerConnection/IOPMrootDomain");
|
||||
io_service_t matched = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPMrootDomain"));
|
||||
io_name_t class_name = "";
|
||||
if (entry) IOObjectGetClass(entry, class_name);
|
||||
CHECK(entry != IO_OBJECT_NULL && !strcmp(class_name, "IOPMrootDomain"), "IOPower path: entry=%u class=%s",
|
||||
entry, class_name);
|
||||
CHECK(matched != IO_OBJECT_NULL, "IOServiceMatching(IOPMrootDomain) found nothing");
|
||||
if (entry) {
|
||||
// Failed MIG requests must release the wake send right exactly once.
|
||||
IONotificationPortRef rejected = IONotificationPortCreate(kIOMasterPortDefault);
|
||||
io_object_t unused = IO_OBJECT_NULL;
|
||||
kern_return_t error = IOServiceAddInterestNotification(rejected, entry, "UnsupportedInterest",
|
||||
callback, &root_port, &unused);
|
||||
CHECK(error != KERN_SUCCESS && unused == IO_OBJECT_NULL, "unsupported interest succeeded");
|
||||
mach_port_status_t status;
|
||||
mach_msg_type_number_t count = MACH_PORT_RECEIVE_STATUS_COUNT;
|
||||
CHECK(mach_port_get_attributes(mach_task_self(), IONotificationPortGetMachPort(rejected),
|
||||
MACH_PORT_RECEIVE_STATUS, (mach_port_info_t)&status, &count) == KERN_SUCCESS &&
|
||||
!status.mps_srights, "failed registration leaked a wake send right");
|
||||
IONotificationPortDestroy(rejected);
|
||||
if (suite) {
|
||||
// A retained notifier with a destroyed destination must not become
|
||||
// an outstanding sleep acknowledgement.
|
||||
IONotificationPortRef dead = IONotificationPortCreate(kIOMasterPortDefault);
|
||||
CHECK(IOServiceAddInterestNotification(dead, entry, kIOAppPowerStateInterest,
|
||||
callback, &root_port, &dead_notifier) == KERN_SUCCESS, "dead-port fixture registration");
|
||||
IONotificationPortDestroy(dead);
|
||||
}
|
||||
}
|
||||
if (entry) IOObjectRelease(entry);
|
||||
if (matched) IOObjectRelease(matched);
|
||||
|
||||
root_port = IORegisterForSystemPower(&root_port, &port, callback, ¬ifier);
|
||||
printf("REGISTER root_port=%u port=%s notifier=%u\n", root_port, port ? "set" : "null", notifier);
|
||||
CHECK(root_port != IO_OBJECT_NULL && port != NULL && notifier != IO_OBJECT_NULL, "IORegisterForSystemPower failed");
|
||||
if (root_port == IO_OBJECT_NULL || port == NULL) {
|
||||
/* Repeat the steps of IORegisterForSystemPower to show which one fails. */
|
||||
io_service_t domain = IORegistryEntryFromPath(kIOMasterPortDefault,
|
||||
kIOPowerPlane ":/IOPowerConnection/IOPMrootDomain");
|
||||
io_connect_t connect = IO_OBJECT_NULL;
|
||||
IONotificationPortRef diag_port = IONotificationPortCreate(kIOMasterPortDefault);
|
||||
kern_return_t open_kr = IOServiceOpen(domain, mach_task_self(), 0, &connect);
|
||||
kern_return_t interest_kr = IOServiceAddInterestNotification(diag_port, domain, kIOAppPowerStateInterest,
|
||||
callback, &root_port, ¬ifier);
|
||||
printf("DIAG domain=%u IOServiceOpen=0x%x connect=%u IOServiceAddInterestNotification=0x%x\n",
|
||||
domain, open_kr, connect, interest_kr);
|
||||
printf("POWER_NOTIFY FAIL failures=%d\n", failures);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Selectors other than allow/cancel are refused cleanly. */
|
||||
uint64_t scalar = 0;
|
||||
kern_return_t kr = IOConnectCallScalarMethod(root_port, 99, &scalar, 1, NULL, NULL);
|
||||
CHECK(kr != KERN_SUCCESS, "unknown root domain selector succeeded");
|
||||
/* A stale acknowledgement is accepted and ignored. */
|
||||
CHECK(IOAllowPowerChange(root_port, 0x12345) == kIOReturnSuccess, "stale IOAllowPowerChange failed");
|
||||
|
||||
CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(port), kCFRunLoopCommonModes);
|
||||
if (suite) {
|
||||
// Fill the notification queue, then keep it full through the host's first
|
||||
// PrepareForSleep. The power event must be retried without losing order.
|
||||
int filled = 0;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
mach_msg_header_t filler = {0};
|
||||
filler.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
|
||||
filler.msgh_remote_port = IONotificationPortGetMachPort(port);
|
||||
filler.msgh_size = sizeof(filler);
|
||||
filler.msgh_id = 42;
|
||||
kern_return_t sent = mach_msg(&filler, MACH_SEND_MSG | MACH_SEND_TIMEOUT,
|
||||
sizeof(filler), 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
|
||||
if (sent == MACH_SEND_TIMED_OUT) break;
|
||||
CHECK(sent == KERN_SUCCESS, "cannot fill notification queue: 0x%x", sent);
|
||||
if (sent != KERN_SUCCESS) break;
|
||||
filled++;
|
||||
}
|
||||
CHECK(filled > 0, "notification queue not filled");
|
||||
printf("CONGESTED queued=%d\n", filled);
|
||||
}
|
||||
printf("READY %.3f ack_delay_ms=%d\n", now_s(), ack_delay_ms);
|
||||
fflush(stdout);
|
||||
|
||||
if (suite) usleep(500000);
|
||||
double end = now_s() + timeout;
|
||||
while (step < 3 && !deregistered && now_s() < end)
|
||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, false);
|
||||
|
||||
CHECK(got_will_sleep == (suite ? 3 : 1), "WillSleep received %d times", got_will_sleep);
|
||||
if (ack_delay_ms >= 0 || suite)
|
||||
CHECK(ack_result == kIOReturnSuccess, "IOAllowPowerChange returned 0x%x", ack_result);
|
||||
if (!deregistered || suite) {
|
||||
CHECK(got_will_power_on == (suite ? 2 : 1), "WillPowerOn received %d times", got_will_power_on);
|
||||
CHECK(got_has_powered_on == (suite ? 2 : 1), "HasPoweredOn received %d times", got_has_powered_on);
|
||||
}
|
||||
|
||||
CHECK(IODeregisterForSystemPower(¬ifier) == kIOReturnSuccess && notifier == IO_OBJECT_NULL,
|
||||
"IODeregisterForSystemPower");
|
||||
CHECK(IOServiceClose(root_port) == KERN_SUCCESS, "IOServiceClose");
|
||||
IONotificationPortDestroy(port);
|
||||
if (dead_notifier) IOObjectRelease(dead_notifier);
|
||||
|
||||
printf("POWER_NOTIFY %s failures=%d\n", failures ? "FAIL" : "PASS", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -124,11 +124,17 @@
|
||||
"revision": "5549ac4cd2db923c256f016a1381b3bfb716730a",
|
||||
"repository": "https://github.com/darlinghq/darling-iokitd.git",
|
||||
"patch": "patches/iokitd.patch",
|
||||
"modified_files": 8,
|
||||
"modified_files": 12,
|
||||
"new_files": [
|
||||
"src/PowerSources.mm",
|
||||
"src/IOInterestNotification.h",
|
||||
"src/IOInterestNotification.mm",
|
||||
"src/IOPMrootDomain.h",
|
||||
"src/IOPMrootDomain.mm",
|
||||
"src/IOPlatformExpertDevice.h",
|
||||
"src/IOPlatformExpertDevice.mm"
|
||||
"src/IOPlatformExpertDevice.mm",
|
||||
"src/LogindSleep.c",
|
||||
"src/LogindSleep.h",
|
||||
"src/PowerSources.mm"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user