From 06e038be3805359f4bbaf44ae99bcaf9bb587c8c Mon Sep 17 00:00:00 2001 From: Matteo Onofri <121062165+Matte1404@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:48:56 +0200 Subject: [PATCH] Fall back to IOPMrootDomain when pmset omits SleepDisabled On macOS 27, `pmset -g` does not print SleepDisabled until disablesleep has been written explicitly. PMSet.read() threw in that state, so keep-awake could not be enabled (issue #1). read() now uses the IOPMrootDomain SleepDisabled property when the pmset output has no usable line. A read that fails both ways still throws, so a failed read never counts as sleep enabled. --- Sources/LidAwakeCore/SleepControl.swift | 23 +++++++++++++++++-- .../SystemAdaptersTests.swift | 19 +++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/Sources/LidAwakeCore/SleepControl.swift b/Sources/LidAwakeCore/SleepControl.swift index 55d728c..78e2736 100644 --- a/Sources/LidAwakeCore/SleepControl.swift +++ b/Sources/LidAwakeCore/SleepControl.swift @@ -1,4 +1,5 @@ import Foundation +import IOKit public protocol SleepControlling { func isAwake() throws -> Bool @@ -15,13 +16,31 @@ protocol PowerSettings { final class PMSet: PowerSettings { let runner: CommandRunning let setup: PrivilegeSetup - init(runner: CommandRunning, setup: PrivilegeSetup) { + let rootDomainSleepDisabled: () -> Bool? + init(runner: CommandRunning, setup: PrivilegeSetup, + rootDomainSleepDisabled: @escaping () -> Bool? = PMSet.readRootDomainSleepDisabled) { self.runner = runner self.setup = setup + self.rootDomainSleepDisabled = rootDomainSleepDisabled } + /// On macOS 27, `pmset -g` omits `SleepDisabled` until it has been written explicitly. + /// The IORegistry value tracks the same setting, so use it when the text output has no usable line. + /// A read that fails both ways still throws; it never assumes sleep is enabled. func read() throws -> Bool { - try Self.parse(runner.run("/usr/bin/pmset", ["-g"], timeout: 10).stdout) + let output = try runner.run("/usr/bin/pmset", ["-g"], timeout: 10).stdout + if let value = try? Self.parse(output) { return value } + if let value = rootDomainSleepDisabled() { return value } + throw ControlError.message("Could not read the current sleep setting.") + } + + static func readRootDomainSleepDisabled() -> Bool? { + let service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("IOPMrootDomain")) + guard service != IO_OBJECT_NULL else { return nil } + defer { IOObjectRelease(service) } + let value = IORegistryEntryCreateCFProperty(service, "SleepDisabled" as CFString, kCFAllocatorDefault, 0)? + .takeRetainedValue() + return value as? Bool } static func parse(_ output: String) throws -> Bool { diff --git a/Tests/LidAwakeCoreTests/SystemAdaptersTests.swift b/Tests/LidAwakeCoreTests/SystemAdaptersTests.swift index 169bba0..5909b3f 100644 --- a/Tests/LidAwakeCoreTests/SystemAdaptersTests.swift +++ b/Tests/LidAwakeCoreTests/SystemAdaptersTests.swift @@ -142,6 +142,25 @@ final class SystemAdaptersTests: XCTestCase { XCTAssertTrue(runner.calls.isEmpty) } + func testPMSetReadFallsBackToRootDomainWhenPMSetOmitsSleepDisabled() throws { + let runner = RecordingRunner() + runner.respond = { _ in CommandOutput(stdout: "System-wide power settings:\nCurrently in use:\n standby 1\n") } + XCTAssertFalse(try PMSet(runner: runner, setup: PrivilegeSetup(runner: runner), rootDomainSleepDisabled: { false }).read()) + XCTAssertTrue(try PMSet(runner: runner, setup: PrivilegeSetup(runner: runner), rootDomainSleepDisabled: { true }).read()) + } + + func testPMSetReadThrowsWhenPMSetAndRootDomainBothFail() { + let runner = RecordingRunner() + runner.respond = { _ in CommandOutput(stdout: "") } + XCTAssertThrowsError(try PMSet(runner: runner, setup: PrivilegeSetup(runner: runner), rootDomainSleepDisabled: { nil }).read()) + } + + func testPMSetReadPrefersPMSetWhenItReportsSleepDisabled() throws { + let runner = RecordingRunner() + runner.respond = { _ in CommandOutput(stdout: " SleepDisabled 1\n") } + XCTAssertTrue(try PMSet(runner: runner, setup: PrivilegeSetup(runner: runner), rootDomainSleepDisabled: { false }).read()) + } + func testPMSetReadPropagatesProcessFailureEvenWithValidLookingOutput() { let runner = RecordingRunner() runner.respond = { _ in throw ControlError.message("pmset unavailable") }