Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

1773 lines
63 KiB
Swift

import Darwin
import Foundation
import IOKit
import IOKit.ps
import MacToolsPluginKit
import SystemConfiguration
protocol SystemStatusSampling: Sendable {
func collectFast(referenceDate: Date) async -> SystemStatusFastSample
func collectSlow() async -> SystemStatusSlowSample
func collectTopProcesses(limit: Int) async -> [SystemStatusTopProcess]
func collectPublicIPAddress() async -> String?
}
actor SystemStatusSampler: SystemStatusSampling {
private let localization: PluginLocalization
private var previousCPUTicks: SystemStatusCPUTicks?
private var previousCPUPowerEnergy: SystemStatusPowerEnergySample?
private var cachedCPUTemperature: Double?
private var cachedGPUTemperature: Double?
private var lastCPUTemperatureDate: Date?
private var lastGPUTemperatureDate: Date?
private var cachedHardware: SystemStatusHardwareSnapshot?
private lazy var smcReader = SystemStatusSMCReader()
private lazy var cpuPowerReader = SystemStatusCPUPowerReader()
private var previousNetworkCounter: SystemStatusNetworkCounter?
private var previousNetworkDate: Date?
private var previousDiskIOCounter: SystemStatusDiskIOCounter?
private var previousDiskIODate: Date?
private var cachedSystemPowerHealthPercent: Int?
private var lastSystemPowerHealthDate: Date?
private var didCacheSystemPowerHealth = false
private var cachedNetworkMetadata: [String: NetworkInterfaceMetadata]?
private var lastNetworkMetadataDate: Date?
private var cachedPrimaryInterfaceName: String?
private var lastPrimaryInterfaceDate: Date?
private var didCachePrimaryInterfaceName = false
private static let systemPowerHealthCacheInterval: TimeInterval = 30
private static let networkMetadataCacheInterval: TimeInterval = 10
init(localization: PluginLocalization = PluginLocalization(bundle: .main)) {
self.localization = localization
}
private var networkInterfaceDisplayNames: NetworkInterfaceDisplayNames {
NetworkInterfaceDisplayNames(
wired: localization.string("network.interface.wired", defaultValue: "有线"),
generic: localization.string("network.interface.generic", defaultValue: "网络"),
multiple: localization.string("network.interface.multiple", defaultValue: "多接口")
)
}
func collectFast(referenceDate: Date) async -> SystemStatusFastSample {
let cpu = await collectCPU(referenceDate: referenceDate)
return SystemStatusFastSample(
cpu: cpu,
memory: Self.collectMemory(),
network: collectNetwork(referenceDate: referenceDate),
disk: collectDiskIO(referenceDate: referenceDate)
)
}
func collectSlow() async -> SystemStatusSlowSample {
SystemStatusSlowSample(
disk: Self.collectDiskCapacity(),
battery: collectBattery(),
gpu: collectGPU(),
hardware: collectHardware()
)
}
func collectTopProcesses(limit: Int = 3) async -> [SystemStatusTopProcess] {
Self.collectTopProcesses(limit: limit)
}
func collectPublicIPAddress() async -> String? {
await Self.collectPublicIPAddress()
}
private func collectCPU(referenceDate: Date) async -> SystemStatusCPUSnapshot {
let temperature = collectCPUTemperature(referenceDate: referenceDate)
var currentDate = referenceDate
var currentTicks = Self.readCPUTicks()
var currentPowerEnergy = cpuPowerReader.readCPUEnergySample(referenceDate: currentDate)
if previousCPUTicks == nil, let initialTicks = currentTicks {
let initialPowerEnergy = currentPowerEnergy
try? await Task.sleep(for: .milliseconds(200))
currentDate = Date()
currentTicks = Self.readCPUTicks()
currentPowerEnergy = cpuPowerReader.readCPUEnergySample(referenceDate: currentDate)
previousCPUTicks = initialTicks
previousCPUPowerEnergy = initialPowerEnergy
}
guard let currentTicks else {
return SystemStatusCPUSnapshot(
usage: nil,
loadAverage1Minute: Self.collectCPULoadAverage(),
temperatureCelsius: temperature,
systemPowerWatts: collectPowerWatts(currentPowerEnergy: currentPowerEnergy),
isCollecting: false
)
}
let usage = previousCPUTicks.flatMap { previousTicks in
SystemStatusCPUUsageCalculator.usage(current: currentTicks, previous: previousTicks)
}
previousCPUTicks = currentTicks
return SystemStatusCPUSnapshot(
usage: usage,
loadAverage1Minute: Self.collectCPULoadAverage(),
temperatureCelsius: temperature,
systemPowerWatts: collectPowerWatts(currentPowerEnergy: currentPowerEnergy),
isCollecting: usage == nil
)
}
private func collectPowerWatts(currentPowerEnergy: SystemStatusPowerEnergySample?) -> Double? {
let cpuPowerWatts = currentPowerEnergy.flatMap { currentPowerEnergy in
defer { previousCPUPowerEnergy = currentPowerEnergy }
return previousCPUPowerEnergy.flatMap { previousPowerEnergy in
SystemStatusPowerCalculator.watts(current: currentPowerEnergy, previous: previousPowerEnergy)
}
}
return cpuPowerWatts
}
private func collectCPUTemperature(referenceDate: Date) -> Double? {
if let lastCPUTemperatureDate, referenceDate.timeIntervalSince(lastCPUTemperatureDate) < 5 {
return cachedCPUTemperature
}
let temperature = Self.collectCPUTemperature(smcReader: smcReader)
cachedCPUTemperature = temperature
lastCPUTemperatureDate = referenceDate
return temperature
}
private func collectGPUTemperature(referenceDate: Date) -> Double? {
if let lastGPUTemperatureDate, referenceDate.timeIntervalSince(lastGPUTemperatureDate) < 5 {
return cachedGPUTemperature
}
let temperature = Self.collectGPUTemperature(smcReader: smcReader)
cachedGPUTemperature = temperature
lastGPUTemperatureDate = referenceDate
return temperature
}
private func collectHardware() -> SystemStatusHardwareSnapshot {
if let cachedHardware {
return cachedHardware.replacingUptime(Self.collectUptimeSeconds())
}
let hardware = SystemStatusHardwareSnapshot(
modelName: Self.collectHardwareString("hw.model"),
chipName: Self.collectHardwareString("machdep.cpu.brand_string"),
macOSVersion: Self.collectMacOSVersionString(),
uptimeSeconds: Self.collectUptimeSeconds(),
totalMemoryBytes: ProcessInfo.processInfo.physicalMemory
)
cachedHardware = hardware
return hardware
}
private func collectNetwork(referenceDate: Date) -> SystemStatusNetworkSnapshot {
guard let currentCounter = currentNetworkCounter(referenceDate: referenceDate) else {
previousNetworkCounter = nil
previousNetworkDate = referenceDate
return SystemStatusNetworkSnapshot(
interfaceName: nil,
ipAddress: nil,
publicIPAddress: nil,
downloadBytesPerSecond: nil,
uploadBytesPerSecond: nil,
isConnected: false,
isCollecting: false
)
}
let rate: SystemStatusNetworkRate?
if
let previousNetworkCounter,
let previousNetworkDate,
previousNetworkCounter.key == currentCounter.key
{
rate = SystemStatusNetworkRateCalculator.rate(
current: currentCounter,
previous: previousNetworkCounter,
elapsedSeconds: referenceDate.timeIntervalSince(previousNetworkDate)
)
} else {
rate = nil
}
previousNetworkCounter = currentCounter
previousNetworkDate = referenceDate
return SystemStatusNetworkSnapshot(
interfaceName: currentCounter.displayName,
ipAddress: currentCounter.ipAddress,
publicIPAddress: nil,
downloadBytesPerSecond: rate?.downloadBytesPerSecond ?? 0,
uploadBytesPerSecond: rate?.uploadBytesPerSecond ?? 0,
isConnected: currentCounter.isUp,
isCollecting: rate == nil
)
}
private static func readCPUTicks() -> SystemStatusCPUTicks? {
let count = MemoryLayout<host_cpu_load_info_data_t>.stride / MemoryLayout<integer_t>.stride
var size = mach_msg_type_number_t(count)
var info = host_cpu_load_info()
let result = withUnsafeMutablePointer(to: &info) { pointer in
pointer.withMemoryRebound(to: integer_t.self, capacity: count) { reboundPointer in
host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, reboundPointer, &size)
}
}
guard result == KERN_SUCCESS else {
return nil
}
return SystemStatusCPUTicks(
user: tickValue(info.cpu_ticks.0),
system: tickValue(info.cpu_ticks.1),
idle: tickValue(info.cpu_ticks.2),
nice: tickValue(info.cpu_ticks.3)
)
}
private static func tickValue(_ value: natural_t) -> UInt64 {
UInt64(value)
}
private static func collectCPULoadAverage() -> Double? {
var averages = [Double](repeating: 0, count: 3)
guard getloadavg(&averages, Int32(averages.count)) > 0 else {
return nil
}
let load = averages[0]
guard load >= 0, load.isFinite else {
return nil
}
return load
}
private static func collectCPUTemperature(smcReader: SystemStatusSMCReader?) -> Double? {
if let smcTemperature = collectSMCCPUTemperature(smcReader: smcReader) {
return smcTemperature
}
let values = collectHIDSensorTemperatures(
keyPrefixes: ["pACC MTR Temp", "eACC MTR Temp"]
)
guard !values.isEmpty else {
return nil
}
return values.reduce(0, +) / Double(values.count)
}
private static func collectSMCCPUTemperature(smcReader: SystemStatusSMCReader?) -> Double? {
guard let smcReader else {
return nil
}
let directKeys = ["TC0D", "TC0E", "TC0F", "TC0P", "TC0H", "TCAD"]
for key in directKeys {
if let value = smcReader.value(for: key), isValidTemperature(value) {
return value
}
}
let appleSiliconKeys = [
"Tp09", "Tp0T", "Tp01", "Tp05", "Tp0D", "Tp0H", "Tp0L", "Tp0P", "Tp0X", "Tp0b",
"Tp1h", "Tp1t", "Tp1p", "Tp1l", "Tp0f", "Tp0j",
"Te05", "Te09", "Te0H", "Te0L", "Te0P", "Te0S",
"Tf04", "Tf09", "Tf0A", "Tf0B", "Tf0D", "Tf0E", "Tf44", "Tf49", "Tf4A", "Tf4B", "Tf4D", "Tf4E",
"Tp0V", "Tp0Y", "Tp0e",
"Tp00", "Tp04", "Tp08", "Tp0C", "Tp0G", "Tp0K", "Tp0O", "Tp0R", "Tp0U", "Tp0a", "Tp0d", "Tp0g", "Tp0m", "Tp0p", "Tp0u", "Tp0y"
]
let values = appleSiliconKeys.compactMap { key in
smcReader.value(for: key)
}.filter(isValidTemperature)
guard !values.isEmpty else {
return nil
}
return values.reduce(0, +) / Double(values.count)
}
private static func collectGPUTemperature(smcReader: SystemStatusSMCReader?) -> Double? {
if let smcTemperature = collectSMCGPUTemperature(smcReader: smcReader) {
return smcTemperature
}
let values = collectHIDSensorTemperatures(
keyPrefixes: ["GPU MTR Temp", "SOC MTR Temp"]
)
guard !values.isEmpty else {
return nil
}
return values.reduce(0, +) / Double(values.count)
}
private static func collectSMCGPUTemperature(smcReader: SystemStatusSMCReader?) -> Double? {
guard let smcReader else {
return nil
}
let keys = [
"TCGC", "TG0D", "TGDD", "TG0H", "TG0P", "TG0T", "TG1D", "TG1P", "TG1H", "TG1T",
"Tg05", "Tg0D", "Tg0L", "Tg0T",
"Tg0f", "Tg0j",
"Tf14", "Tf18", "Tf19", "Tf1A", "Tf24", "Tf28", "Tf29", "Tf2A",
"Tg0G", "Tg0H", "Tg1U", "Tg1k", "Tg0K", "Tg0d", "Tg0e", "Tg0U", "Tg0X", "Tg0g", "Tg1Y", "Tg1c", "Tg1g"
]
let values = keys.compactMap { smcReader.value(for: $0) }.filter(isValidTemperature)
guard !values.isEmpty else {
return nil
}
return values.reduce(0, +) / Double(values.count)
}
nonisolated static func hidSensorTemperatures(output: String, keyPrefixes: [String]) -> [Double] {
let lines = output.components(separatedBy: .newlines)
var isMatchingSensor = false
var values: [Double] = []
for line in lines {
if line.contains("+-o ") || line.contains("| +-o ") {
isMatchingSensor = keyPrefixes.contains { line.localizedCaseInsensitiveContains($0) }
} else if keyPrefixes.contains(where: { line.localizedCaseInsensitiveContains($0) }) {
isMatchingSensor = true
}
guard isMatchingSensor else {
continue
}
let celsiusValues = regexCaptures(#"temperature[^=]*=\s*([0-9]+(?:\.[0-9]+)?)"#, in: line)
.compactMap(Double.init)
.map(normalizedTemperatureCelsius)
.filter(isValidTemperature)
values.append(contentsOf: celsiusValues)
}
return values
}
private static func collectHIDSensorTemperatures(keyPrefixes: [String]) -> [Double] {
var iterator: io_iterator_t = 0
guard IOServiceGetMatchingServices(
kIOMainPortDefault,
IOServiceMatching("IOHIDEventService"),
&iterator
) == KERN_SUCCESS else {
return []
}
defer { IOObjectRelease(iterator) }
var values: [Double] = []
var service = IOIteratorNext(iterator)
while service != 0 {
defer {
IOObjectRelease(service)
service = IOIteratorNext(iterator)
}
var rawProperties: Unmanaged<CFMutableDictionary>?
guard
IORegistryEntryCreateCFProperties(service, &rawProperties, kCFAllocatorDefault, 0) == KERN_SUCCESS,
let properties = rawProperties?.takeRetainedValue() as? [String: Any],
hidSensorMatches(properties: properties, keyPrefixes: keyPrefixes)
else {
continue
}
for key in ["temperature", "Temperature"] {
if
let rawTemperature = numberValue(properties[key]),
isValidTemperature(normalizedTemperatureCelsius(rawTemperature))
{
values.append(normalizedTemperatureCelsius(rawTemperature))
break
}
}
}
return values
}
private static func hidSensorMatches(properties: [String: Any], keyPrefixes: [String]) -> Bool {
let candidates = ["Product", "product", "name", "Name", "IOName"].compactMap { key in
stringValue(properties[key] as Any)
}
return candidates.contains { candidate in
keyPrefixes.contains { candidate.localizedCaseInsensitiveContains($0) }
}
}
private static func isValidTemperature(_ value: Double) -> Bool {
value > 0 && value < 110
}
private static func collectMemory() -> SystemStatusMemorySnapshot {
var stats = vm_statistics64()
var count = mach_msg_type_number_t(
MemoryLayout<vm_statistics64_data_t>.stride / MemoryLayout<integer_t>.stride
)
let result = withUnsafeMutablePointer(to: &stats) { pointer in
pointer.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { reboundPointer in
host_statistics64(mach_host_self(), HOST_VM_INFO64, reboundPointer, &count)
}
}
guard result == KERN_SUCCESS else {
return .empty
}
let pageSize = Double(memoryPageSize())
let active = Double(stats.active_count) * pageSize
let speculative = Double(stats.speculative_count) * pageSize
let inactive = Double(stats.inactive_count) * pageSize
let wired = Double(stats.wire_count) * pageSize
let compressed = Double(stats.compressor_page_count) * pageSize
let purgeable = Double(stats.purgeable_count) * pageSize
let external = Double(stats.external_page_count) * pageSize
let rawUsed = active + inactive + speculative + wired + compressed - purgeable - external
let total = ProcessInfo.processInfo.physicalMemory
let used = UInt64(min(max(rawUsed, 0), Double(total)))
let swapUsage = collectSwapUsage()
return SystemStatusMemorySnapshot(
usedBytes: used,
totalBytes: total,
swapUsedBytes: swapUsage.used,
swapTotalBytes: swapUsage.total
)
}
private func collectGPU() -> SystemStatusGPUSnapshot {
let temperature = collectGPUTemperature(referenceDate: Date())
let matching = IOServiceMatching("IOAccelerator")
var iterator: io_iterator_t = 0
guard IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator) == KERN_SUCCESS else {
let isAvailable = temperature != nil
return SystemStatusGPUSnapshot(
usage: isAvailable ? 0 : nil,
name: nil,
temperatureCelsius: temperature,
isAvailable: isAvailable,
isCollecting: false
)
}
defer { IOObjectRelease(iterator) }
var usages: [Double] = []
var names: [String] = []
var performanceTemperatures: [Double] = []
var didFindAccelerator = false
var service = IOIteratorNext(iterator)
while service != 0 {
defer {
IOObjectRelease(service)
service = IOIteratorNext(iterator)
}
didFindAccelerator = true
if let name = Self.gpuName(service: service) {
names.append(name)
}
let performance = Self.registryPerformanceStatistics(service: service)
if
let performance,
let usage = Self.gpuUtilization(from: performance)
{
usages.append(usage)
}
if
let performance,
let temperature = Self.gpuPerformanceTemperature(from: performance)
{
performanceTemperatures.append(temperature)
}
}
let usage = usages.isEmpty ? nil : min(max(usages.max() ?? 0, 0), 1)
let name = names.first
let resolvedTemperature = temperature ?? performanceTemperatures.max()
let isAvailable = didFindAccelerator || name != nil || resolvedTemperature != nil
guard !usages.isEmpty else {
return SystemStatusGPUSnapshot(
usage: isAvailable ? 0 : nil,
name: name,
temperatureCelsius: resolvedTemperature,
isAvailable: isAvailable,
isCollecting: false
)
}
return SystemStatusGPUSnapshot(
usage: usage,
name: name,
temperatureCelsius: resolvedTemperature,
isAvailable: true,
isCollecting: false
)
}
private static func collectHardwareString(_ key: String) -> String? {
var size = 0
guard sysctlbyname(key, nil, &size, nil, 0) == 0, size > 1 else {
return nil
}
var buffer = [CChar](repeating: 0, count: size)
let result = buffer.withUnsafeMutableBufferPointer { pointer in
sysctlbyname(key, pointer.baseAddress, &size, nil, 0)
}
guard result == 0 else {
return nil
}
let nullIndex = buffer.firstIndex(of: 0) ?? buffer.endIndex
let value = String(decoding: buffer[..<nullIndex].map { UInt8(bitPattern: $0) }, as: UTF8.self)
.trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : value
}
private static func collectMacOSVersionString() -> String {
let version = ProcessInfo.processInfo.operatingSystemVersion
if version.patchVersion > 0 {
return "macOS \(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
}
return "macOS \(version.majorVersion).\(version.minorVersion)"
}
private static func collectUptimeSeconds() -> TimeInterval? {
var bootTime = timeval()
var mib: [Int32] = [CTL_KERN, KERN_BOOTTIME]
var size = MemoryLayout<timeval>.stride
guard sysctl(&mib, UInt32(mib.count), &bootTime, &size, nil, 0) == 0 else {
return nil
}
let bootDate = Date(timeIntervalSince1970: TimeInterval(bootTime.tv_sec))
return max(0, Date().timeIntervalSince(bootDate))
}
nonisolated static func gpuUtilization(from performanceStatistics: [String: Any]) -> Double? {
for key in ["Device Utilization %", "GPU Activity(%)"] {
guard let value = gpuUtilizationValue(performanceStatistics[key]) else {
continue
}
return value
}
return nil
}
private static func gpuUtilizationValue(_ rawValue: Any?) -> Double? {
let value: Double
let isPercentValue: Bool
switch rawValue {
case let intValue as Int:
value = Double(intValue)
isPercentValue = true
case let doubleValue as Double:
value = doubleValue
isPercentValue = doubleValue > 1
case let floatValue as Float:
value = Double(floatValue)
isPercentValue = value > 1
case let numberValue as NSNumber:
value = numberValue.doubleValue
isPercentValue = !CFNumberIsFloatType(numberValue) || value > 1
case let stringValue as String:
let trimmed = stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
guard let parsedValue = Double(trimmed) else {
return nil
}
value = parsedValue
isPercentValue = parsedValue > 1
default:
return nil
}
guard value.isFinite else {
return nil
}
let fraction = isPercentValue ? value / 100 : value
return min(max(fraction, 0), 1)
}
nonisolated static func gpuPerformanceTemperature(from performanceStatistics: [String: Any]) -> Double? {
guard let value = numberValue(performanceStatistics["Temperature(C)"]), isValidTemperature(value) else {
return nil
}
return value
}
nonisolated static func gpuName(from properties: [String: Any]) -> String? {
gpuName { key in
properties[key]
}
}
private static func gpuName(service: io_registry_entry_t) -> String? {
gpuName { key in
registryRawValue(service: service, key: key)
}
}
private static func gpuName(rawValueForKey: (String) -> Any?) -> String? {
for key in ["model", "IOName", "name"] {
guard let rawValue = rawValueForKey(key) else {
continue
}
if let value = stringValue(rawValue), isUserVisibleGPUName(value) {
return normalizedGPUName(value)
}
}
return nil
}
private static func isUserVisibleGPUName(_ value: String) -> Bool {
let lowercased = value.lowercased()
return !value.isEmpty
&& !lowercased.contains("ioaccelerator")
&& !lowercased.contains("accelerator")
&& !lowercased.contains("controller")
}
private static func normalizedGPUName(_ value: String) -> String {
value
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: "Apple ", with: "")
}
private static func memoryPageSize() -> vm_size_t {
var pageSize: vm_size_t = 0
let result = host_page_size(mach_host_self(), &pageSize)
guard result == KERN_SUCCESS, pageSize > 0 else {
return 16_384
}
return pageSize
}
private static func collectSwapUsage() -> (used: UInt64?, total: UInt64?) {
var usage = xsw_usage()
var size = MemoryLayout<xsw_usage>.stride
var mib: [Int32] = [CTL_VM, VM_SWAPUSAGE]
guard sysctl(&mib, UInt32(mib.count), &usage, &size, nil, 0) == 0 else {
return (nil, nil)
}
let total = usage.xsu_total > 0 ? usage.xsu_total : nil
let used = usage.xsu_used > 0 ? usage.xsu_used : UInt64(0)
return (used, total)
}
private static func collectDiskCapacity() -> SystemStatusDiskSnapshot {
let homeURL = FileManager.default.homeDirectoryForCurrentUser
var totalBytes: UInt64?
var availableBytes: UInt64?
do {
let values = try homeURL.resourceValues(forKeys: [
.volumeTotalCapacityKey,
.volumeAvailableCapacityForImportantUsageKey
])
if let totalCapacity = values.volumeTotalCapacity, totalCapacity > 0 {
totalBytes = UInt64(totalCapacity)
}
if let importantCapacity = values.volumeAvailableCapacityForImportantUsage, importantCapacity >= 0 {
availableBytes = UInt64(importantCapacity)
}
} catch {
totalBytes = nil
availableBytes = nil
}
if totalBytes == nil || availableBytes == nil {
if let attributes = try? FileManager.default.attributesOfFileSystem(forPath: homeURL.path) {
if totalBytes == nil, let total = attributes[.systemSize] as? NSNumber {
totalBytes = total.uint64Value
}
if availableBytes == nil, let free = attributes[.systemFreeSize] as? NSNumber {
availableBytes = free.uint64Value
}
}
}
guard let totalBytes, totalBytes > 0, let availableBytes else {
return .empty
}
let clampedAvailable = min(availableBytes, totalBytes)
return SystemStatusDiskSnapshot(
usedBytes: totalBytes - clampedAvailable,
totalBytes: totalBytes,
readBytesPerSecond: nil,
writeBytesPerSecond: nil
)
}
private func collectDiskIO(referenceDate: Date) -> SystemStatusDiskSnapshot {
guard let currentCounter = Self.readDiskIOCounter() else {
previousDiskIOCounter = nil
previousDiskIODate = referenceDate
return SystemStatusDiskSnapshot(
usedBytes: nil,
totalBytes: nil,
readBytesPerSecond: nil,
writeBytesPerSecond: nil
)
}
let rate: SystemStatusDiskIORate?
if let previousDiskIOCounter, let previousDiskIODate {
rate = SystemStatusDiskIORateCalculator.rate(
current: currentCounter,
previous: previousDiskIOCounter,
elapsedSeconds: referenceDate.timeIntervalSince(previousDiskIODate)
)
} else {
rate = nil
}
previousDiskIOCounter = currentCounter
previousDiskIODate = referenceDate
return SystemStatusDiskSnapshot(
usedBytes: nil,
totalBytes: nil,
readBytesPerSecond: rate?.readBytesPerSecond ?? 0,
writeBytesPerSecond: rate?.writeBytesPerSecond ?? 0
)
}
private static func readDiskIOCounter() -> SystemStatusDiskIOCounter? {
var iterator: io_iterator_t = 0
guard IOServiceGetMatchingServices(
kIOMainPortDefault,
IOServiceMatching("IOBlockStorageDriver"),
&iterator
) == KERN_SUCCESS else {
return nil
}
defer { IOObjectRelease(iterator) }
var readBytes: UInt64 = 0
var writeBytes: UInt64 = 0
var foundCounter = false
var service = IOIteratorNext(iterator)
while service != 0 {
defer {
IOObjectRelease(service)
service = IOIteratorNext(iterator)
}
var rawProperties: Unmanaged<CFMutableDictionary>?
guard
IORegistryEntryCreateCFProperties(service, &rawProperties, kCFAllocatorDefault, 0) == KERN_SUCCESS,
let properties = rawProperties?.takeRetainedValue() as? [String: Any],
let statistics = properties["Statistics"] as? [String: Any]
else {
continue
}
if let bytes = statistics["Bytes (Read)"] as? NSNumber {
readBytes &+= bytes.uint64Value
foundCounter = true
}
if let bytes = statistics["Bytes (Write)"] as? NSNumber {
writeBytes &+= bytes.uint64Value
foundCounter = true
}
}
guard foundCounter else {
return nil
}
return SystemStatusDiskIOCounter(readBytes: readBytes, writeBytes: writeBytes)
}
private func collectBattery() -> SystemStatusBatterySnapshot {
guard
let powerSourcesInfo = IOPSCopyPowerSourcesInfo()?.takeRetainedValue(),
let powerSources = IOPSCopyPowerSourcesList(powerSourcesInfo)?.takeRetainedValue() as? [CFTypeRef],
!powerSources.isEmpty
else {
return SystemStatusBatterySnapshot(
isAvailable: false,
level: nil,
state: .unavailable,
timeRemainingMinutes: nil,
adapterWatts: Self.adapterWatts(),
batteryPowerWatts: nil,
temperatureCelsius: nil,
healthPercent: nil,
cycleCount: nil
)
}
var fallbackDescription: [String: Any]?
var batteryDescription: [String: Any]?
for source in powerSources {
guard let description = IOPSGetPowerSourceDescription(powerSourcesInfo, source)?.takeUnretainedValue() as? [String: Any] else {
continue
}
fallbackDescription = fallbackDescription ?? description
if description[kIOPSTypeKey] as? String == kIOPSInternalBatteryType {
batteryDescription = description
break
}
}
guard let description = batteryDescription ?? fallbackDescription else {
return SystemStatusBatterySnapshot(
isAvailable: false,
level: nil,
state: .unavailable,
timeRemainingMinutes: nil,
adapterWatts: Self.adapterWatts(),
batteryPowerWatts: nil,
temperatureCelsius: nil,
healthPercent: nil,
cycleCount: nil
)
}
let maxCapacity = max(description[kIOPSMaxCapacityKey] as? Int ?? 100, 1)
let currentCapacity = min(max(description[kIOPSCurrentCapacityKey] as? Int ?? 0, 0), maxCapacity)
let level = min(max(Double(currentCapacity) / Double(maxCapacity), 0), 1)
let isCharging = description[kIOPSIsChargingKey] as? Bool ?? false
let isCharged = description[kIOPSIsChargedKey] as? Bool ?? false
let powerSource = description[kIOPSPowerSourceStateKey] as? String ?? ""
let state = Self.batteryState(
level: level,
isCharging: isCharging,
isCharged: isCharged,
powerSource: powerSource
)
let timeKey = isCharging ? kIOPSTimeToFullChargeKey : kIOPSTimeToEmptyKey
let registryInfo = Self.collectBatteryRegistryInfo()
return SystemStatusBatterySnapshot(
isAvailable: true,
level: level,
state: state,
timeRemainingMinutes: Self.validBatteryMinutes(description[timeKey]),
adapterWatts: Self.adapterWatts(),
batteryPowerWatts: registryInfo.batteryPowerWatts,
temperatureCelsius: registryInfo.temperatureCelsius,
healthPercent: systemPowerHealthPercent(referenceDate: Date()) ?? registryInfo.healthPercent,
cycleCount: registryInfo.cycleCount
)
}
private func systemPowerHealthPercent(referenceDate: Date) -> Int? {
if didCacheSystemPowerHealth,
let lastSystemPowerHealthDate,
referenceDate.timeIntervalSince(lastSystemPowerHealthDate) < Self.systemPowerHealthCacheInterval {
return cachedSystemPowerHealthPercent
}
let healthPercent: Int?
if let output = Self.runCommand(
path: "/usr/sbin/system_profiler",
arguments: ["SPPowerDataType", "-json"],
timeout: 3
) {
healthPercent = Self.systemPowerBatteryHealthPercent(fromSystemProfilerJSON: output)
} else {
healthPercent = nil
}
cachedSystemPowerHealthPercent = healthPercent
lastSystemPowerHealthDate = referenceDate
didCacheSystemPowerHealth = true
return healthPercent
}
private static func batteryState(
level: Double,
isCharging: Bool,
isCharged: Bool,
powerSource: String
) -> SystemStatusBatteryState {
if isCharged || level >= 0.999 {
return .charged
}
if isCharging {
return .charging
}
if powerSource == "AC Power" {
return .acPower
}
if powerSource == "Battery Power" {
return .unplugged
}
return .unknown
}
private static func validBatteryMinutes(_ value: Any?) -> Int? {
guard let minutes = value as? Int, minutes >= 0 else {
return nil
}
return minutes
}
private static func adapterWatts() -> Int? {
guard
let adapterDetails = IOPSCopyExternalPowerAdapterDetails()?.takeRetainedValue() as? [String: Any],
let watts = adapterDetails[kIOPSPowerAdapterWattsKey] as? Int,
watts > 0
else {
return nil
}
return watts
}
private static func collectBatteryRegistryInfo() -> (
temperatureCelsius: Double?,
healthPercent: Int?,
cycleCount: Int?,
batteryPowerWatts: Double?
) {
let service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSmartBattery"))
guard service != 0 else {
return (nil, nil, nil, nil)
}
defer { IOObjectRelease(service) }
let temperature = registryIntValue(service: service, key: "Temperature")
.map { Double($0) / 100 }
let healthPercent = optionalBatteryHealthPercent(
designCapacity: registryIntValue(service: service, key: "DesignCapacity"),
nominalChargeCapacity: registryIntValue(service: service, key: "NominalChargeCapacity"),
appleRawMaxCapacity: registryIntValue(service: service, key: "AppleRawMaxCapacity")
)
return (
temperature,
healthPercent,
registryIntValue(service: service, key: "CycleCount"),
batteryPowerWatts(service: service)
)
}
private static func batteryPowerWatts(service: io_registry_entry_t) -> Double? {
if
let telemetry = registryDictionaryValue(service: service, key: "PowerTelemetryData"),
let watts = SystemStatusBatteryPowerNormalizer.telemetryWatts(
fromRawMilliwatts: telemetry["BatteryPower"]
)
{
return watts
}
if
let watts = SystemStatusBatteryPowerNormalizer.telemetryWatts(
fromRawMilliwatts: registryRawValue(service: service, key: "BatteryPower")
)
{
return watts
}
let voltageMillivolts = registryNumberValue(service: service, key: "AppleRawBatteryVoltage")
?? registryNumberValue(service: service, key: "Voltage")
let amperageMilliamps = nonzeroNumberValue(service: service, key: "InstantAmperage")
?? nonzeroNumberValue(service: service, key: "Amperage")
return SystemStatusBatteryPowerNormalizer.derivedWatts(
voltageMillivolts: voltageMillivolts,
amperageMilliamps: amperageMilliamps
)
}
nonisolated static func batteryHealthPercent(
designCapacity: Int?,
nominalChargeCapacity: Int?,
appleRawMaxCapacity: Int?
) -> Int {
// ioreg fallback matching Mole status: prefer NominalChargeCapacity, then AppleRawMaxCapacity.
optionalBatteryHealthPercent(
designCapacity: designCapacity,
nominalChargeCapacity: nominalChargeCapacity,
appleRawMaxCapacity: appleRawMaxCapacity
) ?? 0
}
nonisolated static func systemPowerBatteryHealthPercent(fromSystemProfilerJSON output: String) -> Int? {
guard
let data = output.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let sections = json["SPPowerDataType"] as? [[String: Any]]
else {
return nil
}
for section in sections {
guard
let info = section["sppower_battery_health_info"] as? [String: Any],
let rawCapacity = info["sppower_battery_health_maximum_capacity"] as? String,
let percent = batteryHealthPercent(fromSystemProfilerValue: rawCapacity)
else {
continue
}
return percent
}
return nil
}
nonisolated static func batteryHealthPercent(fromSystemProfilerValue rawValue: String) -> Int? {
let normalized = rawValue
.replacingOccurrences(of: "\u{00a0}", with: " ")
.replacingOccurrences(of: "%", with: "")
.trimmingCharacters(in: .whitespacesAndNewlines)
guard let value = Int(normalized), value > 0 else {
return nil
}
return min(max(value, 0), 100)
}
private nonisolated static func optionalBatteryHealthPercent(
designCapacity: Int?,
nominalChargeCapacity: Int?,
appleRawMaxCapacity: Int?
) -> Int? {
guard let designCapacity, designCapacity > 0 else {
return nil
}
let capacity = positiveCapacity(nominalChargeCapacity)
?? positiveCapacity(appleRawMaxCapacity)
guard let capacity else {
return nil
}
let percent = (Double(capacity) * 100 / Double(designCapacity)).rounded()
guard percent > 0 else {
return nil
}
return min(max(Int(percent), 0), 100)
}
private nonisolated static func positiveCapacity(_ value: Int?) -> Int? {
guard let value, value > 0 else {
return nil
}
return value
}
private static func registryIntValue(service: io_registry_entry_t, key: String) -> Int? {
guard let rawValue = registryRawValue(service: service, key: key) else {
return nil
}
if let intValue = rawValue as? Int {
return intValue
}
if let numberValue = rawValue as? NSNumber {
return numberValue.intValue
}
return nil
}
private static func registryNumberValue(service: io_registry_entry_t, key: String) -> Double? {
guard let rawValue = registryRawValue(service: service, key: key) else {
return nil
}
return numberValue(rawValue)
}
private static func nonzeroNumberValue(service: io_registry_entry_t, key: String) -> Double? {
guard let value = registryNumberValue(service: service, key: key), value != 0 else {
return nil
}
return value
}
private static func registryRawValue(service: io_registry_entry_t, key: String) -> Any? {
IORegistryEntryCreateCFProperty(service, key as CFString, kCFAllocatorDefault, 0)?
.takeRetainedValue()
}
private static func registryDictionaryValue(service: io_registry_entry_t, key: String) -> NSDictionary? {
guard let rawValue = registryRawValue(service: service, key: key) else {
return nil
}
return rawValue as? NSDictionary
}
private static func registryPerformanceStatistics(service: io_registry_entry_t) -> [String: Any]? {
guard let rawValue = registryRawValue(service: service, key: "PerformanceStatistics") else {
return nil
}
if let value = rawValue as? [String: Any] {
return value
}
if let value = rawValue as? NSDictionary {
return value as? [String: Any]
}
return nil
}
private static func dictionaryNumberValue(_ dictionary: NSDictionary, key: String) -> Double? {
numberValue(dictionary[key])
}
private static func numberValue(_ rawValue: Any?) -> Double? {
if let intValue = rawValue as? Int {
return Double(intValue)
}
if let doubleValue = rawValue as? Double {
return doubleValue
}
if let numberValue = rawValue as? NSNumber {
return numberValue.doubleValue
}
if let stringValue = rawValue as? String {
return Double(stringValue)
}
return nil
}
private static func stringValue(_ rawValue: Any) -> String? {
if let value = rawValue as? String {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
if let value = rawValue as? Data {
let trimmed = String(decoding: value, as: UTF8.self)
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines.union(.controlCharacters))
return trimmed.isEmpty ? nil : trimmed
}
return nil
}
private func currentNetworkCounter(referenceDate: Date) -> SystemStatusNetworkCounter? {
let interfaceMetadata = networkInterfaceMetadata(referenceDate: referenceDate)
let displayNames = networkInterfaceDisplayNames
let counters = Self.readNetworkCounters(interfaceMetadata: interfaceMetadata, displayNames: displayNames)
let aggregateCounter = Self.readAggregateNetworkCounter(
interfaceMetadata: interfaceMetadata,
displayNames: displayNames
)
guard !counters.isEmpty else {
return aggregateCounter
}
if
let primaryInterface = primaryInterfaceName(referenceDate: referenceDate),
let primaryCounter = counters[primaryInterface]
{
return primaryCounter.replacingCounters(from: aggregateCounter)
}
let candidates = counters.values
.filter { $0.isUp && !Self.isNoiseInterface($0.key) }
.sorted { lhs, rhs in
if lhs.receivedBytes + lhs.sentBytes == rhs.receivedBytes + rhs.sentBytes {
return lhs.key < rhs.key
}
return lhs.receivedBytes + lhs.sentBytes > rhs.receivedBytes + rhs.sentBytes
}
guard !candidates.isEmpty else {
return aggregateCounter
}
return aggregateNetworkCounters(candidates, displayNames: displayNames).replacingCounters(from: aggregateCounter)
}
private func networkInterfaceMetadata(referenceDate: Date) -> [String: NetworkInterfaceMetadata] {
if
let cachedNetworkMetadata,
let lastNetworkMetadataDate,
referenceDate.timeIntervalSince(lastNetworkMetadataDate) < Self.networkMetadataCacheInterval
{
return cachedNetworkMetadata
}
let metadata = Self.networkInterfaceMetadata()
cachedNetworkMetadata = metadata
lastNetworkMetadataDate = referenceDate
return metadata
}
private func primaryInterfaceName(referenceDate: Date) -> String? {
if didCachePrimaryInterfaceName,
let lastPrimaryInterfaceDate,
referenceDate.timeIntervalSince(lastPrimaryInterfaceDate) < Self.networkMetadataCacheInterval {
return cachedPrimaryInterfaceName
}
let name = Self.primaryInterfaceName()
cachedPrimaryInterfaceName = name
lastPrimaryInterfaceDate = referenceDate
didCachePrimaryInterfaceName = true
return name
}
private static func primaryInterfaceName() -> String? {
guard
let global = SCDynamicStoreCopyValue(nil, "State:/Network/Global/IPv4" as CFString) as? [String: Any],
let name = global["PrimaryInterface"] as? String,
!name.isEmpty
else {
return nil
}
return name
}
private static func readNetworkCounters(
interfaceMetadata: [String: NetworkInterfaceMetadata],
displayNames: NetworkInterfaceDisplayNames = .default
) -> [String: SystemStatusNetworkCounter] {
var interfaceAddresses: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&interfaceAddresses) == 0, let firstAddress = interfaceAddresses else {
return [:]
}
defer { freeifaddrs(interfaceAddresses) }
var accumulators: [String: NetworkCounterAccumulator] = [:]
var pointer: UnsafeMutablePointer<ifaddrs>? = firstAddress
while let currentPointer = pointer {
defer { pointer = currentPointer.pointee.ifa_next }
let name = String(cString: currentPointer.pointee.ifa_name)
var accumulator = accumulators[name] ?? NetworkCounterAccumulator(name: name)
accumulator.isUp = accumulator.isUp || (currentPointer.pointee.ifa_flags & UInt32(IFF_UP)) != 0
guard let address = currentPointer.pointee.ifa_addr else {
accumulators[name] = accumulator
continue
}
switch Int32(address.pointee.sa_family) {
case AF_LINK:
if let rawData = currentPointer.pointee.ifa_data {
let data = rawData.assumingMemoryBound(to: if_data.self).pointee
accumulator.receivedBytes = UInt64(data.ifi_ibytes)
accumulator.sentBytes = UInt64(data.ifi_obytes)
}
case AF_INET:
if let address = numericAddress(from: address), !address.hasPrefix("127.") {
accumulator.ipv4Address = address
}
case AF_INET6:
if let address = numericAddress(from: address), !address.hasPrefix("fe80") {
accumulator.ipv6Address = address
}
default:
break
}
accumulators[name] = accumulator
}
return Dictionary(uniqueKeysWithValues: accumulators.map { key, value in
(
key,
value.counter(
displayName: friendlyNetworkInterfaceName(
for: key,
metadata: interfaceMetadata[key],
displayNames: displayNames
)
)
)
})
}
private static func readAggregateNetworkCounter(
interfaceMetadata: [String: NetworkInterfaceMetadata],
displayNames: NetworkInterfaceDisplayNames = .default
) -> SystemStatusNetworkCounter? {
var mib: [Int32] = [CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST2, 0]
var length = 0
guard sysctl(&mib, UInt32(mib.count), nil, &length, nil, 0) == 0, length > 0 else {
return nil
}
var buffer = [UInt8](repeating: 0, count: length)
guard sysctl(&mib, UInt32(mib.count), &buffer, &length, nil, 0) == 0 else {
return nil
}
var receivedBytes: UInt64 = 0
var sentBytes: UInt64 = 0
var interfaceNames: [String] = []
buffer.withUnsafeBytes { rawBuffer in
var offset = 0
while offset + MemoryLayout<if_msghdr>.size <= length {
let messageLength = Int(rawBuffer.loadUnaligned(fromByteOffset: offset, as: UInt16.self))
guard messageLength > 0 else {
break
}
let messageType = rawBuffer.loadUnaligned(fromByteOffset: offset + 3, as: UInt8.self)
if Int32(messageType) == RTM_IFINFO2,
offset + MemoryLayout<if_msghdr2>.size <= length {
let message = rawBuffer.loadUnaligned(fromByteOffset: offset, as: if_msghdr2.self)
var nameBuffer = [CChar](repeating: 0, count: Int(IFNAMSIZ) + 1)
let name = if_indextoname(UInt32(message.ifm_index), &nameBuffer)
.map { String(cString: $0) } ?? ""
if !name.isEmpty && !isNoiseInterface(name) {
receivedBytes &+= message.ifm_data.ifi_ibytes
sentBytes &+= message.ifm_data.ifi_obytes
interfaceNames.append(name)
}
}
offset += messageLength
}
}
guard !interfaceNames.isEmpty else {
return nil
}
return SystemStatusNetworkCounter(
key: "iflist2:\(interfaceNames.sorted().joined(separator: ","))",
displayName: interfaceNames.count == 1
? friendlyNetworkInterfaceName(
for: interfaceNames[0],
metadata: interfaceMetadata[interfaceNames[0]],
displayNames: displayNames
)
: displayNames.multiple,
receivedBytes: receivedBytes,
sentBytes: sentBytes,
ipAddress: nil,
isUp: true
)
}
private func aggregateNetworkCounters(
_ counters: [SystemStatusNetworkCounter],
displayNames: NetworkInterfaceDisplayNames
) -> SystemStatusNetworkCounter {
guard counters.count > 1 else {
return counters[0]
}
let sortedKeys = counters.map(\.key).sorted()
let receivedBytes = counters.reduce(UInt64(0)) { $0 + $1.receivedBytes }
let sentBytes = counters.reduce(UInt64(0)) { $0 + $1.sentBytes }
let ipAddress = counters.first(where: { $0.ipAddress != nil })?.ipAddress
return SystemStatusNetworkCounter(
key: "aggregate:\(sortedKeys.joined(separator: ","))",
displayName: displayNames.multiple,
receivedBytes: receivedBytes,
sentBytes: sentBytes,
ipAddress: ipAddress,
isUp: counters.contains(where: \.isUp)
)
}
private static func numericAddress(from pointer: UnsafePointer<sockaddr>) -> String? {
var host = [CChar](repeating: 0, count: Int(NI_MAXHOST))
let result = getnameinfo(
pointer,
socklen_t(pointer.pointee.sa_len),
&host,
socklen_t(host.count),
nil,
0,
NI_NUMERICHOST
)
guard result == 0 else {
return nil
}
let nullIndex = host.firstIndex(of: 0) ?? host.endIndex
let bytes = host[..<nullIndex].map { UInt8(bitPattern: $0) }
let address = String(decoding: bytes, as: UTF8.self)
return address.isEmpty ? nil : address
}
private static func isNoiseInterface(_ name: String) -> Bool {
let lowercasedName = name.lowercased()
let noisePrefixes = ["lo", "awdl", "utun", "llw", "bridge", "gif", "stf", "xhc", "anpi", "ap"]
return noisePrefixes.contains { lowercasedName.hasPrefix($0) }
}
private static func networkInterfaceMetadata() -> [String: NetworkInterfaceMetadata] {
guard let interfaces = SCNetworkInterfaceCopyAll() as? [SCNetworkInterface] else {
return [:]
}
var metadata: [String: NetworkInterfaceMetadata] = [:]
for interface in interfaces {
guard let bsdName = SCNetworkInterfaceGetBSDName(interface) as String?, !bsdName.isEmpty else {
continue
}
metadata[bsdName] = NetworkInterfaceMetadata(
localizedName: SCNetworkInterfaceGetLocalizedDisplayName(interface) as String?,
interfaceType: SCNetworkInterfaceGetInterfaceType(interface) as String?
)
}
return metadata
}
private static func friendlyNetworkInterfaceName(
for name: String,
metadata: NetworkInterfaceMetadata?,
displayNames: NetworkInterfaceDisplayNames = .default
) -> String {
friendlyNetworkInterfaceName(
for: name,
localizedName: metadata?.localizedName,
interfaceType: metadata?.interfaceType,
wiredDisplayName: displayNames.wired,
genericDisplayName: displayNames.generic
)
}
nonisolated static func friendlyNetworkInterfaceName(
for name: String,
localizedName: String? = nil,
interfaceType: String? = nil,
wiredDisplayName: String = "Ethernet",
genericDisplayName: String = "Network"
) -> String {
let rawName = name.trimmingCharacters(in: .whitespacesAndNewlines)
let lowercasedName = rawName.lowercased()
let localizedName = localizedName?.trimmingCharacters(in: .whitespacesAndNewlines)
let localizedDisplayName = localizedName?.isEmpty == false ? localizedName : nil
let lowercasedLocalizedName = localizedDisplayName?.lowercased() ?? ""
let lowercasedInterfaceType = interfaceType?.lowercased() ?? ""
if isVPNInterfaceName(lowercasedName)
|| lowercasedInterfaceType == (kSCNetworkInterfaceTypePPP as String).lowercased()
|| lowercasedInterfaceType == (kSCNetworkInterfaceTypeIPSec as String).lowercased()
|| lowercasedLocalizedName.contains("vpn")
|| lowercasedLocalizedName.contains("tunnel")
{
return "VPN"
}
if lowercasedInterfaceType == (kSCNetworkInterfaceTypeIEEE80211 as String).lowercased()
|| lowercasedLocalizedName.contains("wi-fi")
|| lowercasedLocalizedName.contains("wifi")
|| lowercasedLocalizedName.contains("airport")
|| lowercasedLocalizedName.contains("无线")
{
return "Wi-Fi"
}
if lowercasedInterfaceType == (kSCNetworkInterfaceTypeEthernet as String).lowercased()
|| lowercasedName.hasPrefix("eth")
|| lowercasedLocalizedName.contains("ethernet")
|| lowercasedLocalizedName.contains("以太网")
|| lowercasedLocalizedName.contains("有线")
|| lowercasedLocalizedName.contains("lan")
|| lowercasedLocalizedName.contains("thunderbolt")
|| lowercasedLocalizedName.contains("usb")
{
return wiredDisplayName
}
return localizedDisplayName ?? (rawName.isEmpty ? genericDisplayName : rawName)
}
private static func isVPNInterfaceName(_ lowercasedName: String) -> Bool {
let vpnPrefixes = ["utun", "tun", "tap", "ppp", "ipsec"]
return vpnPrefixes.contains { lowercasedName.hasPrefix($0) }
}
private static func collectTopProcesses(limit: Int) -> [SystemStatusTopProcess] {
guard let output = runCommand(path: "/bin/ps", arguments: ["-ww", "-Aceo", "pid=,pcpu=,pmem=,rss=,command=", "-r"]) else {
return []
}
return SystemStatusProcessParser.parsePSOutput(output, limit: limit)
}
private static func runCommand(path: String, arguments: [String], timeout: TimeInterval = 1) -> String? {
let process = Process()
process.executableURL = URL(fileURLWithPath: path)
process.arguments = arguments
let outputPipe = Pipe()
let errorPipe = Pipe()
process.standardOutput = outputPipe
process.standardError = errorPipe
do {
try process.run()
} catch {
outputPipe.fileHandleForReading.closeFile()
errorPipe.fileHandleForReading.closeFile()
return nil
}
let deadline = Date().addingTimeInterval(timeout)
while process.isRunning, Date() < deadline {
Thread.sleep(forTimeInterval: 0.02)
}
guard !process.isRunning else {
process.terminate()
outputPipe.fileHandleForReading.closeFile()
errorPipe.fileHandleForReading.closeFile()
return nil
}
process.waitUntilExit()
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
outputPipe.fileHandleForReading.closeFile()
errorPipe.fileHandleForReading.closeFile()
guard let output = String(data: outputData, encoding: .utf8), !output.isEmpty else {
return nil
}
return output
}
private static func regexCaptures(_ pattern: String, in value: String) -> [String] {
guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else {
return []
}
let range = NSRange(value.startIndex..<value.endIndex, in: value)
return regex.matches(in: value, range: range).compactMap { match in
guard match.numberOfRanges > 1, let captureRange = Range(match.range(at: 1), in: value) else {
return nil
}
return String(value[captureRange])
}
}
private static func normalizedTemperatureCelsius(_ value: Double) -> Double {
if value > 1_000 {
return value / 100
}
return value
}
private static func collectPublicIPAddress() async -> String? {
let endpoints = [
URL(string: "https://api.ipify.org")!,
URL(string: "https://ifconfig.me/ip")!
]
for endpoint in endpoints {
var request = URLRequest(url: endpoint)
request.timeoutInterval = 2
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard
let httpResponse = response as? HTTPURLResponse,
(200..<300).contains(httpResponse.statusCode),
let rawValue = String(data: data, encoding: .utf8)
else {
continue
}
let ipAddress = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
if isPublicIPAddressCandidate(ipAddress) {
return ipAddress
}
} catch {
continue
}
}
return nil
}
private static func isPublicIPAddressCandidate(_ value: String) -> Bool {
guard !value.isEmpty, value.count <= 64 else {
return false
}
let allowedCharacters = CharacterSet(charactersIn: "0123456789abcdefABCDEF:.%")
return value.rangeOfCharacter(from: allowedCharacters.inverted) == nil
}
private struct NetworkInterfaceMetadata {
let localizedName: String?
let interfaceType: String?
}
private struct NetworkInterfaceDisplayNames {
let wired: String
let generic: String
let multiple: String
static let `default` = NetworkInterfaceDisplayNames(
wired: "Ethernet",
generic: "Network",
multiple: "Multiple Interfaces"
)
}
private struct NetworkCounterAccumulator {
let name: String
var receivedBytes: UInt64 = 0
var sentBytes: UInt64 = 0
var ipv4Address: String?
var ipv6Address: String?
var isUp = false
func counter(displayName: String) -> SystemStatusNetworkCounter {
SystemStatusNetworkCounter(
key: name,
displayName: displayName,
receivedBytes: receivedBytes,
sentBytes: sentBytes,
ipAddress: ipv4Address ?? ipv6Address,
isUp: isUp
)
}
}
}
enum SystemStatusProcessParser {
static func parsePSOutput(_ rawOutput: String, limit: Int) -> [SystemStatusTopProcess] {
guard limit > 0 else {
return []
}
let processes = rawOutput
.split(whereSeparator: \.isNewline)
.compactMap { parseLine(String($0)) }
.sorted { lhs, rhs in
if lhs.cpuPercent != rhs.cpuPercent {
return lhs.cpuPercent > rhs.cpuPercent
}
if lhs.memoryPercent != rhs.memoryPercent {
return lhs.memoryPercent > rhs.memoryPercent
}
return lhs.pid < rhs.pid
}
return Array(processes.prefix(limit))
}
private static func parseLine(_ line: String) -> SystemStatusTopProcess? {
let fields = line.split(whereSeparator: { $0 == " " || $0 == "\t" })
guard fields.count >= 5 else {
return nil
}
guard
let pid = Int(fields[0]),
pid > 0,
let cpuPercent = Double(fields[1].replacingOccurrences(of: ",", with: ".")),
let memoryPercent = Double(fields[2].replacingOccurrences(of: ",", with: ".")),
let residentKilobytes = UInt64(fields[3])
else {
return nil
}
let command = fields[4...].joined(separator: " ")
guard !command.isEmpty else {
return nil
}
return SystemStatusTopProcess(
pid: pid,
displayName: displayName(for: command),
command: command,
cpuPercent: cpuPercent,
memoryPercent: memoryPercent,
memoryBytes: residentKilobytes * 1_024
)
}
private static func displayName(for command: String) -> String {
let displayCommand = appBundlePath(in: command) ?? executablePath(from: command)
let lastPathComponent = URL(fileURLWithPath: displayCommand).lastPathComponent
guard !lastPathComponent.isEmpty else {
return command
}
return lastPathComponent.replacingOccurrences(of: ".app", with: "")
}
private static func executablePath(from command: String) -> String {
let trimmedCommand = command.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmedCommand.hasPrefix("/") else {
return trimmedCommand
}
if let appRange = trimmedCommand.range(of: ".app/") {
return String(trimmedCommand[..<trimmedCommand.index(before: appRange.upperBound)])
}
if let whitespaceIndex = trimmedCommand.firstIndex(where: { $0 == " " || $0 == "\t" }) {
return String(trimmedCommand[..<whitespaceIndex])
}
return trimmedCommand
}
private static func appBundlePath(in command: String) -> String? {
let trimmedCommand = command.trimmingCharacters(in: .whitespacesAndNewlines)
guard
trimmedCommand.hasPrefix("/"),
let appRange = trimmedCommand.range(of: ".app", options: [.caseInsensitive])
else {
return nil
}
return String(trimmedCommand[..<appRange.upperBound])
}
}