mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-29 13:43:49 +00:00
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 测试全绿
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
/// 生产运行时 adapter(issue #47):现有 `ProxyRuntimeController` 的唯一
|
||||
/// 包装,由应用组合根接线。只把控制器既有能力与观察投影到 workflow 缝上,
|
||||
/// 不复制状态机、generation、健康门禁与系统代理生命周期语义。
|
||||
/// 生产运行时 adapter(issue #47):现有 `ProxyRuntimeController` 与其系统代理
|
||||
/// 观察机(`SystemProxyObserver`,ADR-0022 策略机)的唯一包装,由应用组合根
|
||||
/// 接线。只把既有能力与观察投影到 workflow 缝上,不复制状态机、generation、
|
||||
/// 健康门禁与系统代理生命周期语义。
|
||||
@MainActor
|
||||
final class ControllerProxyRuntimeAdapter: ProxyRuntimeAdapting {
|
||||
private let controller: ProxyRuntimeController
|
||||
@@ -44,13 +45,16 @@ final class ControllerProxyRuntimeAdapter: ProxyRuntimeAdapting {
|
||||
RuntimeListenFacts(listen: controller.listenSettings)
|
||||
}
|
||||
|
||||
/// 控制器的 `objectWillChange` 是 willChange 语义;主队列 hop 落地时被
|
||||
/// 发布的新值已可读,workflow 重观察不会读到半程状态。
|
||||
/// 控制器与系统代理观察机的 `objectWillChange` 都是 willChange 语义;主队列
|
||||
/// hop 落地时被发布的新值已可读,workflow 重观察不会读到半程状态。系统代理
|
||||
/// 呈现事实的存储与发布在观察机上,其发布并入同一再发布链。
|
||||
var changes: AnyPublisher<Void, Never> {
|
||||
controller.objectWillChange
|
||||
.map { _ in () }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.eraseToAnyPublisher()
|
||||
Publishers.Merge(
|
||||
controller.objectWillChange.map { _ in () },
|
||||
controller.systemProxyObserver.objectWillChange.map { _ in () }
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func resyncOnLaunch() async {
|
||||
@@ -66,12 +70,14 @@ final class ControllerProxyRuntimeAdapter: ProxyRuntimeAdapting {
|
||||
}
|
||||
|
||||
func openSystemProxyHelperApproval() async {
|
||||
await controller.openSystemProxyHelperApproval()
|
||||
await controller.systemProxyObserver.openSystemProxyHelperApproval()
|
||||
}
|
||||
|
||||
func repairSystemProxy() async { await controller.repairSystemProxy() }
|
||||
func retrySystemProxyClear() async { await controller.retrySystemProxyClear() }
|
||||
func recheckSystemProxy() async { await controller.recheckSystemProxy() }
|
||||
func repairSystemProxy() async { await controller.systemProxyObserver.repairSystemProxy() }
|
||||
func retrySystemProxyClear() async {
|
||||
await controller.systemProxyObserver.retrySystemProxyClear()
|
||||
}
|
||||
func recheckSystemProxy() async { await controller.systemProxyObserver.recheckSystemProxy() }
|
||||
|
||||
func setProxyMode(_ mode: ProxyMode) async {
|
||||
await controller.setProxyMode(mode)
|
||||
|
||||
@@ -61,14 +61,14 @@ extension ProxyRuntimeController {
|
||||
settings = next
|
||||
if enabled {
|
||||
await convergeAgent()
|
||||
if settings.systemProxyEnabled { startEnabledSystemProxyObservation() }
|
||||
if settings.systemProxyEnabled { systemProxyObserver.startEnabledSystemProxyObservation() }
|
||||
} else {
|
||||
let cleanupOutcome = await cascadeSystemProxyOffForAgentOff()
|
||||
await stopAgent()
|
||||
if let cleanupOutcome, cleanupOutcome.hasOperationFailure {
|
||||
systemProxyState = cleanupOutcome
|
||||
systemProxyObserver.systemProxyState = cleanupOutcome
|
||||
}
|
||||
updateSystemProxyActions()
|
||||
systemProxyObserver.updateSystemProxyActions()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ extension ProxyRuntimeController {
|
||||
}
|
||||
settings = cascade
|
||||
// 持久化的 on→off 迁移才触发清理;先清后停本地监听。
|
||||
return await clearAndStopSystemProxyObservation()
|
||||
return await systemProxyObserver.clearAndStopSystemProxyObservation()
|
||||
}
|
||||
|
||||
/// 系统代理开关(issue #71):先持久化意图。开启时经 helper 注册门禁收敛
|
||||
@@ -100,41 +100,46 @@ extension ProxyRuntimeController {
|
||||
guard persistSettings(next) else { return }
|
||||
settings = next
|
||||
if enabled {
|
||||
systemProxyInitialApplyPending = true
|
||||
systemProxyWasUnavailable = false
|
||||
await convergeSystemProxy()
|
||||
startEnabledSystemProxyObservation()
|
||||
systemProxyObserver.systemProxyInitialApplyPending = true
|
||||
systemProxyObserver.systemProxyWasUnavailable = false
|
||||
await systemProxyObserver.convergeSystemProxy()
|
||||
systemProxyObserver.startEnabledSystemProxyObservation()
|
||||
} else {
|
||||
systemProxyApprovalRequired = false
|
||||
systemProxyInitialApplyPending = false
|
||||
systemProxyReadGeneration += 1
|
||||
systemProxyState = await clearAndStopSystemProxyObservation()
|
||||
updateSystemProxyActions()
|
||||
systemProxyObserver.systemProxyApprovalRequired = false
|
||||
systemProxyObserver.systemProxyInitialApplyPending = false
|
||||
systemProxyObserver.systemProxyReadGeneration += 1
|
||||
systemProxyObserver.systemProxyState =
|
||||
await systemProxyObserver.clearAndStopSystemProxyObservation()
|
||||
systemProxyObserver.updateSystemProxyActions()
|
||||
}
|
||||
}
|
||||
|
||||
/// GUI startup restores the Agent. Healthy proxy intent only inspects; unhealthy
|
||||
/// proxy intent suspends; disabled intent never writes SystemConfiguration.
|
||||
func resyncOnLaunch() async {
|
||||
systemProxyStartupInProgress = true
|
||||
systemProxyObserver.systemProxyStartupInProgress = true
|
||||
guard settings.agentEnabled else {
|
||||
await stopAgent()
|
||||
systemProxyStartupInProgress = false
|
||||
systemProxyObserver.systemProxyStartupInProgress = false
|
||||
if settings.systemProxyEnabled {
|
||||
await suspendSystemProxy()
|
||||
startEnabledSystemProxyObservation()
|
||||
await systemProxyObserver.suspendSystemProxy()
|
||||
systemProxyObserver.startEnabledSystemProxyObservation()
|
||||
}
|
||||
return
|
||||
}
|
||||
let catalog = catalogSnapshotReader.catalogSnapshot
|
||||
await convergeToReexpanded(catalog)
|
||||
systemProxyStartupInProgress = false
|
||||
systemProxyObserver.systemProxyStartupInProgress = false
|
||||
if settings.systemProxyEnabled {
|
||||
lastDesiredSystemProxyConfiguration = desiredSystemProxyConfiguration
|
||||
if systemProxyExitAvailable { await recheckSystemProxy() } else { await suspendSystemProxy() }
|
||||
startEnabledSystemProxyObservation()
|
||||
systemProxyObserver.lastDesiredSystemProxyConfiguration = desiredSystemProxyConfiguration
|
||||
if systemProxyExitAvailable {
|
||||
await systemProxyObserver.recheckSystemProxy()
|
||||
} else {
|
||||
await systemProxyObserver.suspendSystemProxy()
|
||||
}
|
||||
systemProxyObserver.startEnabledSystemProxyObservation()
|
||||
} else {
|
||||
systemProxyState = .idle
|
||||
systemProxyObserver.systemProxyState = .idle
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,19 +156,21 @@ extension ProxyRuntimeController {
|
||||
/// 启 + agent 关闭」组合保持待应用(不静默清理,也无需网络观察)。
|
||||
func stopAgent() async {
|
||||
systemProxyNetworkChangeMonitor.stop()
|
||||
systemProxyHealthTask?.cancel()
|
||||
systemProxyHealthTask = nil
|
||||
systemProxyObservationMode = .stopped
|
||||
systemProxyObserver.systemProxyHealthTask?.cancel()
|
||||
systemProxyObserver.systemProxyHealthTask = nil
|
||||
systemProxyObserver.systemProxyObservationMode = .stopped
|
||||
_ = await execute(.stop, document: nil)
|
||||
state = .off
|
||||
lastDocument = nil
|
||||
skippedServers = []
|
||||
lastActivationFailure = nil
|
||||
if settings.systemProxyEnabled {
|
||||
systemProxyState = .pending
|
||||
systemProxyObserver.systemProxyState = .pending
|
||||
} else {
|
||||
if !systemProxyState.hasOperationFailure { systemProxyState = .idle }
|
||||
updateSystemProxyActions()
|
||||
if !systemProxyState.hasOperationFailure {
|
||||
systemProxyObserver.systemProxyState = .idle
|
||||
}
|
||||
systemProxyObserver.updateSystemProxyActions()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +228,7 @@ extension ProxyRuntimeController {
|
||||
lastDocument = nil
|
||||
skippedServers = []
|
||||
state = .launchFailed(.unreadableSettings)
|
||||
await holdSystemProxyIntent()
|
||||
await systemProxyObserver.holdSystemProxyIntent()
|
||||
}
|
||||
|
||||
/// 活动目标失效(issue #60):清除并持久化 nil,点名原因独立呈现;agent
|
||||
@@ -237,7 +244,7 @@ extension ProxyRuntimeController {
|
||||
}
|
||||
lastActivationFailure = failure
|
||||
skippedServers = []
|
||||
await holdSystemProxyIntent()
|
||||
await systemProxyObserver.holdSystemProxyIntent()
|
||||
if settings.agentEnabled {
|
||||
await deployListeningWithoutTarget()
|
||||
} else {
|
||||
|
||||
@@ -174,8 +174,8 @@ extension ProxyRuntimeController {
|
||||
guard healthy else { return .failed }
|
||||
lastDocument = document
|
||||
switch proxyTail {
|
||||
case .forceApply: await convergeSystemProxy(forceApply: true)
|
||||
case .converge: await convergeSystemProxy()
|
||||
case .forceApply: await systemProxyObserver.convergeSystemProxy(forceApply: true)
|
||||
case .converge: await systemProxyObserver.convergeSystemProxy()
|
||||
case .none: break
|
||||
}
|
||||
return .applied
|
||||
@@ -230,7 +230,7 @@ extension ProxyRuntimeController {
|
||||
ticket.flow = execution.flow
|
||||
guard convergenceIsCurrent(ticket, checking: checking) else { return report(nil) }
|
||||
guard execution.succeeded else {
|
||||
await holdSystemProxyIntent()
|
||||
await systemProxyObserver.holdSystemProxyIntent()
|
||||
return report(false)
|
||||
}
|
||||
let healthy = await presentLaunchHealth(
|
||||
@@ -241,12 +241,12 @@ extension ProxyRuntimeController {
|
||||
if plan.convergeProxyOnIntentChange,
|
||||
settings.systemProxyEnabled != plan.systemProxyIntentAtCapture
|
||||
{
|
||||
await convergeSystemProxy(forceApply: true)
|
||||
await systemProxyObserver.convergeSystemProxy(forceApply: true)
|
||||
} else {
|
||||
state = plan.state
|
||||
}
|
||||
} else {
|
||||
await holdSystemProxyIntent()
|
||||
await systemProxyObserver.holdSystemProxyIntent()
|
||||
}
|
||||
return report(healthy)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,22 @@ extension ProxyRuntimeController {
|
||||
/// 系统代理开关意图(持久化事实;开关 UI 的绑定来源,issue #60)。
|
||||
var systemProxyIntentEnabled: Bool { settings.systemProxyEnabled }
|
||||
|
||||
/// 系统代理观察机的呈现事实门面:存储与发布在 `SystemProxyObserver`,
|
||||
/// 此处只读投影供既有测试与诊断面沿用原名。
|
||||
var systemProxyState: SystemProxyApplicationFacts { systemProxyObserver.systemProxyState }
|
||||
var systemProxyApprovalRequired: Bool { systemProxyObserver.systemProxyApprovalRequired }
|
||||
var systemProxyInspection: SystemProxyInspectionFacts {
|
||||
systemProxyObserver.systemProxyInspection
|
||||
}
|
||||
|
||||
/// Desired system proxy configuration:当前保存设置要求的代理值(ADR-0022)。
|
||||
/// 观察机经 init 闭包读取同一事实源。
|
||||
var desiredSystemProxyConfiguration: SystemProxyConfiguration? {
|
||||
guard let document = lastDocument else { return nil }
|
||||
return try? proxyMode.systemProxyConfiguration(
|
||||
for: document, exceptions: settings.proxyExceptionList)
|
||||
}
|
||||
|
||||
/// Stable app-facing projection for status-menu presentation. The menu does
|
||||
/// not depend on this controller's nested state representation.
|
||||
var runtimeFacts: ProxyRuntimeFacts {
|
||||
|
||||
@@ -122,7 +122,7 @@ extension ProxyRuntimeController {
|
||||
if let receipt, !receiptIsCurrentAndLive(receipt) {
|
||||
return await reportAgentLost(preserveProxyOnFailure: preserveProxyOnFailure)
|
||||
}
|
||||
if convergeProxyOnSuccess { await convergeSystemProxy() }
|
||||
if convergeProxyOnSuccess { await systemProxyObserver.convergeSystemProxy() }
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -136,14 +136,14 @@ extension ProxyRuntimeController {
|
||||
private func reportAgentLost(preserveProxyOnFailure: Bool) async -> Bool {
|
||||
guard !preserveProxyOnFailure else { return false }
|
||||
state = .serviceFailed(.agent)
|
||||
await holdSystemProxyIntent()
|
||||
await systemProxyObserver.holdSystemProxyIntent()
|
||||
return false
|
||||
}
|
||||
|
||||
private func reportRuntimeFileFailure(preserveProxyOnFailure: Bool) async -> Bool {
|
||||
guard !preserveProxyOnFailure else { return false }
|
||||
state = .serviceFailed(.runtimeFile)
|
||||
await holdSystemProxyIntent()
|
||||
await systemProxyObserver.holdSystemProxyIntent()
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ extension ProxyRuntimeController {
|
||||
state = .launchFailed(
|
||||
.localEndpoint(endpoint: "SOCKS", host: "127.0.0.1", port: 0, cause: .unknown))
|
||||
}
|
||||
await holdSystemProxyIntent()
|
||||
await systemProxyObserver.holdSystemProxyIntent()
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -214,19 +214,22 @@ extension ProxyRuntimeController {
|
||||
extension ProxyRuntimeController {
|
||||
/// Uses the existing local endpoint/receipt health seam while the GUI is alive.
|
||||
/// Network path availability is deliberately absent from this decision.
|
||||
/// 任务柄持有在观察机上(cleanup 与停止路径都要取消它);循环体留在本体,
|
||||
/// 因为它同时承担运行时健康呈现,系统代理反应转发给观察机。
|
||||
func startSystemProxyHealthObservation() {
|
||||
guard systemProxyHealthTask == nil else { return }
|
||||
guard systemProxyObserver.systemProxyHealthTask == nil else { return }
|
||||
let interval = systemProxyHealthPollIntervalNanoseconds
|
||||
systemProxyHealthTask = Task { @MainActor [weak self] in
|
||||
systemProxyObserver.systemProxyHealthTask = Task { @MainActor [weak self] in
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(nanoseconds: interval)
|
||||
guard !Task.isCancelled, let self,
|
||||
settings.systemProxyEnabled || systemProxyState.hasOperationFailure
|
||||
else { return }
|
||||
refreshSystemProxyApproval()
|
||||
updateSystemProxyActions()
|
||||
systemProxyObserver.refreshSystemProxyApproval()
|
||||
systemProxyObserver.updateSystemProxyActions()
|
||||
guard settings.systemProxyEnabled, state != .starting,
|
||||
!systemProxyStartupInProgress, settings.agentEnabled, let document = lastDocument
|
||||
!systemProxyObserver.systemProxyStartupInProgress, settings.agentEnabled,
|
||||
let document = lastDocument
|
||||
else { continue }
|
||||
await inspectSystemProxyRuntimeHealth(document)
|
||||
}
|
||||
@@ -242,22 +245,22 @@ extension ProxyRuntimeController {
|
||||
}
|
||||
switch outcome {
|
||||
case .ready:
|
||||
let wasUnavailable = systemProxyWasUnavailable
|
||||
let wasUnavailable = systemProxyObserver.systemProxyWasUnavailable
|
||||
switch state {
|
||||
case .running, .firewallBlocked: break
|
||||
default: await presentFirewallStatus(for: document)
|
||||
}
|
||||
if !systemProxyExitAvailable {
|
||||
await suspendSystemProxy()
|
||||
} else if wasUnavailable || systemProxyInitialApplyPending {
|
||||
await convergeSystemProxy()
|
||||
await systemProxyObserver.suspendSystemProxy()
|
||||
} else if wasUnavailable || systemProxyObserver.systemProxyInitialApplyPending {
|
||||
await systemProxyObserver.convergeSystemProxy()
|
||||
}
|
||||
case .agentLost:
|
||||
state = .serviceFailed(.agent)
|
||||
await suspendSystemProxy()
|
||||
await systemProxyObserver.suspendSystemProxy()
|
||||
case .retry(let failure):
|
||||
if let failure { presentEndpointFailure(failure) } else { state = .serviceFailed(.agent) }
|
||||
await suspendSystemProxy()
|
||||
await systemProxyObserver.suspendSystemProxy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ extension ProxyRuntimeController {
|
||||
guard settings.agentEnabled, state != .off,
|
||||
let currentDocument = lastDocument ?? runtimeFileStore.loadDocument()
|
||||
else {
|
||||
if settings.systemProxyEnabled { systemProxyState = .pending }
|
||||
if settings.systemProxyEnabled { systemProxyObserver.systemProxyState = .pending }
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - 防火墙呈现、wrapper 观测与系统代理门禁
|
||||
// MARK: - 防火墙呈现、wrapper 观测与系统代理出口事实
|
||||
|
||||
extension ProxyRuntimeController {
|
||||
func presentFirewallStatus(for document: SslocalRuntimeDocument) async {
|
||||
@@ -115,78 +115,10 @@ extension ProxyRuntimeController {
|
||||
String(describing: error)
|
||||
}
|
||||
|
||||
// MARK: - 系统代理门禁(issue #60/#71)
|
||||
|
||||
/// helper 可用性门禁(issue #71):已批准即通过——仅当 LaunchDaemon 清单
|
||||
/// 与上次注册时的指纹漂移(app 更新改过清单)才注销重注刷新 launchd 的
|
||||
/// job 定义;未注册则尝试注册(注册本身不弹授权框,成功即记指纹);待批准
|
||||
/// 或注册失败时置位批准路径并保持待应用。
|
||||
func ensureHelperAvailableForApply() async -> Bool {
|
||||
switch systemProxyHelper.status {
|
||||
case .approved:
|
||||
if helperRegistrationDrifted() {
|
||||
do {
|
||||
try systemProxyHelper.unregister()
|
||||
// launchd 对节流中(spawn scheduled)job 的移除是异步的:立即重注
|
||||
// 会命中尚未移除的旧定义(实测注销重注后定义不变)。等过 minimum
|
||||
// runtime(10s)节流窗再重注;间隔内收敛以旧定义失败呈超时。
|
||||
try? await Task.sleep(nanoseconds: helperRefreshDelayNanoseconds)
|
||||
try systemProxyHelper.register()
|
||||
storeHelperRegistrationStamp()
|
||||
} catch {
|
||||
// 刷新失败不记指纹,下次启动重试;状态退回后由下方分支接手。
|
||||
RuntimeLog.emit(.systemProxyHelperRegisterFailed(detail: describe(error)))
|
||||
}
|
||||
}
|
||||
if systemProxyHelper.status == .approved {
|
||||
systemProxyApprovalRequired = false
|
||||
return true
|
||||
}
|
||||
case .notRegistered:
|
||||
do {
|
||||
try systemProxyHelper.register()
|
||||
storeHelperRegistrationStamp()
|
||||
} catch {
|
||||
RuntimeLog.emit(.systemProxyHelperRegisterFailed(detail: describe(error)))
|
||||
}
|
||||
if systemProxyHelper.status == .approved {
|
||||
systemProxyApprovalRequired = false
|
||||
return true
|
||||
}
|
||||
case .requiresApproval:
|
||||
break
|
||||
}
|
||||
systemProxyApprovalRequired = true
|
||||
return false
|
||||
}
|
||||
|
||||
private func helperRegistrationDrifted(defaults: UserDefaults = .standard) -> Bool {
|
||||
SystemProxyHelperRegistrationStamp.drifted(
|
||||
plistData: SystemProxyHelperIdentity.launchDaemonPlistData(in: appBundle),
|
||||
storedStamp: defaults.string(forKey: SystemProxyHelperRegistrationStamp.defaultsKey))
|
||||
}
|
||||
|
||||
private func storeHelperRegistrationStamp(defaults: UserDefaults = .standard) {
|
||||
guard let plistData = SystemProxyHelperIdentity.launchDaemonPlistData(in: appBundle) else {
|
||||
return
|
||||
}
|
||||
defaults.set(
|
||||
SystemProxyHelperRegistrationStamp.stamp(plistData),
|
||||
forKey: SystemProxyHelperRegistrationStamp.defaultsKey)
|
||||
}
|
||||
|
||||
/// Approval continues only an unattempted initial request; failed writes need a user retry.
|
||||
func openSystemProxyHelperApproval() async {
|
||||
systemProxyHelper.openApprovalPath()
|
||||
refreshSystemProxyApproval()
|
||||
if systemProxyInitialApplyPending && !systemProxyState.hasOperationFailure {
|
||||
await convergeSystemProxy()
|
||||
}
|
||||
updateSystemProxyActions()
|
||||
}
|
||||
// MARK: - 系统代理出口事实(issue #60/#71)
|
||||
|
||||
/// 出口可用 = agent 入站健康(回环入口可用,含防火墙仅阻主机态的情形)
|
||||
/// 且模式有出口;直连不依赖活动服务器目标。
|
||||
/// 且模式有出口;直连不依赖活动服务器目标。观察机以闭包读取同一事实源。
|
||||
var systemProxyExitAvailable: Bool {
|
||||
switch state {
|
||||
case .running, .firewallBlocked:
|
||||
@@ -195,128 +127,4 @@ extension ProxyRuntimeController {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Health/exit loss clears unavailable settings while preserving enabled intent.
|
||||
func holdSystemProxyIntent() async {
|
||||
if settings.systemProxyEnabled {
|
||||
await suspendSystemProxy()
|
||||
} else if !systemProxyState.hasOperationFailure {
|
||||
systemProxyState = .idle
|
||||
}
|
||||
}
|
||||
|
||||
/// 无条件清除全部系统代理配置(issue #71):typed 失败原样呈现,不重试。
|
||||
func clearSystemProxyOutcome() async -> SystemProxyControlState {
|
||||
do {
|
||||
try await systemProxy.clear()
|
||||
return .idle
|
||||
} catch {
|
||||
refreshSystemProxyApproval()
|
||||
return .clearFailed(systemProxyFacts(for: error))
|
||||
}
|
||||
}
|
||||
|
||||
func startEnabledSystemProxyObservation() {
|
||||
guard systemProxyObservationMode != .enabled else { return }
|
||||
systemProxyObservationMode = .enabled
|
||||
startSystemProxyHealthObservation()
|
||||
systemProxyNetworkChangeMonitor.start { [weak self] change in
|
||||
self?.handleSystemProxyNetworkChange(change)
|
||||
}
|
||||
}
|
||||
|
||||
func clearAndStopSystemProxyObservation() async -> SystemProxyControlState {
|
||||
if let systemProxyCleanupTask { return await systemProxyCleanupTask.value }
|
||||
let task = Task { @MainActor [weak self] in
|
||||
guard let self else { return SystemProxyControlState.idle }
|
||||
return await performSystemProxyCleanupUntilQuiet()
|
||||
}
|
||||
systemProxyCleanupTask = task
|
||||
let result = await task.value
|
||||
systemProxyCleanupTask = nil
|
||||
return result
|
||||
}
|
||||
|
||||
private func performSystemProxyCleanupUntilQuiet() async -> SystemProxyControlState {
|
||||
if systemProxyObservationMode != .enabled {
|
||||
systemProxyObservationMode = .cleanup
|
||||
systemProxyNetworkChangeMonitor.start { [weak self] change in
|
||||
self?.handleSystemProxyNetworkChange(change)
|
||||
}
|
||||
} else {
|
||||
systemProxyObservationMode = .cleanup
|
||||
}
|
||||
|
||||
guard await waitForSystemProxyOperation() else { return systemProxyState }
|
||||
beginSystemProxyOperation()
|
||||
var outcome: SystemProxyControlState = .idle
|
||||
repeat {
|
||||
systemProxyCleanupRescanRequested = false
|
||||
outcome = await clearSystemProxyOutcome()
|
||||
// Let queued SystemConfiguration notifications reach the main actor. Any
|
||||
// location/service/proxy change during this cleanup starts another full scan.
|
||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||
await Task.yield()
|
||||
} while systemProxyCleanupRescanRequested && !outcome.hasOperationFailure
|
||||
|
||||
systemProxyNetworkChangeMonitor.stop()
|
||||
systemProxyHealthTask?.cancel()
|
||||
systemProxyHealthTask = nil
|
||||
systemProxyObservationMode = .stopped
|
||||
systemProxyInspection = SystemProxyInspectionFacts()
|
||||
endSystemProxyOperation()
|
||||
return outcome
|
||||
}
|
||||
|
||||
private func handleSystemProxyNetworkChange(_ change: SystemProxyNetworkChange) {
|
||||
switch systemProxyObservationMode {
|
||||
case .stopped:
|
||||
return
|
||||
case .cleanup:
|
||||
if !change.isDisjoint(with: [.networkConfiguration, .proxyConfiguration]) {
|
||||
systemProxyCleanupRescanRequested = true
|
||||
}
|
||||
case .enabled:
|
||||
guard settings.systemProxyEnabled else { return }
|
||||
guard !systemProxyInspectionScheduled else { return }
|
||||
systemProxyInspectionScheduled = true
|
||||
Task { @MainActor [weak self] in
|
||||
await Task.yield()
|
||||
guard let self else { return }
|
||||
self.systemProxyInspectionScheduled = false
|
||||
await self.recheckSystemProxy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func systemProxyFacts(for error: Error) -> SystemProxyFailureFacts {
|
||||
if let error = error as? SystemProxyError {
|
||||
return Self.systemProxyFacts(for: error)
|
||||
}
|
||||
if let error = error as? ProxyModeError {
|
||||
return .mode(error)
|
||||
}
|
||||
return .unknown
|
||||
}
|
||||
|
||||
// 显式映射保持与错误族一一对应;错误带关联值,无法用字典键穷举。
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
private static func systemProxyFacts(
|
||||
for error: SystemProxyError
|
||||
) -> SystemProxyFailureFacts {
|
||||
switch error {
|
||||
case .preferencesUnavailable: return .operation(.preferencesUnavailable)
|
||||
case .preferencesBusy: return .operation(.preferencesBusy)
|
||||
case .noCurrentNetworkSet: return .operation(.noCurrentNetworkSet)
|
||||
case .noProxyServices: return .operation(.noProxyServices)
|
||||
case .unreadableService: return .operation(.unreadableService)
|
||||
case .invalidStoredConfiguration: return .operation(.invalidStoredConfiguration)
|
||||
case .cannotWriteService: return .operation(.cannotWriteService)
|
||||
case .commitFailed: return .operation(.commitFailed)
|
||||
case .applyFailed: return .operation(.applyFailed)
|
||||
case .noNetworkLocations: return .operation(.noNetworkLocations)
|
||||
case .invalidRequest: return .operation(.invalidRequest)
|
||||
case .helperUnavailable: return .operation(.helperUnavailable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
// Current configuration observation and explicit operations (issue #73).
|
||||
extension ProxyRuntimeController {
|
||||
/// Explicit initial application, saved-value changes, or real health recovery.
|
||||
/// A healthy GUI startup and passive events only inspect current settings.
|
||||
func convergeSystemProxy(forceApply: Bool = false) async {
|
||||
guard await waitForSystemProxyOperation() else { return }
|
||||
guard settings.systemProxyEnabled, !systemProxyStartupInProgress else { return }
|
||||
guard systemProxyExitAvailable else {
|
||||
await suspendSystemProxy()
|
||||
return
|
||||
}
|
||||
guard let configuration = desiredSystemProxyConfiguration else { return }
|
||||
let needsApplication =
|
||||
forceApply || systemProxyInitialApplyPending || systemProxyWasUnavailable
|
||||
|| lastDesiredSystemProxyConfiguration != configuration
|
||||
if needsApplication {
|
||||
await applySystemProxy(configuration, repairing: false)
|
||||
} else {
|
||||
if systemProxyHelper.status == .approved { _ = await ensureHelperAvailableForApply() }
|
||||
await recheckSystemProxy()
|
||||
}
|
||||
}
|
||||
|
||||
var desiredSystemProxyConfiguration: SystemProxyConfiguration? {
|
||||
guard let document = lastDocument else { return nil }
|
||||
return try? proxyMode.systemProxyConfiguration(
|
||||
for: document, exceptions: settings.proxyExceptionList)
|
||||
}
|
||||
|
||||
private func applySystemProxy(
|
||||
_ configuration: SystemProxyConfiguration, repairing: Bool
|
||||
) async {
|
||||
guard !systemProxyOperationInProgress else { return }
|
||||
beginSystemProxyOperation()
|
||||
defer { endSystemProxyOperation() }
|
||||
guard await ensureHelperAvailableForApply() else {
|
||||
if !systemProxyState.hasOperationFailure { systemProxyState = .pending }
|
||||
updateSystemProxyActions()
|
||||
return
|
||||
}
|
||||
guard settings.systemProxyEnabled, systemProxyExitAvailable else { return }
|
||||
if repairing {
|
||||
guard await prepareSystemProxyRepair(configuration) else { return }
|
||||
}
|
||||
systemProxyState = repairing ? .repairing : .applying
|
||||
updateSystemProxyActions()
|
||||
// Mark the attempt before awaiting: a failed operation is never implicitly retried.
|
||||
systemProxyInitialApplyPending = false
|
||||
systemProxyWasUnavailable = false
|
||||
systemProxyInspection.backgroundUnavailable = false
|
||||
lastDesiredSystemProxyConfiguration = configuration
|
||||
do {
|
||||
if repairing {
|
||||
try await systemProxy.repair(configuration)
|
||||
} else {
|
||||
let outcome = try await systemProxy.apply(configuration)
|
||||
if outcome == .unchanged { RuntimeLog.emit(.systemProxyUnchanged) }
|
||||
}
|
||||
await inspectSystemProxy(configuration: configuration, afterRepair: repairing)
|
||||
} catch {
|
||||
RuntimeLog.emit(.systemProxyWriteFailed(detail: describe(error)))
|
||||
systemProxyState =
|
||||
repairing
|
||||
? .repairFailed(systemProxyFacts(for: error)) : .failed(systemProxyFacts(for: error))
|
||||
await inspectSystemProxy(configuration: configuration, preserveResult: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func prepareSystemProxyRepair(_ configuration: SystemProxyConfiguration) async -> Bool {
|
||||
guard
|
||||
await inspectSystemProxy(
|
||||
configuration: configuration,
|
||||
preserveResult: systemProxyState.hasOperationFailure)
|
||||
else { return false }
|
||||
guard settings.systemProxyEnabled, systemProxyExitAvailable,
|
||||
desiredSystemProxyConfiguration == configuration, systemProxyHelper.status == .approved
|
||||
else { return false }
|
||||
guard !systemProxyInspection.differences.isEmpty else {
|
||||
systemProxyState = .applied
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func repairSystemProxy() async {
|
||||
updateSystemProxyActions()
|
||||
guard systemProxyInspection.canRepair, let configuration = desiredSystemProxyConfiguration
|
||||
else { return }
|
||||
await applySystemProxy(configuration, repairing: true)
|
||||
}
|
||||
|
||||
func recheckSystemProxy() async {
|
||||
refreshSystemProxyApproval()
|
||||
guard settings.systemProxyEnabled, let configuration = desiredSystemProxyConfiguration else {
|
||||
updateSystemProxyActions()
|
||||
return
|
||||
}
|
||||
guard !systemProxyOperationInProgress else { return }
|
||||
await inspectSystemProxy(
|
||||
configuration: configuration,
|
||||
preserveResult: systemProxyState.hasOperationFailure || !systemProxyExitAvailable)
|
||||
updateSystemProxyActions()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func inspectSystemProxy(
|
||||
configuration: SystemProxyConfiguration, afterRepair: Bool = false,
|
||||
preserveResult: Bool = false
|
||||
) async -> Bool {
|
||||
systemProxyReadGeneration += 1
|
||||
let generation = systemProxyReadGeneration
|
||||
do {
|
||||
let services = try await systemProxy.readServices()
|
||||
guard !services.isEmpty else { throw SystemProxyError.noProxyServices }
|
||||
guard generation == systemProxyReadGeneration, settings.systemProxyEnabled,
|
||||
desiredSystemProxyConfiguration == configuration
|
||||
else { return false }
|
||||
let writes = try SystemProxyPlanner.makeApplyPlan(
|
||||
services: services, configuration: configuration
|
||||
).writes
|
||||
let differing = Set(writes.map(\.identifier))
|
||||
systemProxyInspection.differences = services.compactMap { service in
|
||||
guard differing.contains(service.identifier) else { return nil }
|
||||
return SystemProxyDifference(
|
||||
identifier: service.identifier, name: service.name,
|
||||
kind: afterRepair
|
||||
? .remaining
|
||||
: (knownSystemProxyServices.contains(service.identifier) ? .changed : .notApplied))
|
||||
}
|
||||
// Only observed consistency establishes a known service; an unsuccessful write
|
||||
// response cannot establish that this app's configuration was ever present.
|
||||
knownSystemProxyServices.formUnion(
|
||||
services.filter {
|
||||
!differing.contains($0.identifier)
|
||||
}.map(\.identifier))
|
||||
systemProxyInspection.readFailure = nil
|
||||
if !preserveResult && !systemProxyState.isClearFailure && !systemProxyWasUnavailable {
|
||||
systemProxyState = differing.isEmpty ? .applied : .changed
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
guard generation == systemProxyReadGeneration else { return false }
|
||||
systemProxyInspection.differences = []
|
||||
systemProxyInspection.readFailure = systemProxyFacts(for: error)
|
||||
if !preserveResult && !systemProxyState.isClearFailure && !systemProxyWasUnavailable {
|
||||
systemProxyState = .unreadable(systemProxyFacts(for: error))
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func refreshSystemProxyApproval() {
|
||||
systemProxyApprovalRequired =
|
||||
systemProxyHelper.status != .approved
|
||||
&& (settings.systemProxyEnabled || systemProxyState.hasOperationFailure)
|
||||
}
|
||||
|
||||
func updateSystemProxyActions() {
|
||||
refreshSystemProxyApproval()
|
||||
let available = !systemProxyOperationInProgress && !systemProxyApprovalRequired
|
||||
systemProxyInspection.canRepair =
|
||||
available && settings.systemProxyEnabled
|
||||
&& systemProxyExitAvailable && systemProxyInspection.readFailure == nil
|
||||
&& (!systemProxyInspection.differences.isEmpty || systemProxyState.isApplyFailure)
|
||||
systemProxyInspection.canRetryClear = available && systemProxyState.isClearFailure
|
||||
if systemProxyState.hasOperationFailure {
|
||||
startSystemProxyHealthObservation()
|
||||
} else if !settings.systemProxyEnabled {
|
||||
systemProxyHealthTask?.cancel()
|
||||
systemProxyHealthTask = nil
|
||||
}
|
||||
}
|
||||
|
||||
func suspendSystemProxy() async {
|
||||
guard settings.systemProxyEnabled, !systemProxyStartupInProgress else { return }
|
||||
guard !systemProxyWasUnavailable else {
|
||||
updateSystemProxyActions()
|
||||
return
|
||||
}
|
||||
guard await waitForSystemProxyOperation() else { return }
|
||||
guard settings.systemProxyEnabled, !systemProxyWasUnavailable else { return }
|
||||
systemProxyWasUnavailable = true
|
||||
systemProxyInspection.backgroundUnavailable = true
|
||||
systemProxyInitialApplyPending = false
|
||||
beginSystemProxyOperation()
|
||||
defer { endSystemProxyOperation() }
|
||||
let outcome = await clearSystemProxyOutcome()
|
||||
systemProxyState = outcome == .idle ? .paused : outcome
|
||||
if outcome == .idle {
|
||||
systemProxyInspection.differences = []
|
||||
systemProxyInspection.readFailure = nil
|
||||
}
|
||||
}
|
||||
|
||||
func retrySystemProxyClear() async {
|
||||
updateSystemProxyActions()
|
||||
guard systemProxyInspection.canRetryClear else { return }
|
||||
beginSystemProxyOperation()
|
||||
defer { endSystemProxyOperation() }
|
||||
let outcome = await clearSystemProxyOutcome()
|
||||
systemProxyState = outcome == .idle && settings.systemProxyEnabled ? .paused : outcome
|
||||
}
|
||||
|
||||
func beginSystemProxyOperation() {
|
||||
systemProxyOperationInProgress = true
|
||||
systemProxyInspection.isBusy = true
|
||||
systemProxyReadGeneration += 1
|
||||
updateSystemProxyActions()
|
||||
}
|
||||
|
||||
func endSystemProxyOperation() {
|
||||
systemProxyOperationInProgress = false
|
||||
systemProxyInspection.isBusy = false
|
||||
updateSystemProxyActions()
|
||||
}
|
||||
|
||||
func waitForSystemProxyOperation() async -> Bool {
|
||||
while systemProxyOperationInProgress {
|
||||
do { try await Task.sleep(nanoseconds: 10_000_000) } catch { return false }
|
||||
}
|
||||
return !Task.isCancelled
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@ import Foundation
|
||||
/// persist independent intentions. System proxy writes use the privileged helper;
|
||||
/// current configuration is observed read-only and differs from write outcomes.
|
||||
/// Health/exit loss suspends system proxy settings, real recovery reapplies, and
|
||||
/// passive network events only inspect (ADR-0022 / issue #73). GUI exit ends these
|
||||
/// passive network events only inspect (ADR-0022 / issue #73)——该观察/修复策略机
|
||||
/// 独立在 `SystemProxyObserver`:控制器注入意图/出口/期望配置三个事实闭包并
|
||||
/// 转发命令,健康循环保留在本体(循环体含运行时健康呈现)。GUI exit ends these
|
||||
/// observations without stopping the LaunchAgent or adding helper responsibilities.
|
||||
@MainActor
|
||||
final class ProxyRuntimeController: ObservableObject {
|
||||
@@ -27,25 +29,7 @@ final class ProxyRuntimeController: ObservableObject {
|
||||
/// Current configuration or operation result, independent of persisted intent.
|
||||
typealias SystemProxyControlState = SystemProxyApplicationFacts
|
||||
|
||||
enum SystemProxyObservationMode: Equatable {
|
||||
case stopped
|
||||
case enabled
|
||||
case cleanup
|
||||
}
|
||||
|
||||
@Published var state: AgentRunState = .off
|
||||
@Published var systemProxyState: SystemProxyControlState = .idle
|
||||
/// Approval remains actionable even after an off-intent clear failure.
|
||||
@Published var systemProxyApprovalRequired = false
|
||||
@Published var systemProxyInspection = SystemProxyInspectionFacts()
|
||||
var lastDesiredSystemProxyConfiguration: SystemProxyConfiguration?
|
||||
var knownSystemProxyServices: Set<SystemProxyServiceIdentifier> = []
|
||||
var systemProxyOperationInProgress = false
|
||||
var systemProxyStartupInProgress = false
|
||||
var systemProxyInitialApplyPending = false
|
||||
var systemProxyWasUnavailable = false
|
||||
var systemProxyHealthTask: Task<Void, Never>?
|
||||
var systemProxyReadGeneration = 0
|
||||
@Published var settings: ProxySettings
|
||||
/// 当前活动目标(菜单栏状态摘要与级联只读呈现用,issue #31)。machine 是
|
||||
/// 非发布值的普通结构体,代理关闭路径的激活动作不会触碰 state,菜单的
|
||||
@@ -80,6 +64,25 @@ final class ProxyRuntimeController: ObservableObject {
|
||||
let systemProxy: SystemProxyControlling
|
||||
let systemProxyHelper: SystemProxyHelperServicing
|
||||
let systemProxyNetworkChangeMonitor: SystemProxyNetworkChangeMonitoring
|
||||
/// 系统代理观察/修复策略机(ADR-0022):呈现事实的存储与发布点,详情见
|
||||
/// 模块文档。惰性构造:事实闭包需捕获控制器,而闭包成形必须晚于自身
|
||||
/// 全部存储属性的初始化(首次访问发生在 init 之后)。
|
||||
lazy var systemProxyObserver: SystemProxyObserver = SystemProxyObserver(
|
||||
systemProxy: systemProxy,
|
||||
systemProxyHelper: systemProxyHelper,
|
||||
systemProxyNetworkChangeMonitor: systemProxyNetworkChangeMonitor,
|
||||
appBundle: appBundle,
|
||||
helperRefreshDelayNanoseconds: helperRefreshDelayNanoseconds,
|
||||
isIntentEnabled: { [weak self] in self?.settings.systemProxyEnabled ?? false },
|
||||
isExitAvailable: { [weak self] in self?.systemProxyExitAvailable ?? false },
|
||||
desiredConfiguration: { [weak self] in
|
||||
guard let self else { return nil }
|
||||
return self.desiredSystemProxyConfiguration
|
||||
},
|
||||
startSystemProxyHealthObservation: { [weak self] in
|
||||
guard let self else { return }
|
||||
self.startSystemProxyHealthObservation()
|
||||
})
|
||||
let firewallChecker: FirewallStatusChecking
|
||||
let firewallExecutableURLs: [URL]
|
||||
/// 宿主 app bundle 缝(生产为 Bundle.main):内置规则快照与 LaunchDaemon
|
||||
@@ -99,10 +102,6 @@ final class ProxyRuntimeController: ObservableObject {
|
||||
var modeChangeGeneration = 0
|
||||
var lastDocument: SslocalRuntimeDocument?
|
||||
var firewallObservationTask: Task<Void, Never>?
|
||||
var systemProxyObservationMode = SystemProxyObservationMode.stopped
|
||||
var systemProxyCleanupRescanRequested = false
|
||||
var systemProxyCleanupTask: Task<SystemProxyControlState, Never>?
|
||||
var systemProxyInspectionScheduled = false
|
||||
|
||||
@Published var proxyMode: ProxyMode
|
||||
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
import Foundation
|
||||
|
||||
/// 系统代理观察/修复策略机(ADR-0022 / issue #73):收敛、挂起、意图保持、
|
||||
/// 修复、清理重试、helper 门禁与 cleanup-until-quiet 观察循环的唯一属主。
|
||||
/// 意图开关、出口可用性与期望配置由组合方以闭包注入,模块不反向依赖控制器;
|
||||
/// 呈现事实经自身 `objectWillChange` 由运行时 adapter 合入 workflow 再发布链。
|
||||
/// 底层只依赖 `SystemProxyControlling`、helper 注册缝与网络变化监视。
|
||||
@MainActor
|
||||
final class SystemProxyObserver: ObservableObject {
|
||||
enum SystemProxyObservationMode: Equatable {
|
||||
case stopped
|
||||
case enabled
|
||||
case cleanup
|
||||
}
|
||||
|
||||
/// Current configuration or operation result, independent of persisted intent.
|
||||
@Published var systemProxyState: SystemProxyApplicationFacts = .idle
|
||||
/// Approval remains actionable even after an off-intent clear failure.
|
||||
@Published var systemProxyApprovalRequired = false
|
||||
@Published var systemProxyInspection = SystemProxyInspectionFacts()
|
||||
|
||||
var lastDesiredSystemProxyConfiguration: SystemProxyConfiguration?
|
||||
var knownSystemProxyServices: Set<SystemProxyServiceIdentifier> = []
|
||||
var systemProxyOperationInProgress = false
|
||||
var systemProxyStartupInProgress = false
|
||||
var systemProxyInitialApplyPending = false
|
||||
var systemProxyWasUnavailable = false
|
||||
var systemProxyHealthTask: Task<Void, Never>?
|
||||
var systemProxyReadGeneration = 0
|
||||
var systemProxyObservationMode = SystemProxyObservationMode.stopped
|
||||
var systemProxyCleanupRescanRequested = false
|
||||
var systemProxyCleanupTask: Task<SystemProxyApplicationFacts, Never>?
|
||||
var systemProxyInspectionScheduled = false
|
||||
|
||||
let systemProxy: SystemProxyControlling
|
||||
let systemProxyHelper: SystemProxyHelperServicing
|
||||
let systemProxyNetworkChangeMonitor: SystemProxyNetworkChangeMonitoring
|
||||
/// 宿主 app bundle 缝(生产为 Bundle.main):LaunchDaemon 清单指纹的定位基准。
|
||||
let appBundle: Bundle
|
||||
/// 注册清单漂移重注时,注销与重注的间隔(launchd 对节流中 job 的移除异步)。
|
||||
let helperRefreshDelayNanoseconds: UInt64
|
||||
private let isIntentEnabled: @MainActor () -> Bool
|
||||
private let isExitAvailable: @MainActor () -> Bool
|
||||
private let desiredConfiguration: @MainActor () -> SystemProxyConfiguration?
|
||||
/// 健康观察循环属控制器(循环体含运行时健康呈现),策略机只请求调度。
|
||||
private let startSystemProxyHealthObservation: @MainActor () -> Void
|
||||
|
||||
init(
|
||||
systemProxy: SystemProxyControlling,
|
||||
systemProxyHelper: SystemProxyHelperServicing,
|
||||
systemProxyNetworkChangeMonitor: SystemProxyNetworkChangeMonitoring,
|
||||
appBundle: Bundle,
|
||||
helperRefreshDelayNanoseconds: UInt64,
|
||||
isIntentEnabled: @escaping @MainActor () -> Bool,
|
||||
isExitAvailable: @escaping @MainActor () -> Bool,
|
||||
desiredConfiguration: @escaping @MainActor () -> SystemProxyConfiguration?,
|
||||
startSystemProxyHealthObservation: @escaping @MainActor () -> Void
|
||||
) {
|
||||
self.systemProxy = systemProxy
|
||||
self.systemProxyHelper = systemProxyHelper
|
||||
self.systemProxyNetworkChangeMonitor = systemProxyNetworkChangeMonitor
|
||||
self.appBundle = appBundle
|
||||
self.helperRefreshDelayNanoseconds = helperRefreshDelayNanoseconds
|
||||
self.isIntentEnabled = isIntentEnabled
|
||||
self.isExitAvailable = isExitAvailable
|
||||
self.desiredConfiguration = desiredConfiguration
|
||||
self.startSystemProxyHealthObservation = startSystemProxyHealthObservation
|
||||
}
|
||||
|
||||
// MARK: - 收敛与应用
|
||||
|
||||
/// Explicit initial application, saved-value changes, or real health recovery.
|
||||
/// A healthy GUI startup and passive events only inspect current settings.
|
||||
func convergeSystemProxy(forceApply: Bool = false) async {
|
||||
guard await waitForSystemProxyOperation() else { return }
|
||||
guard isIntentEnabled(), !systemProxyStartupInProgress else { return }
|
||||
guard isExitAvailable() else {
|
||||
await suspendSystemProxy()
|
||||
return
|
||||
}
|
||||
guard let configuration = desiredConfiguration() else { return }
|
||||
let needsApplication =
|
||||
forceApply || systemProxyInitialApplyPending || systemProxyWasUnavailable
|
||||
|| lastDesiredSystemProxyConfiguration != configuration
|
||||
if needsApplication {
|
||||
await applySystemProxy(configuration, repairing: false)
|
||||
} else {
|
||||
if systemProxyHelper.status == .approved { _ = await ensureHelperAvailableForApply() }
|
||||
await recheckSystemProxy()
|
||||
}
|
||||
}
|
||||
|
||||
private func applySystemProxy(
|
||||
_ configuration: SystemProxyConfiguration, repairing: Bool
|
||||
) async {
|
||||
guard !systemProxyOperationInProgress else { return }
|
||||
beginSystemProxyOperation()
|
||||
defer { endSystemProxyOperation() }
|
||||
guard await ensureHelperAvailableForApply() else {
|
||||
if !systemProxyState.hasOperationFailure { systemProxyState = .pending }
|
||||
updateSystemProxyActions()
|
||||
return
|
||||
}
|
||||
guard isIntentEnabled(), isExitAvailable() else { return }
|
||||
if repairing {
|
||||
guard await prepareSystemProxyRepair(configuration) else { return }
|
||||
}
|
||||
systemProxyState = repairing ? .repairing : .applying
|
||||
updateSystemProxyActions()
|
||||
// Mark the attempt before awaiting: a failed operation is never implicitly retried.
|
||||
systemProxyInitialApplyPending = false
|
||||
systemProxyWasUnavailable = false
|
||||
systemProxyInspection.backgroundUnavailable = false
|
||||
lastDesiredSystemProxyConfiguration = configuration
|
||||
do {
|
||||
if repairing {
|
||||
try await systemProxy.repair(configuration)
|
||||
} else {
|
||||
let outcome = try await systemProxy.apply(configuration)
|
||||
if outcome == .unchanged { RuntimeLog.emit(.systemProxyUnchanged) }
|
||||
}
|
||||
await inspectSystemProxy(configuration: configuration, afterRepair: repairing)
|
||||
} catch {
|
||||
RuntimeLog.emit(.systemProxyWriteFailed(detail: String(describing: error)))
|
||||
systemProxyState =
|
||||
repairing
|
||||
? .repairFailed(systemProxyFacts(for: error)) : .failed(systemProxyFacts(for: error))
|
||||
await inspectSystemProxy(configuration: configuration, preserveResult: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func prepareSystemProxyRepair(_ configuration: SystemProxyConfiguration) async -> Bool {
|
||||
guard
|
||||
await inspectSystemProxy(
|
||||
configuration: configuration,
|
||||
preserveResult: systemProxyState.hasOperationFailure)
|
||||
else { return false }
|
||||
guard isIntentEnabled(), isExitAvailable(),
|
||||
desiredConfiguration() == configuration, systemProxyHelper.status == .approved
|
||||
else { return false }
|
||||
guard !systemProxyInspection.differences.isEmpty else {
|
||||
systemProxyState = .applied
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func repairSystemProxy() async {
|
||||
updateSystemProxyActions()
|
||||
guard systemProxyInspection.canRepair, let configuration = desiredConfiguration()
|
||||
else { return }
|
||||
await applySystemProxy(configuration, repairing: true)
|
||||
}
|
||||
|
||||
func recheckSystemProxy() async {
|
||||
refreshSystemProxyApproval()
|
||||
guard isIntentEnabled(), let configuration = desiredConfiguration() else {
|
||||
updateSystemProxyActions()
|
||||
return
|
||||
}
|
||||
guard !systemProxyOperationInProgress else { return }
|
||||
await inspectSystemProxy(
|
||||
configuration: configuration,
|
||||
preserveResult: systemProxyState.hasOperationFailure || !isExitAvailable())
|
||||
updateSystemProxyActions()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func inspectSystemProxy(
|
||||
configuration: SystemProxyConfiguration, afterRepair: Bool = false,
|
||||
preserveResult: Bool = false
|
||||
) async -> Bool {
|
||||
systemProxyReadGeneration += 1
|
||||
let generation = systemProxyReadGeneration
|
||||
do {
|
||||
let services = try await systemProxy.readServices()
|
||||
guard !services.isEmpty else { throw SystemProxyError.noProxyServices }
|
||||
guard generation == systemProxyReadGeneration, isIntentEnabled(),
|
||||
desiredConfiguration() == configuration
|
||||
else { return false }
|
||||
let writes = try SystemProxyPlanner.makeApplyPlan(
|
||||
services: services, configuration: configuration
|
||||
).writes
|
||||
let differing = Set(writes.map(\.identifier))
|
||||
systemProxyInspection.differences = services.compactMap { service in
|
||||
guard differing.contains(service.identifier) else { return nil }
|
||||
return SystemProxyDifference(
|
||||
identifier: service.identifier, name: service.name,
|
||||
kind: afterRepair
|
||||
? .remaining
|
||||
: (knownSystemProxyServices.contains(service.identifier) ? .changed : .notApplied))
|
||||
}
|
||||
// Only observed consistency establishes a known service; an unsuccessful write
|
||||
// response cannot establish that this app's configuration was ever present.
|
||||
knownSystemProxyServices.formUnion(
|
||||
services.filter {
|
||||
!differing.contains($0.identifier)
|
||||
}.map(\.identifier))
|
||||
systemProxyInspection.readFailure = nil
|
||||
if !preserveResult && !systemProxyState.isClearFailure && !systemProxyWasUnavailable {
|
||||
systemProxyState = differing.isEmpty ? .applied : .changed
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
guard generation == systemProxyReadGeneration else { return false }
|
||||
systemProxyInspection.differences = []
|
||||
systemProxyInspection.readFailure = systemProxyFacts(for: error)
|
||||
if !preserveResult && !systemProxyState.isClearFailure && !systemProxyWasUnavailable {
|
||||
systemProxyState = .unreadable(systemProxyFacts(for: error))
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func refreshSystemProxyApproval() {
|
||||
systemProxyApprovalRequired =
|
||||
systemProxyHelper.status != .approved
|
||||
&& (isIntentEnabled() || systemProxyState.hasOperationFailure)
|
||||
}
|
||||
|
||||
func updateSystemProxyActions() {
|
||||
refreshSystemProxyApproval()
|
||||
let available = !systemProxyOperationInProgress && !systemProxyApprovalRequired
|
||||
systemProxyInspection.canRepair =
|
||||
available && isIntentEnabled()
|
||||
&& isExitAvailable() && systemProxyInspection.readFailure == nil
|
||||
&& (!systemProxyInspection.differences.isEmpty || systemProxyState.isApplyFailure)
|
||||
systemProxyInspection.canRetryClear = available && systemProxyState.isClearFailure
|
||||
if systemProxyState.hasOperationFailure {
|
||||
startSystemProxyHealthObservation()
|
||||
} else if !isIntentEnabled() {
|
||||
systemProxyHealthTask?.cancel()
|
||||
systemProxyHealthTask = nil
|
||||
}
|
||||
}
|
||||
|
||||
func suspendSystemProxy() async {
|
||||
guard isIntentEnabled(), !systemProxyStartupInProgress else { return }
|
||||
guard !systemProxyWasUnavailable else {
|
||||
updateSystemProxyActions()
|
||||
return
|
||||
}
|
||||
guard await waitForSystemProxyOperation() else { return }
|
||||
guard isIntentEnabled(), !systemProxyWasUnavailable else { return }
|
||||
systemProxyWasUnavailable = true
|
||||
systemProxyInspection.backgroundUnavailable = true
|
||||
systemProxyInitialApplyPending = false
|
||||
beginSystemProxyOperation()
|
||||
defer { endSystemProxyOperation() }
|
||||
let outcome = await clearSystemProxyOutcome()
|
||||
systemProxyState = outcome == .idle ? .paused : outcome
|
||||
if outcome == .idle {
|
||||
systemProxyInspection.differences = []
|
||||
systemProxyInspection.readFailure = nil
|
||||
}
|
||||
}
|
||||
|
||||
func retrySystemProxyClear() async {
|
||||
updateSystemProxyActions()
|
||||
guard systemProxyInspection.canRetryClear else { return }
|
||||
beginSystemProxyOperation()
|
||||
defer { endSystemProxyOperation() }
|
||||
let outcome = await clearSystemProxyOutcome()
|
||||
systemProxyState = outcome == .idle && isIntentEnabled() ? .paused : outcome
|
||||
}
|
||||
|
||||
private func beginSystemProxyOperation() {
|
||||
systemProxyOperationInProgress = true
|
||||
systemProxyInspection.isBusy = true
|
||||
systemProxyReadGeneration += 1
|
||||
updateSystemProxyActions()
|
||||
}
|
||||
|
||||
private func endSystemProxyOperation() {
|
||||
systemProxyOperationInProgress = false
|
||||
systemProxyInspection.isBusy = false
|
||||
updateSystemProxyActions()
|
||||
}
|
||||
|
||||
private func waitForSystemProxyOperation() async -> Bool {
|
||||
while systemProxyOperationInProgress {
|
||||
do { try await Task.sleep(nanoseconds: 10_000_000) } catch { return false }
|
||||
}
|
||||
return !Task.isCancelled
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - helper 门禁(issue #71)
|
||||
|
||||
extension SystemProxyObserver {
|
||||
/// helper 可用性门禁(issue #71):已批准即通过——仅当 LaunchDaemon 清单
|
||||
/// 与上次注册时的指纹漂移(app 更新改过清单)才注销重注刷新 launchd 的
|
||||
/// job 定义;未注册则尝试注册(注册本身不弹授权框,成功即记指纹);待批准
|
||||
/// 或注册失败时置位批准路径并保持待应用。
|
||||
private func ensureHelperAvailableForApply() async -> Bool {
|
||||
switch systemProxyHelper.status {
|
||||
case .approved:
|
||||
if helperRegistrationDrifted() {
|
||||
do {
|
||||
try systemProxyHelper.unregister()
|
||||
// launchd 对节流中(spawn scheduled)job 的移除是异步的:立即重注
|
||||
// 会命中尚未移除的旧定义(实测注销重注后定义不变)。等过 minimum
|
||||
// runtime(10s)节流窗再重注;间隔内收敛以旧定义失败呈超时。
|
||||
try? await Task.sleep(nanoseconds: helperRefreshDelayNanoseconds)
|
||||
try systemProxyHelper.register()
|
||||
storeHelperRegistrationStamp()
|
||||
} catch {
|
||||
// 刷新失败不记指纹,下次启动重试;状态退回后由下方分支接手。
|
||||
RuntimeLog.emit(.systemProxyHelperRegisterFailed(detail: String(describing: error)))
|
||||
}
|
||||
}
|
||||
if systemProxyHelper.status == .approved {
|
||||
systemProxyApprovalRequired = false
|
||||
return true
|
||||
}
|
||||
case .notRegistered:
|
||||
do {
|
||||
try systemProxyHelper.register()
|
||||
storeHelperRegistrationStamp()
|
||||
} catch {
|
||||
RuntimeLog.emit(.systemProxyHelperRegisterFailed(detail: String(describing: error)))
|
||||
}
|
||||
if systemProxyHelper.status == .approved {
|
||||
systemProxyApprovalRequired = false
|
||||
return true
|
||||
}
|
||||
case .requiresApproval:
|
||||
break
|
||||
}
|
||||
systemProxyApprovalRequired = true
|
||||
return false
|
||||
}
|
||||
|
||||
private func helperRegistrationDrifted(defaults: UserDefaults = .standard) -> Bool {
|
||||
SystemProxyHelperRegistrationStamp.drifted(
|
||||
plistData: SystemProxyHelperIdentity.launchDaemonPlistData(in: appBundle),
|
||||
storedStamp: defaults.string(forKey: SystemProxyHelperRegistrationStamp.defaultsKey))
|
||||
}
|
||||
|
||||
private func storeHelperRegistrationStamp(defaults: UserDefaults = .standard) {
|
||||
guard let plistData = SystemProxyHelperIdentity.launchDaemonPlistData(in: appBundle) else {
|
||||
return
|
||||
}
|
||||
defaults.set(
|
||||
SystemProxyHelperRegistrationStamp.stamp(plistData),
|
||||
forKey: SystemProxyHelperRegistrationStamp.defaultsKey)
|
||||
}
|
||||
|
||||
/// Approval continues only an unattempted initial request; failed writes need a user retry.
|
||||
func openSystemProxyHelperApproval() async {
|
||||
systemProxyHelper.openApprovalPath()
|
||||
refreshSystemProxyApproval()
|
||||
if systemProxyInitialApplyPending && !systemProxyState.hasOperationFailure {
|
||||
await convergeSystemProxy()
|
||||
}
|
||||
updateSystemProxyActions()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - 意图保持与清理
|
||||
|
||||
extension SystemProxyObserver {
|
||||
/// Health/exit loss clears unavailable settings while preserving enabled intent.
|
||||
func holdSystemProxyIntent() async {
|
||||
if isIntentEnabled() {
|
||||
await suspendSystemProxy()
|
||||
} else if !systemProxyState.hasOperationFailure {
|
||||
systemProxyState = .idle
|
||||
}
|
||||
}
|
||||
|
||||
/// 无条件清除全部系统代理配置(issue #71):typed 失败原样呈现,不重试。
|
||||
private func clearSystemProxyOutcome() async -> SystemProxyApplicationFacts {
|
||||
do {
|
||||
try await systemProxy.clear()
|
||||
return .idle
|
||||
} catch {
|
||||
refreshSystemProxyApproval()
|
||||
return .clearFailed(systemProxyFacts(for: error))
|
||||
}
|
||||
}
|
||||
|
||||
func startEnabledSystemProxyObservation() {
|
||||
guard systemProxyObservationMode != .enabled else { return }
|
||||
systemProxyObservationMode = .enabled
|
||||
startSystemProxyHealthObservation()
|
||||
systemProxyNetworkChangeMonitor.start { [weak self] change in
|
||||
self?.handleSystemProxyNetworkChange(change)
|
||||
}
|
||||
}
|
||||
|
||||
func clearAndStopSystemProxyObservation() async -> SystemProxyApplicationFacts {
|
||||
if let systemProxyCleanupTask { return await systemProxyCleanupTask.value }
|
||||
let task = Task { @MainActor [weak self] in
|
||||
guard let self else { return SystemProxyApplicationFacts.idle }
|
||||
return await self.performSystemProxyCleanupUntilQuiet()
|
||||
}
|
||||
systemProxyCleanupTask = task
|
||||
let result = await task.value
|
||||
systemProxyCleanupTask = nil
|
||||
return result
|
||||
}
|
||||
|
||||
private func performSystemProxyCleanupUntilQuiet() async -> SystemProxyApplicationFacts {
|
||||
if systemProxyObservationMode != .enabled {
|
||||
systemProxyObservationMode = .cleanup
|
||||
systemProxyNetworkChangeMonitor.start { [weak self] change in
|
||||
self?.handleSystemProxyNetworkChange(change)
|
||||
}
|
||||
} else {
|
||||
systemProxyObservationMode = .cleanup
|
||||
}
|
||||
|
||||
guard await waitForSystemProxyOperation() else { return systemProxyState }
|
||||
beginSystemProxyOperation()
|
||||
var outcome: SystemProxyApplicationFacts = .idle
|
||||
repeat {
|
||||
systemProxyCleanupRescanRequested = false
|
||||
outcome = await clearSystemProxyOutcome()
|
||||
// Let queued SystemConfiguration notifications reach the main actor. Any
|
||||
// location/service/proxy change during this cleanup starts another full scan.
|
||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||
await Task.yield()
|
||||
} while systemProxyCleanupRescanRequested && !outcome.hasOperationFailure
|
||||
|
||||
systemProxyNetworkChangeMonitor.stop()
|
||||
systemProxyHealthTask?.cancel()
|
||||
systemProxyHealthTask = nil
|
||||
systemProxyObservationMode = .stopped
|
||||
systemProxyInspection = SystemProxyInspectionFacts()
|
||||
endSystemProxyOperation()
|
||||
return outcome
|
||||
}
|
||||
|
||||
private func handleSystemProxyNetworkChange(_ change: SystemProxyNetworkChange) {
|
||||
switch systemProxyObservationMode {
|
||||
case .stopped:
|
||||
return
|
||||
case .cleanup:
|
||||
if !change.isDisjoint(with: [.networkConfiguration, .proxyConfiguration]) {
|
||||
systemProxyCleanupRescanRequested = true
|
||||
}
|
||||
case .enabled:
|
||||
guard isIntentEnabled() else { return }
|
||||
guard !systemProxyInspectionScheduled else { return }
|
||||
systemProxyInspectionScheduled = true
|
||||
Task { @MainActor [weak self] in
|
||||
await Task.yield()
|
||||
guard let self else { return }
|
||||
self.systemProxyInspectionScheduled = false
|
||||
await self.recheckSystemProxy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - 失败事实映射
|
||||
|
||||
extension SystemProxyObserver {
|
||||
private func systemProxyFacts(for error: Error) -> SystemProxyFailureFacts {
|
||||
if let error = error as? SystemProxyError {
|
||||
return Self.systemProxyFacts(for: error)
|
||||
}
|
||||
if let error = error as? ProxyModeError {
|
||||
return .mode(error)
|
||||
}
|
||||
return .unknown
|
||||
}
|
||||
|
||||
// 显式映射保持与错误族一一对应;错误带关联值,无法用字典键穷举。
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
private static func systemProxyFacts(
|
||||
for error: SystemProxyError
|
||||
) -> SystemProxyFailureFacts {
|
||||
switch error {
|
||||
case .preferencesUnavailable: return .operation(.preferencesUnavailable)
|
||||
case .preferencesBusy: return .operation(.preferencesBusy)
|
||||
case .noCurrentNetworkSet: return .operation(.noCurrentNetworkSet)
|
||||
case .noProxyServices: return .operation(.noProxyServices)
|
||||
case .unreadableService: return .operation(.unreadableService)
|
||||
case .invalidStoredConfiguration: return .operation(.invalidStoredConfiguration)
|
||||
case .cannotWriteService: return .operation(.cannotWriteService)
|
||||
case .commitFailed: return .operation(.commitFailed)
|
||||
case .applyFailed: return .operation(.applyFailed)
|
||||
case .noNetworkLocations: return .operation(.noNetworkLocations)
|
||||
case .invalidRequest: return .operation(.invalidRequest)
|
||||
case .helperUnavailable: return .operation(.helperUnavailable)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,7 +295,7 @@ extension ProxyRuntimeControllerTests {
|
||||
|
||||
// 批准路径:打开登录项设置并重试收敛;批准达成后 applied。
|
||||
systemProxyHelper.setStatus(.approved)
|
||||
await controller.openSystemProxyHelperApproval()
|
||||
await controller.systemProxyObserver.openSystemProxyHelperApproval()
|
||||
|
||||
XCTAssertEqual(controller.systemProxyState, .applied)
|
||||
XCTAssertFalse(controller.systemProxyApprovalRequired)
|
||||
@@ -517,14 +517,14 @@ extension ProxyRuntimeControllerTests {
|
||||
let unregisterCountAfterEnable = systemProxyHelper.unregisterCount
|
||||
|
||||
// 指纹一致:后续收敛不注销重注。
|
||||
await controller.convergeSystemProxy()
|
||||
await controller.systemProxyObserver.convergeSystemProxy()
|
||||
XCTAssertEqual(systemProxyHelper.registerCount, registerCountAfterEnable, "指纹一致不重注")
|
||||
XCTAssertEqual(systemProxyHelper.unregisterCount, unregisterCountAfterEnable)
|
||||
XCTAssertEqual(controller.systemProxyState, .applied)
|
||||
|
||||
// 指纹漂移(模拟 app 更新改了清单):注销重注一次并更新指纹。
|
||||
defaults.set("drifted-stamp", forKey: stampKey)
|
||||
await controller.convergeSystemProxy()
|
||||
await controller.systemProxyObserver.convergeSystemProxy()
|
||||
XCTAssertEqual(systemProxyHelper.unregisterCount, unregisterCountAfterEnable + 1, "漂移触发重注")
|
||||
XCTAssertEqual(systemProxyHelper.registerCount, registerCountAfterEnable + 1)
|
||||
XCTAssertEqual(defaults.string(forKey: stampKey), stampAfterRegister, "重注后记录当前清单指纹")
|
||||
|
||||
Reference in New Issue
Block a user