c08fc277a9
A native macOS network monitor that renders live traffic as a top-down neon city: your Mac is downtown, remote orgs are glowing districts, and traffic is cars of light driving the roads. Architecture: - NetworkCityCore: swappable data layer behind a ConnectionSource protocol (nettop-backed today), plus pure/tested logic — traffic diffing, org classification (reverse-DNS + CIDR), traffic classes, hub-and-spoke expansion policy, label anti-overlap, and latency→distance layout. - NetworkCityApp: SwiftUI + SpriteKit city — auto-expanding districts, protocol-coloured cars, click-to-inspect panel, and latency-as-distance (districts glide to their measured RTT). - nettop-probe: CLI proof of the data layer. 44 tests passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
58 lines
2.3 KiB
Swift
58 lines
2.3 KiB
Swift
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<ConnectionSnapshot> {
|
|
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)
|
|
}
|
|
}
|