import Foundation // MARK: - 防火墙呈现、wrapper 观测与系统代理出口事实 extension ProxyRuntimeController { func presentFirewallStatus(for document: RuntimeDeploymentFacts) async { let generation = flowGeneration let preparation = runtimePreparationGeneration guard document.listen.lan.effectiveMode.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) { await 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) { await probe.probe(host: host, port: port, timeout: timeout) }.value } /// 显式停止协议次序:注销(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? { guard catalogSnapshotReader.isCatalogAvailable else { return nil } let effect = machine.catalogDidCommit( catalog, 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: - 系统代理出口事实 /// 出口可用 = agent 入站健康(回环入口可用,含防火墙仅阻主机态的情形) /// 且模式有出口;直连不依赖活动服务器目标。观察机以闭包读取同一事实源。 var systemProxyExitAvailable: Bool { switch state { case .running, .firewallBlocked: return proxyMode == .direct || activeTargetID != nil case .off, .starting, .launchFailed, .requiresApproval, .serviceFailed: return false } } }