Der Regler war eine Pseudobedienung. Der SMC lehnt Schreibzugriffe auf CHLT ab — der Helfer hat es mit korrekter Größe versucht und „geschrieben (false), zurückgelesen 85" gemeldet. Das Sicherheitsnetz hat also die Wahrheit gesagt; die Oberfläche hat sie nur nicht gezeigt. Weiter zu raten hieße, im Lade-Subsystem Keys durchzuprobieren, und das ist die eine Operation in diesem Projekt, die Hardware dauerhaft beschädigen kann. Also: das Limit wird gelesen und angezeigt, mit einem Weg in die Systemeinstellungen, wo es tatsächlich geht. Der Schreibpfad ist aus dem privilegierten Helfer entfernt — ein Root-Dienst trägt nur, was benutzt wird. Die Akku-Kachel zeigte „40 %" und einen Verlauf über die letzten Sekunden. Der Ladestand ändert sich in Minuten, nicht in Sekunden; der Graph war eine waagerechte Linie. Jetzt: verbleibende Zeit bzw. Zeit bis voll, Leistung in Watt mit Richtung, Ladelimit, Zustand, Zyklen — und der Energiesparmodus. Der erklärt, warum die Maschine langsamer wirkt, und ohne diesen Hinweis sucht man den Grund woanders. In der Menüleiste standen fünf nackte Prozentzahlen nebeneinander. Man sah Zahlen und wusste nicht, welche wozu gehört. Zwei neue Darstellungen: Kürzel und Wert („CPU 34 %") sowie Symbol und Wert. Beim Akku stehen sie zuoberst in der Auswahl. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
93 lines
3.8 KiB
Swift
93 lines
3.8 KiB
Swift
import Foundation
|
|
|
|
/// Die Schnittstelle zwischen App und privilegiertem Helfer.
|
|
///
|
|
/// **Die App sagt dem Helfer nicht, wie warm es ist.** Der Helfer misst selbst.
|
|
/// Andernfalls hinge das Sicherheitsnetz an der Ehrlichkeit eines Prozesses,
|
|
/// der abstürzen, hängen oder — bei einer manipulierten Kopie — schlicht lügen
|
|
/// kann. Der Herzschlag sagt deshalb nur „ich lebe noch", nichts weiter.
|
|
@objc public protocol OnyxHelperProtocol {
|
|
|
|
/// Version des Helfers. Läuft nach einem App-Update noch die alte, muss sie
|
|
/// ersetzt werden — sonst passen die Aufrufe nicht mehr zusammen.
|
|
func version(reply: @escaping (String) -> Void)
|
|
|
|
/// Aktueller Zustand aller Lüfter, als JSON-kodierter ``FanStatus``.
|
|
///
|
|
/// JSON statt eines eigenen Typs: `NSSecureCoding` über XPC verlangt
|
|
/// registrierte Klassen auf beiden Seiten, und dafür ist die Datenmenge
|
|
/// hier zu klein.
|
|
func fanStatus(reply: @escaping (Data?) -> Void)
|
|
|
|
/// Setzt eine Wunschdrehzahl. Der Helfer prüft sie gegen sein
|
|
/// Sicherheitsnetz und kann sie begrenzen oder ablehnen.
|
|
func setFan(index: Int, targetRPM: Double, reply: @escaping (Data?) -> Void)
|
|
|
|
/// Zurück zur Automatik.
|
|
func setFanAutomatic(index: Int, reply: @escaping (Data?) -> Void)
|
|
|
|
/// „Ich lebe noch." Bleibt er aus, fallen alle Lüfter auf Automatik zurück.
|
|
func heartbeat(reply: @escaping (Bool) -> Void)
|
|
|
|
/// Das aktuelle Ladelimit in Prozent, `-1` wenn keines gilt.
|
|
///
|
|
/// Nur lesen. Der SMC lehnt Schreibzugriffe auf `CHLT` ab — nachgemessen,
|
|
/// siehe docs/spikes/A-smc.md. macOS setzt den Wert offenbar auf einem
|
|
/// anderen Weg als über den SMC-Userclient.
|
|
///
|
|
/// `Int` statt `Int?`: Optionals sind über `@objc` nicht darstellbar.
|
|
func chargeLimit(reply: @escaping (Int) -> Void)
|
|
}
|
|
|
|
/// Was der Helfer über einen Lüfter berichtet.
|
|
public struct FanStatus: Codable, Equatable, Sendable {
|
|
public let index: Int
|
|
public let currentRPM: Double
|
|
public let targetRPM: Double
|
|
public let limits: FanLimits
|
|
public let isManual: Bool
|
|
/// Warum gerade die Automatik läuft — `nil` im manuellen Betrieb.
|
|
public let automaticReason: String?
|
|
|
|
public init(index: Int, currentRPM: Double, targetRPM: Double, limits: FanLimits,
|
|
isManual: Bool, automaticReason: String?) {
|
|
self.index = index
|
|
self.currentRPM = currentRPM
|
|
self.targetRPM = targetRPM
|
|
self.limits = limits
|
|
self.isManual = isManual
|
|
self.automaticReason = automaticReason
|
|
}
|
|
}
|
|
|
|
public struct FanStatusReport: Codable, Equatable, Sendable {
|
|
public let fans: [FanStatus]
|
|
public let hottestCelsius: Double?
|
|
public let helperVersion: String
|
|
|
|
public init(fans: [FanStatus], hottestCelsius: Double?, helperVersion: String) {
|
|
self.fans = fans
|
|
self.hottestCelsius = hottestCelsius
|
|
self.helperVersion = helperVersion
|
|
}
|
|
}
|
|
|
|
public enum OnyxHelper {
|
|
/// Muss zum Namen der Plist unter `Contents/Library/LaunchDaemons` passen.
|
|
public static let machServiceName = "com.scarriffleservices.onyx.helper"
|
|
public static let version = "1"
|
|
|
|
/// Nur ein Prozess mit exakt dieser Signatur darf mit dem Helfer sprechen.
|
|
///
|
|
/// Ohne diese Prüfung könnte **jedes** Programm auf dem Rechner die Lüfter
|
|
/// eines root-Dienstes steuern — der Mach-Dienst ist systemweit sichtbar.
|
|
public static let clientRequirement =
|
|
"anchor apple generic and identifier \"com.scarriffleservices.onyx\" "
|
|
+ "and certificate leaf[subject.OU] = \"PP34X97WS3\""
|
|
|
|
/// Und die App spricht nur mit einem Helfer, der ebenso signiert ist.
|
|
public static let helperRequirement =
|
|
"anchor apple generic and identifier \"com.scarriffleservices.onyx.helper\" "
|
|
+ "and certificate leaf[subject.OU] = \"PP34X97WS3\""
|
|
}
|