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