Files
OpenStats/Packages/OpenStatsKit/Sources/Metrics/NetworkProcessSampler.swift
T
gentpan 8875b6e709 每项独立菜单栏与详情弹窗、网络详情与 DNS、侧边栏主窗口与统一外观
- 菜单栏可每项独立(点击弹出该项窄详情)或合并为一个(点击打开主窗口),详情区块可在设置中勾选
- 网络详情:镜像流量历史、ICMP 连接探测格子、接口与 Wi-Fi、本地与公网 IPv4 / IPv6、SVG 国旗归属地与 ASN、各进程流量
- DNS:刷新缓存,一键切换自动 / Cloudflare / Google / 腾讯 / 阿里云或手动填写;辅助工具协议升至 3,服务名与地址二次校验
- 主窗口:侧边栏切换仪表盘、各指标详情与工具页,宽高可调,卡片按比例伸缩
- 统一外观:白底蓝 / 黑底蓝,窗口、设置与弹窗共用底色,去掉玻璃效果
- 新增网络解析、DNS 校验与国旗代码校验的单元测试;更新 README、架构文档与隐私说明
2026-09-13 05:39:10 +05:00

92 lines
4.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Darwin
import Foundation
public struct NetworkProcessUsage: Sendable, Equatable, Identifiable {
public var id: Int32 { pid }
public let pid: Int32
public let name: String
public let appBundlePath: String?
public var download: Double // 字节 / 秒
public var upload: Double
}
/// 各进程的网络速率。系统没有公开接口,读取 /usr/bin/nettop 的累计字节数并与上次比较。
/// 只在网络详情打开时运行,每次约 40 ms。
public struct NetworkProcessSampler: Sendable {
private var previous: [Int32: (download: UInt64, upload: UInt64)] = [:]
private var previousTime: UInt64 = 0
public init() {}
public mutating func sample(limit: Int = 8) -> [NetworkProcessUsage] {
guard let output = Self.runNettop() else { return [] }
let now = clock_gettime_nsec_np(CLOCK_UPTIME_RAW)
let seconds = previousTime > 0 ? Double(now - previousTime) / 1_000_000_000 : 0
let totals = Self.parse(output)
var results: [NetworkProcessUsage] = []
if seconds > 0 {
for (pid, entry) in totals {
guard let before = previous[pid] else { continue }
let download = entry.download >= before.download ? Double(entry.download - before.download) / seconds : 0
let upload = entry.upload >= before.upload ? Double(entry.upload - before.upload) / seconds : 0
guard download + upload > 0 else { continue }
let meta = ProcessSampler.meta(for: pid)
results.append(NetworkProcessUsage(pid: pid, name: meta.bundle.map { URL(fileURLWithPath: $0).deletingPathExtension().lastPathComponent } ?? meta.name,
appBundlePath: meta.bundle, download: download, upload: upload))
}
}
previous = totals.mapValues { ($0.download, $0.upload) }
previousTime = now
// 同一应用的多个进程合并为一行
var merged: [String: NetworkProcessUsage] = [:]
for usage in results {
let key = usage.appBundlePath ?? "\(usage.pid)"
if var existing = merged[key] {
existing.download += usage.download
existing.upload += usage.upload
merged[key] = existing
} else {
merged[key] = usage
}
}
return merged.values
.sorted { $0.download + $0.upload > $1.download + $1.upload }
.prefix(limit)
.map { $0 }
}
/// 解析 `nettop -P -L 1 -x -J bytes_in,bytes_out` 的 CSV:`进程名.PID,入字节,出字节,`
static func parse(_ output: String) -> [Int32: (name: String, download: UInt64, upload: UInt64)] {
var result: [Int32: (name: String, download: UInt64, upload: UInt64)] = [:]
for line in output.split(whereSeparator: \.isNewline).dropFirst() {
let columns = line.split(separator: ",", omittingEmptySubsequences: false)
guard columns.count >= 3,
let dot = columns[0].lastIndex(of: "."),
let pid = Int32(columns[0][columns[0].index(after: dot)...]),
let download = UInt64(columns[1]),
let upload = UInt64(columns[2]) else { continue }
result[pid] = (String(columns[0][..<dot]), download, upload)
}
return result
}
private static func runNettop() -> String? {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/nettop")
process.arguments = ["-P", "-L", "1", "-n", "-x", "-J", "bytes_in,bytes_out"]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = FileHandle.nullDevice
do {
try process.run()
} catch {
return nil
}
let data = pipe.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit()
return process.terminationStatus == 0 ? String(decoding: data, as: UTF8.self) : nil
}
}