summaryrefslogtreecommitdiff
path: root/DomainDig/ReachabilityService.swift
blob: 3fb7dd99aebc6e699fc5742b918241b33e6bde52 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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 -> ServiceResult<[PortReachability]> {
        async let port443 = check(domain: domain, port: 443)
        async let port80 = check(domain: domain, port: 80)
        let results = await [port443, port80]
        return results.isEmpty ? .empty("No reachability results") : .success(results)
    }
}

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