diff --git a/Tun/Punchnet/Actors/SDLContextActor.swift b/Tun/Punchnet/Actors/SDLContextActor.swift index 001e7bd..7901426 100644 --- a/Tun/Punchnet/Actors/SDLContextActor.swift +++ b/Tun/Punchnet/Actors/SDLContextActor.swift @@ -86,7 +86,7 @@ actor SDLContextActor { nonisolated private let arpServer: ArpServer // 网络状态变化的健康 - private var monitor: SDLNetworkMonitor? + private var monitor: SDLPathMonitor? private var monitorWorker: Task? // 内部socket通讯 @@ -338,20 +338,25 @@ actor SDLContextActor { self.monitorWorker = nil // 启动monitor - let monitor = SDLNetworkMonitor() + let monitor = SDLPathMonitor() monitor.start() SDLLogger.log("[SDLContext] monitor started") self.monitor = monitor - self.monitorWorker = Task.detached { - for await event in monitor.eventStream { - switch event { - case .changed: - // 需要重新探测网络的nat类型 - await self.probeNatType() - SDLLogger.log("didNetworkPathChanged, nat type is:") - case .unreachable: - SDLLogger.log("didNetworkPathUnreachable") + self.monitorWorker = Task { + for await status in monitor.statusStream() { + switch status { + case .satisfied: + SDLLogger.log("[Path] stable satisfied", for: .debug) + // await contextActor.networkPathDidBecomeSatisfied(snapshot) + case .unsatisfied: + SDLLogger.log("[Path] stable unsatisfied", for: .debug) + // await contextActor.networkPathDidBecomeUnsatisfied(snapshot) + case .requiresConnection: + SDLLogger.log("[Path] stable requiresConnection", for: .debug) + + @unknown default: + SDLLogger.log("[Path] stable unknown", for: .debug) } } } diff --git a/Tun/Punchnet/NWPath/SDLPathDebounceActor.swift b/Tun/Punchnet/NWPath/SDLPathDebounceActor.swift new file mode 100644 index 0000000..e050f78 --- /dev/null +++ b/Tun/Punchnet/NWPath/SDLPathDebounceActor.swift @@ -0,0 +1,54 @@ +// +// SDLPathDebounceActor.swift +// punchnet +// +// Created by 安礼成 on 2026/4/29. +// + +import Foundation +import Network + +actor SDLPathDebounceActor { + private var lastSnapshot: SDLPathSnapshot? + private var debounceTask: Task? + + private let delay: Duration + + public let statusStream: AsyncStream + private let statusCont: AsyncStream.Continuation + + init(delay: Duration = .seconds(2)) { + self.delay = delay + let (stream, cont) = AsyncStream.makeStream(of: NWPath.Status.self) + self.statusStream = stream + self.statusCont = cont + } + + func submit(_ snapshot: SDLPathSnapshot) { + guard snapshot != lastSnapshot else { + return + } + + self.lastSnapshot = snapshot + + self.debounceTask?.cancel() + self.debounceTask = Task { [delay] in + try? await Task.sleep(for: delay) + + guard !Task.isCancelled else { + return + } + await self.handleStableSnapshot(snapshot) + } + } + + private func handleStableSnapshot(_ snapshot: SDLPathSnapshot) async { + self.statusCont.yield(snapshot.status) + } + + func stop() { + debounceTask?.cancel() + debounceTask = nil + } + +} diff --git a/Tun/Punchnet/NWPath/SDLPathMonitor.swift b/Tun/Punchnet/NWPath/SDLPathMonitor.swift new file mode 100644 index 0000000..62c72be --- /dev/null +++ b/Tun/Punchnet/NWPath/SDLPathMonitor.swift @@ -0,0 +1,38 @@ +// +// SDLPathMonitor.swift +// punchnet +// +// Created by 安礼成 on 2026/4/29. +// +import Foundation +import Network + +final class SDLPathMonitor { + private let monitor = NWPathMonitor() + private let queue = DispatchQueue(label: "com.sdlan.path-monitor") + private let debouncer = SDLPathDebounceActor(delay: .seconds(2)) + + func start() { + monitor.pathUpdateHandler = { path in + let snapshot = SDLPathSnapshot(path) + + Task { + await self.debouncer.submit(snapshot) + } + } + + monitor.start(queue: queue) + } + + func statusStream() -> AsyncStream { + return self.debouncer.statusStream + } + + func stop() { + Task { + await debouncer.stop() + } + monitor.cancel() + } + +} diff --git a/Tun/Punchnet/NWPath/SDLPathSnapshot.swift b/Tun/Punchnet/NWPath/SDLPathSnapshot.swift new file mode 100644 index 0000000..3b6d455 --- /dev/null +++ b/Tun/Punchnet/NWPath/SDLPathSnapshot.swift @@ -0,0 +1,32 @@ +// +// PathSnapshot.swift +// punchnet +// +// Created by 安礼成 on 2026/4/29. +// +import Foundation +import Network + +struct SDLPathSnapshot: Equatable, Sendable { + let status: NWPath.Status + let isExpensive: Bool + let isConstrained: Bool + let supportsIPv4: Bool + let supportsIPv6: Bool + let supportsDNS: Bool + let usesWiFi: Bool + let usesWired: Bool + let usesCellular: Bool + + init(_ path: NWPath) { + self.status = path.status + self.isExpensive = path.isExpensive + self.isConstrained = path.isConstrained + self.supportsIPv4 = path.supportsIPv4 + self.supportsIPv6 = path.supportsIPv6 + self.supportsDNS = path.supportsDNS + self.usesWiFi = path.usesInterfaceType(.wifi) + self.usesWired = path.usesInterfaceType(.wiredEthernet) + self.usesCellular = path.usesInterfaceType(.cellular) + } +}