Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/Application/ProxyRuntime/ProxyRuntimeController+Convergence.swift

401 lines
16 KiB
Swift

import Foundation
/// 一次计划执行的结果:动作是否全部按序成功,以及本次执行占用的并发流
/// 代际;收敛票据用它推进 flow,避免调用方手算 `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 {
guard runtimeReloadProcesses.isEmpty else { return false }
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(),
configurationIsCurrent: preparedContract.map { runtimeFileStore.contains($0) } ?? false)
return actions.isEmpty
}
/// reexpand 三-case 开关的收敛主体(显式收敛、设置保存、启动重同步共用):
/// deployed → 部署派生文档;目标失效 → 清除并按意图收敛;无目标 → 空监听。
/// 幂等跳过由 apply 层的 plan 空判定承担;目录重校验(跳过名单、目标失效
/// 清除)已在 `reexpand` 完成,不受其影响。
func convergeToReexpanded(_ catalog: ConfigurationCatalog) async {
guard !settingsUnreadable else {
state = .launchFailed(.unreadableSettings)
return
}
guard catalogSnapshotReader.isCatalogAvailable 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()
}
}
/// 一个意图入口隐藏派生、执行政策和失败呈现;调用者不组合执行选项。
enum RuntimeConvergenceIntent {
case deployment(SslocalRuntimeDocument)
case modeTransition
case savedRules
}
enum ConvergenceResult {
case applied
case unchanged
/// 派生或部署失败;失败呈现已经处理。
case failed
/// 被新意图取代,不得发布旧结果。
case superseded
}
@discardableResult
func convergeRuntime(_ intent: RuntimeConvergenceIntent) async -> ConvergenceResult {
guard !settingsUnreadable else {
state = .launchFailed(.unreadableSettings)
return .failed
}
guard catalogSnapshotReader.isCatalogAvailable else { return .failed }
switch intent {
case .deployment(let source): return await convergeDeployment(source)
case .modeTransition: return await convergeModeTransition()
case .savedRules: return await convergeSavedRules()
}
}
private func convergeDeployment(_ source: SslocalRuntimeDocument) async -> ConvergenceResult {
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() async -> ConvergenceResult {
await convergeChangedIntent(policy: .modeTransition)
}
private func convergeSavedRules() async -> ConvergenceResult {
guard proxyMode == .rule else { return .unchanged }
let revision = ruleDocuments.revision
let result = await convergeChangedIntent(policy: .savedRules)
switch result {
case .applied, .unchanged:
if revision == ruleDocuments.revision { ruleApplicationFailure = nil }
case .superseded:
break
case .failed:
if revision == ruleDocuments.revision {
ruleApplicationFailure = ProxyRuntimeFacts(state: state).failure ?? .service(.runtimeFile)
}
}
return result
}
/// 三种意图的执行差异只在 module 内选择,不能由调用者拼成任意组合。
private enum DeploymentPolicy {
case deployment
case modeTransition
case savedRules
}
private enum ChangedIntentSource {
case ready(SslocalRuntimeDocument)
case completed(ConvergenceResult)
}
private struct PreparedChangedIntent {
let document: SslocalRuntimeDocument
let contract: PreparedRuntimeContract
let ticket: ConvergenceTicket
}
private enum ChangedIntentPreparation {
case ready(PreparedChangedIntent)
case completed(ConvergenceResult)
}
private struct RuntimeApplyAttempt {
let document: SslocalRuntimeDocument
let contract: PreparedRuntimeContract?
let policy: DeploymentPolicy
let reloadingProcessID: Int32?
let reloadID: UUID
}
private enum ApplyPreparation {
case ready(RuntimeApplyAttempt)
case completed(ConvergenceResult)
}
/// 模式与规则变更从同一份已保存资料重新展开,不以输出配置作为派生输入。
private func convergeChangedIntent(policy: DeploymentPolicy) async -> ConvergenceResult {
switch await changedIntentSource(policy) {
case .completed(let result): return result
case .ready(let source): return await prepareAndConverge(source, policy: policy)
}
}
private func changedIntentSource(_ policy: DeploymentPolicy) async -> ChangedIntentSource {
guard settings.agentEnabled, state != .off else {
if policy == .modeTransition { systemProxyObserver.modeTransitionAwaitingRuntime() }
return .completed(.unchanged)
}
switch reexpand(in: catalogSnapshotReader.catalogSnapshot) {
case .deployed(let configuration): return .ready(configuration.document)
case .clearedAndStopped(let failure):
return .completed(await handleCleared(failure))
case nil: return .ready(SslocalRuntimeDocument(servers: [], listen: settings.listen))
}
}
private func prepareAndConverge(
_ source: SslocalRuntimeDocument, policy: DeploymentPolicy
) async -> ConvergenceResult {
let intent: PreparedChangedIntent
switch await prepareChangedIntent(source, policy: policy) {
case .completed(let result): return result
case .ready(let prepared): intent = prepared
}
if let unchanged = await unchangedIntentResult(intent, policy: policy) { return unchanged }
var ticket = intent.ticket
if policy == .savedRules {
modeChangeGeneration += 1
ticket = convergenceTicket()
}
return await apply(intent.document, ticket: &ticket, policy: policy)
}
private func prepareChangedIntent(
_ source: SslocalRuntimeDocument, policy: DeploymentPolicy
) async -> ChangedIntentPreparation {
var ticket = convergenceTicket()
let next: SslocalRuntimeDocument
let contract: PreparedRuntimeContract
do {
next = try await runtimeDocument(source, for: proxyMode)
contract = try PreparedRuntimeContract(next)
} catch RulePreparationError.superseded {
return .completed(.superseded)
} catch {
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
if policy == .savedRules {
ruleApplicationFailure = .service(.runtimeFile)
} else {
state = .serviceFailed(.runtimeFile)
}
return .completed(.failed)
}
ticket.preparation = runtimePreparationGeneration
return .ready(PreparedChangedIntent(document: next, contract: contract, ticket: ticket))
}
private func unchangedIntentResult(
_ intent: PreparedChangedIntent, policy: DeploymentPolicy
) async -> ConvergenceResult? {
let unchanged = runtimeFileStore.contains(intent.contract)
let hasNoDeploymentFailure = runtimeFileStore.readFailure() == nil
if policy == .savedRules, unchanged, ruleApplicationFailure == nil, hasNoDeploymentFailure {
return .unchanged
}
guard policy == .modeTransition, unchanged, hasNoDeploymentFailure,
runtimeIsAlreadyRunning, aclReloadProcessID(for: intent.document) != nil
else { return nil }
guard settings.systemProxyEnabled else { return .unchanged }
await systemProxyObserver.convergeSystemProxy()
return .applied
}
private var runtimeIsAlreadyRunning: Bool {
switch state {
case .running, .firewallBlocked: return true
default: return false
}
}
/// 仅 ACL 变化且原有子进程仍健康时,继续沿用监听事实。
private func aclReloadProcessID(for document: SslocalRuntimeDocument) -> Int32? {
switch state {
case .running, .firewallBlocked: break
default: return nil
}
guard agent.status == .registered, case .running = wrapperState(),
let identity = document.replacingACL(nil).intentSHA256,
runtimeObservation?.configurationIntentSHA256 == identity
else { return nil }
return effectiveRuntimeListenerProcessID
?? runtimeReloadProcesses.values.first(where: { processIsAlive($0) })
}
/// 共用执行脊柱;票据不越过意图 seam。
private func apply(
_ document: SslocalRuntimeDocument,
ticket: inout ConvergenceTicket,
policy: DeploymentPolicy
) async -> ConvergenceResult {
let preparation = await prepareApply(document, ticket: ticket, policy: policy)
let attempt: RuntimeApplyAttempt
switch preparation {
case .completed(let result): return result
case .ready(let prepared): attempt = prepared
}
defer { runtimeReloadProcesses.removeValue(forKey: attempt.reloadID) }
let execution = await execute(
.run(document), document: document, preparedContract: attempt.contract)
ticket.flow = execution.flow
return await completeApply(execution, attempt: attempt, ticket: &ticket)
}
private func prepareApply(
_ document: SslocalRuntimeDocument, ticket: ConvergenceTicket,
policy: DeploymentPolicy
) async -> ApplyPreparation {
let contract = try? PreparedRuntimeContract(document)
if runtimeFileStore.blocksDeployment(document) {
state = .serviceFailed(.agent)
await systemProxyObserver.holdSystemProxyIntent()
return .completed(.failed)
}
if convergencePlanIsEmpty(document, preparedContract: contract) {
RuntimeLog.emit(.contractUnchanged)
ruleApplicationFailure = nil
return .completed(.unchanged)
}
guard convergenceIsCurrent(ticket) else { return .completed(.superseded) }
let reloadingProcessID = aclReloadProcessID(for: document)
let reloadID = UUID()
if let reloadingProcessID { runtimeReloadProcesses[reloadID] = reloadingProcessID }
runtimeObservation = RuntimeDeploymentFacts(document)
if reloadingProcessID == nil { state = .starting }
return .ready(
RuntimeApplyAttempt(
document: document, contract: contract, policy: policy,
reloadingProcessID: reloadingProcessID, reloadID: reloadID))
}
private func completeApply(
_ execution: RuntimeExecutionOutcome, attempt: RuntimeApplyAttempt,
ticket: inout ConvergenceTicket
) async -> ConvergenceResult {
ticket.flow = execution.flow
guard convergenceIsCurrent(ticket) else { return .superseded }
guard execution.succeeded else {
if state != .requiresApproval,
runtimeFileStore.receiptIdentity(for: attempt.document) != nil
{
await failDeployment(ticket: &ticket)
}
return .failed
}
let healthy = await presentLaunchHealth(
attempt.document,
requiresReceipt: attempt.policy != .deployment || attempt.document.aclRuntime != nil,
convergeProxyOnSuccess: attempt.policy == .deployment,
preserveProxyOnFailure: false, preparedContract: attempt.contract,
reloadingProcessID: attempt.reloadingProcessID)
guard convergenceIsCurrent(ticket) else { return .superseded }
guard healthy else {
await failDeployment(ticket: &ticket)
return .failed
}
do { try runtimeFileStore.settleSuccessfulDeployment() } catch {
state = .serviceFailed(.runtimeFile)
await failDeployment(ticket: &ticket)
return .failed
}
runtimeObservation = RuntimeDeploymentFacts(attempt.document)
ruleApplicationFailure = nil
await convergeProxyAfterApply(attempt.policy)
return .applied
}
private func convergeProxyAfterApply(_ policy: DeploymentPolicy) async {
switch policy {
case .modeTransition: await systemProxyObserver.convergeSystemProxy(forceApply: true)
case .savedRules: await systemProxyObserver.convergeSystemProxy()
case .deployment: break
}
}
/// 发布后失败保留配置及失败身份,停止服务但不改写两个开关意图。
private func failDeployment(ticket: inout ConvergenceTicket) async {
guard convergenceIsCurrent(ticket) else { return }
var failureState = state
do { try runtimeFileStore.recordFailure(reason: "GUI deployment verification failed") } catch {
failureState = .serviceFailed(.runtimeFile)
}
let stopped = await unregisterFailedDeployment()
guard convergenceIsCurrent(ticket) else { return }
if stopped { state = failureState } else { state = .serviceFailed(.agent) }
await systemProxyObserver.holdSystemProxyIntent()
}
private func unregisterFailedDeployment() async -> Bool {
do { try agent.unregister() } catch { return false }
await waitForWrapperExit()
return wrapperState() == .notRunning && !runtimeFileStore.hasUnconfirmedChild
}
}