解决NE到主App的通讯问题
This commit is contained in:
parent
98386ded25
commit
7e2e744bdb
@ -21,6 +21,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
let shared = UserDefaults(suiteName: "group.com.jihe.punchnetmac")
|
let shared = UserDefaults(suiteName: "group.com.jihe.punchnetmac")
|
||||||
let msg = shared?.string(forKey: "test_msg")
|
let msg = shared?.string(forKey: "test_msg")
|
||||||
SDLLogger.log("NE read message: \(msg ?? "failed")", for: .debug)
|
SDLLogger.log("NE read message: \(msg ?? "failed")", for: .debug)
|
||||||
|
SDLTunnelAppNotifier.shared.clear()
|
||||||
|
|
||||||
SDLNotificationCenter.shared.post(.vpnStatusChanged)
|
SDLNotificationCenter.shared.post(.vpnStatusChanged)
|
||||||
|
|
||||||
|
|||||||
@ -70,7 +70,7 @@ actor SDLContextActor {
|
|||||||
private var monitorWorker: Task<Void, Never>?
|
private var monitorWorker: Task<Void, Never>?
|
||||||
|
|
||||||
// 内部socket通讯
|
// 内部socket通讯
|
||||||
private var noticeClient: SDLNoticeClient?
|
// 改为基于 App Group + Darwin Notification 的通知
|
||||||
|
|
||||||
// 流量统计
|
// 流量统计
|
||||||
nonisolated private let flowTracer = SDLFlowTracer()
|
nonisolated private let flowTracer = SDLFlowTracer()
|
||||||
@ -114,6 +114,7 @@ actor SDLContextActor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.readyState = .starting
|
self.readyState = .starting
|
||||||
|
self.prepareTunnelNotifier()
|
||||||
self.startMonitor()
|
self.startMonitor()
|
||||||
|
|
||||||
// 启动arp的定时清理任务
|
// 启动arp的定时清理任务
|
||||||
@ -129,14 +130,7 @@ actor SDLContextActor {
|
|||||||
await quicClient.waitClose()
|
await quicClient.waitClose()
|
||||||
SDLLogger.log("[SDLContext] quicClient closed!!!!")
|
SDLLogger.log("[SDLContext] quicClient closed!!!!")
|
||||||
}
|
}
|
||||||
|
|
||||||
await self.supervisor.addWorker(name: "noticeClient") {
|
|
||||||
let noticeClient = try self.startNoticeClient()
|
|
||||||
SDLLogger.log("[SDLContext] noticeClient running!!!!")
|
|
||||||
try await noticeClient.waitClose()
|
|
||||||
SDLLogger.log("[SDLContext] noticeClient closed!!!!")
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.supervisor.addWorker(name: "udpHole") {
|
await self.supervisor.addWorker(name: "udpHole") {
|
||||||
let udpHole = try await self.startUDPHole()
|
let udpHole = try await self.startUDPHole()
|
||||||
SDLLogger.log("[SDLContext] udp running!!!!")
|
SDLLogger.log("[SDLContext] udp running!!!!")
|
||||||
@ -217,15 +211,12 @@ actor SDLContextActor {
|
|||||||
return quicClient
|
return quicClient
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startNoticeClient() throws -> SDLNoticeClient {
|
private func prepareTunnelNotifier() {
|
||||||
// 启动noticeClient
|
// 启动noticeClient
|
||||||
let noticeClient = try SDLNoticeClient(noticePort: self.config.noticePort)
|
// 旧的 UDP NoticeClient 已移除,改为初始化基于 App Group 的通知通道。
|
||||||
noticeClient.start()
|
SDLTunnelAppNotifier.shared.clear()
|
||||||
|
|
||||||
SDLLogger.log("[SDLContext] noticeClient started")
|
SDLLogger.log("[SDLContext] tunnelAppNotifier ready")
|
||||||
self.noticeClient = noticeClient
|
|
||||||
|
|
||||||
return noticeClient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startMonitor() {
|
private func startMonitor() {
|
||||||
@ -386,11 +377,15 @@ actor SDLContextActor {
|
|||||||
self.updatePolicyTask?.cancel()
|
self.updatePolicyTask?.cancel()
|
||||||
self.updatePolicyTask = nil
|
self.updatePolicyTask = nil
|
||||||
|
|
||||||
self.noticeClient?.stop()
|
|
||||||
self.noticeClient = nil
|
|
||||||
self.sessionToken = nil
|
self.sessionToken = nil
|
||||||
self.dataCipher = nil
|
self.dataCipher = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func publishTunnelEvent(level: SDLTunnelAppNotifier.Event.Level = .error,
|
||||||
|
code: Int? = nil,
|
||||||
|
message: String) {
|
||||||
|
SDLTunnelAppNotifier.shared.publish(level: level, code: code, message: message)
|
||||||
|
}
|
||||||
|
|
||||||
private func setNatType(natType: SDLNATProberActor.NatType) {
|
private func setNatType(natType: SDLNATProberActor.NatType) {
|
||||||
self.natType = natType
|
self.natType = natType
|
||||||
@ -711,8 +706,7 @@ extension SDLContextActor {
|
|||||||
switch errorCode {
|
switch errorCode {
|
||||||
case .invalidToken, .nodeDisabled:
|
case .invalidToken, .nodeDisabled:
|
||||||
self.superRegistrationStateMachine.handleFailure()
|
self.superRegistrationStateMachine.handleFailure()
|
||||||
let alertNotice = NoticeMessage.alert(alert: errorMessage)
|
self.publishTunnelEvent(level: .fatal, code: Int(errorCode.rawValue), message: errorMessage)
|
||||||
self.noticeClient?.send(data: alertNotice)
|
|
||||||
// 报告错误并退出
|
// 报告错误并退出
|
||||||
let error = NSError(domain: "com.jihe.punchnet.tun", code: -1)
|
let error = NSError(domain: "com.jihe.punchnet.tun", code: -1)
|
||||||
self.failReady(error)
|
self.failReady(error)
|
||||||
@ -720,8 +714,7 @@ extension SDLContextActor {
|
|||||||
|
|
||||||
case .noIpAddress, .networkFault, .internalFault:
|
case .noIpAddress, .networkFault, .internalFault:
|
||||||
self.superRegistrationStateMachine.handleRetryableNak()
|
self.superRegistrationStateMachine.handleRetryableNak()
|
||||||
let alertNotice = NoticeMessage.alert(alert: errorMessage)
|
self.publishTunnelEvent(level: .warning, code: Int(errorCode.rawValue), message: errorMessage)
|
||||||
self.noticeClient?.send(data: alertNotice)
|
|
||||||
}
|
}
|
||||||
SDLLogger.log("[SDLContext] Get a SuperNak message exit")
|
SDLLogger.log("[SDLContext] Get a SuperNak message exit")
|
||||||
|
|
||||||
@ -742,8 +735,7 @@ extension SDLContextActor {
|
|||||||
self.sendPeerPacket(type: .register, data: registerData, remoteAddress: remoteAddress)
|
self.sendPeerPacket(type: .register, data: registerData, remoteAddress: remoteAddress)
|
||||||
}
|
}
|
||||||
case .shutdown(let message):
|
case .shutdown(let message):
|
||||||
let alertNotice = NoticeMessage.alert(alert: message)
|
self.publishTunnelEvent(level: .fatal, message: message)
|
||||||
self.noticeClient?.send(data: alertNotice)
|
|
||||||
|
|
||||||
// 报告错误并退出
|
// 报告错误并退出
|
||||||
let error = NSError(domain: "com.jihe.punchnet.tun", code: -2)
|
let error = NSError(domain: "com.jihe.punchnet.tun", code: -2)
|
||||||
|
|||||||
@ -52,8 +52,6 @@ public class SDLConfiguration {
|
|||||||
let serverIp: String
|
let serverIp: String
|
||||||
let stunServers: [String]
|
let stunServers: [String]
|
||||||
|
|
||||||
let noticePort: Int
|
|
||||||
|
|
||||||
lazy var stunSocketAddress: SocketAddress = {
|
lazy var stunSocketAddress: SocketAddress = {
|
||||||
let stunServer = stunServers[0]
|
let stunServer = stunServers[0]
|
||||||
return try! SocketAddress.makeAddressResolvingHost(stunServer, port: 1365)
|
return try! SocketAddress.makeAddressResolvingHost(stunServer, port: 1365)
|
||||||
@ -84,7 +82,6 @@ public class SDLConfiguration {
|
|||||||
clientId: String,
|
clientId: String,
|
||||||
networkAddress: NetworkAddress,
|
networkAddress: NetworkAddress,
|
||||||
hostname: String,
|
hostname: String,
|
||||||
noticePort: Int,
|
|
||||||
accessToken: String,
|
accessToken: String,
|
||||||
identityId: UInt32,
|
identityId: UInt32,
|
||||||
exitNode: ExitNode?) {
|
exitNode: ExitNode?) {
|
||||||
@ -94,7 +91,6 @@ public class SDLConfiguration {
|
|||||||
self.stunServers = stunServers
|
self.stunServers = stunServers
|
||||||
self.clientId = clientId
|
self.clientId = clientId
|
||||||
self.networkAddress = networkAddress
|
self.networkAddress = networkAddress
|
||||||
self.noticePort = noticePort
|
|
||||||
self.accessToken = accessToken
|
self.accessToken = accessToken
|
||||||
self.identityId = identityId
|
self.identityId = identityId
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
@ -110,7 +106,6 @@ extension SDLConfiguration {
|
|||||||
guard let version = options["version"] as? Int,
|
guard let version = options["version"] as? Int,
|
||||||
let serverHost = options["server_host"] as? String,
|
let serverHost = options["server_host"] as? String,
|
||||||
let stunAssistHost = options["stun_assist_host"] as? String,
|
let stunAssistHost = options["stun_assist_host"] as? String,
|
||||||
let noticePort = options["notice_port"] as? Int,
|
|
||||||
let accessToken = options["access_token"] as? String,
|
let accessToken = options["access_token"] as? String,
|
||||||
let identityId = options["identity_id"] as? UInt32,
|
let identityId = options["identity_id"] as? UInt32,
|
||||||
let clientId = options["client_id"] as? String,
|
let clientId = options["client_id"] as? String,
|
||||||
@ -141,7 +136,6 @@ extension SDLConfiguration {
|
|||||||
clientId: clientId,
|
clientId: clientId,
|
||||||
networkAddress: networkAddress,
|
networkAddress: networkAddress,
|
||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
noticePort: noticePort,
|
|
||||||
accessToken: accessToken,
|
accessToken: accessToken,
|
||||||
identityId: identityId,
|
identityId: identityId,
|
||||||
exitNode: exitNode)
|
exitNode: exitNode)
|
||||||
|
|||||||
47
punchnet/Core/SDLTunnelAppEventStore.swift
Normal file
47
punchnet/Core/SDLTunnelAppEventStore.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// SDLTunnelAppEventStore.swift
|
||||||
|
// punchnet
|
||||||
|
//
|
||||||
|
// Created by 安礼成 on 2026/4/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct SDLTunnelAppEventStore {
|
||||||
|
struct Event: Codable, Sendable, Identifiable {
|
||||||
|
enum Level: String, Codable, Sendable {
|
||||||
|
case info
|
||||||
|
case warning
|
||||||
|
case error
|
||||||
|
case fatal
|
||||||
|
}
|
||||||
|
|
||||||
|
let id: String
|
||||||
|
let timestamp: TimeInterval
|
||||||
|
let level: Level
|
||||||
|
let code: Int?
|
||||||
|
let message: String
|
||||||
|
}
|
||||||
|
|
||||||
|
static let appGroupSuiteName = "group.com.jihe.punchnetmac"
|
||||||
|
static let latestEventKey = "tunnel.latestEvent"
|
||||||
|
|
||||||
|
static func loadLatestEvent() -> Event? {
|
||||||
|
guard let shared = UserDefaults(suiteName: self.appGroupSuiteName),
|
||||||
|
let data = shared.data(forKey: self.latestEventKey),
|
||||||
|
let event = try? JSONDecoder().decode(Event.self, from: data) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
static func clearLatestEvent() {
|
||||||
|
guard let shared = UserDefaults(suiteName: self.appGroupSuiteName) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shared.removeObject(forKey: self.latestEventKey)
|
||||||
|
shared.synchronize()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -36,7 +36,6 @@ struct SystemConfig {
|
|||||||
accessToken: String,
|
accessToken: String,
|
||||||
identityId: UInt32,
|
identityId: UInt32,
|
||||||
hostname: String,
|
hostname: String,
|
||||||
noticePort: Int,
|
|
||||||
exitNodeIp: String?) -> [String: NSObject] {
|
exitNodeIp: String?) -> [String: NSObject] {
|
||||||
// guard let serverIp = DNSResolver.resolveAddrInfos(serverHost).first,
|
// guard let serverIp = DNSResolver.resolveAddrInfos(serverHost).first,
|
||||||
// let stunAssistIp = DNSResolver.resolveAddrInfos(stunAssistHost).first else {
|
// let stunAssistIp = DNSResolver.resolveAddrInfos(stunAssistHost).first else {
|
||||||
@ -54,7 +53,6 @@ struct SystemConfig {
|
|||||||
"server_host": serverHost as NSObject,
|
"server_host": serverHost as NSObject,
|
||||||
"stun_assist_host": stunAssistHost as NSObject,
|
"stun_assist_host": stunAssistHost as NSObject,
|
||||||
"hostname": hostname as NSObject,
|
"hostname": hostname as NSObject,
|
||||||
"notice_port": noticePort as NSObject,
|
|
||||||
"network_address": [
|
"network_address": [
|
||||||
"network_id": networkId as NSObject,
|
"network_id": networkId as NSObject,
|
||||||
"ip": ip as NSObject,
|
"ip": ip as NSObject,
|
||||||
|
|||||||
@ -15,18 +15,16 @@ struct AppContextError: Error {
|
|||||||
@Observable
|
@Observable
|
||||||
class AppContext {
|
class AppContext {
|
||||||
private var vpnManager = VPNManager.shared
|
private var vpnManager = VPNManager.shared
|
||||||
|
|
||||||
var noticePort: Int
|
|
||||||
|
|
||||||
// 调用 "/connect" 之后的网络信息
|
// 调用 "/connect" 之后的网络信息
|
||||||
var networkContext: SDLAPIClient.NetworkContext? = nil
|
var networkContext: SDLAPIClient.NetworkContext? = nil
|
||||||
|
|
||||||
// 在menu里面需要使用
|
// 在menu里面需要使用
|
||||||
var vpnOptions: [String: NSObject]? = nil
|
var vpnOptions: [String: NSObject]? = nil
|
||||||
|
|
||||||
// 当前app所处的场景
|
// 当前app所处的场景
|
||||||
var appScene: AppScene = .login(username: nil)
|
var appScene: AppScene = .login(username: nil)
|
||||||
|
|
||||||
// 当前的场景
|
// 当前的场景
|
||||||
enum AppScene: Equatable {
|
enum AppScene: Equatable {
|
||||||
case login(username: String?)
|
case login(username: String?)
|
||||||
@ -34,26 +32,26 @@ class AppContext {
|
|||||||
case register
|
case register
|
||||||
case resetPassword
|
case resetPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登陆凭证
|
// 登陆凭证
|
||||||
var loginCredit: Credit?
|
var loginCredit: Credit?
|
||||||
|
|
||||||
// 判断用户是否登陆
|
// 判断用户是否登陆
|
||||||
var isLogined: Bool {
|
var isLogined: Bool {
|
||||||
return loginCredit != nil
|
return loginCredit != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Credit {
|
enum Credit {
|
||||||
case token(token: String, session: SDLAPIClient.NetworkSession)
|
case token(token: String, session: SDLAPIClient.NetworkSession)
|
||||||
case accountAndPasword(account: String, password: String, session: SDLAPIClient.NetworkSession)
|
case accountAndPasword(account: String, password: String, session: SDLAPIClient.NetworkSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObservationIgnored
|
@ObservationIgnored
|
||||||
var networkSession: SDLAPIClient.NetworkSession? {
|
var networkSession: SDLAPIClient.NetworkSession? {
|
||||||
guard let loginCredit = self.loginCredit else {
|
guard let loginCredit = self.loginCredit else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch loginCredit {
|
switch loginCredit {
|
||||||
case .token(_, let session):
|
case .token(_, let session):
|
||||||
return session
|
return session
|
||||||
@ -61,11 +59,17 @@ class AppContext {
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(noticePort: Int) {
|
var tunnelEvent: SDLTunnelAppEventStore.Event? = nil
|
||||||
self.noticePort = noticePort
|
|
||||||
|
@ObservationIgnored
|
||||||
|
private var lastTunnelEventID: String?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.observeTunnelEvent()
|
||||||
|
self.consumeLatestTunnelEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginWith(token: String) async throws {
|
func loginWith(token: String) async throws {
|
||||||
let networkSession = try await SDLAPIClient.loginWithToken(token: token)
|
let networkSession = try await SDLAPIClient.loginWithToken(token: token)
|
||||||
self.loginCredit = .token(token: token, session: networkSession)
|
self.loginCredit = .token(token: token, session: networkSession)
|
||||||
@ -83,44 +87,44 @@ class AppContext {
|
|||||||
try KeychainStore.shared.save(data, account: "accountAndPasword")
|
try KeychainStore.shared.save(data, account: "accountAndPasword")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接到对应的网络
|
// 连接到对应的网络
|
||||||
func connectNetwork() async throws {
|
func connectNetwork() async throws {
|
||||||
guard let session = self.networkSession else {
|
guard let session = self.networkSession else {
|
||||||
throw AppContextError(message: "未登陆")
|
throw AppContextError(message: "未登陆")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 避免重复连接
|
// 避免重复连接
|
||||||
guard !vpnManager.isConnected else {
|
guard !vpnManager.isConnected else {
|
||||||
throw AppContextError(message: "网络已经连接")
|
throw AppContextError(message: "网络已经连接")
|
||||||
}
|
}
|
||||||
|
|
||||||
self.networkContext = try await SDLAPIClient.connectNetwork(accesToken: session.accessToken)
|
self.networkContext = try await SDLAPIClient.connectNetwork(accesToken: session.accessToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func changeExitNodeIp(exitNodeIp: String) async throws -> Data {
|
func changeExitNodeIp(exitNodeIp: String) async throws -> Data {
|
||||||
// 避免重复连接
|
// 避免重复连接
|
||||||
guard vpnManager.isConnected else {
|
guard vpnManager.isConnected else {
|
||||||
throw AppContextError(message: "网络未连接")
|
throw AppContextError(message: "网络未连接")
|
||||||
}
|
}
|
||||||
|
|
||||||
var exitNodeIpChanged = NEMessage.ExitNodeIpChanged()
|
var exitNodeIpChanged = NEMessage.ExitNodeIpChanged()
|
||||||
exitNodeIpChanged.ip = exitNodeIp
|
exitNodeIpChanged.ip = exitNodeIp
|
||||||
|
|
||||||
var neMessage = NEMessage()
|
var neMessage = NEMessage()
|
||||||
neMessage.message = .exitNodeIpChanged(exitNodeIpChanged)
|
neMessage.message = .exitNodeIpChanged(exitNodeIpChanged)
|
||||||
|
|
||||||
let message = try neMessage.serializedData()
|
let message = try neMessage.serializedData()
|
||||||
|
|
||||||
return try await self.vpnManager.sendMessage(message)
|
return try await self.vpnManager.sendMessage(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动tun
|
// 启动tun
|
||||||
func startTun() async throws {
|
func startTun() async throws {
|
||||||
guard let session = self.networkSession, let context = self.networkContext else {
|
guard let session = self.networkSession, let context = self.networkContext else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = SystemConfig.getOptions(
|
let options = SystemConfig.getOptions(
|
||||||
networkId: UInt32(session.networkId),
|
networkId: UInt32(session.networkId),
|
||||||
networkDomain: session.networkDomain,
|
networkDomain: session.networkDomain,
|
||||||
@ -129,12 +133,11 @@ class AppContext {
|
|||||||
accessToken: session.accessToken,
|
accessToken: session.accessToken,
|
||||||
identityId: context.identityId,
|
identityId: context.identityId,
|
||||||
hostname: context.hostname,
|
hostname: context.hostname,
|
||||||
noticePort: noticePort,
|
|
||||||
exitNodeIp: self.loadExitNodeIp()
|
exitNodeIp: self.loadExitNodeIp()
|
||||||
)
|
)
|
||||||
try await self.vpnManager.enableVpn(options: options)
|
try await self.vpnManager.enableVpn(options: options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 断开网络连接
|
// 断开网络连接
|
||||||
func stopTun() async throws {
|
func stopTun() async throws {
|
||||||
try await self.vpnManager.disableVpn()
|
try await self.vpnManager.disableVpn()
|
||||||
@ -146,14 +149,14 @@ class AppContext {
|
|||||||
self.networkContext = nil
|
self.networkContext = nil
|
||||||
self.loginCredit = nil
|
self.loginCredit = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadCacheToken() -> String? {
|
func loadCacheToken() -> String? {
|
||||||
if let data = try? KeychainStore.shared.load(account: "token") {
|
if let data = try? KeychainStore.shared.load(account: "token") {
|
||||||
return String(data: data, encoding: .utf8)
|
return String(data: data, encoding: .utf8)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadCacheUsernameAndPassword() -> (String, String)? {
|
func loadCacheUsernameAndPassword() -> (String, String)? {
|
||||||
if let data = try? KeychainStore.shared.load(account: "accountAndPasword"),
|
if let data = try? KeychainStore.shared.load(account: "accountAndPasword"),
|
||||||
let str = String(data: data, encoding: .utf8) {
|
let str = String(data: data, encoding: .utf8) {
|
||||||
@ -164,24 +167,49 @@ class AppContext {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理网络出口数据
|
// 处理网络出口数据
|
||||||
extension AppContext {
|
extension AppContext {
|
||||||
|
|
||||||
func loadExitNodeIp() -> String? {
|
func loadExitNodeIp() -> String? {
|
||||||
if let data = try? KeychainStore.shared.load(account: "exitNodeIp") {
|
if let data = try? KeychainStore.shared.load(account: "exitNodeIp") {
|
||||||
return String(data: data, encoding: .utf8)
|
return String(data: data, encoding: .utf8)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveExitNodeIp(exitNodeIp: String) async throws {
|
func saveExitNodeIp(exitNodeIp: String) async throws {
|
||||||
// 将数据缓存到keychain
|
// 将数据缓存到keychain
|
||||||
if let data = exitNodeIp.data(using: .utf8) {
|
if let data = exitNodeIp.data(using: .utf8) {
|
||||||
try KeychainStore.shared.save(data, account: "exitNodeIp")
|
try KeychainStore.shared.save(data, account: "exitNodeIp")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dismissTunnelEvent() {
|
||||||
|
self.tunnelEvent = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func observeTunnelEvent() {
|
||||||
|
SDLNotificationCenter.shared.addObserver(for: .tunnelEventChanged) { [weak self] _ in
|
||||||
|
self?.consumeLatestTunnelEvent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func consumeLatestTunnelEvent() {
|
||||||
|
guard let event = SDLTunnelAppEventStore.loadLatestEvent(),
|
||||||
|
self.lastTunnelEventID != event.id else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.lastTunnelEventID = event.id
|
||||||
|
self.tunnelEvent = event
|
||||||
|
SDLTunnelAppEventStore.clearLatestEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
SDLNotificationCenter.shared.removeObserver(for: .tunnelEventChanged)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import SwiftUI
|
|||||||
struct AppRootView: View {
|
struct AppRootView: View {
|
||||||
@Environment(AppContext.self) var appContext: AppContext
|
@Environment(AppContext.self) var appContext: AppContext
|
||||||
@State private var updateManager = AppUpdateManager.shared
|
@State private var updateManager = AppUpdateManager.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
// 1. 主要界面容器
|
// 1. 主要界面容器
|
||||||
@ -35,7 +35,7 @@ struct AppRootView: View {
|
|||||||
insertion: .move(edge: .trailing).combined(with: .opacity),
|
insertion: .move(edge: .trailing).combined(with: .opacity),
|
||||||
removal: .move(edge: .leading).combined(with: .opacity) // 修改为 leading 更有“流转”感
|
removal: .move(edge: .leading).combined(with: .opacity) // 修改为 leading 更有“流转”感
|
||||||
))
|
))
|
||||||
|
|
||||||
// 自动更新遮罩层级
|
// 自动更新遮罩层级
|
||||||
if updateManager.showUpdateOverlay, let info = updateManager.updateInfo {
|
if updateManager.showUpdateOverlay, let info = updateManager.updateInfo {
|
||||||
updateOverlay(info: info)
|
updateOverlay(info: info)
|
||||||
@ -46,12 +46,19 @@ struct AppRootView: View {
|
|||||||
.animation(.spring(duration: 0.4), value: updateManager.showUpdateOverlay)
|
.animation(.spring(duration: 0.4), value: updateManager.showUpdateOverlay)
|
||||||
// 关键:macOS 标准毛玻璃背景
|
// 关键:macOS 标准毛玻璃背景
|
||||||
.background(VisualEffectView(material: .hudWindow, blendingMode: .behindWindow))
|
.background(VisualEffectView(material: .hudWindow, blendingMode: .behindWindow))
|
||||||
|
.alert("提示", isPresented: tunnelEventPresented, presenting: appContext.tunnelEvent) { _ in
|
||||||
|
Button("确定", role: .cancel) {
|
||||||
|
appContext.dismissTunnelEvent()
|
||||||
|
}
|
||||||
|
} message: { event in
|
||||||
|
Text(event.message)
|
||||||
|
}
|
||||||
.task {
|
.task {
|
||||||
let checkUpdateResult = await updateManager.checkUpdate(isManual: false)
|
let checkUpdateResult = await updateManager.checkUpdate(isManual: false)
|
||||||
NSLog("[RootView] checkUpdateResult: \(checkUpdateResult)")
|
NSLog("[RootView] checkUpdateResult: \(checkUpdateResult)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将遮罩抽离,保持 body 清爽
|
// 将遮罩抽离,保持 body 清爽
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func updateOverlay(info: SDLAPIClient.AppUpgradeInfo) -> some View {
|
private func updateOverlay(info: SDLAPIClient.AppUpgradeInfo) -> some View {
|
||||||
@ -63,7 +70,7 @@ struct AppRootView: View {
|
|||||||
updateManager.showUpdateOverlay = false
|
updateManager.showUpdateOverlay = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppUpdateView(info: info) {
|
AppUpdateView(info: info) {
|
||||||
updateManager.showUpdateOverlay = false
|
updateManager.showUpdateOverlay = false
|
||||||
}
|
}
|
||||||
@ -76,5 +83,16 @@ struct AppRootView: View {
|
|||||||
))
|
))
|
||||||
.zIndex(100) // 确保更新遮罩永远在最上层
|
.zIndex(100) // 确保更新遮罩永远在最上层
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var tunnelEventPresented: Binding<Bool> {
|
||||||
|
Binding(
|
||||||
|
get: { self.appContext.tunnelEvent != nil },
|
||||||
|
set: { isPresented in
|
||||||
|
if !isPresented {
|
||||||
|
self.appContext.dismissTunnelEvent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -292,5 +292,5 @@ struct CustomSecureField: View {
|
|||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
LoginView()
|
LoginView()
|
||||||
.environment(AppContext(noticePort: 0))
|
.environment(AppContext())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,6 @@ struct punchnetApp: App {
|
|||||||
|
|
||||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||||
|
|
||||||
private var noticeServer: UDPNoticeCenterServer
|
|
||||||
@State private var appContext: AppContext
|
@State private var appContext: AppContext
|
||||||
@AppStorage("hasAcceptedPrivacy") var hasAcceptedPrivacy: Bool = false
|
@AppStorage("hasAcceptedPrivacy") var hasAcceptedPrivacy: Bool = false
|
||||||
// UI 控制状态:是否显示弹窗
|
// UI 控制状态:是否显示弹窗
|
||||||
@ -38,9 +37,7 @@ struct punchnetApp: App {
|
|||||||
@State private var vpnManager = VPNManager.shared
|
@State private var vpnManager = VPNManager.shared
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.noticeServer = UDPNoticeCenterServer()
|
self.appContext = AppContext()
|
||||||
self.noticeServer.start()
|
|
||||||
self.appContext = AppContext(noticePort: self.noticeServer.port)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user