Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/Application/ProxyRuntime/ProxyRuntimeController+Convergence.swift
T
Qiu Yuzhou fb428f12bc refactor(app): 按分层重组 App/ 源码目录
App/ 根目录原有 73 个散落源文件,已无法从目录读出任何分组信息。

按「目录即分层」重组为 Composition / Application / Presentation / UI /
PlatformEffects / Resources 六个目录,UI 层再按子视图分目录(Workspace、
Home、Servers(含 Plugin)、Subscriptions、Rules、Settings、Diagnostics、
StatusMenu)。纯移动:类型名、模块名与 import 均未改动,App/ 根层不再有
散落文件。

配套改动:

- project.yml:Core 测试镜像的 excludes 由裸文件名改为 `**/` 递归形式。
  XcodeGen 的 exclude 不跨 `/`,裸名只匹配 App 根层;MainApp.swift 下移后
  旧写法会静默失效,把 @main 组合根编进测试镜像。info.path 与
  entitlements.path 指向 App/Resources/;并排除 `**/*.md`——App/ 下任何
  非源码文件都会被 XcodeGen 收进 Resources 阶段并封进签名后的 bundle。
- SkeletonTests 的架构守卫由「默认扫描全部 App/ + 7 个豁免文件 + 1 个豁免
  前缀 + 4 个豁免目录」改为正面圈定 App/UI/,豁免登记全部删除;新增运行时
  适配器放进 Application/ 即自动落在扫描范围外,无需再登记。
- README 工程结构说明同步;根 .gitignore 的生成物路径随 Resources/ 归位。

验证:clean build 0 error;task test 1022 用例全通过(含两个架构守卫);
task format 无改动(证明纯移动);task lint 与移动前同为 21 条内容型
warning;packaging-gate.sh ALL CHECKS PASSED,确认 plist/entitlements 归位
后签名与 entitlements 语义不变。

注意:迁移时须先删旧的生成物再 xcodegen generate,否则遗留的 App/Info.plist
会被当作普通资源扫进 Resources 阶段。
2026-10-05 19:56:18 +08:00

