Skip to content
Draft
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
118 changes: 118 additions & 0 deletions Modules/Sensors/fanPower.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//
// fanPower.swift
// Sensors
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//

import IOKit
import IOKit.pwr_mgt
import Kit

// IOKit/IOMessage.h constants — not Swift-bridged via IOKit.pwr_mgt umbrella.
private let kIOMessageSystemWillSleep: UInt32 = 0xe0000280
private let kIOMessageSystemHasPoweredOn: UInt32 = 0xe0000300

// Manages fan mode/speed snapshot across sleep/wake cycles at the IOKit layer.
// NSWorkspace.didWakeNotification arrives after SMC is ready only intermittently;
// IORegisterForSystemPower fires earlier and is not tied to any UI lifecycle.
internal final class FanPowerManager {
internal static let shared = FanPowerManager()

// Snapshot keyed by fan id: (mode, speed) captured just before sleep.
private var snapshot: [Int: (mode: FanMode, speed: Int)] = [:]

// Fan list registered by the reader on discovery.
private var fans: [Fan] = []

private var notifyPort: IONotificationPortRef?
private var notifierObject: io_object_t = 0
private var rootPort: io_connect_t = IO_OBJECT_NULL

private init() {
self.registerIOPower()
}

internal func register(fan: Fan) {
guard !self.fans.contains(where: { $0.id == fan.id }) else { return }
self.fans.append(fan)
}

private func registerIOPower() {
// Retain self for the C callback context; released in deinit via IODeregisterForSystemPower.
let selfPtr = Unmanaged.passRetained(self).toOpaque()

let callback: IOServiceInterestCallback = { refcon, _, messageType, messageArgument in
guard let refcon else { return }
let manager = Unmanaged<FanPowerManager>.fromOpaque(refcon).takeUnretainedValue()
switch messageType {
case kIOMessageSystemWillSleep:
manager.handleWillSleep()
IOAllowPowerChange(manager.rootPort, Int(bitPattern: messageArgument))
case kIOMessageSystemHasPoweredOn:
manager.handleHasPoweredOn()
default:
break
}
}

self.rootPort = IORegisterForSystemPower(selfPtr, &self.notifyPort, callback, &self.notifierObject)

guard self.rootPort != IO_OBJECT_NULL, let port = self.notifyPort else {
error("FanPowerManager: IORegisterForSystemPower failed")
Unmanaged<FanPowerManager>.fromOpaque(selfPtr).release()
return
}

IONotificationPortSetDispatchQueue(port, DispatchQueue.global(qos: .utility))
}

private func handleWillSleep() {
guard SMCHelper.shared.isActive() else { return }

self.snapshot.removeAll()

for fan in self.fans {
guard let mode = fan.customMode, !mode.isAutomatic else { continue }
self.snapshot[fan.id] = (mode: mode, speed: fan.customSpeed ?? 0)
SMCHelper.shared.setFanMode(fan.id, mode: FanMode.automatic.rawValue)
}

debug("FanPowerManager: snapshotted \(self.snapshot.count) fan(s) before sleep")
}

private func handleHasPoweredOn() {
guard !self.snapshot.isEmpty else { return }

// Give the SMC ~2 s to settle after wake before restoring.
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 2) { [weak self] in
guard let self else { return }
guard SMCHelper.shared.isActive() else {
debug("FanPowerManager: helper not active on wake, skipping restore")
return
}

for (id, entry) in self.snapshot {
SMCHelper.shared.setFanMode(id, mode: entry.mode.rawValue)
if !entry.mode.isAutomatic && entry.speed > 0 {
SMCHelper.shared.setFanSpeed(id, speed: entry.speed)
}
}

debug("FanPowerManager: restored \(self.snapshot.count) fan(s) after wake")
self.snapshot.removeAll()
}
}

deinit {
if self.notifierObject != 0 {
IODeregisterForSystemPower(&self.notifierObject)
}
if self.rootPort != IO_OBJECT_NULL {
IOServiceClose(self.rootPort)
}
if let port = self.notifyPort {
IONotificationPortDestroy(port)
}
}
}
3 changes: 3 additions & 0 deletions Modules/Sensors/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public class Sensors: Module {
self.popupView.setup(self.sensorsReader?.list.sensors)
self.portalView.setup(self.sensorsReader?.list.sensors)
self.notificationsView.setup(self.sensorsReader?.list.sensors)
self.sensorsReader?.list.sensors.compactMap({ $0 as? Fan }).forEach {
FanPowerManager.shared.register(fan: $0)
}

self.settingsView.callback = { [weak self] in
self?.sensorsReader?.read()
Expand Down
55 changes: 0 additions & 55 deletions Modules/Sensors/popup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,6 @@ internal class FanView: NSStackView {
return self.fan.value
}
}
private var resetModeAfterSleep: Bool = false
private var controlState: Bool
private var fanValue: FanValue {
FanValue(rawValue: Store.shared.string(key: "Sensors_popup_fanValue", defaultValue: FanValue.percentage.rawValue)) ?? .percentage
Expand All @@ -518,9 +517,6 @@ internal class FanView: NSStackView {
self.edgeInsets.top + self.edgeInsets.bottom + (self.spacing*CGFloat(self.arrangedSubviews.count))
}

private var willSleepMode: FanMode? = nil // fan mode before sleep
private var willSleepSpeed: Int? = nil // fan speed before sleep

public init(_ fan: Fan, width: CGFloat, callback: @escaping (() -> Void)) {
self.fan = fan
self.sizeCallback = callback
Expand All @@ -544,8 +540,6 @@ internal class FanView: NSStackView {
self.nameAndSpeed()
self.setupControls()

NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(self.wakeListener), name: NSWorkspace.didWakeNotification, object: nil)
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(self.sleepListener), name: NSWorkspace.willSleepNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.syncFanSpeed), name: .syncFansControl, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.changeHelperState), name: .fanHelperState, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.controlCallback), name: .toggleFanControl, object: nil)
Expand All @@ -567,7 +561,6 @@ internal class FanView: NSStackView {
}

