Phase 5: Hardware- und Netzwerkmonitor, Panel und Menüleiste
Sechs neue Widgets (CPU, GPU, Speicher, Akku, Sensoren, Netzwerk) und dieselben sechs als einzeln aktivierbare Menüleisten-Module mit fünf Darstellungsarten. Panel-Widget und Menüleisten-Modul derselben Größe teilen sich die Messschleife über Referenzzählung. Ohne das liefe für jede Anzeige ein eigener Timer mit denselben IOKit- und SMC-Abfragen — bei sechs Modulen und ebenso vielen Widgets ein Vielfaches der nötigen Arbeit. Meldet sich der letzte Konsument ab, hört die Schleife auf; im Ruhezustand misst Onyx nichts. Die Differenzrechnung ist testgetrieben, und die Tests haben zwei echte Fehler gefunden. Erstens: mein Überlaufschutz beim Durchsatz hielt einen zurückgesetzten Zähler (Interface-Wechsel) für einen Überlauf und zeigte 4,3 GB/s. Da if_data64 64-Bit-Zähler liefert, die bei 10 Gbit/s erst nach 470 Jahren umlaufen, ist ein kleinerer Wert immer ein Zurücksetzen — die Unterscheidung produzierte nur den Ausreißer, den sie verhindern sollte. Zweitens: ByteCountFormatter schrieb bei null „Zero KB/s" statt „0 KB/s". Bei den CPU-Zählern bleibt der Überlaufschutz nötig: die sind 32 Bit breit und laufen nach gut 400 Tagen wirklich um, deshalb `&-` statt `-`. Sensoren kommen über den SMC-Leser aus Spike A statt über die private IOHID-Schnittstelle — auf dieser Maschine nachweislich verifiziert, und die dort gefundene Little-Endian-Eigenheit ist berücksichtigt. Nur eine kurze Liste benannter Fühler statt aller 3486 Keys: eine Wand aus Kürzeln ist keine Information. Gegengemessen statt vermutet: 15 Kerne, 39,8 W, GPU 50 %, 16,6/24 GB mit kritischem Druck, Akku 68 Zyklen bei 100 % Gesundheit, fünf Sensoren, zwei Lüfter, en0 mit 661 KB/s. Menüleisten-Module zeichnen in labelColor statt in den Onyx-Farben — nur die Systemfarbe passt sich heller wie dunkler Leiste an. Feste Breite je Darstellungsart, sonst schiebt jeder Messwert die halbe Leiste hin und her. Standardmäßig ist kein Modul aktiv: sechs neue Symbole beim ersten Start wären eine Zumutung. 145 Tests grün.
This commit is contained in:
143
Packages/OnyxKit/Sources/MetricsProvider/SMCReader.swift
Normal file
143
Packages/OnyxKit/Sources/MetricsProvider/SMCReader.swift
Normal file
@@ -0,0 +1,143 @@
|
||||
import Foundation
|
||||
import IOKit
|
||||
import OSLog
|
||||
|
||||
private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "SMC")
|
||||
|
||||
/// Liest Werte aus dem System Management Controller. **Ausschließlich lesend.**
|
||||
///
|
||||
/// Es gibt hier bewusst keinen Schreibpfad. Lüfterdrehzahl und Ladegrenze
|
||||
/// verlangen root und gehören in den privilegierten Helfer aus Phase 6 — nicht
|
||||
/// in einen Typ, der im normalen App-Prozess läuft und von jedem Widget
|
||||
/// erreichbar ist.
|
||||
///
|
||||
/// Die Struktur und die Erkenntnisse stammen aus `docs/spikes/A-smc.md`, gemessen
|
||||
/// auf Mac17,9. Wichtigster Punkt von dort: **alle Mehrbyte-Werte sind
|
||||
/// little-endian.** Der verbreitete Beispielcode nimmt big-endian an — er
|
||||
/// stammt aus der Intel-Ära und liefert auf Apple Silicon Unsinn.
|
||||
public final class SMCReader: @unchecked Sendable {
|
||||
|
||||
// MARK: - Aufbau des AppleSMC-UserClients
|
||||
|
||||
private struct Version { var major: UInt8 = 0; var minor: UInt8 = 0
|
||||
var build: UInt8 = 0; var reserved: UInt8 = 0
|
||||
var release: UInt16 = 0 }
|
||||
private struct PLimitData { var version: UInt16 = 0; var length: UInt16 = 0
|
||||
var cpuPLimit: UInt32 = 0; var gpuPLimit: UInt32 = 0
|
||||
var memPLimit: UInt32 = 0 }
|
||||
private struct KeyInfo { var dataSize: UInt32 = 0; var dataType: UInt32 = 0
|
||||
var dataAttributes: UInt8 = 0 }
|
||||
|
||||
private struct Param {
|
||||
var key: UInt32 = 0
|
||||
var vers = Version()
|
||||
var pLimitData = PLimitData()
|
||||
var keyInfo = KeyInfo()
|
||||
var padding: UInt16 = 0
|
||||
var result: UInt8 = 0
|
||||
var status: UInt8 = 0
|
||||
var data8: UInt8 = 0
|
||||
var data32: UInt32 = 0
|
||||
var bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) =
|
||||
(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
}
|
||||
|
||||
private static let handleYPCEvent: UInt32 = 2
|
||||
private static let readKey: UInt8 = 5
|
||||
private static let getKeyInfo: UInt8 = 9
|
||||
|
||||
private var connection: io_connect_t = 0
|
||||
private let lock = NSLock()
|
||||
|
||||
public init?() {
|
||||
let service = IOServiceGetMatchingService(kIOMainPortDefault,
|
||||
IOServiceMatching("AppleSMC"))
|
||||
guard service != 0 else {
|
||||
log.error("AppleSMC nicht gefunden — Sensoren bleiben leer")
|
||||
return nil
|
||||
}
|
||||
defer { IOObjectRelease(service) }
|
||||
guard IOServiceOpen(service, mach_task_self_, 0, &connection) == kIOReturnSuccess else {
|
||||
log.error("AppleSMC nicht zu öffnen")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
deinit { if connection != 0 { IOServiceClose(connection) } }
|
||||
|
||||
// MARK: - Lesen
|
||||
|
||||
/// Ein `flt`-Wert (Lüfterdrehzahl, Temperatur, Leistung).
|
||||
public func float(_ key: String) -> Double? {
|
||||
guard let value = read(key), value.type == "flt ", value.bytes.count >= 4 else { return nil }
|
||||
// Little-endian, siehe Klassenkommentar.
|
||||
let bits = UInt32(value.bytes[3]) << 24 | UInt32(value.bytes[2]) << 16
|
||||
| UInt32(value.bytes[1]) << 8 | UInt32(value.bytes[0])
|
||||
return Double(Float(bitPattern: bits))
|
||||
}
|
||||
|
||||
/// Ein vorzeichenloser Ganzzahlwert beliebiger Breite.
|
||||
public func integer(_ key: String) -> UInt64? {
|
||||
guard let value = read(key) else { return nil }
|
||||
var result: UInt64 = 0
|
||||
for (index, byte) in value.bytes.prefix(8).enumerated() {
|
||||
result |= UInt64(byte) << (8 * index) // little-endian
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func exists(_ key: String) -> Bool { read(key) != nil }
|
||||
|
||||
private struct Value { let type: String; let bytes: [UInt8] }
|
||||
|
||||
private func read(_ key: String) -> Value? {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
var info = Param()
|
||||
info.key = Self.fourCC(key)
|
||||
info.data8 = Self.getKeyInfo
|
||||
guard let described = call(info) else { return nil }
|
||||
|
||||
var command = Param()
|
||||
command.key = Self.fourCC(key)
|
||||
command.keyInfo = described.keyInfo
|
||||
command.data8 = Self.readKey
|
||||
guard let output = call(command) else { return nil }
|
||||
|
||||
let size = Int(min(described.keyInfo.dataSize, 32))
|
||||
let all = withUnsafeBytes(of: output.bytes) { Array($0) }
|
||||
return Value(type: Self.fourCCString(described.keyInfo.dataType),
|
||||
bytes: Array(all.prefix(size)))
|
||||
}
|
||||
|
||||
private func call(_ input: Param) -> Param? {
|
||||
var input = input
|
||||
var output = Param()
|
||||
var size = MemoryLayout<Param>.stride
|
||||
|
||||
let result = IOConnectCallStructMethod(connection, Self.handleYPCEvent,
|
||||
&input, MemoryLayout<Param>.stride,
|
||||
&output, &size)
|
||||
// Ein nicht vorhandener Key meldet sich über `result` und ist kein
|
||||
// Fehler: welche Keys es gibt, unterscheidet sich je nach Modell.
|
||||
guard result == kIOReturnSuccess, output.result == 0 else { return nil }
|
||||
return output
|
||||
}
|
||||
|
||||
private static func fourCC(_ string: String) -> UInt32 {
|
||||
var value: UInt32 = 0
|
||||
for character in string.utf8.prefix(4) { value = value << 8 | UInt32(character) }
|
||||
return value
|
||||
}
|
||||
|
||||
private static func fourCCString(_ value: UInt32) -> String {
|
||||
let bytes = [UInt8((value >> 24) & 0xFF), UInt8((value >> 16) & 0xFF),
|
||||
UInt8((value >> 8) & 0xFF), UInt8(value & 0xFF)]
|
||||
return String(bytes: bytes, encoding: .ascii) ?? "????"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user