383 lines
16 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
/// 代际之后捕获。
private struct ConvergenceTicket: Equatable {
/// 并发流代际;每次 execute 之后由调用方以实际占用值推进。
var flow: Int
/// 模式切换代际;捕获后不再变化。
let mode: Int
/// 运行时文档派生代际;每次派生之后由调用方以当前值推进。
var preparation: Int
}
extension ProxyRuntimeController {
/// 以当前代际捕获收敛票据。
private func convergenceTicket() -> ConvergenceTicket {
ConvergenceTicket(
flow: flowGeneration, mode: modeChangeGeneration,
preparation: runtimePreparationGeneration)
}
/// 票据是否仍代表当前收敛:并发流 / 模式切换 / 派生三个代际均未前进,
/// 且会话内 agent 意图仍开启。
private func convergenceIsCurrent(_ ticket: ConvergenceTicket) -> Bool {
ticket.flow == flowGeneration && ticket.mode == modeChangeGeneration
&& ticket.preparation == runtimePreparationGeneration
&& settings.agentEnabled
}
/// 派生文档对当前运行时是否无事可做(GLOSSARY.md「有效值未变的提交不做
/// 运行时收敛」):磁盘契约与派生字节逐位相同、agent 已注册且 wrapper
/// 存活、控制器处于健康运行态——计划层动作序列为空即幂等跳过。判定在
/// 置 `starting` 之前进行,跳过路径不闪状态、不重走健康门;磁盘漂移或
/// wrapper 失踪时动作序列自然非空,走完整 deploy 自愈。
private 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()
}
}
/// 一个意图入口隐藏派生、执行政策、恢复和失败呈现;调用者不组合执行选项。
enum RuntimeConvergenceIntent {
case deployment(SslocalRuntimeDocument)
case modeTransition(ModeTransitionSnapshot)
case savedRules
}
/// 用户改变模式前的事实,由意图命令捕获;恢复计划只在收敛实现中构造。
struct ModeTransitionSnapshot {
let settings: ProxySettings
let mode: ProxyMode
let document: SslocalRuntimeDocument?
let state: AgentRunState
}
enum ConvergenceResult {
case applied
case unchanged
/// 派生或部署失败;该意图要求的恢复与失败呈现已经处理。
case failed
/// 被新意图取代,不得恢复或发布旧结果。
case superseded
}
@discardableResult
func convergeRuntime(_ intent: RuntimeConvergenceIntent) async -> ConvergenceResult {
switch intent {
case .deployment(let source): return await convergeDeployment(source)
case .modeTransition(let snapshot): return await convergeModeTransition(snapshot)
case .savedRules: return await convergeSavedRules()
}
}
private func convergeDeployment(_ source: SslocalRuntimeDocument) async -> ConvergenceResult {
if listenSettingsUnreadable || settingsUnreadable {
RuntimeLog.emit(.activationFailed(reason: "listen settings unreadable"))
_ = await execute(.stop, document: nil)
lastDocument = nil
skippedServers = []
state = .launchFailed(.unreadableSettings)
await systemProxyObserver.holdSystemProxyIntent()
return .failed
}
let contract: PreparedRuntimeContract
do {
contract = try PreparedRuntimeContract(await runtimeDocument(source, for: proxyMode))
} catch RulePreparationError.superseded {
return .superseded
} catch {
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
state = .serviceFailed(.runtimeFile)
return .failed
}
var ticket = convergenceTicket()
return await apply(contract.document, ticket: &ticket, policy: .deployment)
}
private func convergeModeTransition(_ snapshot: ModeTransitionSnapshot) async -> ConvergenceResult
{
var ticket = convergenceTicket()
guard settings.agentEnabled, state != .off,
let current = lastDocument ?? runtimeFileStore.loadDocument()
else {
systemProxyObserver.modeTransitionAwaitingRuntime()
return .unchanged
}
let plan = RollbackPlan(
document: snapshot.document ?? current, state: snapshot.state,
payload: .modeTransition(
mode: snapshot.mode, ruleDefaultAction: snapshot.settings.ruleDefaultAction),
systemProxyIntentAtCapture: snapshot.settings.systemProxyEnabled,
convergeProxyOnIntentChange: true)
let next: SslocalRuntimeDocument
do {
next = try await runtimeDocument(current, for: proxyMode)
} catch RulePreparationError.superseded {
return .superseded
} catch {
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
ticket.preparation = runtimePreparationGeneration
await recoverModeTransition(plan, ticket: &ticket)
return .failed
}
// 派生会推进 preparation;仅该意图在派生前捕获了收敛票据。
ticket.preparation = runtimePreparationGeneration
guard next.aclRuntime != current.aclRuntime else {
// 保留原有特殊路径:无系统代理意图即结束;否则只检查健康,不部署或恢复。
guard settings.systemProxyEnabled else { return .unchanged }
state = .starting
return await presentLaunchHealth(next) ? .applied : .failed
}
let result = await apply(next, ticket: &ticket, policy: .modeTransition)
if case .failed = result { await recoverModeTransition(plan, ticket: &ticket) }
return result
}
private func recoverModeTransition(_ plan: RollbackPlan, ticket: inout ConvergenceTicket) async {
let report = await restore(plan, ticket: &ticket)
if report.runtimeHealthy == true, report.payloadFailureDescription != nil {
state = .serviceFailed(.persistence)
}
}
private func convergeSavedRules() async -> ConvergenceResult {
guard proxyMode == .rule, settings.agentEnabled, state != .off,
let current = lastDocument ?? runtimeFileStore.loadDocument()
else { return .unchanged }
let revision = ruleDocuments.revision
let plan = RollbackPlan(
document: current, state: state, payload: .runtimeOnly,
systemProxyIntentAtCapture: settings.systemProxyEnabled,
convergeProxyOnIntentChange: false)
let next: SslocalRuntimeDocument
do {
next = try await runtimeDocument(current, for: proxyMode)
} catch RulePreparationError.superseded {
return .superseded
} catch {
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
ruleApplicationFailure = .service(.runtimeFile)
return .failed
}
guard next.aclRuntime != current.aclRuntime || ruleApplicationFailure != nil else {
ruleApplicationFailure = nil
return .unchanged
}
modeChangeGeneration += 1
var ticket = convergenceTicket()
let result = await apply(next, ticket: &ticket, policy: .savedRules)
switch result {
case .applied, .unchanged:
if revision == ruleDocuments.revision { ruleApplicationFailure = nil }
case .superseded:
break
case .failed:
let failure = ProxyRuntimeFacts(state: state).failure ?? .service(.runtimeFile)
_ = await restore(plan, ticket: &ticket)
if convergenceIsCurrent(ticket), revision == ruleDocuments.revision {
ruleApplicationFailure = failure
}
}
return result
}
/// 三种意图的执行差异只在 module 内选择,不能由调用者拼成任意组合。
private enum DeploymentPolicy {
case deployment
case modeTransition
case savedRules
}
/// 共用执行脊柱;恢复在同一 module 的意图实现中,票据不越过意图 seam。
private func apply(
_ document: SslocalRuntimeDocument,
ticket: inout ConvergenceTicket,
policy: DeploymentPolicy
) 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: policy != .deployment || document.aclRuntime != nil,
convergeProxyOnSuccess: policy == .deployment,
preserveProxyOnFailure: policy != .deployment, preparedContract: contract)
guard convergenceIsCurrent(ticket) else { return .superseded }
guard healthy else { return .failed }
lastDocument = document
ruleApplicationFailure = nil
switch policy {
case .modeTransition: await systemProxyObserver.convergeSystemProxy(forceApply: true)
case .savedRules: await systemProxyObserver.convergeSystemProxy()
case .deployment: break
}
return .applied
}
/// 按回滚计划整体恢复:先查时效(票据含会话内 agent 意图)再动任何事实——
/// 被取代或 agent 已被用户关闭的回滚不得把旧载荷写回持久层、也不得把
/// 旧文档重新拉起;随后重持久化载荷 → 计划执行拉回旧文档(含按需拉起,
/// 由计划层对注销/信号/注册的裁决保证与旧 restore 例程等价)→ 启动健康
/// 门。载荷持久化失败不阻断文档恢复;文档恢复失败或健康不过即撤下系统
/// 代理意图。
private 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)
}
}
/// 收敛失败时的整体回滚计划:恢复到哪个运行时文档、健康通过后呈现哪个
/// 状态、以及除文档外还要重持久化的载荷。
private 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
}
/// 回滚报告:载荷重持久化与旧运行时恢复的结果。
private struct RestoreReport {
/// 载荷持久化失败的点名描述;nil = 成功。
let payloadFailureDescription: String?
/// 旧运行时是否恢复到健康;nil = 恢复未尝试(被弃权或被取代)。
let runtimeHealthy: Bool?
}