Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/Application/ProxyRuntime/ProxyRuntimeController+Preparation.swift
Qiu Yuzhou f6abd4343e feat(rules)!: add isolated routing presets
Add Mainland China proxy routing with independent rule workspaces.
Preserve existing routing choices through equivalent preset mappings.

BREAKING CHANGE: existing custom rules and enablement settings are
not migrated. Recreate them in the appropriate preset.
2026-10-25 14:32:12 +08:00

76 lines
3.4 KiB
Swift

import Foundation
// 运行状态收敛 module 的配置派生实现;异步计算前捕获事实,恢复后拒绝旧结果。
extension ProxyRuntimeController {
enum RulePreparationError: Error { case superseded }
private struct RulePreparationTicket {
let generation: Int
let flow: Int
let modeGeneration: Int
let settings: ProxyRuntimeSettings
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.rulePreset) : []
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 }
let lan = ticket.settings.listen.lan
guard lan.isEnabled && lan.authenticationEnabled else { return result.authenticatingLAN(nil) }
guard let password = lan.password else {
throw ProxyListeningStoreError.credentialFailure(detail: String(localized: "局域网身份验证密码不可读"))
}
let auth = RuntimeListenerAuthentication(
username: lan.username, password: password,
directory: runtimeFileStore.fileURL.deletingLastPathComponent().standardizedFileURL.path)
guard auth.isValid else {
throw ProxyListeningStoreError.credentialFailure(detail: String(localized: "局域网身份验证凭据无效"))
}
return result.authenticatingLAN(auth)
} 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.rulePreset == settings.rulePreset
&& ticket.settings.agentEnabled == settings.agentEnabled
&& ticket.target == activeTargetID
&& (ticket.ruleRevision == nil || ticket.ruleRevision == ruleDocuments.revision)
}
private func runtimeBuiltinRules(_ preset: RulePreset) async throws -> [ProxyRule] {
var snapshots: [RuleSnapshot] = []
for source in preset.sources { snapshots.append(try await ruleSnapshots.load(source)) }
let captured = snapshots
return await Task.detached(priority: .userInitiated) {
captured.flatMap { preset.rules(from: $0) }
}.value
}
}