Skip to content

Commit 722f0fe

Browse files
committed
fix: fixed Network reader blocking forever on a hung nettop process (#3499)
1 parent 43e4a94 commit 722f0fe

2 files changed

Lines changed: 89 additions & 111 deletions

File tree

Kit/helpers.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,76 @@ public func process(path: String, arguments: [String]) -> String? {
10261026
return output
10271027
}
10281028

1029+
public func process(path: String, arguments: [String], environment: [String: String]? = nil, timeout: TimeInterval) -> String? {
1030+
let task = Process()
1031+
task.executableURL = URL(fileURLWithPath: path)
1032+
task.arguments = arguments
1033+
if let environment {
1034+
task.environment = environment
1035+
}
1036+
1037+
let inputPipe = Pipe()
1038+
let outputPipe = Pipe()
1039+
let errorPipe = Pipe()
1040+
task.standardInput = inputPipe
1041+
task.standardOutput = outputPipe
1042+
task.standardError = errorPipe
1043+
1044+
let exited = DispatchGroup()
1045+
exited.enter()
1046+
task.terminationHandler = { _ in exited.leave() }
1047+
1048+
do {
1049+
try task.run()
1050+
} catch let err {
1051+
task.terminationHandler = nil
1052+
exited.leave()
1053+
debug("\(path): \(err.localizedDescription)")
1054+
return nil
1055+
}
1056+
1057+
var outputData = Data()
1058+
let drained = DispatchGroup()
1059+
drained.enter()
1060+
DispatchQueue.global(qos: .utility).async {
1061+
outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
1062+
drained.leave()
1063+
}
1064+
drained.enter()
1065+
DispatchQueue.global(qos: .utility).async {
1066+
_ = errorPipe.fileHandleForReading.readDataToEndOfFile()
1067+
drained.leave()
1068+
}
1069+
1070+
var timedOut = false
1071+
if exited.wait(timeout: .now() + timeout) == .timedOut {
1072+
timedOut = true
1073+
task.terminate()
1074+
if exited.wait(timeout: .now() + 2) == .timedOut {
1075+
kill(task.processIdentifier, SIGKILL)
1076+
_ = exited.wait(timeout: .now() + 2)
1077+
}
1078+
}
1079+
1080+
inputPipe.fileHandleForWriting.closeFile()
1081+
guard drained.wait(timeout: .now() + 2) == .success else {
1082+
error("\(path) did not exit within \(Int(timeout))s and could not be killed")
1083+
return nil
1084+
}
1085+
outputPipe.fileHandleForReading.closeFile()
1086+
errorPipe.fileHandleForReading.closeFile()
1087+
1088+
if timedOut {
1089+
error("\(path) did not exit within \(Int(timeout))s, terminated")
1090+
return nil
1091+
}
1092+
1093+
let output = String(data: outputData, encoding: .utf8)
1094+
guard let output, !output.isEmpty else { return nil }
1095+
1096+
return output
1097+
}
1098+
10291099
public class SettingsContainerView: NSStackView {
10301100
public init() {
10311101
super.init(frame: NSRect.zero)

Modules/Net/readers.swift

Lines changed: 19 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -321,44 +321,15 @@ internal class UsageReader: Reader<Network_Usage>, CWEventDelegate {
321321
}
322322

323323
private func readProcessBandwidth() -> Bandwidth {
324-
let task = Process()
325-
task.executableURL = URL(fileURLWithPath: "/usr/bin/nettop")
326-
task.arguments = ["-P", "-L", "1", "-n", "-k", "time,interface,state,rx_dupe,rx_ooo,re-tx,rtt_avg,rcvsize,tx_win,tc_class,tc_mgt,cc_algo,P,C,R,W,arch"]
327-
task.environment = [
328-
"NSUnbufferedIO": "YES",
329-
"LC_ALL": "en_US.UTF-8"
330-
]
331-
332-
let inputPipe = Pipe()
333-
let outputPipe = Pipe()
334-
let errorPipe = Pipe()
335-
336-
task.standardInput = inputPipe
337-
task.standardOutput = outputPipe
338-
task.standardError = errorPipe
339-
340-
defer {
341-
if task.isRunning {
342-
task.terminate()
343-
}
344-
task.waitUntilExit()
345-
inputPipe.fileHandleForWriting.closeFile()
346-
outputPipe.fileHandleForReading.closeFile()
347-
errorPipe.fileHandleForReading.closeFile()
348-
}
349-
350-
do {
351-
try task.run()
352-
} catch let err {
353-
error("read bandwidth from processes: \(err)", log: self.log)
354-
return Bandwidth()
355-
}
356-
357-
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
358-
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
359-
let output = String(data: outputData, encoding: .utf8)
360-
_ = String(data: errorData, encoding: .utf8)
361-
guard let output, !output.isEmpty else { return Bandwidth() }
324+
guard let output = process(
325+
path: "/usr/bin/nettop",
326+
arguments: ["-P", "-L", "1", "-n", "-k", "time,interface,state,rx_dupe,rx_ooo,re-tx,rtt_avg,rcvsize,tx_win,tc_class,tc_mgt,cc_algo,P,C,R,W,arch"],
327+
environment: [
328+
"NSUnbufferedIO": "YES",
329+
"LC_ALL": "en_US.UTF-8"
330+
],
331+
timeout: 5
332+
) else { return Bandwidth() }
362333

363334
var totalUpload: Int64 = 0
364335
var totalDownload: Int64 = 0
@@ -510,41 +481,7 @@ internal class UsageReader: Reader<Network_Usage>, CWEventDelegate {
510481
}
511482

512483
private func systemProfilerAirport(timeout: TimeInterval) -> String? {
513-
let task = Process()
514-
task.executableURL = URL(fileURLWithPath: "/usr/sbin/system_profiler")
515-
task.arguments = ["SPAirPortDataType", "-json"]
516-
517-
let outputPipe = Pipe()
518-
task.standardOutput = outputPipe
519-
task.standardError = Pipe()
520-
521-
do {
522-
try task.run()
523-
} catch let err {
524-
error("read SPAirPortDataType: \(err)", log: self.log)
525-
return nil
526-
}
527-
528-
var output: String?
529-
let group = DispatchGroup()
530-
group.enter()
531-
DispatchQueue.global(qos: .utility).async {
532-
let data = outputPipe.fileHandleForReading.readDataToEndOfFile()
533-
output = String(data: data, encoding: .utf8)
534-
group.leave()
535-
}
536-
537-
if group.wait(timeout: .now() + timeout) == .timedOut {
538-
if task.isRunning {
539-
task.terminate()
540-
}
541-
error("SPAirPortDataType timed out after \(Int(timeout))s, terminating", log: self.log)
542-
return nil
543-
}
544-
545-
task.waitUntilExit()
546-
guard let output, !output.isEmpty else { return nil }
547-
return output
484+
return process(path: "/usr/sbin/system_profiler", arguments: ["SPAirPortDataType", "-json"], timeout: timeout)
548485
}
549486

550487
private func getLocalIP(_ pointer: UnsafeMutablePointer<ifaddrs>) {
@@ -727,44 +664,15 @@ public class ProcessReader: Reader<[Network_Process]> {
727664
return
728665
}
729666

730-
let task = Process()
731-
task.executableURL = URL(fileURLWithPath: "/usr/bin/nettop")
732-
task.arguments = ["-P", "-L", "1", "-n", "-k", "time,interface,state,rx_dupe,rx_ooo,re-tx,rtt_avg,rcvsize,tx_win,tc_class,tc_mgt,cc_algo,P,C,R,W,arch"]
733-
task.environment = [
734-
"NSUnbufferedIO": "YES",
735-
"LC_ALL": "en_US.UTF-8"
736-
]
737-
738-
let inputPipe = Pipe()
739-
let outputPipe = Pipe()
740-
let errorPipe = Pipe()
741-
742-
task.standardInput = inputPipe
743-
task.standardOutput = outputPipe
744-
task.standardError = errorPipe
745-
746-
defer {
747-
if task.isRunning {
748-
task.terminate()
749-
}
750-
task.waitUntilExit()
751-
inputPipe.fileHandleForWriting.closeFile()
752-
outputPipe.fileHandleForReading.closeFile()
753-
errorPipe.fileHandleForReading.closeFile()
754-
}
755-
756-
do {
757-
try task.run()
758-
} catch let error {
759-
print(error)
760-
return
761-
}
762-
763-
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
764-
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
765-
let output = String(data: outputData, encoding: .utf8)
766-
_ = String(data: errorData, encoding: .utf8)
767-
guard let output, !output.isEmpty else { return }
667+
guard let output = process(
668+
path: "/usr/bin/nettop",
669+
arguments: ["-P", "-L", "1", "-n", "-k", "time,interface,state,rx_dupe,rx_ooo,re-tx,rtt_avg,rcvsize,tx_win,tc_class,tc_mgt,cc_algo,P,C,R,W,arch"],
670+
environment: [
671+
"NSUnbufferedIO": "YES",
672+
"LC_ALL": "en_US.UTF-8"
673+
],
674+
timeout: 5
675+
) else { return }
768676

769677
var list: [Network_Process] = []
770678
var firstLine = false

0 commit comments

Comments
 (0)