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
48 changes: 29 additions & 19 deletions Kit/helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -912,27 +912,37 @@ public class SMCHelper {

private var connection: NSXPCConnection? = nil

public func setFanSpeed(_ id: Int, speed: Int) {
guard let helper = self.helper(nil) else { return }
helper.setFanSpeed(id: id, value: speed) { result in
if let result, !result.isEmpty {
NSLog("set fan speed: \(result)")
}
}
public func setFanSpeed(_ id: Int, speed: Int, completion: ((String?) -> Void)? = nil) {
guard let helper = self.helper(forCall: completion) else { return }
helper.setFanSpeed(id: id, value: speed) { completion?($0) }
}

public func setFanMode(_ id: Int, mode: Int) {
guard let helper = self.helper(nil) else { return }
helper.setFanMode(id: id, mode: mode) { result in
if let result, !result.isEmpty {
NSLog("set fan mode: \(result)")
}
}

public func setFanMode(_ id: Int, mode: Int, completion: ((String?) -> Void)? = nil) {
guard let helper = self.helper(forCall: completion) else { return }
helper.setFanMode(id: id, mode: mode) { completion?($0) }
}

public func resetFanControl() {
guard let helper = self.helper(nil) else { return }
helper.resetFanControl { _ in }

public func resetFanControl(completion: ((String?) -> Void)? = nil) {
guard let helper = self.helper(forCall: completion) else { return }
helper.resetFanControl { completion?($0) }
}

// Per-call helper that wires the XPC errorHandler to the caller's completion
// so connection failures surface as errors instead of dropping the reply.
private func helper(forCall completion: ((String?) -> Void)?) -> HelperProtocol? {
guard let connection = self.helperConnection() else {
completion?("Fan helper not available")
return nil
}
let proxy = connection.remoteObjectProxyWithErrorHandler { error in
completion?("Fan helper unreachable: \(error.localizedDescription)")
}
guard let service = proxy as? HelperProtocol else {
completion?("Fan helper proxy unavailable")
return nil
}
service.setSMCPath(Bundle.main.path(forResource: "smc", ofType: nil)!)
return service
}

public func isActive() -> Bool {
Expand Down
63 changes: 47 additions & 16 deletions Modules/Sensors/popup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ internal class Popup: PopupWrapper {
let fanViews = self.list.values.compactMap { $0 as? FanView }
guard !fanViews.isEmpty else { return }
guard fanViews.allSatisfy({ $0.fan.mode.isAutomatic }) else { return }
SMCHelper.shared.resetFanControl()
SMCHelper.shared.resetFanControl { error in
if let error { NSLog("resetFanControl error: \(error)") }
}
}
#endif

Expand Down Expand Up @@ -551,9 +553,11 @@ internal class FanView: NSStackView {
NotificationCenter.default.addObserver(self, selector: #selector(self.controlCallback), name: .toggleFanControl, object: nil)

if let fanMode = self.fan.customMode, self.speedState && fanMode != FanMode.automatic {
SMCHelper.shared.setFanMode(fan.id, mode: fanMode.rawValue)
SMCHelper.shared.setFanMode(fan.id, mode: fanMode.rawValue) { [weak self] error in
if let error { self?.showFanError(error) }
}
self.modeButtons?.setMode(FanMode(rawValue: fanMode.rawValue) ?? .automatic)

self.setSpeed(value: Int(self.speed), then: {
DispatchQueue.main.async {
self.sliderValueField?.textColor = .systemBlue
Expand Down Expand Up @@ -651,17 +655,23 @@ internal class FanView: NSStackView {
if let fan = self?.fan, mode == .automatic || fan.mode != mode {
self?.fan.mode = mode
self?.fan.customMode = mode
SMCHelper.shared.setFanMode(fan.id, mode: mode.rawValue)
SMCHelper.shared.setFanMode(fan.id, mode: mode.rawValue) { [weak self] error in
if let error { self?.showFanError(error) }
}
}
self?.toggleControlView(mode == .forced)
}
buttons.off = { [weak self] in
if let fan = self?.fan {
if self?.fan.mode != .forced {
self?.fan.mode = .forced
SMCHelper.shared.setFanMode(fan.id, mode: FanMode.forced.rawValue)
SMCHelper.shared.setFanMode(fan.id, mode: FanMode.forced.rawValue) { [weak self] error in
if let error { self?.showFanError(error) }
}
}
SMCHelper.shared.setFanSpeed(fan.id, speed: 0) { [weak self] error in
if let error { self?.showFanError(error) }
}
SMCHelper.shared.setFanSpeed(fan.id, speed: 0)
self?.fan.customSpeed = 0
}
self?.toggleControlView(false)
Expand All @@ -670,9 +680,13 @@ internal class FanView: NSStackView {
if let fan = self?.fan {
if self?.fan.mode != .forced {
self?.fan.mode = .forced
SMCHelper.shared.setFanMode(fan.id, mode: FanMode.forced.rawValue)
SMCHelper.shared.setFanMode(fan.id, mode: FanMode.forced.rawValue) { [weak self] error in
if let error { self?.showFanError(error) }
}
}
SMCHelper.shared.setFanSpeed(fan.id, speed: Int(fan.maxSpeed)) { [weak self] error in
if let error { self?.showFanError(error) }
}
SMCHelper.shared.setFanSpeed(fan.id, speed: Int(fan.maxSpeed))
self?.fan.customSpeed = Int(fan.maxSpeed)
}
self?.toggleControlView(false)
Expand Down Expand Up @@ -783,21 +797,35 @@ internal class FanView: NSStackView {
self.sliderValueField?.stringValue = "\(value) RPM"
self.sliderValueField?.textColor = .secondaryLabelColor
self.fan.customSpeed = value

self.debouncer?.cancel()

let task = DispatchWorkItem { [weak self] in
DispatchQueue.global(qos: .userInteractive).async { [weak self] in
if let id = self?.fan.id {
SMCHelper.shared.setFanSpeed(id, speed: value)
guard let self else { return }
SMCHelper.shared.setFanSpeed(self.fan.id, speed: value) { [weak self] error in
if let error {
self?.showFanError(error)
// TODO: revert slider to previous value on failure — requires
// storing the pre-attempt speed before mutating fan.customSpeed above
}
then()
}
}

self.debouncer = task
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3, execute: task)
}

private func showFanError(_ message: String) {
DispatchQueue.main.async {
let alert = NSAlert()
alert.messageText = localizedString("Fan control failed")
alert.informativeText = message
alert.alertStyle = .warning
alert.addButton(withTitle: localizedString("OK"))
alert.runModal()
}
}

@objc private func sliderCallback(_ sender: NSSlider) {
var value = sender.doubleValue
Expand Down Expand Up @@ -847,7 +875,9 @@ internal class FanView: NSStackView {
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)
SMCHelper.shared.setFanMode(self.fan.id, mode: mode.rawValue) { [weak self] error in
if let error { self?.showFanError(error) }
}
self.modeButtons?.setMode(mode)
if !mode.isAutomatic {
self.setSpeed(value: speed, then: {
Expand All @@ -873,9 +903,10 @@ internal class FanView: NSStackView {

@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
// Best-effort reset on sleep; errors not surfaced as user can't act on them while sleeping
SMCHelper.shared.setFanMode(fan.id, mode: FanMode.automatic.rawValue)
self.modeButtons?.setMode(.automatic)
}
Expand Down
26 changes: 13 additions & 13 deletions SMC/Helper/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,36 +122,36 @@ extension Helper {
return
}
let result = syncShell("\(smc) fan \(id) -m \(mode)")

if let error = result.error, !error.isEmpty {
NSLog("error set fan mode: \(error)")
completion(nil)
completion("Failed to set fan \(id) mode to \(mode) — SMC rejected write")
return
}
completion(result.output)

completion(nil)
}
}

func setFanSpeed(id: Int, value: Int, completion: (String?) -> Void) {
smcQueue.sync {
guard let smc = self.smc else {
completion("missing smc tool")
return
}

let result = syncShell("\(smc) fan \(id) -v \(value)")

if let error = result.error, !error.isEmpty {
NSLog("error set fan speed: \(error)")
completion(nil)
completion("Failed to set fan \(id) speed to \(value) RPM — SMC rejected write")
return
}
completion(result.output)

completion(nil)
}
}

func resetFanControl(completion: (String?) -> Void) {
smcQueue.sync {
guard let smc = self.smc else {
Expand All @@ -161,10 +161,10 @@ extension Helper {
let result = syncShell("\(smc) reset")
if let error = result.error, !error.isEmpty {
NSLog("error reset fan control: \(error)")
completion(nil)
completion("Failed to reset fan control — SMC rejected write")
return
}
completion(result.output)
completion(nil)
}
}

Expand Down
Loading