- 菜单栏可每项独立(点击弹出该项窄详情)或合并为一个(点击打开主窗口),详情区块可在设置中勾选 - 网络详情:镜像流量历史、ICMP 连接探测格子、接口与 Wi-Fi、本地与公网 IPv4 / IPv6、SVG 国旗归属地与 ASN、各进程流量 - DNS:刷新缓存,一键切换自动 / Cloudflare / Google / 腾讯 / 阿里云或手动填写;辅助工具协议升至 3,服务名与地址二次校验 - 主窗口:侧边栏切换仪表盘、各指标详情与工具页,宽高可调,卡片按比例伸缩 - 统一外观:白底蓝 / 黑底蓝,窗口、设置与弹窗共用底色,去掉玻璃效果 - 新增网络解析、DNS 校验与国旗代码校验的单元测试;更新 README、架构文档与隐私说明
91 lines
3.5 KiB
Swift
91 lines
3.5 KiB
Swift
import Darwin
|
|
import Foundation
|
|
|
|
public struct ProcessSampler {
|
|
private struct Identity: Hashable {
|
|
let pid: Int32
|
|
let startTime: UInt64
|
|
}
|
|
|
|
struct Meta {
|
|
let name: String
|
|
let path: String?
|
|
let bundle: String?
|
|
}
|
|
|
|
private var previousCPU: [Identity: UInt64] = [:]
|
|
private var previousTime: UInt64 = 0
|
|
private var metaCache: [Identity: Meta] = [:]
|
|
|
|
public init() {}
|
|
|
|
/// 返回按 CPU 排序的进程;无权限读取的进程(通常属于 root)会被跳过。
|
|
public mutating func sample(limit: Int = 50) -> [ProcessUsage] {
|
|
let capacity = Int(proc_listallpids(nil, 0)) + 64
|
|
guard capacity > 64 else { return [] }
|
|
var pids = [pid_t](repeating: 0, count: capacity)
|
|
let count = Int(proc_listallpids(&pids, Int32(capacity * MemoryLayout<pid_t>.stride)))
|
|
guard count > 0 else { return [] }
|
|
|
|
// CPU 时间与墙钟时间都用 mach 绝对时间单位,比值无需换算时基
|
|
let now = mach_absolute_time()
|
|
let elapsed = previousTime > 0 ? now - previousTime : 0
|
|
|
|
var cpuTimes: [Identity: UInt64] = [:]
|
|
var liveMeta: [Identity: Meta] = [:]
|
|
var results: [ProcessUsage] = []
|
|
results.reserveCapacity(count)
|
|
|
|
for pid in pids.prefix(count) where pid > 0 {
|
|
var info = rusage_info_v4()
|
|
let status = withUnsafeMutablePointer(to: &info) {
|
|
$0.withMemoryRebound(to: rusage_info_t?.self, capacity: 1) {
|
|
proc_pid_rusage(pid, RUSAGE_INFO_V4, $0)
|
|
}
|
|
}
|
|
guard status == 0 else { continue }
|
|
|
|
let identity = Identity(pid: pid, startTime: info.ri_proc_start_abstime)
|
|
let cpuTime = info.ri_user_time + info.ri_system_time
|
|
cpuTimes[identity] = cpuTime
|
|
|
|
let meta = metaCache[identity] ?? Self.meta(for: pid)
|
|
liveMeta[identity] = meta
|
|
|
|
var cpu = 0.0
|
|
if elapsed > 0, let before = previousCPU[identity], cpuTime >= before {
|
|
cpu = Double(cpuTime - before) / Double(elapsed)
|
|
}
|
|
results.append(ProcessUsage(pid: pid, name: meta.name, executablePath: meta.path,
|
|
appBundlePath: meta.bundle, cpu: cpu, memory: info.ri_phys_footprint))
|
|
}
|
|
|
|
previousCPU = cpuTimes
|
|
previousTime = now
|
|
metaCache = liveMeta
|
|
|
|
results.sort { $0.cpu == $1.cpu ? $0.memory > $1.memory : $0.cpu > $1.cpu }
|
|
return Array(results.prefix(limit))
|
|
}
|
|
|
|
static func meta(for pid: pid_t) -> Meta {
|
|
var pathBuffer = [CChar](repeating: 0, count: 4 * Int(MAXPATHLEN))
|
|
let pathLength = proc_pidpath(pid, &pathBuffer, UInt32(pathBuffer.count))
|
|
let path = pathLength > 0 ? String(nullTerminated: pathBuffer) : nil
|
|
|
|
var name = path.map { URL(fileURLWithPath: $0).lastPathComponent } ?? ""
|
|
if name.isEmpty {
|
|
var nameBuffer = [CChar](repeating: 0, count: 256)
|
|
proc_name(pid, &nameBuffer, UInt32(nameBuffer.count))
|
|
name = String(nullTerminated: nameBuffer)
|
|
}
|
|
|
|
// 取最外层 .app,辅助进程也能显示主应用图标
|
|
let bundle = path.flatMap { path -> String? in
|
|
guard let range = path.range(of: ".app/") else { return nil }
|
|
return String(path[..<range.lowerBound]) + ".app"
|
|
}
|
|
return Meta(name: name.isEmpty ? "PID \(pid)" : name, path: path, bundle: bundle)
|
|
}
|
|
}
|