import Foundation import Observation @MainActor @Observable final class SystemMonitor { var processes: [ProcessRecord] = [] var services: [ServiceRecord] = [] var appHistory: [AppUsageRecord] = [] var appHistoryStartDate = Date() var processActivity: [Int32: ProcessActivityRate] = [:] var snapshot = SystemSnapshot() var cpuHistory: [MetricSample] = [] var coreHistories: [[MetricSample]] = [] var memoryHistory: [MetricSample] = [] var diskReadHistory: [MetricSample] = [] var diskWriteHistory: [MetricSample] = [] var diskLatencyHistory: [MetricSample] = [] var diskActiveHistory: [MetricSample] = [] var gpuHistory: [MetricSample] = [] var gpuRendererHistory: [MetricSample] = [] var gpuTilerHistory: [MetricSample] = [] var networkReceiveHistory: [MetricSample] = [] var networkSendHistory: [MetricSample] = [] var hardware = HardwareSnapshot() var systemPowerHistory: [MetricSample] = [] var updateSpeed: UpdateSpeed { didSet { UserDefaults.standard.set(updateSpeed.rawValue, forKey: "updateSpeed") } } var lastUpdated: Date? var errorMessage: String? private var updateTask: Task? private var previousCoreTicks: [CoreTicks] = [] private var previousNetworkCounters: NetworkCounters? private var previousNetworkDate: Date? private var previousDiskCounters: DiskCounters? private var previousDiskDate: Date? private var previousProcessCPU: [Int32: TimeInterval] = [:] private var previousProcessNetwork: [Int32: ProcessNetworkTotals] = [:] private var previousProcessNetworkDate: Date? private var previousProcessIO: [Int32: ProcessIOTotals] = [:] private var previousProcessIODate: Date? private var appNetworkTask: Task? private var lastAppNetworkSample: Date? private var lastPortSample: Date? init() { updateSpeed = UpdateSpeed(rawValue: UserDefaults.standard.string(forKey: "updateSpeed") ?? "") ?? .normal loadAppHistory() } var isPaused: Bool { get { updateSpeed == .paused } set { updateSpeed = newValue ? .paused : .normal } } func start() { guard updateTask == nil else { return } refresh() updateTask = Task { [weak self] in while !Task.isCancelled { let interval = self?.updateSpeed.interval ?? .seconds(2) try? await Task.sleep(for: interval) guard let self, !self.isPaused else { continue } self.refresh() } } } func refresh() { Task { let result = await Task.detached(priority: .userInitiated) { ProcessScanner.capture() }.value let refreshPorts = lastPortSample.map { Date().timeIntervalSince($0) >= 10 } ?? true let existingPorts = hardware.ports let hardwareResult = await Task.detached(priority: .utility) { HardwareScanner.capture(existingPorts: existingPorts, refreshPorts: refreshPorts) }.value processes = result.processes updateCPUHistory(with: result.processes) updateProcessDiskActivity(result.processIO, current: result.processes) services = result.services var refreshedSnapshot = result.snapshot refreshedSnapshot.corePercents = CoreCPUReader.capture(previous: &previousCoreTicks) let diskDate = Date() if let previousDiskCounters, let previousDiskDate { let elapsed = max(0.001, diskDate.timeIntervalSince(previousDiskDate)) let readBytes = result.disk.readBytes >= previousDiskCounters.readBytes ? result.disk.readBytes - previousDiskCounters.readBytes : 0 let writeBytes = result.disk.writeBytes >= previousDiskCounters.writeBytes ? result.disk.writeBytes - previousDiskCounters.writeBytes : 0 let readOperations = result.disk.readOperations >= previousDiskCounters.readOperations ? result.disk.readOperations - previousDiskCounters.readOperations : 0 let writeOperations = result.disk.writeOperations >= previousDiskCounters.writeOperations ? result.disk.writeOperations - previousDiskCounters.writeOperations : 0 let readTime = result.disk.readTime >= previousDiskCounters.readTime ? result.disk.readTime - previousDiskCounters.readTime : 0 let writeTime = result.disk.writeTime >= previousDiskCounters.writeTime ? result.disk.writeTime - previousDiskCounters.writeTime : 0 let operations = readOperations + writeOperations let serviceTime = readTime + writeTime refreshedSnapshot.diskReadRate = Double(readBytes) / elapsed refreshedSnapshot.diskWriteRate = Double(writeBytes) / elapsed refreshedSnapshot.diskOperationsPerSecond = Double(operations) / elapsed refreshedSnapshot.diskLatencyMilliseconds = operations > 0 ? Double(serviceTime) / Double(operations) / 1_000_000 : 0 refreshedSnapshot.diskActivePercent = min(100, Double(serviceTime) / (elapsed * 1_000_000_000) * 100) } previousDiskCounters = result.disk previousDiskDate = diskDate let networkDate = Date() if let previousNetworkCounters, let previousNetworkDate { let elapsed = max(0.001, networkDate.timeIntervalSince(previousNetworkDate)) if result.network.received >= previousNetworkCounters.received { refreshedSnapshot.networkReceiveRate = Double(result.network.received - previousNetworkCounters.received) / elapsed } if result.network.sent >= previousNetworkCounters.sent { refreshedSnapshot.networkSendRate = Double(result.network.sent - previousNetworkCounters.sent) / elapsed } } refreshedSnapshot.networkInterface = result.network.interface previousNetworkCounters = result.network previousNetworkDate = networkDate snapshot = refreshedSnapshot hardware = hardwareResult if refreshPorts { lastPortSample = Date() } lastUpdated = Date() errorMessage = result.error appendHistory(cpu: snapshot.cpuPercent, memory: snapshot.memoryPercent) sampleAppNetworkIfNeeded() } } func resetAppHistory() { appHistory = [] appHistoryStartDate = Date() previousProcessCPU = Dictionary(uniqueKeysWithValues: processes.map { ($0.pid, $0.cpuTime) }) previousProcessNetwork = [:] saveAppHistory() } func activeProcess(for usage: AppUsageRecord) -> ProcessRecord? { processes.first { AppIdentity.forProcess($0).id == usage.id } } func terminate(_ process: ProcessRecord, force: Bool = false) { guard process.pid != getpid() else { errorMessage = "Puter cannot end itself." return } let signal = force ? SIGKILL : SIGTERM if kill(process.pid, signal) == 0 { processes.removeAll { $0.pid == process.pid } Task { try? await Task.sleep(for: .milliseconds(400)) refresh() } } else { errorMessage = "Could not end \(process.displayName). You may not have permission." } } func terminateTree(_ process: ProcessRecord) { let descendants = descendantPIDs(of: process.pid) for pid in descendants.reversed() { _ = kill(pid, SIGTERM) } terminate(process) } func terminateGroup(_ group: [ProcessRecord]) { guard !group.contains(where: { $0.pid == getpid() }) else { errorMessage = "Puter cannot end an application group that contains itself." return } let pids = Set(group.map(\.pid)) var failed = false for process in group { if kill(process.pid, SIGTERM) != 0 { failed = true } } processes.removeAll { pids.contains($0.pid) } if failed { errorMessage = "Some processes could not be ended. You may not have permission." } Task { try? await Task.sleep(for: .milliseconds(500)) refresh() } } func sendSignal(_ signal: Int32, to process: ProcessRecord, successMessage: String? = nil) { guard process.pid != getpid() else { errorMessage = "Puter cannot control itself." return } if kill(process.pid, signal) != 0 { errorMessage = "Could not control \(process.displayName). You may not have permission." } else if let successMessage { errorMessage = successMessage } Task { try? await Task.sleep(for: .milliseconds(300)) refresh() } } func setPriority(_ priority: Int, for process: ProcessRecord) { let command = Process() command.executableURL = URL(fileURLWithPath: "/usr/bin/renice") command.arguments = ["-n", "\(priority)", "-p", "\(process.pid)"] command.standardOutput = FileHandle.nullDevice command.standardError = FileHandle.nullDevice do { try command.run() command.waitUntilExit() if command.terminationStatus != 0 { errorMessage = "Could not change priority for \(process.displayName). Higher priorities may require administrator access." } } catch { errorMessage = error.localizedDescription } } func startService(_ service: ServiceRecord) { runServiceCommand(["kickstart", serviceTarget(service)], action: "start", service: service) } func stopService(_ service: ServiceRecord) { runServiceCommand(["kill", "SIGTERM", serviceTarget(service)], action: "stop", service: service) } func restartService(_ service: ServiceRecord) { runServiceCommand(["kickstart", "-k", serviceTarget(service)], action: "restart", service: service) } private func serviceTarget(_ service: ServiceRecord) -> String { "\(service.domain.launchctlPrefix)/\(service.label)" } private func runServiceCommand(_ arguments: [String], action: String, service: ServiceRecord) { let command = Process() let errorPipe = Pipe() command.executableURL = URL(fileURLWithPath: "/bin/launchctl") command.arguments = arguments command.standardOutput = FileHandle.nullDevice command.standardError = errorPipe do { try command.run() command.waitUntilExit() if command.terminationStatus != 0 { let message = String(decoding: errorPipe.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self) .trimmingCharacters(in: .whitespacesAndNewlines) errorMessage = message.isEmpty ? "Could not \(action) \(service.displayName)." : message } Task { try? await Task.sleep(for: .milliseconds(500)) refresh() } } catch { errorMessage = error.localizedDescription } } private func descendantPIDs(of parentPID: Int32) -> [Int32] { let children = processes.filter { $0.parentPID == parentPID }.map(\.pid) return children + children.flatMap { descendantPIDs(of: $0) } } private func appendHistory(cpu: Double, memory: Double) { let now = Date() cpuHistory.append(MetricSample(date: now, value: cpu)) memoryHistory.append(MetricSample(date: now, value: memory)) networkReceiveHistory.append(MetricSample(date: now, value: snapshot.networkReceiveRate)) networkSendHistory.append(MetricSample(date: now, value: snapshot.networkSendRate)) systemPowerHistory.append(MetricSample(date: now, value: hardware.battery.systemPowerWatts)) diskReadHistory.append(MetricSample(date: now, value: snapshot.diskReadRate)) diskWriteHistory.append(MetricSample(date: now, value: snapshot.diskWriteRate)) diskLatencyHistory.append(MetricSample(date: now, value: snapshot.diskLatencyMilliseconds)) diskActiveHistory.append(MetricSample(date: now, value: snapshot.diskActivePercent)) gpuHistory.append(MetricSample(date: now, value: snapshot.gpuPercent)) gpuRendererHistory.append(MetricSample(date: now, value: snapshot.gpuRendererPercent)) gpuTilerHistory.append(MetricSample(date: now, value: snapshot.gpuTilerPercent)) if coreHistories.count != snapshot.corePercents.count { coreHistories = Array(repeating: [], count: snapshot.corePercents.count) } for index in snapshot.corePercents.indices { coreHistories[index].append(MetricSample(date: now, value: snapshot.corePercents[index])) } let cutoff = now.addingTimeInterval(-60) cpuHistory.removeAll { $0.date < cutoff } memoryHistory.removeAll { $0.date < cutoff } networkReceiveHistory.removeAll { $0.date < cutoff } networkSendHistory.removeAll { $0.date < cutoff } systemPowerHistory.removeAll { $0.date < cutoff } diskReadHistory.removeAll { $0.date < cutoff } diskWriteHistory.removeAll { $0.date < cutoff } diskLatencyHistory.removeAll { $0.date < cutoff } diskActiveHistory.removeAll { $0.date < cutoff } gpuHistory.removeAll { $0.date < cutoff } gpuRendererHistory.removeAll { $0.date < cutoff } gpuTilerHistory.removeAll { $0.date < cutoff } for index in coreHistories.indices { coreHistories[index].removeAll { $0.date < cutoff } } } private func updateCPUHistory(with current: [ProcessRecord]) { var records = Dictionary(uniqueKeysWithValues: appHistory.map { ($0.id, $0) }) let now = Date() for process in current where process.user == NSUserName() { let identity = AppIdentity.forProcess(process) var usage = records[identity.id] ?? AppUsageRecord( id: identity.id, name: identity.name, executablePath: identity.path, user: process.user, cpuSeconds: 0, networkReceivedBytes: 0, networkSentBytes: 0, lastSeen: now ) if let old = previousProcessCPU[process.pid], process.cpuTime >= old { usage.cpuSeconds += process.cpuTime - old } usage.lastSeen = now records[identity.id] = usage } previousProcessCPU = Dictionary(uniqueKeysWithValues: current.map { ($0.pid, $0.cpuTime) }) appHistory = Array(records.values) saveAppHistory() } private func sampleAppNetworkIfNeeded() { guard appNetworkTask == nil, lastAppNetworkSample.map({ Date().timeIntervalSince($0) >= 10 }) ?? true else { return } lastAppNetworkSample = Date() appNetworkTask = Task { [weak self] in let totals = await Task.detached(priority: .utility) { AppNetworkScanner.capture() }.value guard let self else { return } self.mergeNetworkHistory(totals) self.appNetworkTask = nil } } private func mergeNetworkHistory(_ totals: [Int32: ProcessNetworkTotals]) { var records = Dictionary(uniqueKeysWithValues: appHistory.map { ($0.id, $0) }) let processByPID = Dictionary(uniqueKeysWithValues: processes.map { ($0.pid, $0) }) let now = Date() let elapsed = previousProcessNetworkDate.map { max(0.001, now.timeIntervalSince($0)) } for (pid, total) in totals { guard let process = processByPID[pid], process.user == NSUserName() else { continue } let identity = AppIdentity.forProcess(process) guard var usage = records[identity.id] else { continue } if let old = previousProcessNetwork[pid] { if total.received >= old.received { usage.networkReceivedBytes += total.received - old.received } if total.sent >= old.sent { usage.networkSentBytes += total.sent - old.sent } } records[identity.id] = usage } if let elapsed { for (pid, total) in totals { guard let old = previousProcessNetwork[pid] else { continue } var activity = processActivity[pid] ?? ProcessActivityRate() activity.networkReceive = total.received >= old.received ? Double(total.received - old.received) / elapsed : 0 activity.networkSend = total.sent >= old.sent ? Double(total.sent - old.sent) / elapsed : 0 processActivity[pid] = activity } } previousProcessNetwork = totals previousProcessNetworkDate = now appHistory = Array(records.values) saveAppHistory() } private func updateProcessDiskActivity(_ totals: [Int32: ProcessIOTotals], current: [ProcessRecord]) { let now = Date() let activePIDs = Set(current.map(\.pid)) processActivity = processActivity.filter { activePIDs.contains($0.key) } if let previousProcessIODate { let elapsed = max(0.001, now.timeIntervalSince(previousProcessIODate)) for (pid, total) in totals { guard let old = previousProcessIO[pid] else { continue } var activity = processActivity[pid] ?? ProcessActivityRate() activity.diskRead = total.read >= old.read ? Double(total.read - old.read) / elapsed : 0 activity.diskWrite = total.written >= old.written ? Double(total.written - old.written) / elapsed : 0 processActivity[pid] = activity } } previousProcessIO = totals previousProcessIODate = now } private func loadAppHistory() { guard let data = try? Data(contentsOf: AppHistoryPersistence.url), let state = try? JSONDecoder().decode(AppHistoryPersistence.State.self, from: data) else { return } appHistoryStartDate = state.startDate appHistory = state.records } private func saveAppHistory() { let state = AppHistoryPersistence.State(startDate: appHistoryStartDate, records: appHistory) guard let data = try? JSONEncoder().encode(state) else { return } do { try FileManager.default.createDirectory(at: AppHistoryPersistence.directory, withIntermediateDirectories: true) try data.write(to: AppHistoryPersistence.url, options: .atomic) } catch { errorMessage = "App history could not be saved: \(error.localizedDescription)" } } } private enum AppHistoryPersistence { struct State: Codable { let startDate: Date let records: [AppUsageRecord] } static let directory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] .appendingPathComponent("MacTaskManager", isDirectory: true) static let url = directory.appendingPathComponent("app-history.json") } private struct AppIdentity { let id: String let name: String let path: String static func forProcess(_ process: ProcessRecord) -> AppIdentity { let path = process.executablePath if let range = path.range(of: ".app/Contents/", options: .caseInsensitive) { let appPath = String(path[.. [Int32: ProcessNetworkTotals] { let process = Process() let pipe = Pipe() process.executableURL = URL(fileURLWithPath: "/usr/bin/nettop") process.arguments = ["-P", "-x", "-l", "1", "-J", "bytes_in,bytes_out"] process.standardOutput = pipe process.standardError = FileHandle.nullDevice do { try process.run() let data = pipe.fileHandleForReading.readDataToEndOfFile() process.waitUntilExit() guard process.terminationStatus == 0 else { return [:] } return parse(String(decoding: data, as: UTF8.self)) } catch { return [:] } } private static func parse(_ output: String) -> [Int32: ProcessNetworkTotals] { var result: [Int32: ProcessNetworkTotals] = [:] for line in output.split(separator: "\n").dropFirst() { let fields = line.split(whereSeparator: { $0 == " " || $0 == "\t" }) guard fields.count >= 3, let received = UInt64(fields[fields.count - 2]), let sent = UInt64(fields[fields.count - 1]) else { continue } let identity = fields.dropLast(2).joined(separator: " ") guard let dot = identity.lastIndex(of: "."), let pid = Int32(identity[identity.index(after: dot)...]) else { continue } result[pid] = ProcessNetworkTotals(received: received, sent: sent) } return result } } private struct NetworkCounters: Sendable { let received: UInt64 let sent: UInt64 let interface: String } private struct DiskCounters: Sendable { var readBytes: UInt64 = 0 var writeBytes: UInt64 = 0 var readOperations: UInt64 = 0 var writeOperations: UInt64 = 0 var readTime: UInt64 = 0 var writeTime: UInt64 = 0 } private struct GPUCounters: Sendable { var devicePercent = 0.0 var rendererPercent = 0.0 var tilerPercent = 0.0 var memoryBytes: UInt64 = 0 var allocatedBytes: UInt64 = 0 var coreCount = 0 } private struct MemoryCounters: Sendable { var used: UInt64 = 0 var active: UInt64 = 0 var wired: UInt64 = 0 var compressed: UInt64 = 0 var cached: UInt64 = 0 var swapUsed: UInt64 = 0 var swapTotal: UInt64 = 0 } private struct MemoryHardwareInfo: Sendable { var type = "Unified" var speed = "Not reported by macOS" var manufacturer = "Apple unified memory" } private struct CoreTicks { let active: UInt64 let idle: UInt64 } private enum CoreCPUReader { static func capture(previous: inout [CoreTicks]) -> [Double] { var processorCount: natural_t = 0 var info: processor_info_array_t? var infoCount: mach_msg_type_number_t = 0 let result = host_processor_info( mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &processorCount, &info, &infoCount ) guard result == KERN_SUCCESS, let info else { return [] } defer { vm_deallocate( mach_task_self_, vm_address_t(UInt(bitPattern: info)), vm_size_t(infoCount) * vm_size_t(MemoryLayout.stride) ) } let current = (0.. CoreTicks in let offset = core * Int(CPU_STATE_MAX) let user = UInt64(info[offset + Int(CPU_STATE_USER)]) let system = UInt64(info[offset + Int(CPU_STATE_SYSTEM)]) let nice = UInt64(info[offset + Int(CPU_STATE_NICE)]) let idle = UInt64(info[offset + Int(CPU_STATE_IDLE)]) return CoreTicks(active: user + system + nice, idle: idle) } defer { previous = current } guard previous.count == current.count else { return Array(repeating: 0, count: current.count) } return zip(current, previous).map { current, old in let activeDelta = current.active >= old.active ? current.active - old.active : 0 let idleDelta = current.idle >= old.idle ? current.idle - old.idle : 0 let total = activeDelta + idleDelta return total > 0 ? min(100, Double(activeDelta) / Double(total) * 100) : 0 } } } private enum ProcessScanner { private static let memoryHardware = memoryHardwareInfo() struct Result: Sendable { let processes: [ProcessRecord] let services: [ServiceRecord] let snapshot: SystemSnapshot let network: NetworkCounters let disk: DiskCounters let processIO: [Int32: ProcessIOTotals] let error: String? } static func capture() -> Result { let ps = run("/bin/ps", arguments: ["-axo", "pid=,ppid=,user=,%cpu=,%mem=,rss=,state=,etime=,time=,pri=,nice=,comm="]) let threads = threadCounts() let records = parseProcesses(ps.output, threadCounts: threads) let userServices = parseServices(run("/bin/launchctl", arguments: ["list"]).output, domain: .user) let systemServices = parseSystemServices(run("/bin/launchctl", arguments: ["print", "system"]).output) let services = userServices + systemServices let cpu = min(100, records.reduce(0) { $0 + $1.cpu }) let totalMemory = ProcessInfo.processInfo.physicalMemory let memory = memoryCounters(total: totalMemory) let disk = diskUsage() var snapshot = SystemSnapshot() snapshot.cpuPercent = cpu snapshot.memoryUsed = memory.used snapshot.memoryTotal = totalMemory snapshot.memoryActive = memory.active snapshot.memoryWired = memory.wired snapshot.memoryCompressed = memory.compressed snapshot.memoryCached = memory.cached snapshot.swapUsed = memory.swapUsed snapshot.swapTotal = memory.swapTotal snapshot.memoryType = memoryHardware.type snapshot.memorySpeed = memoryHardware.speed snapshot.memoryManufacturer = memoryHardware.manufacturer snapshot.diskFree = disk.free snapshot.diskTotal = disk.total snapshot.removableVolumes = removableVolumes() snapshot.processCount = records.count snapshot.threadCount = threads.values.reduce(0, +) snapshot.uptime = ProcessInfo.processInfo.systemUptime let gpu = gpuCounters() snapshot.gpuPercent = gpu.devicePercent snapshot.gpuRendererPercent = gpu.rendererPercent snapshot.gpuTilerPercent = gpu.tilerPercent snapshot.gpuMemoryBytes = gpu.memoryBytes snapshot.gpuAllocatedBytes = gpu.allocatedBytes snapshot.gpuCoreCount = gpu.coreCount return Result( processes: records, services: services, snapshot: snapshot, network: networkCounters(), disk: diskCounters(), processIO: processIOTotals(records.map(\.pid)), error: ps.error ) } private static func processIOTotals(_ pids: [Int32]) -> [Int32: ProcessIOTotals] { var totals: [Int32: ProcessIOTotals] = [:] for pid in pids { var usage = rusage_info_v2() let result = withUnsafeMutablePointer(to: &usage) { pointer in pointer.withMemoryRebound(to: rusage_info_t?.self, capacity: 1) { proc_pid_rusage(pid, RUSAGE_INFO_V2, $0) } } if result == 0 { totals[pid] = ProcessIOTotals(read: usage.ri_diskio_bytesread, written: usage.ri_diskio_byteswritten) } } return totals } private static func diskCounters() -> DiskCounters { let output = run( "/usr/sbin/ioreg", arguments: ["-r", "-c", "IOBlockStorageDriver", "-k", "Statistics", "-a"] ).output guard let data = output.data(using: .utf8), let drivers = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [[String: Any]] else { return DiskCounters() } var result = DiskCounters() for driver in drivers { guard let statistics = driver["Statistics"] as? [String: Any] else { continue } func value(_ key: String) -> UInt64 { (statistics[key] as? NSNumber)?.uint64Value ?? 0 } result.readBytes += value("Bytes (Read)") result.writeBytes += value("Bytes (Write)") result.readOperations += value("Operations (Read)") result.writeOperations += value("Operations (Write)") result.readTime += value("Total Time (Read)") result.writeTime += value("Total Time (Write)") } return result } private static func gpuCounters() -> GPUCounters { let output = run( "/usr/sbin/ioreg", arguments: ["-r", "-c", "AGXAccelerator", "-l"] ).output func number(_ key: String) -> UInt64 { UInt64(output.firstMatch("\"\(NSRegularExpression.escapedPattern(for: key))\" *= *(\\d+)") ?? "0") ?? 0 } return GPUCounters( devicePercent: Double(number("Device Utilization %")), rendererPercent: Double(number("Renderer Utilization %")), tilerPercent: Double(number("Tiler Utilization %")), memoryBytes: number("In use system memory"), allocatedBytes: number("Alloc system memory"), coreCount: Int(number("gpu-core-count")) ) } private static func parseProcesses(_ output: String, threadCounts: [Int32: Int]) -> [ProcessRecord] { output.split(separator: "\n").compactMap { line in let fields = line.split(maxSplits: 11, whereSeparator: { $0 == " " || $0 == "\t" }) guard fields.count == 12, let pid = Int32(fields[0]), let ppid = Int32(fields[1]), let cpu = Double(fields[3]), let memory = Double(fields[4]), let rssKB = UInt64(fields[5]), let priority = Int(fields[9]), let nice = Int(fields[10]) else { return nil } let path = String(fields[11]) let name = URL(fileURLWithPath: path).lastPathComponent let normalizedCPU = cpu / Double(max(1, ProcessInfo.processInfo.activeProcessorCount)) return ProcessRecord( pid: pid, parentPID: ppid, name: name, executablePath: path, user: String(fields[2]), cpu: normalizedCPU, memoryPercent: memory, residentBytes: rssKB * 1024, state: String(fields[6]), elapsed: String(fields[7]), cpuTime: parseCPUTime(String(fields[8])), threadCount: threadCounts[pid] ?? 0, openFileCount: openFileCount(pid), architecture: BinaryArchitectureReader.architecture(at: path), priority: priority, nice: nice ) } } private static func parseCPUTime(_ value: String) -> TimeInterval { let dayParts = value.split(separator: "-", maxSplits: 1).map(String.init) let days = dayParts.count == 2 ? Double(dayParts[0]) ?? 0 : 0 let clock = (dayParts.count == 2 ? dayParts[1] : dayParts[0]).split(separator: ":").compactMap { Double($0) } guard !clock.isEmpty else { return days * 86_400 } var multiplier = 1.0 var seconds = 0.0 for component in clock.reversed() { seconds += component * multiplier multiplier *= 60 } return days * 86_400 + seconds } private static func parseServices(_ output: String, domain: ServiceDomain) -> [ServiceRecord] { output.split(separator: "\n").dropFirst().compactMap { line in let fields = line.split(maxSplits: 2, whereSeparator: { $0 == " " || $0 == "\t" }) guard fields.count == 3, let status = Int32(fields[1]) else { return nil } let pid = fields[0] == "-" ? nil : Int32(fields[0]) let label = String(fields[2]).trimmingCharacters(in: .whitespacesAndNewlines) guard !label.isEmpty else { return nil } return ServiceRecord(label: label, pid: pid, lastExitStatus: status, domain: domain) } } private static func parseSystemServices(_ output: String) -> [ServiceRecord] { guard let start = output.range(of: "\n\tservices = {\n") else { return [] } let body = output[start.upperBound...] guard let end = body.range(of: "\n\t}") else { return [] } return body[.. 0 ? rawPID : nil, lastExitStatus: status, domain: .system ) } } private static func memoryCounters(total: UInt64) -> MemoryCounters { let vm = run("/usr/bin/vm_stat", arguments: []).output let pageSize = UInt64(vm.firstMatch(#"page size of (\d+) bytes"#) ?? "4096") ?? 4096 func pages(_ label: String) -> UInt64 { UInt64(vm.firstMatch("\(NSRegularExpression.escapedPattern(for: label)): +([0-9]+)") ?? "0") ?? 0 } let free = (pages("Pages free") + pages("Pages speculative")) * pageSize var swap = xsw_usage() var swapSize = MemoryLayout.size _ = sysctlbyname("vm.swapusage", &swap, &swapSize, nil, 0) return MemoryCounters( used: total > free ? total - free : 0, active: pages("Pages active") * pageSize, wired: pages("Pages wired down") * pageSize, compressed: pages("Pages occupied by compressor") * pageSize, cached: (pages("Pages inactive") + pages("Pages purgeable")) * pageSize, swapUsed: swap.xsu_used, swapTotal: swap.xsu_total ) } private static func memoryHardwareInfo() -> MemoryHardwareInfo { let output = run("/usr/sbin/system_profiler", arguments: ["SPMemoryDataType", "-detailLevel", "mini"]).output let processor = run("/usr/sbin/sysctl", arguments: ["-n", "machdep.cpu.brand_string"]).output .trimmingCharacters(in: .whitespacesAndNewlines) let reportedSpeed = output.firstMatch(#"Speed: +([^\n]+)"#) return MemoryHardwareInfo( type: output.firstMatch(#"Type: +([^\n]+)"#) ?? "Unified", speed: reportedSpeed ?? MemoryBandwidthCatalog.description( for: processor, cpuCoreCount: ProcessInfo.processInfo.activeProcessorCount ), manufacturer: output.firstMatch(#"Manufacturer: +([^\n]+)"#) ?? "Apple unified memory" ) } private static func removableVolumes() -> [VolumeSnapshot] { let keys: [URLResourceKey] = [ .volumeNameKey, .volumeLocalizedFormatDescriptionKey, .volumeIsRemovableKey, .volumeIsEjectableKey, .volumeTotalCapacityKey, .volumeAvailableCapacityForImportantUsageKey ] let urls = FileManager.default.mountedVolumeURLs( includingResourceValuesForKeys: keys, options: [.skipHiddenVolumes] ) ?? [] return urls.compactMap { url in guard let values = try? url.resourceValues(forKeys: Set(keys)), values.volumeIsRemovable == true || values.volumeIsEjectable == true else { return nil } let capacity = UInt64(max(0, values.volumeTotalCapacity ?? 0)) let available = UInt64(max(0, values.volumeAvailableCapacityForImportantUsage ?? 0)) return VolumeSnapshot( id: url.path, name: values.volumeName ?? url.lastPathComponent, mountPath: url.path, fileSystem: values.volumeLocalizedFormatDescription ?? "Removable storage", capacity: capacity, available: available, isEjectable: values.volumeIsEjectable ?? false ) } .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } } private static func diskUsage() -> (free: UInt64, total: UInt64) { guard let values = try? URL(fileURLWithPath: "/").resourceValues(forKeys: [ .volumeAvailableCapacityForImportantUsageKey, .volumeTotalCapacityKey ]) else { return (0, 0) } return (UInt64(max(0, values.volumeAvailableCapacityForImportantUsage ?? 0)), UInt64(max(0, values.volumeTotalCapacity ?? 0))) } private static func threadCounts() -> [Int32: Int] { let result = run("/bin/ps", arguments: ["-M", "-axo", "pid="]).output let rawCounts = result.split(separator: "\n").reduce(into: [Int32: Int]()) { counts, line in guard let last = line.split(whereSeparator: { $0 == " " || $0 == "\t" }).last, let pid = Int32(last) else { return } counts[pid, default: 0] += 1 } return rawCounts.mapValues { max(1, $0 - 1) } } private static func openFileCount(_ pid: Int32) -> Int { let bytes = Int(proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0)) return bytes > 0 ? bytes / MemoryLayout.stride : 0 } private static func networkCounters() -> NetworkCounters { let route = run("/sbin/route", arguments: ["-n", "get", "default"]).output let primary = route.firstMatch(#"interface: +([^\s]+)"#) ?? "Network" let netstat = run("/usr/sbin/netstat", arguments: ["-ibn"]).output var received: UInt64 = 0 var sent: UInt64 = 0 var matchedPrimary = false for line in netstat.split(separator: "\n") { let fields = line.split(whereSeparator: { $0 == " " || $0 == "\t" }).map(String.init) guard fields.count >= 10, fields[2].hasPrefix(" (output: String, error: String?) { let process = Process() let pipe = Pipe() process.executableURL = URL(fileURLWithPath: path) process.arguments = arguments process.standardOutput = pipe process.standardError = FileHandle.nullDevice do { try process.run() let data = pipe.fileHandleForReading.readDataToEndOfFile() process.waitUntilExit() return (String(decoding: data, as: UTF8.self), process.terminationStatus == 0 ? nil : "System data is temporarily unavailable.") } catch { return ("", error.localizedDescription) } } } private enum HardwareScanner { static func capture(existingPorts: [HardwarePortSnapshot], refreshPorts: Bool) -> HardwareSnapshot { HardwareSnapshot( battery: batterySnapshot(), ports: refreshPorts ? portSnapshots() : existingPorts, fans: fanSnapshots(), thermalState: thermalState(), capturedAt: Date() ) } private static func fanSnapshots() -> [FanSnapshot] { let tool = FanControlService.toolPath guard FileManager.default.isExecutableFile(atPath: tool) else { return [] } let output = run(tool, arguments: ["fans"]) guard output.contains("Number of fans:") else { return [] } return output.components(separatedBy: "\n\n").compactMap { block in guard let identifier = block.firstMatch(#"(?m)^([0-9]+):"#).flatMap(Int.init) else { return nil } func number(_ label: String) -> Double { Double(block.firstMatch("(?m)^\(NSRegularExpression.escapedPattern(for: label)): ([0-9.]+)") ?? "0") ?? 0 } let name = block.firstMatch(#"(?m)^[0-9]+: (.+)$"#) ?? "Fan \(identifier + 1)" let mode = block.firstMatch(#"(?m)^Mode: (.+)$"#) ?? "automatic" return FanSnapshot( id: identifier, name: name.replacingOccurrences(of: "#\(identifier)", with: "\(identifier + 1)"), actualRPM: number("Actual speed"), minimumRPM: number("Minimal speed"), maximumRPM: number("Maximum speed"), targetRPM: number("Target speed"), mode: mode ) }.sorted { $0.id < $1.id } } private static func batterySnapshot() -> BatterySnapshot { let output = run("/usr/sbin/ioreg", arguments: ["-r", "-c", "AppleSmartBattery", "-l"]) guard output.contains("AppleSmartBattery") else { return BatterySnapshot() } func topNumber(_ key: String) -> Double { Double(output.firstMatch("(?m)^ \"\(NSRegularExpression.escapedPattern(for: key))\" = (-?[0-9]+)") ?? "0") ?? 0 } func topSignedNumber(_ key: String) -> Double { let raw = output.firstMatch("(?m)^ \"\(NSRegularExpression.escapedPattern(for: key))\" = (-?[0-9]+)") ?? "0" if let signed = Int64(raw) { return Double(signed) } if let unsigned = UInt64(raw) { return Double(Int64(bitPattern: unsigned)) } return 0 } func topBool(_ key: String) -> Bool { (output.firstMatch("(?m)^ \"\(NSRegularExpression.escapedPattern(for: key))\" = (Yes|No)") ?? "No") == "Yes" } func topString(_ key: String) -> String { output.firstMatch("(?m)^ \"\(NSRegularExpression.escapedPattern(for: key))\" = \"([^\"]*)\"") ?? "" } let adapterLine = output.firstMatch(#"(?m)^ "AdapterDetails" = \{([^\n]+)\}"#) ?? "" let telemetryLine = output.firstMatch(#"(?m)^ "PowerTelemetryData" = \{([^\n]+)\}"#) ?? "" let externalPowerConnected = topBool("ExternalConnected") let parsedAdapter = adapterLine.isEmpty ? nil : adapterSnapshot(from: adapterLine) let adapter = externalPowerConnected && (parsedAdapter?.ratedWatts ?? 0) > 0 ? parsedAdapter : nil let rawTime = Int(topNumber("TimeRemaining")) let rawTemperature = topNumber("Temperature") let temperature = rawTemperature > 1_000 ? rawTemperature / 10 - 273.15 : rawTemperature let systemPowerMW = Double(telemetryLine.firstMatch(#""SystemPowerIn"=([0-9]+)"#) ?? "0") ?? 0 return BatterySnapshot( isPresent: topBool("BatteryInstalled"), chargePercent: topNumber("CurrentCapacity"), isCharging: topBool("IsCharging"), isFullyCharged: topBool("FullyCharged"), externalPowerConnected: externalPowerConnected, cycleCount: Int(topNumber("CycleCount")), designCycleCount: Int(topNumber("DesignCycleCount9C")), currentCapacityMAh: topNumber("AppleRawCurrentCapacity"), fullChargeCapacityMAh: topNumber("NominalChargeCapacity"), designCapacityMAh: topNumber("DesignCapacity"), voltageVolts: topNumber("Voltage") / 1_000, currentAmps: { let amps = topSignedNumber("Amperage") / 1_000 return abs(amps) <= 100 ? amps : 0 }(), temperatureCelsius: temperature, timeRemainingMinutes: rawTime > 0 && rawTime < 65_535 ? rawTime : nil, serial: topString("Serial"), adapter: adapter, systemPowerWatts: systemPowerMW / 1_000 ) } private static func adapterSnapshot(from line: String) -> PowerAdapterSnapshot { func number(_ key: String) -> Double { Double(line.firstMatch("\"\(NSRegularExpression.escapedPattern(for: key))\"=([0-9]+)") ?? "0") ?? 0 } func string(_ key: String) -> String { line.firstMatch("\"\(NSRegularExpression.escapedPattern(for: key))\"=\"([^\"]*)\"") ?? "" } let regex = try? NSRegularExpression(pattern: #""MaxCurrent"=([0-9]+),"MaxVoltage"=([0-9]+)"#) let range = NSRange(line.startIndex.. PowerProfile? in guard let currentRange = Range(match.range(at: 1), in: line), let voltageRange = Range(match.range(at: 2), in: line), let current = Double(line[currentRange]), let voltage = Double(line[voltageRange]) else { return nil } return PowerProfile(voltageVolts: voltage / 1_000, currentAmps: current / 1_000) } return PowerAdapterSnapshot( name: string("Name").isEmpty ? "USB-C power adapter" : string("Name"), manufacturer: string("Manufacturer").isEmpty ? "Not reported" : string("Manufacturer"), serial: string("SerialString"), ratedWatts: number("Watts"), negotiatedVoltage: number("AdapterVoltage") / 1_000, negotiatedCurrent: number("Current") / 1_000, profiles: profiles ) } private static func thermalState() -> String { switch ProcessInfo.processInfo.thermalState { case .nominal: "Nominal" case .fair: "Fair" case .serious: "Serious" case .critical: "Critical" @unknown default: "Unknown" } } private static func portSnapshots() -> [HardwarePortSnapshot] { let data = runData( "/usr/sbin/system_profiler", arguments: ["SPUSBDataType", "SPThunderboltDataType", "-json", "-detailLevel", "full"] ) guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [] } var ports: [HardwarePortSnapshot] = [] for (busIndex, value) in (root["SPThunderboltDataType"] as? [[String: Any]] ?? []).enumerated() { let foundDevices = devices(in: value["_items"], depth: 0, prefix: "tb-\(busIndex)") let receptacles = value.keys.filter { $0.hasPrefix("receptacle_") }.sorted() for (index, key) in receptacles.enumerated() { guard let receptacle = value[key] as? [String: Any] else { continue } let rawStatus = receptacle["receptacle_status_key"] as? String ?? "" let connected = !rawStatus.contains("no_devices") let identifier = receptacle["receptacle_id_key"] as? String ?? "\(index + 1)" ports.append(HardwarePortSnapshot( id: "thunderbolt-\(busIndex)-\(identifier)", name: "USB-C / Thunderbolt Port \(identifier)", transport: "Thunderbolt / USB4", maximumSpeed: receptacle["current_speed_key"] as? String ?? "Not reported", isConnected: connected || !foundDevices.isEmpty, status: connected ? "Device connected" : "Available", devices: foundDevices )) } } for (index, controller) in (root["SPUSBDataType"] as? [[String: Any]] ?? []).enumerated() { let foundDevices = devices(in: controller["_items"], depth: 0, prefix: "usb-\(index)") guard !foundDevices.isEmpty else { continue } ports.append(HardwarePortSnapshot( id: "usb-controller-\(index)", name: controller["_name"] as? String ?? "USB Controller", transport: "USB", maximumSpeed: controller["device_speed"] as? String ?? "See connected devices", isConnected: true, status: "\(foundDevices.count) connected device\(foundDevices.count == 1 ? "" : "s")", devices: foundDevices )) } return ports.sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending } } private static func devices(in raw: Any?, depth: Int, prefix: String) -> [ConnectedHardwareDevice] { guard let items = raw as? [[String: Any]] else { return [] } return items.enumerated().flatMap { index, item -> [ConnectedHardwareDevice] in let name = item["_name"] as? String ?? "Connected device" let vendor = item["manufacturer"] as? String ?? item["vendor_name"] as? String ?? item["vendor_id"] as? String ?? "Not reported" let device = ConnectedHardwareDevice( id: "\(prefix)-\(index)-\(name)-\(depth)", name: name, vendor: vendor, speed: item["device_speed"] as? String ?? item["current_speed_key"] as? String ?? "Not reported", currentAvailableMA: integer(from: item["current_available"]), currentRequiredMA: integer(from: item["current_required"]), depth: depth ) return [device] + devices(in: item["_items"], depth: depth + 1, prefix: "\(prefix)-\(index)") } } private static func integer(from value: Any?) -> Int? { if let number = value as? NSNumber { return number.intValue } if let string = value as? String, let raw = string.firstMatch(#"([0-9]+)"#) { return Int(raw) } return nil } private static func run(_ path: String, arguments: [String]) -> String { String(decoding: runData(path, arguments: arguments), as: UTF8.self) } private static func runData(_ path: String, arguments: [String]) -> Data { let process = Process() let pipe = Pipe() process.executableURL = URL(fileURLWithPath: path) process.arguments = arguments process.standardOutput = pipe process.standardError = FileHandle.nullDevice do { try process.run() let data = pipe.fileHandleForReading.readDataToEndOfFile() process.waitUntilExit() return process.terminationStatus == 0 ? data : Data() } catch { return Data() } } } private enum BinaryArchitectureReader { private static let machO64: UInt32 = 0xfeedfacf private static let machO64Swapped: UInt32 = 0xcffaedfe private static let fat32: UInt32 = 0xcafebabe private static let fat32Swapped: UInt32 = 0xbebafeca private static let fat64: UInt32 = 0xcafebabf private static let fat64Swapped: UInt32 = 0xbfbafeca private static let arm64: UInt32 = 0x0100000c private static let x86_64: UInt32 = 0x01000007 static func architecture(at path: String) -> String { guard path.hasPrefix("/"), let handle = try? FileHandle(forReadingFrom: URL(fileURLWithPath: path)) else { return "Unknown" } defer { try? handle.close() } guard let data = try? handle.read(upToCount: 512), data.count >= 8 else { return "Unknown" } let magicBE = read(data, offset: 0, bigEndian: true) let magicLE = read(data, offset: 0, bigEndian: false) if magicBE == fat32 || magicBE == fat64 || magicBE == fat32Swapped || magicBE == fat64Swapped { let bigEndian = magicBE == fat32 || magicBE == fat64 let is64 = magicBE == fat64 || magicBE == fat64Swapped let count = min(Int(read(data, offset: 4, bigEndian: bigEndian)), 16) let stride = is64 ? 32 : 20 var architectures: Set = [] for index in 0.. UInt32 { guard offset + 4 <= data.count else { return 0 } let bytes = data[offset..<(offset + 4)] if bigEndian { return bytes.reduce(0) { ($0 << 8) | UInt32($1) } } return bytes.reversed().reduce(0) { ($0 << 8) | UInt32($1) } } } private extension String { func firstMatch(_ pattern: String) -> String? { guard let regex = try? NSRegularExpression(pattern: pattern), let match = regex.firstMatch(in: self, range: NSRange(startIndex..., in: self)), match.numberOfRanges > 1, let range = Range(match.range(at: 1), in: self) else { return nil } return String(self[range]) } }