mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-02 08:33:46 +00:00
refactor(runtime): route catalog sync through the convergence spine
- catalogDidCommit 的 deployed 分支体(派生 + 幂等跳过 + deployPrepared) 与 deploy() 逐语义等价,改走 deploy();runtimeAlreadyConverged 独立机制 删除,plan 空判定由 apply 层的 convergencePlanIsEmpty 统一承担。 - deploy/deployPrepared 返回 ConvergenceResult:被取代的派生按 .superseded 呈现为 .revalidated,其余路径仍由状态面映射。 - reexpand 三-case 开关收敛为 convergeToReexpanded:convergeAgent / updateSettings / resyncOnLaunch 各变一行;catalogDidCommit 保留按 case 的结构化 outcome 映射。 - 统一设置持久化 epilogue 为 persistSettings:setAgentEnabled / setSystemProxyEnabled / transitionMode 三处相同 save-fail 语义折叠 (updateSettings 的 throw 变体与级联关闭的 return-nil 变体语义不同, 保持原样)。 - SkeletonTests 运行时适配器豁免名单登记 +Convergence.swift。
This commit is contained in:
@@ -57,13 +57,7 @@ extension ProxyRuntimeController {
|
||||
}
|
||||
var next = settings
|
||||
next.agentEnabled = enabled
|
||||
do {
|
||||
try settingsStore.save(next)
|
||||
} catch {
|
||||
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
|
||||
state = .serviceFailed(.persistence)
|
||||
return
|
||||
}
|
||||
guard persistSettings(next) else { return }
|
||||
settings = next
|
||||
if enabled {
|
||||
await convergeAgent()
|
||||
@@ -103,13 +97,7 @@ extension ProxyRuntimeController {
|
||||
guard enabled != settings.systemProxyEnabled else { return }
|
||||
var next = settings
|
||||
next.systemProxyEnabled = enabled
|
||||
do {
|
||||
try settingsStore.save(next)
|
||||
} catch {
|
||||
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
|
||||
state = .serviceFailed(.persistence)
|
||||
return
|
||||
}
|
||||
guard persistSettings(next) else { return }
|
||||
settings = next
|
||||
if enabled {
|
||||
systemProxyInitialApplyPending = true
|
||||
@@ -139,14 +127,7 @@ extension ProxyRuntimeController {
|
||||
return
|
||||
}
|
||||
let catalog = catalogSnapshotReader.catalogSnapshot
|
||||
switch reexpand(in: catalog) {
|
||||
case .deployed(let configuration):
|
||||
await deploy(configuration.document)
|
||||
case .clearedAndStopped(let failure):
|
||||
await handleCleared(failure)
|
||||
case nil:
|
||||
await deployListeningWithoutTarget()
|
||||
}
|
||||
await convergeToReexpanded(catalog)
|
||||
systemProxyStartupInProgress = false
|
||||
if settings.systemProxyEnabled {
|
||||
lastDesiredSystemProxyConfiguration = desiredSystemProxyConfiguration
|
||||
@@ -162,15 +143,7 @@ extension ProxyRuntimeController {
|
||||
/// Agent 意图开启的收敛:注册态不是事实来源,意图才是——未注册也会注册,
|
||||
/// 已注册则重校验目标并部署。偏好重置后也走此路径,把运行时收敛到出厂设置。
|
||||
func convergeAgent() async {
|
||||
let catalog = catalogSnapshotReader.catalogSnapshot
|
||||
switch reexpand(in: catalog) {
|
||||
case .deployed(let configuration):
|
||||
await deploy(configuration.document)
|
||||
case .clearedAndStopped(let failure):
|
||||
await handleCleared(failure)
|
||||
case nil:
|
||||
await deployListeningWithoutTarget()
|
||||
}
|
||||
await convergeToReexpanded(catalogSnapshotReader.catalogSnapshot)
|
||||
}
|
||||
|
||||
/// Agent 意图关闭的收敛(issue #71):只停止本地监听,不做系统代理清理。
|
||||
@@ -203,38 +176,41 @@ extension ProxyRuntimeController {
|
||||
await deploy(document)
|
||||
}
|
||||
|
||||
/// 把「运行定义档」意图推到运行时:计划动作 → 顺序执行 → 端点健康呈现。
|
||||
/// 把「运行定义档」意图推到运行时:派生文档 → 收敛脊柱 apply(幂等跳过、
|
||||
/// 计划执行、健康门)。
|
||||
@discardableResult
|
||||
func deploy(_ sourceDocument: SslocalRuntimeDocument) async -> Bool {
|
||||
func deploy(_ sourceDocument: SslocalRuntimeDocument) async -> ConvergenceResult {
|
||||
if listenSettingsUnreadable || settingsUnreadable {
|
||||
await refuseDeployForUnreadableListenSettings()
|
||||
return false
|
||||
return .failed
|
||||
}
|
||||
let contract: PreparedRuntimeContract
|
||||
do {
|
||||
contract = try PreparedRuntimeContract(await runtimeDocument(sourceDocument, for: proxyMode))
|
||||
} catch RulePreparationError.superseded {
|
||||
return false
|
||||
return .superseded
|
||||
} catch {
|
||||
// 规则快照缺失/损坏:不静默退化成全局(issue #63 AC4)。
|
||||
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
|
||||
state = .serviceFailed(.runtimeFile)
|
||||
return false
|
||||
return .failed
|
||||
}
|
||||
return await deployPrepared(contract, preparation: runtimePreparationGeneration)
|
||||
}
|
||||
|
||||
func deployPrepared(_ contract: PreparedRuntimeContract, preparation: Int) async -> Bool {
|
||||
guard preparation == runtimePreparationGeneration else { return false }
|
||||
@discardableResult
|
||||
func deployPrepared(
|
||||
_ contract: PreparedRuntimeContract, preparation: Int
|
||||
) async -> ConvergenceResult {
|
||||
guard preparation == runtimePreparationGeneration else { return .superseded }
|
||||
let document = contract.document
|
||||
var ticket = convergenceTicket()
|
||||
let result = await apply(
|
||||
return await apply(
|
||||
document, ticket: &ticket, checking: [.preparation],
|
||||
requiresReceipt: document.aclRuntime != nil,
|
||||
convergeProxyOnSuccess: true,
|
||||
preserveProxyOnFailure: false,
|
||||
proxyTail: .none, rollback: nil)
|
||||
return result != .failed
|
||||
}
|
||||
|
||||
/// D8「任何路径不静默改端口」:监听设置不可读时以占位出厂端口部署等于
|
||||
|
||||
@@ -64,7 +64,8 @@ extension ProxyRuntimeController {
|
||||
/// 派生文档对当前运行时是否无事可做(CONTEXT.md「有效值未变的提交不做
|
||||
/// 运行时收敛」):磁盘契约与派生字节逐位相同、agent 已注册且 wrapper
|
||||
/// 存活、控制器处于健康运行态——计划层动作序列为空即幂等跳过。判定在
|
||||
/// 置 `starting` 之前进行,跳过路径不闪状态、不重走健康门。
|
||||
/// 置 `starting` 之前进行,跳过路径不闪状态、不重走健康门;磁盘漂移或
|
||||
/// wrapper 失踪时动作序列自然非空,走完整 deploy 自愈。
|
||||
func convergencePlanIsEmpty(
|
||||
_ document: SslocalRuntimeDocument, preparedContract: PreparedRuntimeContract?
|
||||
) -> Bool {
|
||||
@@ -82,6 +83,34 @@ extension ProxyRuntimeController {
|
||||
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 {
|
||||
/// 部署已执行且启动健康通过。
|
||||
|
||||
@@ -41,13 +41,7 @@ extension ProxyRuntimeController {
|
||||
var next = settings
|
||||
next.preferredMode = mode.kind
|
||||
next.ruleDefaultAction = ruleDefaultAction
|
||||
do {
|
||||
try settingsStore.save(next)
|
||||
} catch {
|
||||
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
|
||||
state = .serviceFailed(.persistence)
|
||||
return
|
||||
}
|
||||
guard persistSettings(next) else { return }
|
||||
settings = next
|
||||
proxyMode = mode
|
||||
modeChangeGeneration += 1
|
||||
|
||||
@@ -7,26 +7,17 @@ extension ProxyRuntimeController {
|
||||
/// 不回读磁盘(磁盘仍是重启与跨进程恢复的权威来源)。agent 意图开启时
|
||||
/// 原子更新运行时;目标失效 → 清除目标但 agent 继续监听。返回结构化收敛
|
||||
/// 结果,健康检查耗时属于本调用的异步收敛阶段,不改变「目录已提交」的事实。
|
||||
/// 「有效值未变的提交不做运行时收敛」由收敛脊柱 apply 层的 plan 空判定
|
||||
/// 承担(`convergencePlanIsEmpty`);派生被更新的收敛取代时按重校验呈现。
|
||||
func catalogDidCommit(snapshot catalog: ConfigurationCatalog) async -> RuntimeSyncOutcome {
|
||||
switch reexpand(in: catalog) {
|
||||
case .deployed(let configuration):
|
||||
guard settings.agentEnabled else {
|
||||
return .revalidated
|
||||
}
|
||||
do {
|
||||
let document = try await runtimeDocument(configuration.document, for: proxyMode)
|
||||
let preparation = runtimePreparationGeneration
|
||||
let contract = try PreparedRuntimeContract(document)
|
||||
if runtimeAlreadyConverged(with: contract) {
|
||||
RuntimeLog.emit(.contractUnchanged)
|
||||
} else {
|
||||
_ = await deployPrepared(contract, preparation: preparation)
|
||||
}
|
||||
} catch RulePreparationError.superseded {
|
||||
let result = await deploy(configuration.document)
|
||||
if case .superseded = result {
|
||||
return .revalidated
|
||||
} catch {
|
||||
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
|
||||
state = .serviceFailed(.runtimeFile)
|
||||
}
|
||||
return Self.syncOutcome(
|
||||
agentState: state, systemProxyState: systemProxyState,
|
||||
@@ -50,28 +41,6 @@ extension ProxyRuntimeController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 目录提交去抖(CONTEXT.md「有效值未变的提交不做运行时收敛」):派生文档
|
||||
/// (含模式/ACL 合并)与磁盘契约逐字节相等、agent 已注册且 wrapper 存活、
|
||||
/// 控制器处于健康运行态时,本次提交对运行时无事可做——不写契约、不发信号、
|
||||
/// 不闪 starting、不重走健康门与系统代理收敛。判定复用计划层语义
|
||||
/// (`ProxyRuntimePlan.actions` 返回空 = 幂等跳过):磁盘漂移或 wrapper 失踪
|
||||
/// 时动作序列自然非空,走完整 deploy 自愈。目录重校验(跳过名单、目标失效
|
||||
/// 清除)已在 `reexpand` 完成,不受此门影响。
|
||||
private func runtimeAlreadyConverged(with contract: PreparedRuntimeContract) -> Bool {
|
||||
switch state {
|
||||
case .running, .firewallBlocked:
|
||||
break
|
||||
case .off, .starting, .launchFailed, .requiresApproval, .serviceFailed:
|
||||
return false
|
||||
}
|
||||
let actions = ProxyRuntimePlan.actions(
|
||||
intent: .run(contract.document),
|
||||
agentStatus: agent.status,
|
||||
wrapper: wrapperState(),
|
||||
contractOnDisk: runtimeFileStore.readData(), preparedContract: contract)
|
||||
return actions.isEmpty
|
||||
}
|
||||
|
||||
/// 部署后的控制器状态 → 结构化收敛结果:健康通过(含防火墙受阻的运行态)
|
||||
/// 视为已收敛;系统代理写入失败携带点名细节,待应用不算目录收敛失败。
|
||||
private static func syncOutcome(
|
||||
@@ -103,14 +72,7 @@ extension ProxyRuntimeController {
|
||||
listenSettingsUnreadable = false
|
||||
settingsUnreadable = false
|
||||
guard state != .off else { return }
|
||||
switch reexpand(in: catalog) {
|
||||
case .deployed(let configuration):
|
||||
await deploy(configuration.document)
|
||||
case .clearedAndStopped(let failure):
|
||||
await handleCleared(failure)
|
||||
case nil:
|
||||
await deployListeningWithoutTarget()
|
||||
}
|
||||
await convergeToReexpanded(catalog)
|
||||
}
|
||||
|
||||
/// Applies the post-import 2.0 runtime boundary without touching
|
||||
|
||||
@@ -61,9 +61,10 @@ final class CatalogWorkflowArchitectureTests: XCTestCase {
|
||||
"MainApp.swift",
|
||||
"CatalogCommitCoordinator.swift",
|
||||
"ProxyRuntimeController.swift",
|
||||
// 控制器按命令面/设置与目录同步/系统代理门禁/事实投影分文件(同属运行时适配器)。
|
||||
// 控制器按命令面/设置与目录同步/收敛脊柱/系统代理门禁/事实投影分文件(同属运行时适配器)。
|
||||
"ProxyRuntimeController+Commands.swift",
|
||||
"ProxyRuntimeController+SettingsSync.swift",
|
||||
"ProxyRuntimeController+Convergence.swift",
|
||||
"ProxyRuntimeController+SystemProxyGate.swift",
|
||||
"ProxyRuntimeController+Facts.swift",
|
||||
"SystemProxyHelperService.swift",
|
||||
|
||||
Reference in New Issue
Block a user