Files
puter/Sources/Puter/FanControlService.swift
T

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))
}
}
}