deinit {
NSWorkspace.shared.notificationCenter.removeObserver(self)
NotificationCenter.default.removeObserver(self, name: .syncFansControl, object: nil)
NotificationCenter.default.removeObserver(self, name: .fanHelperState, object: nil)
NotificationCenter.default.removeObserver(self, name: .toggleSettings, object: nil)
Expand Down Expand Up @@ -841,45 +834,6 @@ internal class FanView: NSStackView {
NotificationCenter.default.post(name: .syncFansControl, object: nil, userInfo: ["speed": Int(self.fan.maxSpeed)])
}

@objc private func wakeListener(aNotification: NSNotification) {
self.resetModeAfterSleep = true

if self.speedState {
if let mode = self.willSleepMode, let speed = self.willSleepSpeed {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
SMCHelper.shared.setFanMode(self.fan.id, mode: mode.rawValue)
self.modeButtons?.setMode(mode)
if !mode.isAutomatic {
self.setSpeed(value: speed, then: {
DispatchQueue.main.async {
self.sliderValueField?.textColor = .systemBlue
}
})
}
}
}
self.willSleepMode = nil
self.willSleepSpeed = nil
}

if let value = self.fan.customSpeed, !self.fan.mode.isAutomatic {
self.setSpeed(value: value, then: {
DispatchQueue.main.async {
self.sliderValueField?.textColor = .systemBlue
}
})
}
}

@objc private func sleepListener(aNotification: NSNotification) {
guard SMCHelper.shared.isActive(), let mode = self.fan.customMode, !mode.isAutomatic else { return }

self.willSleepMode = mode
self.willSleepSpeed = self.fan.customSpeed
SMCHelper.shared.setFanMode(fan.id, mode: FanMode.automatic.rawValue)
self.modeButtons?.setMode(.automatic)
}

@objc private func syncFanSpeed(_ notification: Notification) {
guard self.syncState else { return }
var speed = notification.userInfo?["speed"] as? Int
Expand Down Expand Up @@ -919,15 +873,6 @@ internal class FanView: NSStackView {
v.setValue(ColorValue(Double(percentage) / 100))
}

if self.resetModeAfterSleep && !value.mode.isAutomatic {
if self.sliderValueField?.stringValue != "" && self.slider?.doubleValue != value.value {
self.slider?.doubleValue = value.value
self.sliderValueField?.stringValue = ""
}
self.modeButtons?.setMode(.forced)
self.resetModeAfterSleep = false
}

self.ready = true
}
})
Expand Down
4 changes: 4 additions & 0 deletions Stats.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@
9AE29AF6249A52B00071B02D /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9AE29AF4249A52870071B02D /* config.plist */; };
9AE29AFB249A53DC0071B02D /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE29AF9249A53780071B02D /* readers.swift */; };
9AE29AFC249A53DC0071B02D /* values.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE29AF7249A53420071B02D /* values.swift */; };
9AE29AFE249A53DC0071B02D /* fanPower.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE29AFD249A53DC0071B02D /* fanPower.swift */; };
9AEBBE4D28D773430082A6A1 /* Dot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AEBBE4C28D773430082A6A1 /* Dot.swift */; };
9AF9EE0924648751005D2270 /* Disk.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AF9EE0224648751005D2270 /* Disk.framework */; };
9AF9EE0A24648751005D2270 /* Disk.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9AF9EE0224648751005D2270 /* Disk.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
Expand Down Expand Up @@ -714,6 +715,7 @@
9AE29AF4249A52870071B02D /* config.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = config.plist; path = Modules/Sensors/config.plist; sourceTree = SOURCE_ROOT; };
9AE29AF7249A53420071B02D /* values.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = values.swift; path = Modules/Sensors/values.swift; sourceTree = SOURCE_ROOT; };
9AE29AF9249A53780071B02D /* readers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = readers.swift; path = Modules/Sensors/readers.swift; sourceTree = SOURCE_ROOT; };
9AE29AFD249A53DC0071B02D /* fanPower.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = fanPower.swift; path = Modules/Sensors/fanPower.swift; sourceTree = SOURCE_ROOT; };
9AEBBE4C28D773430082A6A1 /* Dot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dot.swift; sourceTree = "<group>"; };
9AF9EE0224648751005D2270 /* Disk.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Disk.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9AF9EE0524648751005D2270 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1280,6 +1282,7 @@
children = (
9AE29AF1249A50CD0071B02D /* main.swift */,
9AE29AF9249A53780071B02D /* readers.swift */,
9AE29AFD249A53DC0071B02D /* fanPower.swift */,
9A58DE9D24B363D800716A9F /* popup.swift */,
5CA518372B543FE600EBCCC4 /* portal.swift */,
9A58DE9F24B363F300716A9F /* settings.swift */,
Expand Down Expand Up @@ -2246,6 +2249,7 @@
9AB6D03926447CAA003215A5 /* reader.m in Sources */,
9AE29AFB249A53DC0071B02D /* readers.swift in Sources */,
9AE29AFC249A53DC0071B02D /* values.swift in Sources */,
9AE29AFE249A53DC0071B02D /* fanPower.swift in Sources */,
9A58DE9E24B363D800716A9F /* popup.swift in Sources */,
9AE29AF3249A51D70071B02D /* main.swift in Sources */,
9A58DEA024B363F300716A9F /* settings.swift in Sources */,
Expand Down