From dc35e70798be8627bac34b2615c5409cbfe5e7a7 Mon Sep 17 00:00:00 2001 From: Qiu Yuzhou Date: Sat, 3 Oct 2026 10:30:47 +0800 Subject: [PATCH] refactor(runtime): capture convergence tickets instead of hand-checked generations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit execute 现在返回 RuntimeExecutionOutcome(成功与否 + 本次占用的并发流 代际),mode 与 rules 路径的 24 处手写代际 guard 收拢为 ConvergenceTicket 的 convergenceIsCurrent 检查;两处 flowGeneration + 1 手算随之消失。 行为逐字节保持:mode 路径的 mode 代际仍在派生前捕获、preparation 分量 在派生后推进;rules 路径四个代际事实全查不变。 --- .../App/ProxyRuntimeController+Commands.swift | 28 ++++++--- .../ProxyRuntimeController+Convergence.swift | 63 +++++++++++++++++++ .../ProxyRuntimeController+CustomRules.swift | 58 +++++++---------- .../App/ProxyRuntimeController+Mode.swift | 56 ++++++++--------- .../Tests/RuleEnablementControllerTests.swift | 2 +- 5 files changed, 133 insertions(+), 74 deletions(-) create mode 100644 ShadowsocksX-NG2/App/ProxyRuntimeController+Convergence.swift diff --git a/ShadowsocksX-NG2/App/ProxyRuntimeController+Commands.swift b/ShadowsocksX-NG2/App/ProxyRuntimeController+Commands.swift index 172eb78a..6337177b 100644 --- a/ShadowsocksX-NG2/App/ProxyRuntimeController+Commands.swift +++ b/ShadowsocksX-NG2/App/ProxyRuntimeController+Commands.swift @@ -228,9 +228,8 @@ extension ProxyRuntimeController { guard preparation == runtimePreparationGeneration else { return false } let document = contract.document lastDocument = document - guard await execute(.run(document), document: document, preparedContract: contract) else { - return false - } + let execution = await execute(.run(document), document: document, preparedContract: contract) + guard execution.succeeded else { return false } guard preparation == runtimePreparationGeneration else { return false } state = .starting return await presentLaunchHealth( @@ -276,15 +275,26 @@ extension ProxyRuntimeController { // MARK: - 计划动作执行 extension ProxyRuntimeController { - /// 按计划顺序执行动作;返回 false 表示中途失败、状态已呈现(后续动作与 - /// 健康探测都不应继续)。 + /// 把「运行定义档」意图推到运行时:计划动作 → 顺序执行。返回全部动作 + /// 是否成功与本次占用的并发流代际。 func execute( _ intent: RuntimeIntent, document: SslocalRuntimeDocument?, preparedContract: PreparedRuntimeContract? = nil - ) async -> Bool { + ) async -> RuntimeExecutionOutcome { cancelFirewallObservation() flowGeneration += 1 - let generation = flowGeneration + let flow = flowGeneration + let succeeded = await performPlannedActions( + intent, document: document, preparedContract: preparedContract, flow: flow) + return RuntimeExecutionOutcome(flow: flow, succeeded: succeeded) + } + + /// 按计划顺序执行动作;返回 false 表示中途失败、状态已呈现(后续动作与 + /// 健康探测都不应继续)。 + private func performPlannedActions( + _ intent: RuntimeIntent, document: SslocalRuntimeDocument?, + preparedContract: PreparedRuntimeContract?, flow: Int + ) async -> Bool { let preparation = runtimePreparationGeneration let contract: PreparedRuntimeContract? if case .run(let desired) = intent { @@ -307,11 +317,11 @@ extension ProxyRuntimeController { RuntimeLog.emit(.contractUnchanged) } for action in actions { - guard generation == flowGeneration, preparation == runtimePreparationGeneration, + guard flow == flowGeneration, preparation == runtimePreparationGeneration, await perform(action, preparedContract: contract) else { return false } } - return generation == flowGeneration && preparation == runtimePreparationGeneration + return flow == flowGeneration && preparation == runtimePreparationGeneration } /// 执行单个动作;返回 false 表示应终止后续动作(状态已呈现)。 diff --git a/ShadowsocksX-NG2/App/ProxyRuntimeController+Convergence.swift b/ShadowsocksX-NG2/App/ProxyRuntimeController+Convergence.swift new file mode 100644 index 00000000..d78b59c6 --- /dev/null +++ b/ShadowsocksX-NG2/App/ProxyRuntimeController+Convergence.swift @@ -0,0 +1,63 @@ +import Foundation + +/// 一次计划执行的结果:动作是否全部按序成功,以及本次执行占用的并发流 +/// 代际。占用代际供收敛票据推进 flow 分量,取代调用方对 execute 内部 +/// 「恰好自增一次」时序的依赖(原 `flowGeneration + 1` 手算)。 +struct RuntimeExecutionOutcome { + /// 本次执行占用的 `flowGeneration`。 + let flow: Int + /// 全部计划动作是否按序成功。 + let succeeded: Bool +} + +/// 收敛票据:运行时收敛操作的代际快照。入口捕获三个并发代际,之后每步 +/// 推进用它判定「从入口到现在,并发流 / 模式切换 / 运行时派生是否前进了」; +/// 被更新提交或新意图取代的旧收敛不得再变更运行时状态或写契约。 +/// +/// 各路径校验的代际子集不同:mode 切换的 mode 代际在派生前捕获、派生 +/// 代际在派生后推进(派生自身会推进它);rules 部署四个事实全查(含 +/// 会话内 agent 意图);目录 / 启动路径只查派生代际。 +struct ConvergenceTicket: Equatable { + /// 票据时效校验覆盖的代际事实子集。 + struct Checks: OptionSet { + let rawValue: Int + /// 并发流代际:任何后续 execute 都使票据失效。 + static let flow = Checks(rawValue: 1 << 0) + /// 模式切换代际。 + static let mode = Checks(rawValue: 1 << 1) + /// 运行时文档派生代际。 + static let preparation = Checks(rawValue: 1 << 2) + /// 会话内 agent 意图仍开启。 + static let agentEnabled = Checks(rawValue: 1 << 3) + } + + /// 并发流代际;每次 execute 之后由调用方以实际占用值推进。 + var flow: Int + /// 模式切换代际;捕获后不再变化。 + let mode: Int + /// 运行时文档派生代际;每次派生之后由调用方以当前值推进。 + var preparation: Int +} + +extension ProxyRuntimeController { + /// 以当前代际捕获收敛票据。 + func convergenceTicket() -> ConvergenceTicket { + ConvergenceTicket( + flow: flowGeneration, mode: modeChangeGeneration, + preparation: runtimePreparationGeneration) + } + + /// 票据是否仍代表当前收敛:`checking` 列出的代际事实全部未前进。 + func convergenceIsCurrent( + _ ticket: ConvergenceTicket, checking: ConvergenceTicket.Checks + ) -> Bool { + var current = true + if checking.contains(.flow) { current = current && ticket.flow == flowGeneration } + if checking.contains(.mode) { current = current && ticket.mode == modeChangeGeneration } + if checking.contains(.preparation) { + current = current && ticket.preparation == runtimePreparationGeneration + } + if checking.contains(.agentEnabled) { current = current && settings.agentEnabled } + return current + } +} diff --git a/ShadowsocksX-NG2/App/ProxyRuntimeController+CustomRules.swift b/ShadowsocksX-NG2/App/ProxyRuntimeController+CustomRules.swift index d7b168df..28b6560e 100644 --- a/ShadowsocksX-NG2/App/ProxyRuntimeController+CustomRules.swift +++ b/ShadowsocksX-NG2/App/ProxyRuntimeController+CustomRules.swift @@ -100,43 +100,37 @@ extension ProxyRuntimeController { return .runtimeUnchanged } - let preparation = runtimePreparationGeneration modeChangeGeneration += 1 - let generation = modeChangeGeneration - let runtimeGeneration = flowGeneration + 1 + var ticket = convergenceTicket() + let checks: ConvergenceTicket.Checks = [.flow, .mode, .preparation, .agentEnabled] let contract = try? PreparedRuntimeContract(nextDocument) lastDocument = nextDocument state = .starting - guard await execute(.run(nextDocument), document: nextDocument, preparedContract: contract) - else { - guard ruleDeploymentIsCurrent(preparation, generation, runtimeGeneration) - else { - return .runtimeChanged(rulesRestored: false) - } - return await restoreCustomRules(previousRules, snapshot: snapshot) - } - - guard ruleDeploymentIsCurrent(preparation, generation, runtimeGeneration) - else { + let execution = await execute( + .run(nextDocument), document: nextDocument, preparedContract: contract) + ticket.flow = execution.flow + guard convergenceIsCurrent(ticket, checking: checks) else { return .runtimeChanged(rulesRestored: false) } + guard execution.succeeded else { + return await restoreCustomRules(previousRules, snapshot: snapshot, ticket: ticket) + } + let healthy = await presentLaunchHealth( nextDocument, requiresReceipt: true, convergeProxyOnSuccess: false, preserveProxyOnFailure: true, preparedContract: contract) - guard ruleDeploymentIsCurrent(preparation, generation, runtimeGeneration) - else { + guard convergenceIsCurrent(ticket, checking: checks) else { return .runtimeChanged(rulesRestored: false) } guard healthy else { - return await restoreCustomRules(previousRules, snapshot: snapshot) + return await restoreCustomRules(previousRules, snapshot: snapshot, ticket: ticket) } lastDocument = nextDocument await convergeSystemProxy() - guard ruleDeploymentIsCurrent(preparation, generation, runtimeGeneration) - else { + guard convergenceIsCurrent(ticket, checking: checks) else { return .runtimeChanged(rulesRestored: false) } return .applied @@ -154,13 +148,6 @@ extension ProxyRuntimeController { } } - private func ruleDeploymentIsCurrent( - _ preparation: Int, _ mode: Int, _ runtime: Int - ) -> Bool { - preparation == runtimePreparationGeneration && mode == modeChangeGeneration - && runtime == flowGeneration && settings.agentEnabled - } - /// 保存前校验(issue #66 AC2):固定本地冲突与重复整批拒绝并返回可解释原因。 /// 同行动覆盖和相反行动遮蔽不阻止保存;编译时按当前骨架解释完整集合。 func validateCustomRulesForPersistence(_ rules: [CustomRule]) -> ( @@ -177,8 +164,10 @@ extension ProxyRuntimeController { private func restoreCustomRules( _ previousRules: CustomRuleDocument, - snapshot: ModeTransitionSnapshotForRules + snapshot: ModeTransitionSnapshotForRules, + ticket: ConvergenceTicket ) async -> CustomRuleUpdateOutcome { + var ticket = ticket var failures: [String] = [] do { try ruleDocuments.save(previousRules) @@ -195,19 +184,17 @@ extension ProxyRuntimeController { guard settings.agentEnabled else { return .runtimeChanged(rulesRestored: rulesRestored) } - let modeGeneration = modeChangeGeneration - let preparation = runtimePreparationGeneration - let runtimeGeneration = flowGeneration + 1 + let checks: ConvergenceTicket.Checks = [.flow, .mode, .preparation, .agentEnabled] let contract = try? PreparedRuntimeContract(previousDocument) lastDocument = previousDocument state = .starting - let executed = await execute( + let execution = await execute( .run(previousDocument), document: previousDocument, preparedContract: contract) - guard ruleDeploymentIsCurrent(preparation, modeGeneration, runtimeGeneration) - else { + ticket.flow = execution.flow + guard convergenceIsCurrent(ticket, checking: checks) else { return .runtimeChanged(rulesRestored: rulesRestored) } - guard executed else { + guard execution.succeeded else { failures.append(String(describing: state)) await holdSystemProxyIntent() return .recoveryFailed(detail: failures.joined(separator: "; "), rulesRestored: rulesRestored) @@ -215,8 +202,7 @@ extension ProxyRuntimeController { let healthy = await presentLaunchHealth( previousDocument, requiresReceipt: true, convergeProxyOnSuccess: false, preparedContract: contract) - guard ruleDeploymentIsCurrent(preparation, modeGeneration, runtimeGeneration) - else { + guard convergenceIsCurrent(ticket, checking: checks) else { return .runtimeChanged(rulesRestored: rulesRestored) } if !healthy { diff --git a/ShadowsocksX-NG2/App/ProxyRuntimeController+Mode.swift b/ShadowsocksX-NG2/App/ProxyRuntimeController+Mode.swift index ab1d9710..b1388f65 100644 --- a/ShadowsocksX-NG2/App/ProxyRuntimeController+Mode.swift +++ b/ShadowsocksX-NG2/App/ProxyRuntimeController+Mode.swift @@ -51,7 +51,7 @@ extension ProxyRuntimeController { settings = next proxyMode = mode modeChangeGeneration += 1 - let generation = modeChangeGeneration + var ticket = convergenceTicket() guard settings.agentEnabled, state != .off, let currentDocument = lastDocument ?? runtimeFileStore.loadDocument() @@ -68,10 +68,13 @@ extension ProxyRuntimeController { } catch { // 快照缺失/损坏:不静默退化,恢复旧模式与旧子选项。 RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error))) + ticket.preparation = runtimePreparationGeneration await restoreModeTransition( - snapshot: snapshot.resolvingDocument(currentDocument), generation: generation) + snapshot: snapshot.resolvingDocument(currentDocument), ticket: ticket) return } + // 派生自身推进了派生代际:票据以派生后的当前值推进 preparation 分量。 + ticket.preparation = runtimePreparationGeneration guard nextDocument.aclRuntime != currentDocument.aclRuntime else { guard settings.systemProxyEnabled else { return } state = .starting @@ -80,8 +83,7 @@ extension ProxyRuntimeController { } await deployModeTransition( - nextDocument, snapshot: snapshot.resolvingDocument(currentDocument), - generation: generation, preparation: runtimePreparationGeneration) + nextDocument, snapshot: snapshot.resolvingDocument(currentDocument), ticket: ticket) } enum RulePreparationError: Error { case superseded } @@ -167,23 +169,24 @@ extension ProxyRuntimeController { private func deployModeTransition( _ document: SslocalRuntimeDocument, snapshot: ModeTransitionSnapshot, - generation: Int, preparation: Int + ticket: ConvergenceTicket ) async { - guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else { + guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else { return } let contract = try? PreparedRuntimeContract(document) lastDocument = document state = .starting - guard await execute(.run(document), document: document, preparedContract: contract) else { - guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else { + let execution = await execute(.run(document), document: document, preparedContract: contract) + guard execution.succeeded else { + guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else { return } - await restoreModeTransition(snapshot: snapshot, generation: generation) + await restoreModeTransition(snapshot: snapshot, ticket: ticket) return } - guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else { + guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else { return } let healthy = await presentLaunchHealth( @@ -191,11 +194,11 @@ extension ProxyRuntimeController { requiresReceipt: true, convergeProxyOnSuccess: false, preserveProxyOnFailure: true, preparedContract: contract) - guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else { + guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else { return } guard healthy else { - await restoreModeTransition(snapshot: snapshot, generation: generation) + await restoreModeTransition(snapshot: snapshot, ticket: ticket) return } @@ -205,9 +208,8 @@ extension ProxyRuntimeController { private func restoreModeTransition( snapshot: ModeTransitionSnapshot, - generation: Int + ticket: ConvergenceTicket ) async { - let preparation = runtimePreparationGeneration // 两个调用点都经 resolvingDocument 补齐文档;防御性解包失败即无事可做。 guard let previousDocument = snapshot.document else { return } let restoredSettings = restoredSettings(for: snapshot) @@ -218,7 +220,7 @@ extension ProxyRuntimeController { persistenceFailed = true RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error))) } - guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else { + guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else { return } settings = restoredSettings @@ -228,10 +230,10 @@ extension ProxyRuntimeController { let contract = try? PreparedRuntimeContract(previousDocument) guard await restoreRuntimeDocument( - previousDocument, generation: generation, preparedContract: contract) + previousDocument, ticket: ticket, preparedContract: contract) else { return } - guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else { + guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else { return } state = .starting @@ -239,7 +241,7 @@ extension ProxyRuntimeController { previousDocument, requiresReceipt: true, convergeProxyOnSuccess: false, preparedContract: contract) - guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else { + guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else { return } guard restored else { @@ -267,7 +269,7 @@ extension ProxyRuntimeController { /// 代理并返回 false(健康检查由调用方继续)。 private func restoreRuntimeDocument( _ previousDocument: SslocalRuntimeDocument, - generation: Int, preparedContract: PreparedRuntimeContract? + ticket: ConvergenceTicket, preparedContract: PreparedRuntimeContract? ) async -> Bool { let expectedDigest = preparedContract?.sha256 ?? previousDocument.deploymentSHA256 let currentDocument = runtimeFileStore.loadDocument() @@ -298,7 +300,7 @@ extension ProxyRuntimeController { if !previousInstanceIsRunning { guard await relaunchPreviousInstance( - wrapper: wrapper, previousDocument: previousDocument, generation: generation, + wrapper: wrapper, previousDocument: previousDocument, ticket: ticket, preparedContract: preparedContract) else { return false } } @@ -310,9 +312,8 @@ extension ProxyRuntimeController { private func relaunchPreviousInstance( wrapper: WrapperProcessState, previousDocument: SslocalRuntimeDocument, - generation: Int, preparedContract: PreparedRuntimeContract? + ticket: ConvergenceTicket, preparedContract: PreparedRuntimeContract? ) async -> Bool { - let preparation = runtimePreparationGeneration switch wrapper { case .running(let pid): guard sendSignal(pid, SIGUSR1) == 0 else { @@ -321,12 +322,11 @@ extension ProxyRuntimeController { return false } case .notRunning: - guard - await execute( - .run(previousDocument), document: previousDocument, - preparedContract: preparedContract) - else { - guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else { + let execution = await execute( + .run(previousDocument), document: previousDocument, + preparedContract: preparedContract) + guard execution.succeeded else { + guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else { return false } await holdSystemProxyIntent() diff --git a/ShadowsocksX-NG2/Tests/RuleEnablementControllerTests.swift b/ShadowsocksX-NG2/Tests/RuleEnablementControllerTests.swift index efa89edf..4ad179f4 100644 --- a/ShadowsocksX-NG2/Tests/RuleEnablementControllerTests.swift +++ b/ShadowsocksX-NG2/Tests/RuleEnablementControllerTests.swift @@ -26,7 +26,7 @@ extension ProxyRuntimeControllerTests { await fulfillment(of: [started], timeout: 3) try FileManager.default.removeItem(at: runtimeStore.pidFileURL) let completed = await oldOperation.value - XCTAssertFalse(completed) + XCTAssertFalse(completed.succeeded) XCTAssertEqual(runtimeStore.loadDocument(), previous, "Stale stop must not delete the contract") loader.release.signal() await newMode.value