Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions Sources/LidAwakeCore/SleepControl.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import IOKit

public protocol SleepControlling {
func isAwake() throws -> Bool
Expand All @@ -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 {
Expand Down
19 changes: 19 additions & 0 deletions Tests/LidAwakeCoreTests/SystemAdaptersTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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") }
Expand Down