Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/Application/ProxyRuntime/SystemProxyNetworkChangeMonitor.swift
T
Qiu Yuzhou fb428f12bc refactor(app): 按分层重组 App/ 源码目录
App/ 根目录原有 73 个散落源文件,已无法从目录读出任何分组信息。

按「目录即分层」重组为 Composition / Application / Presentation / UI /
PlatformEffects / Resources 六个目录,UI 层再按子视图分目录(Workspace、
Home、Servers(含 Plugin)、Subscriptions、Rules、Settings、Diagnostics、
StatusMenu)。纯移动:类型名、模块名与 import 均未改动,App/ 根层不再有
散落文件。

配套改动:

- project.yml:Core 测试镜像的 excludes 由裸文件名改为 `**/` 递归形式。
  XcodeGen 的 exclude 不跨 `/`,裸名只匹配 App 根层;MainApp.swift 下移后
  旧写法会静默失效,把 @main 组合根编进测试镜像。info.path 与
  entitlements.path 指向 App/Resources/;并排除 `**/*.md`——App/ 下任何
  非源码文件都会被 XcodeGen 收进 Resources 阶段并封进签名后的 bundle。
- SkeletonTests 的架构守卫由「默认扫描全部 App/ + 7 个豁免文件 + 1 个豁免
  前缀 + 4 个豁免目录」改为正面圈定 App/UI/,豁免登记全部删除;新增运行时
  适配器放进 Application/ 即自动落在扫描范围外,无需再登记。
- README 工程结构说明同步;根 .gitignore 的生成物路径随 Resources/ 归位。

验证:clean build 0 error;task test 1022 用例全通过(含两个架构守卫);
task format 无改动(证明纯移动);task lint 与移动前同为 21 条内容型
warning;packaging-gate.sh ALL CHECKS PASSED,确认 plist/entitlements 归位
后签名与 entitlements 语义不变。

注意:迁移时须先删旧的生成物再 xcodegen generate,否则遗留的 App/Info.plist
会被当作普通资源扫进 Resources 阶段。
2026-10-05 19:56:18 +08:00

116 lines
3.9 KiB
Swift

import Foundation
import Network
import SystemConfiguration
struct SystemProxyNetworkChange: OptionSet, Sendable {
let rawValue: Int
static let networkConfiguration = SystemProxyNetworkChange(rawValue: 1 << 0)
static let proxyConfiguration = SystemProxyNetworkChange(rawValue: 1 << 1)
static let networkPath = SystemProxyNetworkChange(rawValue: 1 << 2)
}
@MainActor
protocol SystemProxyNetworkChangeMonitoring: AnyObject {
func start(
handler: @escaping @MainActor @Sendable (SystemProxyNetworkChange) -> Void)
func stop()
}
@MainActor
final class NoopSystemProxyNetworkChangeMonitor: SystemProxyNetworkChangeMonitoring {
func start(handler: @escaping @MainActor @Sendable (SystemProxyNetworkChange) -> Void) {}
func stop() {}
}
/// Observes network-location/service setup changes, proxy dictionary changes, and
/// app-visible path changes. Consumers decide which classes matter for each intent.
@MainActor
final class SystemProxyNetworkChangeMonitor: SystemProxyNetworkChangeMonitoring {
private let callbackBox = SystemProxyNetworkChangeCallbackBox()
private var dynamicStore: SCDynamicStore?
private var pathMonitor: NWPathMonitor?
func start(
handler: @escaping @MainActor @Sendable (SystemProxyNetworkChange) -> Void
) {
stop()
callbackBox.setHandler(handler)
startDynamicStoreObserver()
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { [weak callbackBox] _ in
callbackBox?.emit(.networkPath)
}
monitor.start(queue: DispatchQueue.global(qos: .utility))
pathMonitor = monitor
}
func stop() {
callbackBox.setHandler(nil)
if let dynamicStore {
SCDynamicStoreSetDispatchQueue(dynamicStore, nil)
self.dynamicStore = nil
}
pathMonitor?.cancel()
pathMonitor = nil
}
private func startDynamicStoreObserver() {
var context = SCDynamicStoreContext(
version: 0,
info: Unmanaged.passUnretained(callbackBox).toOpaque(),
retain: nil,
release: nil,
copyDescription: nil)
guard
let store = SCDynamicStoreCreate(
kCFAllocatorDefault, "ShadowsocksX-NG2.SystemProxy" as CFString,
systemProxyDynamicStoreCallback, &context)
else { return }
let locationKey = SCDynamicStoreKeyCreateLocation(kCFAllocatorDefault)
let proxiesKey = SCDynamicStoreKeyCreateProxies(kCFAllocatorDefault)
let keys = [locationKey, proxiesKey].compactMap { $0 } as CFArray
// Setup changes include service membership/interface configuration and each
// service's Proxies dictionary. Runtime global proxy notifications are explicit.
let patterns = ["Setup:/Network/.*"] as CFArray
guard SCDynamicStoreSetNotificationKeys(store, keys, patterns) else { return }
guard SCDynamicStoreSetDispatchQueue(store, DispatchQueue.main) else { return }
dynamicStore = store
}
}
private final class SystemProxyNetworkChangeCallbackBox: @unchecked Sendable {
private let lock = NSLock()
private var handler: (@MainActor @Sendable (SystemProxyNetworkChange) -> Void)?
func setHandler(
_ handler: (@MainActor @Sendable (SystemProxyNetworkChange) -> Void)?
) {
lock.withLock { self.handler = handler }
}
func emit(_ change: SystemProxyNetworkChange) {
let currentHandler = lock.withLock { handler }
guard let currentHandler else { return }
Task { @MainActor in currentHandler(change) }
}
}
private let systemProxyDynamicStoreCallback: SCDynamicStoreCallBack = { _, changedKeys, info in
guard let info else { return }
let callbackBox = Unmanaged<SystemProxyNetworkChangeCallbackBox>.fromOpaque(info)
.takeUnretainedValue()
let keys = changedKeys as? [String] ?? []
var changes: SystemProxyNetworkChange = []
for key in keys {
if key.hasSuffix("/Proxies") || key == "State:/Network/Global/Proxies" {
changes.insert(.proxyConfiguration)
} else {
changes.insert(.networkConfiguration)
}
}
if !changes.isEmpty { callbackBox.emit(changes) }
}