import Foundation /// A `ConnectionSource` backed by repeatedly shelling out to `/usr/bin/nettop`. /// /// Each tick runs a fresh one-shot `nettop` (`-L 1`). Because byte counts are /// lifetime-cumulative and stable across invocations, the consumer diffs /// successive snapshots to recover per-interval traffic. No elevated privileges /// required — that's the whole point of starting here. public final class NettopSource: ConnectionSource { private let nettopPath: String public init(nettopPath: String = "/usr/bin/nettop") { self.nettopPath = nettopPath } public func snapshots(every interval: TimeInterval) -> AsyncStream { AsyncStream { continuation in let task = Task { [nettopPath] in while !Task.isCancelled { if let output = try? Self.runNettop(at: nettopPath) { let conns = NettopParser.parseConnections(output) continuation.yield(ConnectionSnapshot(timestamp: Date(), connections: conns)) } do { try await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000)) } catch { break // cancelled } } continuation.finish() } continuation.onTermination = { _ in task.cancel() } } } /// Runs one-shot nettop and returns its stdout. Reads the pipe to EOF *before* /// waiting on exit so large output can't deadlock a full pipe buffer. static func runNettop(at path: String) throws -> String { let process = Process() process.executableURL = URL(fileURLWithPath: path) // -L 1 : one sample then exit -x : raw bytes (no unit suffixes) // -n : numeric (skip DNS; we enrich ourselves) // -J : pick only the columns we parse process.arguments = ["-L", "1", "-x", "-n", "-J", "time,bytes_in,bytes_out"] let pipe = Pipe() process.standardOutput = pipe process.standardError = FileHandle.nullDevice try process.run() let data = pipe.fileHandleForReading.readDataToEndOfFile() process.waitUntilExit() return String(decoding: data, as: UTF8.self) } }