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