summaryrefslogtreecommitdiff
path: root/DomainDig/ReachabilityService.swift
diff options
context:
space:
mode:
Diffstat (limited to 'DomainDig/ReachabilityService.swift')
-rw-r--r--DomainDig/ReachabilityService.swift67
1 files changed, 67 insertions, 0 deletions
diff --git a/DomainDig/ReachabilityService.swift b/DomainDig/ReachabilityService.swift
new file mode 100644
index 0000000..0bb30ae
--- /dev/null
+++ b/DomainDig/ReachabilityService.swift
@@ -0,0 +1,67 @@
+import Foundation
+import Network
+
+struct ReachabilityService {
+ static func check(domain: String, port: UInt16) async -> PortReachability {
+ await withCheckedContinuation { continuation in
+ let host = NWEndpoint.Host(domain)
+ let nwPort = NWEndpoint.Port(rawValue: port)!
+ let connection = NWConnection(host: host, port: nwPort, using: .tcp)
+ let context = ConnectionContext(port: port, connection: connection, continuation: continuation)
+
+ connection.stateUpdateHandler = { state in
+ switch state {
+ case .ready:
+ context.finish(reachable: true)
+ case .failed, .cancelled:
+ context.finish(reachable: false)
+ default:
+ break
+ }
+ }
+
+ let queue = DispatchQueue(label: "reachability.\(port)")
+ connection.start(queue: queue)
+
+ queue.asyncAfter(deadline: .now() + 5) {
+ context.finish(reachable: false)
+ }
+ }
+ }
+
+ static func checkAll(domain: String) async -> [PortReachability] {
+ async let port443 = check(domain: domain, port: 443)
+ async let port80 = check(domain: domain, port: 80)
+ return await [port443, port80]
+ }
+}
+
+private final class ConnectionContext: @unchecked Sendable {
+ private let port: UInt16
+ private let connection: NWConnection
+ private let continuation: CheckedContinuation<PortReachability, Never>
+ private let start = CFAbsoluteTimeGetCurrent()
+ private let lock = NSLock()
+ private nonisolated(unsafe) var resumed = false
+
+ init(port: UInt16, connection: NWConnection, continuation: CheckedContinuation<PortReachability, Never>) {
+ self.port = port
+ self.connection = connection
+ self.continuation = continuation
+ }
+
+ nonisolated func finish(reachable: Bool) {
+ lock.lock()
+ guard !resumed else {
+ lock.unlock()
+ return
+ }
+ resumed = true
+ lock.unlock()
+
+ let elapsed = CFAbsoluteTimeGetCurrent() - start
+ let ms = reachable ? Int(elapsed * 1000) : nil
+ connection.cancel()
+ continuation.resume(returning: PortReachability(port: port, reachable: reachable, latencyMs: ms))
+ }
+}