Files
timban c08fc277a9 Initial commit: NetworkCity — network traffic as a neon city
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>
2026-06-13 08:43:32 -07:00

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)
}
}