mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-15 02:37:32 +00:00
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 测试通过。
266 lines
12 KiB
Swift
266 lines
12 KiB
Swift
import Foundation
|
|
|
|
/// 一次计划执行的结果:动作是否全部按序成功,以及本次执行占用的并发流
|
|
/// 代际。占用代际供收敛票据推进 flow 分量,取代调用方对 execute 内部
|
|
/// 「恰好自增一次」时序的依赖(原 `flowGeneration + 1` 手算)。
|
|
struct RuntimeExecutionOutcome {
|
|
/// 本次执行占用的 `flowGeneration`。
|
|
let flow: Int
|
|
/// 全部计划动作是否按序成功。
|
|
let succeeded: Bool
|
|
}
|
|
|
|
/// 收敛票据:运行时收敛操作的代际快照。入口捕获三个并发代际,之后每步
|
|
/// 推进用它判定「从入口到现在,并发流 / 模式切换 / 运行时派生是否前进了,
|
|
/// 会话内 agent 意图是否仍开启」;被更新提交或新意图取代的旧收敛不得再
|
|
/// 变更运行时状态或写契约。
|
|
///
|
|
/// 四项失效事实由票据恒全查,不随调用路径增减——「什么会使一次收敛失效」
|
|
/// 是票据的固有语义(逐路径手选子集曾让 agent 关闭在 mode 切换的健康门
|
|
/// await 窗口内被误分类为失败并触发复活性的 restore)。各路径只负责捕获
|
|
/// 与推进时机:flow 分量在 execute 后以实际占用值推进,mode 切换路径在
|
|
/// 派生后推进 preparation 分量(派生自身会推进它),rules 部署在推进 mode
|
|
/// 代际之后捕获。
|
|
struct ConvergenceTicket: Equatable {
|
|
/// 并发流代际;每次 execute 之后由调用方以实际占用值推进。
|
|
var flow: Int
|
|
/// 模式切换代际;捕获后不再变化。
|
|
let mode: Int
|
|
/// 运行时文档派生代际;每次派生之后由调用方以当前值推进。
|
|
var preparation: Int
|
|
}
|
|
|
|
extension ProxyRuntimeController {
|
|
/// 以当前代际捕获收敛票据。
|
|
func convergenceTicket() -> ConvergenceTicket {
|
|
ConvergenceTicket(
|
|
flow: flowGeneration, mode: modeChangeGeneration,
|
|
preparation: runtimePreparationGeneration)
|
|
}
|
|
|
|
/// 票据是否仍代表当前收敛:并发流 / 模式切换 / 派生三个代际均未前进,
|
|
/// 且会话内 agent 意图仍开启。
|
|
func convergenceIsCurrent(_ ticket: ConvergenceTicket) -> Bool {
|
|
ticket.flow == flowGeneration && ticket.mode == modeChangeGeneration
|
|
&& ticket.preparation == runtimePreparationGeneration
|
|
&& settings.agentEnabled
|
|
}
|
|
|
|
/// 派生文档对当前运行时是否无事可做(CONTEXT.md「有效值未变的提交不做
|
|
/// 运行时收敛」):磁盘契约与派生字节逐位相同、agent 已注册且 wrapper
|
|
/// 存活、控制器处于健康运行态——计划层动作序列为空即幂等跳过。判定在
|
|
/// 置 `starting` 之前进行,跳过路径不闪状态、不重走健康门;磁盘漂移或
|
|
/// wrapper 失踪时动作序列自然非空,走完整 deploy 自愈。
|
|
func convergencePlanIsEmpty(
|
|
_ document: SslocalRuntimeDocument, preparedContract: PreparedRuntimeContract?
|
|
) -> Bool {
|
|
switch state {
|
|
case .running, .firewallBlocked:
|
|
break
|
|
case .off, .starting, .launchFailed, .requiresApproval, .serviceFailed:
|
|
return false
|
|
}
|
|
let actions = ProxyRuntimePlan.actions(
|
|
intent: .run(document),
|
|
agentStatus: agent.status,
|
|
wrapper: wrapperState(),
|
|
contractOnDisk: runtimeFileStore.readData(), preparedContract: preparedContract)
|
|
return actions.isEmpty
|
|
}
|
|
|
|
/// 统一的设置持久化 epilogue:保存失败不静默偏离持久化事实——记录日志、
|
|
/// 呈现 `serviceFailed(.persistence)`,调用方放弃本次收敛。
|
|
func persistSettings(_ proposed: ProxySettings) -> Bool {
|
|
do {
|
|
try settingsStore.save(proposed)
|
|
return true
|
|
} catch {
|
|
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
|
|
state = .serviceFailed(.persistence)
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// reexpand 三-case 开关的收敛主体(显式收敛、设置保存、启动重同步共用):
|
|
/// deployed → 部署派生文档;目标失效 → 清除并按意图收敛;无目标 → 空监听。
|
|
/// 幂等跳过由 apply 层的 plan 空判定承担;目录重校验(跳过名单、目标失效
|
|
/// 清除)已在 `reexpand` 完成,不受其影响。
|
|
func convergeToReexpanded(_ catalog: ConfigurationCatalog) async {
|
|
switch reexpand(in: catalog) {
|
|
case .deployed(let configuration):
|
|
await deploy(configuration.document)
|
|
case .clearedAndStopped(let failure):
|
|
await handleCleared(failure)
|
|
case nil:
|
|
await deployListeningWithoutTarget()
|
|
}
|
|
}
|
|
|
|
/// 收敛脊柱 apply 层的结果。
|
|
enum ConvergenceResult {
|
|
/// 部署已执行且启动健康通过。
|
|
case applied
|
|
/// 派生契约与运行中运行时逐字节相同:不写契约、不发信号、不闪
|
|
/// starting、不重走健康门与系统代理收敛(幂等跳过)。
|
|
case unchanged
|
|
/// 执行或健康门失败;回滚由调用方按 RollbackPlan 处理。
|
|
case failed
|
|
/// 被更新的收敛取代:不得再变更任何状态。
|
|
case superseded
|
|
}
|
|
|
|
/// 成功收敛后的系统代理收尾方式。
|
|
enum ProxyConvergenceTail {
|
|
/// 强制重收敛(mode 切换:系统代理值随模式 ACL 变化)。
|
|
case forceApply
|
|
/// 常规收敛(rules 提交)。
|
|
case converge
|
|
/// 无需收尾(deploy 路径由健康门自身的 convergeProxyOnSuccess 收敛)。
|
|
case none
|
|
}
|
|
|
|
/// 收敛脊柱 apply 层:把一个已派生的运行时定义推到运行时——幂等跳过、
|
|
/// 计划执行、启动健康门、成功后按参数收敛系统代理。失败不在此回滚:
|
|
/// 返回 `.failed` 由调用方按 RollbackPlan 整体恢复。
|
|
///
|
|
/// 票据 inout 传参:execute / 派生后的代际推进对调用方可见,调用方
|
|
/// 在 apply 之后的时效检查使用的是推进过的票据。
|
|
///
|
|
/// `starting` 在 execute 之前置位:`.run` 计划的动作全部同步执行,二者
|
|
/// 之间不存在可见的挂起点,与旧路径「deploy 后置位」不可区分。
|
|
func apply(
|
|
_ document: SslocalRuntimeDocument,
|
|
ticket: inout ConvergenceTicket,
|
|
requiresReceipt: Bool,
|
|
convergeProxyOnSuccess: Bool,
|
|
preserveProxyOnFailure: Bool,
|
|
proxyTail: ProxyConvergenceTail,
|
|
rollback: RollbackPlan?
|
|
) async -> ConvergenceResult {
|
|
let contract = try? PreparedRuntimeContract(document)
|
|
if convergencePlanIsEmpty(document, preparedContract: contract) {
|
|
RuntimeLog.emit(.contractUnchanged)
|
|
ruleApplicationFailure = nil
|
|
return .unchanged
|
|
}
|
|
guard convergenceIsCurrent(ticket) else { return .superseded }
|
|
lastDocument = document
|
|
state = .starting
|
|
let execution = await execute(.run(document), document: document, preparedContract: contract)
|
|
ticket.flow = execution.flow
|
|
guard convergenceIsCurrent(ticket) else { return .superseded }
|
|
guard execution.succeeded else { return .failed }
|
|
let healthy = await presentLaunchHealth(
|
|
document,
|
|
requiresReceipt: requiresReceipt,
|
|
convergeProxyOnSuccess: convergeProxyOnSuccess,
|
|
preserveProxyOnFailure: preserveProxyOnFailure, preparedContract: contract)
|
|
guard convergenceIsCurrent(ticket) else { return .superseded }
|
|
guard healthy else { return .failed }
|
|
lastDocument = document
|
|
ruleApplicationFailure = nil
|
|
switch proxyTail {
|
|
case .forceApply: await systemProxyObserver.convergeSystemProxy(forceApply: true)
|
|
case .converge: await systemProxyObserver.convergeSystemProxy()
|
|
case .none: break
|
|
}
|
|
return .applied
|
|
}
|
|
|
|
/// 按回滚计划整体恢复:先查时效(票据含会话内 agent 意图)再动任何事实——
|
|
/// 被取代或 agent 已被用户关闭的回滚不得把旧载荷写回持久层、也不得把
|
|
/// 旧文档重新拉起;随后重持久化载荷 → 计划执行拉回旧文档(含按需拉起,
|
|
/// 由计划层对注销/信号/注册的裁决保证与旧 restore 例程等价)→ 启动健康
|
|
/// 门。载荷持久化失败不阻断文档恢复;文档恢复失败或健康不过即撤下系统
|
|
/// 代理意图。
|
|
func restore(_ plan: RollbackPlan, ticket: inout ConvergenceTicket) async -> RestoreReport {
|
|
guard convergenceIsCurrent(ticket) else {
|
|
return RestoreReport(payloadFailureDescription: nil, runtimeHealthy: nil)
|
|
}
|
|
var payloadFailureDescription: String?
|
|
var restoredSettingsForMemory: ProxySettings?
|
|
var restoredMode: ProxyMode?
|
|
switch plan.payload {
|
|
case .modeTransition(let mode, let ruleDefaultAction):
|
|
// 按快照还原偏好(模式与子选项回退,其余字段保留当前值)。
|
|
var restored = settings
|
|
restored.preferredMode = mode.kind
|
|
restored.ruleDefaultAction = ruleDefaultAction
|
|
do {
|
|
try settingsStore.save(restored)
|
|
} catch {
|
|
payloadFailureDescription = String(describing: error)
|
|
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
|
|
}
|
|
restoredSettingsForMemory = restored
|
|
restoredMode = mode
|
|
case .runtimeOnly:
|
|
break
|
|
}
|
|
func report(_ healthy: Bool?) -> RestoreReport {
|
|
RestoreReport(
|
|
payloadFailureDescription: payloadFailureDescription, runtimeHealthy: healthy)
|
|
}
|
|
if let restoredSettingsForMemory { settings = restoredSettingsForMemory }
|
|
if let restoredMode { proxyMode = restoredMode }
|
|
lastDocument = plan.document
|
|
state = .starting
|
|
let contract = try? PreparedRuntimeContract(plan.document)
|
|
let execution = await execute(
|
|
.run(plan.document), document: plan.document, preparedContract: contract)
|
|
ticket.flow = execution.flow
|
|
guard convergenceIsCurrent(ticket) else { return report(nil) }
|
|
guard execution.succeeded else {
|
|
await systemProxyObserver.holdSystemProxyIntent()
|
|
return report(false)
|
|
}
|
|
let healthy = await presentLaunchHealth(
|
|
plan.document, requiresReceipt: true, convergeProxyOnSuccess: false,
|
|
preparedContract: contract)
|
|
guard convergenceIsCurrent(ticket) else { return report(nil) }
|
|
if healthy {
|
|
if plan.convergeProxyOnIntentChange,
|
|
settings.systemProxyEnabled != plan.systemProxyIntentAtCapture
|
|
{
|
|
await systemProxyObserver.convergeSystemProxy(forceApply: true)
|
|
} else {
|
|
state = plan.state
|
|
}
|
|
} else {
|
|
await systemProxyObserver.holdSystemProxyIntent()
|
|
}
|
|
return report(healthy)
|
|
}
|
|
}
|
|
|
|
/// 收敛失败时的整体回滚计划:恢复到哪个运行时文档、健康通过后呈现哪个
|
|
/// 状态、以及除文档外还要重持久化的载荷。
|
|
struct RollbackPlan {
|
|
/// 回滚载荷:除运行时文档外还要恢复的持久化事实。
|
|
enum Payload {
|
|
/// mode 切换:设置快照的模式与规则子选项回退(其余字段保留当前值),
|
|
/// 会话内 proxyMode 一并还原。
|
|
case modeTransition(mode: ProxyMode, ruleDefaultAction: RuleDefaultAction)
|
|
/// Rule application restores only the previous runtime, never saved intent.
|
|
case runtimeOnly
|
|
}
|
|
|
|
/// 回滚目标文档(调用方已用当次加载值补齐)。
|
|
let document: SslocalRuntimeDocument
|
|
/// 健康通过后呈现的状态(收敛开始前捕获)。
|
|
let state: ProxyRuntimeController.AgentRunState
|
|
let payload: Payload
|
|
/// 捕获时的系统代理意图:恢复健康后若当前意图已变化,强制重收敛一次。
|
|
let systemProxyIntentAtCapture: Bool
|
|
/// 是否在意图变化时强制重收敛(mode 切换路径;rules 路径为 false)。
|
|
let convergeProxyOnIntentChange: Bool
|
|
}
|
|
|
|
/// 回滚报告:载荷重持久化与旧运行时恢复的结果。
|
|
struct RestoreReport {
|
|
/// 载荷持久化失败的点名描述;nil = 成功。
|
|
let payloadFailureDescription: String?
|
|
/// 旧运行时是否恢复到健康;nil = 恢复未尝试(被弃权或被取代)。
|
|
let runtimeHealthy: Bool?
|
|
}
|