173 lines
7.3 KiB
Swift
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())
|
|
}
|
|
}
|