Files
puter/Sources/Puter/SystemReportExporter.swift
T

173 lines
7.3 KiB
Swift

import AppKit
import Foundation
import UniformTypeIdentifiers
@MainActor
enum SystemReportExporter {
static func export(monitor: SystemMonitor, selectedResource: String? = nil) {
let panel = NSSavePanel()
panel.title = "Export system report"
panel.message = "Save the current performance, process, power, battery, and connected-hardware snapshot as JSON."
panel.prompt = "Export"
panel.allowedContentTypes = [.json]
panel.canCreateDirectories = true
panel.nameFieldStringValue = "Task-Manager-Report-\(filenameDate()).json"
let data: Data
do {
data = try JSONSerialization.data(
withJSONObject: report(monitor: monitor, selectedResource: selectedResource),
options: [.prettyPrinted, .sortedKeys]
)
} catch {
monitor.errorMessage = "Could not create the system report: \(error.localizedDescription)"
return
}
panel.begin { response in
guard response == .OK, let destination = panel.url else { return }
do {
try data.write(to: destination, options: .atomic)
} catch {
monitor.errorMessage = "Could not export the system report: \(error.localizedDescription)"
}
}
}
private static func report(monitor: SystemMonitor, selectedResource: String?) -> [String: Any] {
let snapshot = monitor.snapshot
let battery = monitor.hardware.battery
return [
"generatedAt": ISO8601DateFormatter().string(from: Date()),
"selectedResource": selectedResource ?? "All",
"system": [
"cpuPercent": snapshot.cpuPercent,
"logicalProcessors": snapshot.corePercents.count,
"memoryBytes": snapshot.memoryTotal,
"memoryUsedBytes": snapshot.memoryUsed,
"memoryType": snapshot.memoryType,
"memorySpeed": snapshot.memorySpeed,
"memoryManufacturer": snapshot.memoryManufacturer,
"diskReadBytesPerSecond": snapshot.diskReadRate,
"diskWriteBytesPerSecond": snapshot.diskWriteRate,
"diskActivePercent": snapshot.diskActivePercent,
"gpuPercent": snapshot.gpuPercent,
"gpuCoreCount": snapshot.gpuCoreCount,
"networkInterface": snapshot.networkInterface,
"networkReceiveBytesPerSecond": snapshot.networkReceiveRate,
"networkSendBytesPerSecond": snapshot.networkSendRate,
"processCount": snapshot.processCount,
"threadCount": snapshot.threadCount,
"uptimeSeconds": snapshot.uptime
],
"power": [
"thermalState": monitor.hardware.thermalState,
"systemPowerWatts": battery.systemPowerWatts,
"externalPowerConnected": battery.externalPowerConnected,
"lowPowerMode": ProcessInfo.processInfo.isLowPowerModeEnabled
],
"battery": batteryReport(battery),
"ports": monitor.hardware.ports.map(portReport),
"topProcesses": topProcesses(monitor),
"topUsers": topUsers(monitor)
]
}
private static func batteryReport(_ battery: BatterySnapshot) -> [String: Any] {
var result: [String: Any] = [
"present": battery.isPresent,
"chargePercent": battery.chargePercent,
"charging": battery.isCharging,
"fullyCharged": battery.isFullyCharged,
"healthPercent": battery.healthPercent,
"cycleCount": battery.cycleCount,
"designCycleCount": battery.designCycleCount,
"temperatureCelsius": battery.temperatureCelsius,
"voltageVolts": battery.voltageVolts,
"fullChargeCapacityMAh": battery.fullChargeCapacityMAh,
"designCapacityMAh": battery.designCapacityMAh
]
if let adapter = battery.adapter {
result["adapter"] = [
"name": adapter.name,
"manufacturer": adapter.manufacturer,
"ratedWatts": adapter.ratedWatts,
"negotiatedVoltage": adapter.negotiatedVoltage,
"negotiatedCurrent": adapter.negotiatedCurrent,
"negotiatedWatts": adapter.negotiatedWatts,
"powerProfiles": adapter.profiles.map {
["voltage": $0.voltageVolts, "current": $0.currentAmps, "watts": $0.watts]
}
]
}
return result
}
private static func portReport(_ port: HardwarePortSnapshot) -> [String: Any] {
[
"name": port.name,
"transport": port.transport,
"maximumSpeed": port.maximumSpeed,
"connected": port.isConnected,
"status": port.status,
"devices": port.devices.map {
var device: [String: Any] = ["name": $0.name, "vendor": $0.vendor, "speed": $0.speed]
if let value = $0.currentAvailableMA { device["currentAvailableMA"] = value }
if let value = $0.currentRequiredMA { device["currentRequiredMA"] = value }
return device
}
]
}
private static func topProcesses(_ monitor: SystemMonitor) -> [[String: Any]] {
monitor.processes.sorted { $0.cpu > $1.cpu }.prefix(20).map { process in
let activity = monitor.processActivity[process.pid] ?? ProcessActivityRate()
return [
"name": process.displayName,
"pid": process.pid,
"user": process.user,
"cpuPercent": process.cpu,
"residentBytes": process.residentBytes,
"diskBytesPerSecond": activity.diskTotal,
"networkBytesPerSecond": activity.networkTotal
]
}
}
private static func topUsers(_ monitor: SystemMonitor) -> [[String: Any]] {
struct Totals {
var processes = 0
var cpu = 0.0
var memory: UInt64 = 0
var disk = 0.0
var network = 0.0
}
var users: [String: Totals] = [:]
for process in monitor.processes {
let activity = monitor.processActivity[process.pid] ?? ProcessActivityRate()
users[process.user, default: Totals()].processes += 1
users[process.user, default: Totals()].cpu += process.cpu
users[process.user, default: Totals()].memory += process.residentBytes
users[process.user, default: Totals()].disk += activity.diskTotal
users[process.user, default: Totals()].network += activity.networkTotal
}
return users.map { user, totals in
[
"user": user,
"processes": totals.processes,
"cpuPercent": totals.cpu,
"residentBytes": totals.memory,
"diskBytesPerSecond": totals.disk,
"networkBytesPerSecond": totals.network
]
}
.sorted { ($0["cpuPercent"] as? Double ?? 0) > ($1["cpuPercent"] as? Double ?? 0) }
}
private static func filenameDate() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd-HHmmss"
return formatter.string(from: Date())
}
}