Compare commits

...
Author SHA1 Message Date
gronod c8743e2578 feat(ui): integrate Milestone 3 telemetry views, process inspector, and reactive cards
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Canceled after 0s
2026-09-08 12:54:34 +01:00
3 changed files with 1046 additions and 202 deletions
+391 -3
View File
@@ -81,6 +81,143 @@ public struct PowerMetrics: Sendable {
public var sensorReadings: [(key: String, name: String, value: Double, unit: String)] = [] public var sensorReadings: [(key: String, name: String, value: Double, unit: String)] = []
} }
// MARK: - Milestone 3 Advanced Telemetry Models
public struct KernelCountersMetrics: Sendable {
public var contextSwitchesPerSec: Double = 0
public var syscallsPerSec: Double = 0
public var pageFaultsPerSec: Double = 0
public var cowFaultsPerSec: Double = 0
public var zeroFillFaultsPerSec: Double = 0
public var pageinsPerSec: Double = 0
public var pageoutsPerSec: Double = 0
public var totalContextSwitches: UInt64 = 0
public var totalSyscalls: UInt64 = 0
public var totalPageFaults: UInt64 = 0
}
public struct SystemLoadMetrics: Sendable {
public var load1Min: Double = 0
public var load5Min: Double = 0
public var load15Min: Double = 0
public var taskCount: Int = 0
public var threadCount: Int = 0
public var machFactor: Double = 0
}
public struct DiskIOItem: Identifiable, Sendable {
public var id: String { bsdName }
public let bsdName: String
public let readBps: Double
public let writeBps: Double
public let readIOPS: Double
public let writeIOPS: Double
public let totalBytesRead: UInt64
public let totalBytesWritten: UInt64
}
public struct NetworkBandwidthItem: Identifiable, Sendable {
public var id: String { interfaceName }
public let interfaceName: String
public let ipAddress: String
public let isUp: Bool
public let downloadBps: Double
public let uploadBps: Double
public let downloadPps: Double
public let uploadPps: Double
public let totalBytesIn: UInt64
public let totalBytesOut: UInt64
}
public struct SocketItem: Identifiable, Sendable {
public var id: String { "\(pid)_\(fd)_\(localAddress):\(localPort)" }
public let pid: Int32
public let fd: Int32
public let processName: String
public let protocolName: String
public let localAddress: String
public let localPort: UInt16
public let remoteAddress: String
public let remotePort: UInt16
public let tcpState: String
}
public struct ProcessItem: Identifiable, Sendable {
public var id: Int32 { pid }
public let pid: Int32
public let ppid: Int32
public let name: String
public let cpuPercent: Double
public let residentSize: UInt64
public let virtualSize: UInt64
public let threadCount: Int32
public let uid: UInt32
public let username: String
public let isStopped: Bool
public let isZombie: Bool
}
public struct GPUCardItem: Identifiable, Sendable {
public var id: String { name }
public let name: String
public let isDiscrete: Bool
public let isLowPower: Bool
public let vramTotalBytes: UInt64
public let vramUsedBytes: UInt64
public let utilizationPercent: Double
public let temperature: Double
}
public struct BatteryHealthMetrics: Sendable {
public var hasBattery: Bool = false
public var installed: Bool = false
public var healthCondition: String = "Normal"
public var healthPercent: Double = 100.0
public var cycleCount: Int = 0
public var designCycleCount: Int = 1000
public var currentCapacity: Int = 0
public var maxCapacity: Int = 0
public var designCapacity: Int = 0
public var temperature: Double = 0
public var voltage: Double = 0
public var amperage: Double = 0
public var watts: Double = 0
public var isCharging: Bool = false
public var isCharged: Bool = false
public var externalConnected: Bool = false
public var timeRemainingMinutes: Int = -1
public var manufacturer: String = "Apple"
public var serial: String = ""
public var adapterWatts: Int = 0
}
public struct PeripheralDeviceItem: Identifiable, Sendable {
public var id: String { "\(busType)_\(locationID)_\(name)" }
public let name: String
public let busType: String
public let vendorName: String
public let vendorID: UInt32
public let productID: UInt32
public let serialNumber: String
public let locationID: UInt64
public let isBuiltIn: Bool
}
public struct AudioDeviceItem: Identifiable, Sendable {
public var id: UInt32 { deviceID }
public let deviceID: UInt32
public let name: String
public let manufacturer: String
public let isInput: Bool
public let isOutput: Bool
public let isDefaultInput: Bool
public let isDefaultOutput: Bool
public let sampleRate: Double
public let channelCount: UInt32
public let volume: Float
public let isMuted: Bool
}
// MARK: - SystemTelemetryStore // MARK: - SystemTelemetryStore
@Observable @Observable
@@ -95,7 +232,7 @@ public final class SystemTelemetryStore {
/// Raw snapshots organized by provider identifier /// Raw snapshots organized by provider identifier
public private(set) var latestSnapshot: [String: [String: Any]] = [:] public private(set) var latestSnapshot: [String: [String: Any]] = [:]
/// Structured Telemetry Metrics /// Milestone 2 Structured Metrics
public private(set) var cpuLoad = CPULoadMetrics() public private(set) var cpuLoad = CPULoadMetrics()
public private(set) var memory = MemoryMetrics() public private(set) var memory = MemoryMetrics()
public private(set) var storageVolumes: [StorageVolumeItem] = [] public private(set) var storageVolumes: [StorageVolumeItem] = []
@@ -104,6 +241,21 @@ public final class SystemTelemetryStore {
public private(set) var componentTemps: [ComponentThermalItem] = [] public private(set) var componentTemps: [ComponentThermalItem] = []
public private(set) var power = PowerMetrics() public private(set) var power = PowerMetrics()
/// Milestone 3 Structured Metrics
public private(set) var kernelCounters = KernelCountersMetrics()
public private(set) var systemLoad = SystemLoadMetrics()
public private(set) var diskIO: [DiskIOItem] = []
public private(set) var networkBandwidth: [NetworkBandwidthItem] = []
public private(set) var networkSockets: [SocketItem] = []
public private(set) var processes: [ProcessItem] = []
public private(set) var gpus: [GPUCardItem] = []
public private(set) var batteryHealth = BatteryHealthMetrics()
public private(set) var peripherals: [PeripheralDeviceItem] = []
public private(set) var audioDevices: [AudioDeviceItem] = []
/// Process detail inspection helper
public let processInspector = MMProcessDetailInspector()
/// System identification /// System identification
public let hostModel: String public let hostModel: String
public let osVersion: String public let osVersion: String
@@ -125,7 +277,7 @@ public final class SystemTelemetryStore {
} }
private func registerDefaultProviders() { private func registerDefaultProviders() {
// Register all Primary System & Thermal Telemetry providers (M2) // Milestone 2 Primary System & Thermal Telemetry
coordinator.register(MMCPULoadProvider()) coordinator.register(MMCPULoadProvider())
coordinator.register(MMMemoryTelemetryProvider()) coordinator.register(MMMemoryTelemetryProvider())
coordinator.register(MMStorageTelemetryProvider()) coordinator.register(MMStorageTelemetryProvider())
@@ -133,6 +285,18 @@ public final class SystemTelemetryStore {
coordinator.register(MMFanTelemetryProvider()) coordinator.register(MMFanTelemetryProvider())
coordinator.register(MMComponentThermalProvider()) coordinator.register(MMComponentThermalProvider())
coordinator.register(MMPowerTelemetryProvider()) coordinator.register(MMPowerTelemetryProvider())
// Milestone 3 Advanced Kernel, Process & Peripheral Telemetry
coordinator.register(MMKernelTelemetryProvider())
coordinator.register(MMLoadAverageProvider())
coordinator.register(MMDiskIOProvider())
coordinator.register(MMNetworkBandwidthProvider())
coordinator.register(MMNetworkSocketsProvider())
coordinator.register(MMProcessTelemetryProvider())
coordinator.register(MMGPUTelemetryProvider())
coordinator.register(MMBatteryTelemetryProvider())
coordinator.register(MMPeripheralsProvider())
coordinator.register(MMAudioTelemetryProvider())
} }
private func setupCoordinator() { private func setupCoordinator() {
@@ -178,7 +342,7 @@ public final class SystemTelemetryStore {
self.memory = m self.memory = m
} }
// 3. Storage // 3. Storage Volumes
if let stgDict = snapshot["com.i3omb.macmonitor.telemetry.storage"], if let stgDict = snapshot["com.i3omb.macmonitor.telemetry.storage"],
let volArray = stgDict["volumes"] as? [[String: Any]] { let volArray = stgDict["volumes"] as? [[String: Any]] {
self.storageVolumes = volArray.compactMap { dict in self.storageVolumes = volArray.compactMap { dict in
@@ -269,6 +433,230 @@ public final class SystemTelemetryStore {
} }
self.power = m self.power = m
} }
// 8. Kernel Counters
if let kDict = snapshot["com.i3omb.macmonitor.telemetry.kernel"] {
var m = KernelCountersMetrics()
m.contextSwitchesPerSec = (kDict["contextSwitchesPerSec"] as? NSNumber)?.doubleValue ?? 0
m.syscallsPerSec = (kDict["syscallsPerSec"] as? NSNumber)?.doubleValue ?? 0
m.pageFaultsPerSec = (kDict["pageFaultsPerSec"] as? NSNumber)?.doubleValue ?? 0
m.cowFaultsPerSec = (kDict["cowFaultsPerSec"] as? NSNumber)?.doubleValue ?? 0
m.zeroFillFaultsPerSec = (kDict["zeroFillFaultsPerSec"] as? NSNumber)?.doubleValue ?? 0
m.pageinsPerSec = (kDict["pageinsPerSec"] as? NSNumber)?.doubleValue ?? 0
m.pageoutsPerSec = (kDict["pageoutsPerSec"] as? NSNumber)?.doubleValue ?? 0
m.totalContextSwitches = (kDict["totalContextSwitches"] as? NSNumber)?.uint64Value ?? 0
m.totalSyscalls = (kDict["totalSyscalls"] as? NSNumber)?.uint64Value ?? 0
m.totalPageFaults = (kDict["totalPageFaults"] as? NSNumber)?.uint64Value ?? 0
self.kernelCounters = m
}
// 9. System Load Average & Mach Factor
if let loadDict = snapshot["com.i3omb.macmonitor.telemetry.systemload"] {
var m = SystemLoadMetrics()
m.load1Min = (loadDict["load1Min"] as? NSNumber)?.doubleValue ?? 0
m.load5Min = (loadDict["load5Min"] as? NSNumber)?.doubleValue ?? 0
m.load15Min = (loadDict["load15Min"] as? NSNumber)?.doubleValue ?? 0
m.taskCount = (loadDict["taskCount"] as? NSNumber)?.intValue ?? 0
m.threadCount = (loadDict["threadCount"] as? NSNumber)?.intValue ?? 0
m.machFactor = (loadDict["machFactor"] as? NSNumber)?.doubleValue ?? 0
self.systemLoad = m
}
// 10. Disk I/O & IOPS
if let diskDict = snapshot["com.i3omb.macmonitor.telemetry.diskio"],
let disks = diskDict["disks"] as? [[String: Any]] {
self.diskIO = disks.compactMap { d in
guard let bsd = d["bsdName"] as? String else { return nil }
return DiskIOItem(
bsdName: bsd,
readBps: (d["readBps"] as? NSNumber)?.doubleValue ?? 0,
writeBps: (d["writeBps"] as? NSNumber)?.doubleValue ?? 0,
readIOPS: (d["readIOPS"] as? NSNumber)?.doubleValue ?? 0,
writeIOPS: (d["writeIOPS"] as? NSNumber)?.doubleValue ?? 0,
totalBytesRead: (d["totalBytesRead"] as? NSNumber)?.uint64Value ?? 0,
totalBytesWritten: (d["totalBytesWritten"] as? NSNumber)?.uint64Value ?? 0
)
}
}
// 11. Network Bandwidth
if let netDict = snapshot["com.i3omb.macmonitor.telemetry.network.bandwidth"],
let ifaces = netDict["interfaces"] as? [[String: Any]] {
self.networkBandwidth = ifaces.compactMap { d in
guard let name = d["interfaceName"] as? String else { return nil }
return NetworkBandwidthItem(
interfaceName: name,
ipAddress: d["ipAddress"] as? String ?? "",
isUp: (d["isUp"] as? NSNumber)?.boolValue ?? false,
downloadBps: (d["downloadBps"] as? NSNumber)?.doubleValue ?? 0,
uploadBps: (d["uploadBps"] as? NSNumber)?.doubleValue ?? 0,
downloadPps: (d["downloadPps"] as? NSNumber)?.doubleValue ?? 0,
uploadPps: (d["uploadPps"] as? NSNumber)?.doubleValue ?? 0,
totalBytesIn: (d["totalBytesIn"] as? NSNumber)?.uint64Value ?? 0,
totalBytesOut: (d["totalBytesOut"] as? NSNumber)?.uint64Value ?? 0
)
}
}
// 12. Network Sockets
if let sockDict = snapshot["com.i3omb.macmonitor.telemetry.network.sockets"],
let socks = sockDict["sockets"] as? [[String: Any]] {
self.networkSockets = socks.compactMap { d in
let pid = (d["pid"] as? NSNumber)?.int32Value ?? 0
let fd = (d["fd"] as? NSNumber)?.int32Value ?? 0
return SocketItem(
pid: pid,
fd: fd,
processName: d["processName"] as? String ?? "",
protocolName: d["protocol"] as? String ?? "TCP",
localAddress: d["localAddress"] as? String ?? "*",
localPort: (d["localPort"] as? NSNumber)?.uint16Value ?? 0,
remoteAddress: d["remoteAddress"] as? String ?? "*",
remotePort: (d["remotePort"] as? NSNumber)?.uint16Value ?? 0,
tcpState: d["tcpState"] as? String ?? "ESTABLISHED"
)
}
}
// 13. Processes
if let procDict = snapshot["com.i3omb.macmonitor.telemetry.process"],
let procs = procDict["processes"] as? [[String: Any]] {
self.processes = procs.compactMap { d in
guard let pid = (d["pid"] as? NSNumber)?.int32Value else { return nil }
return ProcessItem(
pid: pid,
ppid: (d["ppid"] as? NSNumber)?.int32Value ?? 0,
name: d["name"] as? String ?? "Unknown",
cpuPercent: (d["cpuPercent"] as? NSNumber)?.doubleValue ?? 0,
residentSize: (d["residentSize"] as? NSNumber)?.uint64Value ?? 0,
virtualSize: (d["virtualSize"] as? NSNumber)?.uint64Value ?? 0,
threadCount: (d["threadCount"] as? NSNumber)?.int32Value ?? 1,
uid: (d["uid"] as? NSNumber)?.uint32Value ?? 0,
username: d["username"] as? String ?? "",
isStopped: (d["isStopped"] as? NSNumber)?.boolValue ?? false,
isZombie: (d["isZombie"] as? NSNumber)?.boolValue ?? false
)
}
}
// 14. GPU Telemetry
if let gpuDict = snapshot["com.i3omb.macmonitor.telemetry.gpu"],
let cards = gpuDict["gpus"] as? [[String: Any]] {
self.gpus = cards.compactMap { d in
guard let name = d["name"] as? String else { return nil }
return GPUCardItem(
name: name,
isDiscrete: (d["isDiscrete"] as? NSNumber)?.boolValue ?? false,
isLowPower: (d["isLowPower"] as? NSNumber)?.boolValue ?? false,
vramTotalBytes: (d["vramTotalBytes"] as? NSNumber)?.uint64Value ?? 0,
vramUsedBytes: (d["vramUsedBytes"] as? NSNumber)?.uint64Value ?? 0,
utilizationPercent: (d["utilizationPercent"] as? NSNumber)?.doubleValue ?? 0,
temperature: (d["temperature"] as? NSNumber)?.doubleValue ?? 0
)
}
}
// 15. Battery Health
if let battDict = snapshot["com.i3omb.macmonitor.telemetry.battery"] {
var m = BatteryHealthMetrics()
m.hasBattery = (battDict["hasBattery"] as? NSNumber)?.boolValue ?? false
m.installed = (battDict["installed"] as? NSNumber)?.boolValue ?? false
m.healthCondition = battDict["healthCondition"] as? String ?? "Normal"
m.healthPercent = (battDict["healthPercent"] as? NSNumber)?.doubleValue ?? 100.0
m.cycleCount = (battDict["cycleCount"] as? NSNumber)?.intValue ?? 0
m.designCycleCount = (battDict["designCycleCount"] as? NSNumber)?.intValue ?? 1000
m.currentCapacity = (battDict["currentCapacity"] as? NSNumber)?.intValue ?? 0
m.maxCapacity = (battDict["maxCapacity"] as? NSNumber)?.intValue ?? 0
m.designCapacity = (battDict["designCapacity"] as? NSNumber)?.intValue ?? 0
m.temperature = (battDict["temperature"] as? NSNumber)?.doubleValue ?? 0
m.voltage = (battDict["voltage"] as? NSNumber)?.doubleValue ?? 0
m.amperage = (battDict["amperage"] as? NSNumber)?.doubleValue ?? 0
m.watts = (battDict["watts"] as? NSNumber)?.doubleValue ?? 0
m.isCharging = (battDict["isCharging"] as? NSNumber)?.boolValue ?? false
m.isCharged = (battDict["isCharged"] as? NSNumber)?.boolValue ?? false
m.externalConnected = (battDict["externalConnected"] as? NSNumber)?.boolValue ?? false
m.timeRemainingMinutes = (battDict["timeRemainingMinutes"] as? NSNumber)?.intValue ?? -1
m.manufacturer = battDict["manufacturer"] as? String ?? "Apple"
m.serial = battDict["serial"] as? String ?? ""
m.adapterWatts = (battDict["adapterWatts"] as? NSNumber)?.intValue ?? 0
self.batteryHealth = m
}
// 16. Connected Peripherals
if let periphDict = snapshot["com.i3omb.macmonitor.telemetry.peripherals"] {
var allDevs: [PeripheralDeviceItem] = []
if let usb = periphDict["usbDevices"] as? [[String: Any]] {
allDevs.append(contentsOf: usb.compactMap { d in
guard let name = d["name"] as? String else { return nil }
return PeripheralDeviceItem(
name: name,
busType: "USB",
vendorName: d["vendorName"] as? String ?? "",
vendorID: (d["vendorID"] as? NSNumber)?.uint32Value ?? 0,
productID: (d["productID"] as? NSNumber)?.uint32Value ?? 0,
serialNumber: d["serialNumber"] as? String ?? "",
locationID: (d["locationID"] as? NSNumber)?.uint64Value ?? 0,
isBuiltIn: (d["isBuiltIn"] as? NSNumber)?.boolValue ?? false
)
})
}
if let tb = periphDict["thunderboltDevices"] as? [[String: Any]] {
allDevs.append(contentsOf: tb.compactMap { d in
guard let name = d["name"] as? String else { return nil }
return PeripheralDeviceItem(
name: name,
busType: "Thunderbolt",
vendorName: d["vendorName"] as? String ?? "",
vendorID: (d["vendorID"] as? NSNumber)?.uint32Value ?? 0,
productID: (d["productID"] as? NSNumber)?.uint32Value ?? 0,
serialNumber: "",
locationID: 0,
isBuiltIn: false
)
})
}
if let pci = periphDict["pciDevices"] as? [[String: Any]] {
allDevs.append(contentsOf: pci.compactMap { d in
guard let name = d["name"] as? String else { return nil }
return PeripheralDeviceItem(
name: name,
busType: "PCI",
vendorName: "Apple / Intel",
vendorID: (d["vendorID"] as? NSNumber)?.uint32Value ?? 0,
productID: (d["productID"] as? NSNumber)?.uint32Value ?? 0,
serialNumber: "",
locationID: 0,
isBuiltIn: true
)
})
}
self.peripherals = allDevs
}
// 17. Audio Devices
if let audioDict = snapshot["com.i3omb.macmonitor.telemetry.audio"],
let devs = audioDict["devices"] as? [[String: Any]] {
self.audioDevices = devs.compactMap { d in
guard let devID = (d["deviceID"] as? NSNumber)?.uint32Value else { return nil }
return AudioDeviceItem(
deviceID: devID,
name: d["name"] as? String ?? "Audio Device",
manufacturer: d["manufacturer"] as? String ?? "Apple Inc.",
isInput: (d["isInput"] as? NSNumber)?.boolValue ?? false,
isOutput: (d["isOutput"] as? NSNumber)?.boolValue ?? false,
isDefaultInput: (d["isDefaultInput"] as? NSNumber)?.boolValue ?? false,
isDefaultOutput: (d["isDefaultOutput"] as? NSNumber)?.boolValue ?? false,
sampleRate: (d["sampleRate"] as? NSNumber)?.doubleValue ?? 44100.0,
channelCount: (d["channelCount"] as? NSNumber)?.uint32Value ?? 2,
volume: (d["volume"] as? NSNumber)?.floatValue ?? 0,
isMuted: (d["isMuted"] as? NSNumber)?.boolValue ?? false
)
}
}
}
public func terminateProcess(pid: pid_t, force: Bool) -> Bool {
return MMProcessTelemetryProvider.terminateProcess(withPID: pid, force: force)
} }
public func start() { public func start() {
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -28,7 +28,7 @@ final class MacMonitorCoreTests: XCTestCase {
func testSystemTelemetryStoreInitialStateAndProviders() { func testSystemTelemetryStoreInitialStateAndProviders() {
let store = SystemTelemetryStore.shared let store = SystemTelemetryStore.shared
XCTAssertNotNil(store) XCTAssertNotNil(store)
XCTAssertEqual(store.registeredProviderCount, 7) XCTAssertEqual(store.registeredProviderCount, 17)
XCTAssertFalse(store.hostModel.isEmpty) XCTAssertFalse(store.hostModel.isEmpty)
XCTAssertFalse(store.kernelVersion.isEmpty) XCTAssertFalse(store.kernelVersion.isEmpty)
XCTAssertGreaterThanOrEqual(store.physicalCpuCount, 1) XCTAssertGreaterThanOrEqual(store.physicalCpuCount, 1)