mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-14 10:13:54 +00:00
195 lines
7.9 KiB
Swift
195 lines
7.9 KiB
Swift
import Foundation
|
|
|
|
extension ProxyRuntimeController {
|
|
/// The choice persists with the settings snapshot. ACL-backed modes (direct
|
|
/// and global) also deploy and verify the ACL runtime even when system proxy
|
|
/// is off, because manually connected SOCKS and HTTP clients use the same ACL.
|
|
func setProxyMode(_ mode: ProxyMode) async {
|
|
guard ProxyMode.availableModes.contains(mode) else { return }
|
|
guard mode != proxyMode else { return }
|
|
await transitionMode(mode, ruleDefaultAction: settings.ruleDefaultAction)
|
|
}
|
|
|
|
/// 规则模式子选项(issue #63):切换「未匹配时代理/直连」并重部署 ACL。
|
|
/// 失败保留旧模式、旧子选项与旧系统代理应用状态。
|
|
func setRuleDefaultAction(_ action: RuleDefaultAction) async {
|
|
guard action != settings.ruleDefaultAction else { return }
|
|
await transitionMode(proxyMode, ruleDefaultAction: action)
|
|
}
|
|
|
|
/// 模式切换前的可恢复快照:失败时按它原样还原。
|
|
private struct ModeTransitionSnapshot {
|
|
let settings: ProxySettings
|
|
let mode: ProxyMode
|
|
/// 切换前的运行时文档;无活动文档时为 `nil`,恢复前由调用方补上当次加载值。
|
|
let document: SslocalRuntimeDocument?
|
|
let state: AgentRunState
|
|
|
|
/// 用当次加载的文档补齐快照(原 `previousDocument ?? currentDocument` 语义)。
|
|
func resolvingDocument(_ fallback: SslocalRuntimeDocument) -> ModeTransitionSnapshot {
|
|
ModeTransitionSnapshot(
|
|
settings: settings, mode: mode, document: document ?? fallback, state: state)
|
|
}
|
|
}
|
|
|
|
private func transitionMode(
|
|
_ mode: ProxyMode,
|
|
ruleDefaultAction: RuleDefaultAction
|
|
) async {
|
|
let snapshot = ModeTransitionSnapshot(
|
|
settings: settings, mode: proxyMode, document: lastDocument, state: state)
|
|
var next = settings
|
|
next.preferredMode = mode.kind
|
|
next.ruleDefaultAction = ruleDefaultAction
|
|
guard persistSettings(next) else { return }
|
|
settings = next
|
|
proxyMode = mode
|
|
modeChangeGeneration += 1
|
|
var ticket = convergenceTicket()
|
|
|
|
guard settings.agentEnabled, state != .off,
|
|
let currentDocument = lastDocument ?? runtimeFileStore.loadDocument()
|
|
else {
|
|
systemProxyObserver.modeTransitionAwaitingRuntime()
|
|
return
|
|
}
|
|
|
|
let nextDocument: SslocalRuntimeDocument
|
|
do {
|
|
nextDocument = try await runtimeDocument(currentDocument, for: mode)
|
|
} catch RulePreparationError.superseded {
|
|
return
|
|
} catch {
|
|
// 快照缺失/损坏:不静默退化,恢复旧模式与旧子选项。
|
|
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
|
|
ticket.preparation = runtimePreparationGeneration
|
|
await restoreModeTransition(
|
|
snapshot: snapshot.resolvingDocument(currentDocument), ticket: ticket)
|
|
return
|
|
}
|
|
// 派生自身推进了派生代际:票据以派生后的当前值推进 preparation 分量。
|
|
ticket.preparation = runtimePreparationGeneration
|
|
guard nextDocument.aclRuntime != currentDocument.aclRuntime else {
|
|
guard settings.systemProxyEnabled else { return }
|
|
state = .starting
|
|
_ = await presentLaunchHealth(nextDocument)
|
|
return
|
|
}
|
|
|
|
await deployModeTransition(
|
|
nextDocument, snapshot: snapshot.resolvingDocument(currentDocument), ticket: ticket)
|
|
}
|
|
|
|
enum RulePreparationError: Error { case superseded }
|
|
|
|
private struct RulePreparationTicket {
|
|
let generation: Int
|
|
let flow: Int
|
|
let modeGeneration: Int
|
|
let settings: ProxySettings
|
|
let target: NodeID?
|
|
let ruleRevision: Int?
|
|
}
|
|
|
|
/// Capture mutable facts before leaving the UI actor. A stale task may not
|
|
/// mutate runtime state or write a contract, even when its calculation fails.
|
|
func runtimeDocument(
|
|
_ source: SslocalRuntimeDocument, for mode: ProxyMode
|
|
) async throws -> SslocalRuntimeDocument {
|
|
runtimePreparationGeneration += 1
|
|
let ticket = RulePreparationTicket(
|
|
generation: runtimePreparationGeneration, flow: flowGeneration,
|
|
modeGeneration: modeChangeGeneration, settings: settings, target: activeTargetID,
|
|
ruleRevision: mode == .rule ? ruleDocuments.revision : nil)
|
|
do {
|
|
let user = mode == .rule ? try ruleDocuments.load() : nil
|
|
let builtIn =
|
|
mode == .rule ? try await runtimeBuiltinRules(ticket.settings.ruleDefaultAction) : []
|
|
let aclURL = runtimeFileStore.aclFileURL
|
|
let result = await Task.detached(priority: .userInitiated) {
|
|
RuleRuntimeCompiler.compile(
|
|
.init(
|
|
source: source, mode: mode, defaultAction: ticket.settings.ruleDefaultAction,
|
|
document: user, builtIn: builtIn, aclURL: aclURL))
|
|
}.value
|
|
guard preparationIsCurrent(ticket) else { throw RulePreparationError.superseded }
|
|
return result
|
|
} catch {
|
|
guard preparationIsCurrent(ticket) else { throw RulePreparationError.superseded }
|
|
throw error
|
|
}
|
|
}
|
|
|
|
private func preparationIsCurrent(_ ticket: RulePreparationTicket) -> Bool {
|
|
ticket.generation == runtimePreparationGeneration && ticket.flow == flowGeneration
|
|
&& ticket.modeGeneration == modeChangeGeneration
|
|
&& ticket.settings.listen == settings.listen
|
|
&& ticket.settings.preferredMode == settings.preferredMode
|
|
&& ticket.settings.ruleDefaultAction == settings.ruleDefaultAction
|
|
&& ticket.settings.agentEnabled == settings.agentEnabled
|
|
&& ticket.target == activeTargetID
|
|
&& (ticket.ruleRevision == nil || ticket.ruleRevision == ruleDocuments.revision)
|
|
}
|
|
|
|
private func runtimeBuiltinRules(_ action: RuleDefaultAction) async throws -> [ProxyRule] {
|
|
switch action {
|
|
case .proxyWhenUnmatched:
|
|
let geolocation = try await ruleSnapshots.load(.geolocationCN)
|
|
let china = try await ruleSnapshots.load(.chinaIPv4)
|
|
return await Task.detached(priority: .userInitiated) {
|
|
BuiltinRuleCatalog.chinaDirectRules(from: [geolocation, china])
|
|
}.value
|
|
case .directWhenUnmatched:
|
|
let snapshot = try await ruleSnapshots.load(.gfwlist)
|
|
return await Task.detached(priority: .userInitiated) {
|
|
BuiltinRuleCatalog.gfwlistRules(from: snapshot)
|
|
}.value
|
|
}
|
|
}
|
|
|
|
private func deployModeTransition(
|
|
_ document: SslocalRuntimeDocument,
|
|
snapshot: ModeTransitionSnapshot,
|
|
ticket: ConvergenceTicket
|
|
) async {
|
|
var ticket = ticket
|
|
// 两个调用点都经 resolvingDocument 补齐文档;防御性解包失败即无事可做。
|
|
guard let rollbackDocument = snapshot.document else { return }
|
|
let plan = RollbackPlan(
|
|
document: rollbackDocument, state: snapshot.state,
|
|
payload: .modeTransition(
|
|
mode: snapshot.mode, ruleDefaultAction: snapshot.settings.ruleDefaultAction),
|
|
systemProxyIntentAtCapture: snapshot.settings.systemProxyEnabled,
|
|
convergeProxyOnIntentChange: true)
|
|
let result = await apply(
|
|
document, ticket: &ticket,
|
|
requiresReceipt: true,
|
|
convergeProxyOnSuccess: false,
|
|
preserveProxyOnFailure: true,
|
|
proxyTail: .forceApply, rollback: plan)
|
|
if case .failed = result {
|
|
await restoreModeTransition(snapshot: snapshot, ticket: ticket)
|
|
}
|
|
}
|
|
|
|
/// mode 切换的失败回滚:按快照整体恢复;载荷持久化失败以 serviceFailed
|
|
/// 呈现(仅在旧运行时恢复到健康时)。
|
|
private func restoreModeTransition(
|
|
snapshot: ModeTransitionSnapshot,
|
|
ticket: ConvergenceTicket
|
|
) async {
|
|
var ticket = ticket
|
|
guard let previousDocument = snapshot.document else { return }
|
|
let plan = RollbackPlan(
|
|
document: previousDocument, state: snapshot.state,
|
|
payload: .modeTransition(
|
|
mode: snapshot.mode, ruleDefaultAction: snapshot.settings.ruleDefaultAction),
|
|
systemProxyIntentAtCapture: snapshot.settings.systemProxyEnabled,
|
|
convergeProxyOnIntentChange: true)
|
|
let report = await restore(plan, ticket: &ticket)
|
|
if report.runtimeHealthy == true, report.payloadFailureDescription != nil {
|
|
state = .serviceFailed(.persistence)
|
|
}
|
|
}
|
|
}
|