Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/ProxyRuntimeController+SystemProxyGate.swift
T
Qiu Yuzhou eac30c4abd refactor(sysproxy): 系统代理策略机抽为 SystemProxyObserver 模块
ADR-0022 的观察/修复/挂起策略此前溶解在控制器 15 个可变字段里、由
Gate/Inspection/Commands/Health/Mode 五个文件交叉读写。现在策略机独立为
SystemProxyObserver:意图开关、出口可用性、期望配置与健康循环调度以
@MainActor 闭包注入,模块不反向依赖控制器;底层继续只依赖
SystemProxyControlling / helper 注册缝 / 网络变化监视三条既有缝。

- 3 个 @Published 呈现事实随迁,控制器留同名只读门面(测试与诊断面零改动);
  adapter 再发布链 merge 观察机 objectWillChange,主队列 hop 语义不变
- 健康循环留在控制器(循环体含运行时健康呈现),任务柄归观察机;
  cleanup-until-quiet、网络变化 inspection 调度与操作互斥全部内聚
- 方法体逐字搬运(settings.systemProxyEnabled→isIntentEnabled() 等机械替换),
  语义零改动;既有 777 测试全绿
2026-10-03 12:44:20 +08:00

131 lines
4.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
// MARK: - 防火墙呈现、wrapper 观测与系统代理出口事实
extension ProxyRuntimeController {
func presentFirewallStatus(for document: SslocalRuntimeDocument) async {
let generation = flowGeneration
let preparation = runtimePreparationGeneration
guard document.listen.listenerMode.exposesNetworkInterfaces else {
state = .running
return
}
let blocked = await blockedFirewallExecutable()
guard generation == flowGeneration, preparation == runtimePreparationGeneration else { return }
if let blocked {
presentFirewallBlocked(blocked)
return
}
state = .running
observeFirewall(generation: flowGeneration)
}
private func blockedFirewallExecutable() async -> URL? {
for executableURL in firewallExecutableURLs {
let status = await firewallStatus(for: executableURL)
if status == .blocked { return executableURL }
}
return nil
}
private func presentFirewallBlocked(_ executableURL: URL) {
let name = executableURL.lastPathComponent
state = .firewallBlocked(FirewallBlockedFacts(executableName: name))
}
private func observeFirewall(generation: Int) {
firewallObservationTask?.cancel()
let interval = firewallPollIntervalNanoseconds
firewallObservationTask = Task { @MainActor [weak self] in
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: interval)
guard !Task.isCancelled, let self, generation == flowGeneration else { return }
if let blocked = await blockedFirewallExecutable() {
presentFirewallBlocked(blocked)
firewallObservationTask = nil
return
}
}
}
}
func cancelFirewallObservation() {
firewallObservationTask?.cancel()
firewallObservationTask = nil
}
private func firewallStatus(for executableURL: URL) async -> FirewallBlockStatus {
let checker = firewallChecker
return await Task.detached(priority: .utility) {
checker.status(for: executableURL)
}.value
}
func probeAsync(
host: String, port: Int, timeout: TimeInterval
) async -> EndpointHealthProbe.Outcome {
let probe = probe
return await Task.detached(priority: .utility) {
probe.probe(host: host, port: port, timeout: timeout)
}.value
}
/// 显式停止协议次序(D2):注销(SIGTERM wrapper → wrapper 停 sslocal 并
/// 等待)完成后才允许后续删文件动作。最多等 5 秒,超时也继续(launchd 会
/// 兜底结束进程)。
func waitForWrapperExit() async {
let deadline = Date().addingTimeInterval(5)
while wrapperState() != .notRunning && Date() < deadline {
try? await Task.sleep(nanoseconds: 100_000_000)
}
}
func wrapperState() -> WrapperProcessState {
guard
let data = try? Data(contentsOf: runtimeFileStore.pidFileURL),
let text = String(data: data, encoding: .utf8)?.trimmingCharacters(
in: .whitespacesAndNewlines),
let pid = Int32(text)
else { return .notRunning }
guard sendSignal(pid, 0) == 0 else { return .notRunning }
return .running(pid: pid)
}
// MARK: - 目录同步
/// Re-expands one command-start snapshot; a concurrently published catalog
/// is intentionally observed by the next command, not halfway through this one.
func reexpand(in catalog: ConfigurationCatalog) -> ActivationEffect? {
let effect = machine.catalogDidCommit(
catalog, credentials: credentials, plugins: plugins,
options: settings.runtimeDocumentOptions)
activeTargetID = machine.activeTargetID
switch effect {
case .deployed(let configuration):
skippedServers = configuration.skippedServers
case .clearedAndStopped:
skippedServers = []
case nil:
break
}
return effect
}
func describe(_ error: Error) -> String {
String(describing: error)
}
// MARK: - 系统代理出口事实(issue #60/#71)
/// 出口可用 = agent 入站健康(回环入口可用,含防火墙仅阻主机态的情形)
/// 且模式有出口;直连不依赖活动服务器目标。观察机以闭包读取同一事实源。
var systemProxyExitAvailable: Bool {
switch state {
case .running, .firewallBlocked:
return proxyMode == .direct || activeTargetID != nil
case .off, .starting, .launchFailed, .requiresApproval, .serviceFailed:
return false
}
}
}