import Foundation import OSLog import MetricsProvider private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "Network") /// Durchsatz je Programm. public struct ProcessTraffic: Equatable, Sendable, Identifiable { public let name: String public let pid: pid_t public let downloadRate: Double public let uploadRate: Double public var id: pid_t { pid } } /// Liest den Durchsatz je Programm über `nettop`. /// /// Es gibt dafür keine Bibliotheksschnittstelle — die zugrundeliegende /// `NetworkStatistics`-API ist privat und verlangt eine Entitlement, die Apple /// nicht vergibt. `nettop` ist Apples eigenes Werkzeug dafür, liegt im System /// und läuft **ohne root** für die Prozesse des eigenen Nutzers. /// /// Deshalb wird es nur abgefragt, solange jemand hinschaut: einen /// Fremdprozess im Sekundentakt zu starten wäre teurer als der gesamte übrige /// Monitor zusammen. public final class ProcessTrafficSampler: @unchecked Sendable { private var previous: [pid_t: (received: UInt64, sent: UInt64)] = [:] private var previousTime: Date? private let lock = NSLock() public init() {} public func sample(limit: Int = 6) -> [ProcessTraffic] { guard let output = runNettop() else { return [] } let now = Date() lock.lock() defer { lock.unlock() } let elapsed = previousTime.map { now.timeIntervalSince($0) } ?? 0 var current: [pid_t: (received: UInt64, sent: UInt64)] = [:] var results: [ProcessTraffic] = [] // Format: `name.pid,bytes_in,bytes_out,` for line in output.split(separator: "\n").dropFirst() { let fields = line.split(separator: ",", omittingEmptySubsequences: false) guard fields.count >= 3 else { continue } let identifier = String(fields[0]) guard let separator = identifier.lastIndex(of: "."), let pid = pid_t(identifier[identifier.index(after: separator)...]), let received = UInt64(fields[1]), let sent = UInt64(fields[2]) else { continue } let name = String(identifier[.. 0, let before = previous[pid] else { continue } let down = Throughput.rate(previous: before.received, current: received, elapsed: elapsed) let up = Throughput.rate(previous: before.sent, current: sent, elapsed: elapsed) guard down + up > 0 else { continue } results.append(ProcessTraffic(name: name, pid: pid, downloadRate: down, uploadRate: up)) } previous = current previousTime = now return Array(results.sorted { $0.downloadRate + $0.uploadRate > $1.downloadRate + $1.uploadRate }.prefix(limit)) } private func runNettop() -> String? { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/nettop") // `-P` fasst je Prozess zusammen, `-x` unterdrückt Einheiten, `-L 1` // nimmt genau eine Messung und beendet sich dann. process.arguments = ["-P", "-x", "-L", "1", "-J", "bytes_in,bytes_out"] let pipe = Pipe() process.standardOutput = pipe process.standardError = FileHandle.nullDevice do { try process.run() let data = pipe.fileHandleForReading.readDataToEndOfFile() process.waitUntilExit() return String(data: data, encoding: .utf8) } catch { log.error("nettop nicht ausführbar: \(error.localizedDescription, privacy: .public)") return nil } } } /// Ermittelt die öffentliche IP-Adresse. /// /// **Standardmäßig aus.** Dafür geht eine Anfrage an einen fremden Dienst, und /// wer einen Systemmonitor benutzt, rechnet nicht damit, dass der nach draußen /// telefoniert. Wird sie eingeschaltet, steht der benutzte Dienst in der /// Oberfläche — man soll nachlesen können, wem die Adresse bekannt wird. @MainActor @Observable public final class PublicAddressLookup { public private(set) var address: String? public private(set) var isLoading = false public private(set) var failed = false /// Der Dienst, der gefragt wird. Bewusst als sichtbare Angabe und nicht /// versteckt im Code. public static let serviceName = "api.ipify.org" private static let endpoint = URL(string: "https://api.ipify.org?format=json")! private var lastFetch: Date? /// Mindestabstand zwischen zwei Abfragen. Die öffentliche Adresse ändert /// sich selten; sie bei jedem Öffnen des Popovers neu zu holen wäre /// unnötiger Verkehr nach draußen. private static let minimumInterval: TimeInterval = 15 * 60 public init() {} public func refresh(force: Bool = false) { if !force, let last = lastFetch, Date().timeIntervalSince(last) < Self.minimumInterval { return } guard !isLoading else { return } isLoading = true failed = false Task { [weak self] in defer { Task { @MainActor in self?.isLoading = false } } do { let (data, _) = try await URLSession.shared.data(from: Self.endpoint) let decoded = try JSONDecoder().decode([String: String].self, from: data) await MainActor.run { self?.address = decoded["ip"] self?.lastFetch = Date() } } catch { log.error("Öffentliche IP nicht abrufbar: \(error.localizedDescription, privacy: .public)") await MainActor.run { self?.failed = true } } } } /// Vergisst die Adresse — beim Abschalten der Funktion. public func clear() { address = nil lastFetch = nil failed = false } }