68 lines
2.5 KiB
Swift
68 lines
2.5 KiB
Swift
import Foundation
|
|
|
|
enum FanControlError: LocalizedError {
|
|
case backendUnavailable
|
|
case commandFailed(String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .backendUnavailable:
|
|
"The SMC control backend is unavailable on this Mac."
|
|
case .commandFailed(let message):
|
|
message.isEmpty ? "The fan command did not complete." : message
|
|
}
|
|
}
|
|
}
|
|
|
|
enum FanControlService {
|
|
static var toolPath: String {
|
|
if let bundled = Bundle.main.url(forResource: "smc", withExtension: nil)?.path,
|
|
FileManager.default.isExecutableFile(atPath: bundled) {
|
|
return bundled
|
|
}
|
|
return "/Applications/Stats.app/Contents/Resources/smc"
|
|
}
|
|
|
|
static var isAvailable: Bool {
|
|
FileManager.default.isExecutableFile(atPath: toolPath)
|
|
}
|
|
|
|
static func setManualTargets(_ targets: [Int: Double]) throws {
|
|
guard isAvailable else { throw FanControlError.backendUnavailable }
|
|
let commands = targets.sorted { $0.key < $1.key }.flatMap { id, speed in
|
|
let safeID = max(0, id)
|
|
let safeSpeed = max(0, Int(speed.rounded()))
|
|
return ["\(quotedTool) fan \(safeID) -m 1", "\(quotedTool) fan \(safeID) -v \(safeSpeed)"]
|
|
}
|
|
try runPrivileged(commands.joined(separator: " && "))
|
|
}
|
|
|
|
static func restoreAutomatic() throws {
|
|
guard isAvailable else { throw FanControlError.backendUnavailable }
|
|
try runPrivileged("\(quotedTool) reset")
|
|
}
|
|
|
|
private static var quotedTool: String {
|
|
"'" + toolPath.replacingOccurrences(of: "'", with: "'\\''") + "'"
|
|
}
|
|
|
|
private static func runPrivileged(_ shellCommand: String) throws {
|
|
let escaped = shellCommand
|
|
.replacingOccurrences(of: "\\", with: "\\\\")
|
|
.replacingOccurrences(of: "\"", with: "\\\"")
|
|
let script = "do shell script \"\(escaped)\" with administrator privileges"
|
|
let process = Process()
|
|
let errorPipe = Pipe()
|
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
process.arguments = ["-e", script]
|
|
process.standardOutput = FileHandle.nullDevice
|
|
process.standardError = errorPipe
|
|
try process.run()
|
|
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
|
|
process.waitUntilExit()
|
|
guard process.terminationStatus == 0 else {
|
|
throw FanControlError.commandFailed(String(decoding: errorData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines))
|
|
}
|
|
}
|
|
}
|