Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/ProxyRuntimeController+SystemProxyGate.swift
T
Qiu Yuzhou 16a2384498 fix(runtime): 收敛票据恒查全部失效事实,agent 关闭不再被旧收敛复活
mode/catalog 路径的票据子集缺 .flow/.agentEnabled:agent 关闭落在
mode 切换健康门 await 窗口内会被误分类为失败并触发 restore,把用户
显式的 off 意图静默撤销。删除逐 call site 挑子集的 Checks 政策,
convergenceIsCurrent 恒查 flow/mode/preparation/agentEnabled 四事实;
deployPrepared 的手工 preparation 守卫随之删除。

restore 门禁先行:agent 意图与票据时效先于回滚载荷持久化——竞态下
不再把旧 mode 写回持久层(内存/持久层分叉、用户切换被无声撤销)。

轮询写入守卫对齐同一票据词汇:防火墙轮询 await 恢复后补查代际
(cancel 后不得把 .firewallBlocked 盖回 .off);系统代理健康 Task
post-await 守卫补 agentEnabled。

新增 5 个交错/门禁测试(BlockingProbe/BlockingFirewallChecker 暂停
缝;修复前实测全红:agent 重注册、running 复活、.direct 意图被改回
.global、载荷写回、.failed 误分类);CONTEXT.md 增补 Convergence
ticket 词条。全量 792 测试通过。
2026-10-03 18:44:58 +08:00

134 lines
4.6 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() {
// await 恢复后代际可能已被 execute 推进(含取消本任务的 stop 路径):
// 过期的防火墙事实不得盖到最新状态呈现上。
guard generation == flowGeneration else { return }
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
}
}
}