mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 23:04:49 +00:00
154 lines
5.8 KiB
Swift
154 lines
5.8 KiB
Swift
import Foundation
|
|
|
|
// MARK: - 自定义规则更新结果
|
|
|
|
/// The result of accepting and saving a rule document, independent of runtime.
|
|
/// `busy`/`superseded` are transaction-gate rejections: no save was attempted.
|
|
enum CustomRuleUpdateOutcome: Equatable, Sendable {
|
|
/// 规则已保存;运行规则模式时应用。
|
|
case saved
|
|
/// 校验拒绝:整批不落地,旧规则保持不变;附可解释原因。
|
|
case rejected([RejectedCustomRule])
|
|
/// 持久化失败:旧规则保持不变。
|
|
case persistenceFailed
|
|
case busy
|
|
/// 事务门拒绝:意图捕获后事实已前进(集合版本/完整性变化),本次更新未尝试保存。
|
|
case superseded
|
|
case invalidDocument(detail: String)
|
|
|
|
var isSuccess: Bool {
|
|
switch self {
|
|
case .saved: true
|
|
default: false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Final persisted facts, independent of whether runtime application succeeded.
|
|
struct RuleDocumentCommit: Sendable {
|
|
let outcome: CustomRuleUpdateOutcome
|
|
let document: CustomRuleDocument?
|
|
}
|
|
|
|
// MARK: - 结果呈现文案
|
|
|
|
// outcome→文案映射与 outcome 类型同住,供状态行与编辑器两个视图共享。
|
|
|
|
extension RulesCommitFeedback {
|
|
var summary: String {
|
|
guard outcome.isSuccess else { return outcome.rulesMessage }
|
|
let count: String
|
|
switch operation {
|
|
case .enablement(let enabled):
|
|
count = String.localizedStringWithFormat(
|
|
RulesCopy.text(enabled ? "已启用 %lld 条规则" : "已禁用 %lld 条规则"), Int64(changedCount))
|
|
case .add: count = RulesCopy.text("已新增规则")
|
|
case .edit: count = RulesCopy.text("已编辑规则")
|
|
case .delete:
|
|
count = String.localizedStringWithFormat(
|
|
RulesCopy.text("已删除 %lld 条自定义规则"), Int64(changedCount))
|
|
}
|
|
return count + " · " + RulesCopy.text("已保存")
|
|
}
|
|
}
|
|
|
|
extension CustomRuleUpdateOutcome {
|
|
var rulesMessage: String {
|
|
switch self {
|
|
case .saved: RulesCopy.text("已保存")
|
|
case .persistenceFailed: RulesCopy.text("保存失败,规则未更改")
|
|
case .busy: RulesCopy.text("正在更新规则…")
|
|
case .superseded: RulesCopy.text("规则集合已变化,操作未执行。")
|
|
case .invalidDocument, .rejected: RulesCopy.text("无法更新规则")
|
|
}
|
|
}
|
|
|
|
var failureDetail: String? {
|
|
switch self {
|
|
case .invalidDocument(let detail): detail
|
|
case .rejected(let rejected): rejected.map(\.explanation).joined(separator: "\n")
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
var nextStep: String? {
|
|
switch self {
|
|
case .persistenceFailed, .busy, .superseded: String(localized: "请重试规则操作。")
|
|
case .invalidDocument, .rejected: String(localized: "请检查规则数据后再操作。")
|
|
default: nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 自定义规则命令面
|
|
|
|
extension ProxyRuntimeController {
|
|
/// Validate and atomically save intent; runtime application is a separate worker.
|
|
/// Deployment failure never restores an older saved rule document.
|
|
/// 全局和直连模式不加载自定义规则,但持久化仍然进行(切换到规则模式后生效)。
|
|
func commitRuleDocument(_ document: CustomRuleDocument) async -> RuleDocumentCommit {
|
|
let previous = try? ruleDocuments.load()
|
|
let outcome = await updateRuleDocument(document)
|
|
return RuleDocumentCommit(outcome: outcome, document: ruleDocuments.current ?? previous)
|
|
}
|
|
|
|
func updateRuleDocument(_ document: CustomRuleDocument) async -> CustomRuleUpdateOutcome {
|
|
guard !isUpdatingRules else { return .busy }
|
|
isUpdatingRules = true
|
|
defer { isUpdatingRules = false }
|
|
let validation = validateCustomRulesForPersistence(document.rules)
|
|
guard validation.rejected.isEmpty else { return .rejected(validation.rejected) }
|
|
guard
|
|
document.disabledIdentities.isDisjoint(
|
|
with:
|
|
Set(RuleCoverage.fixedLocalMatches.map { ProxyRule(action: .direct, match: $0) }))
|
|
else { return .invalidDocument(detail: "Fixed local policy cannot be disabled") }
|
|
do {
|
|
_ = try ruleDocuments.load()
|
|
try ruleDocuments.save(document)
|
|
} catch {
|
|
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
|
|
return .persistenceFailed
|
|
}
|
|
scheduleRuleApplication()
|
|
return .saved
|
|
}
|
|
|
|
/// One worker owns rule deployments. Saves never cancel an in-flight restart;
|
|
/// another saved revision is coalesced and applied after that restart finishes.
|
|
func scheduleRuleApplication() {
|
|
ruleApplicationGeneration += 1
|
|
guard ruleApplicationTask == nil else { return }
|
|
ruleApplicationTask = Task { [weak self] in
|
|
guard let self else { return }
|
|
defer { self.ruleApplicationTask = nil }
|
|
while !Task.isCancelled {
|
|
let generation = self.ruleApplicationGeneration
|
|
do { try await self.ruleApplicationDelay() } catch { return }
|
|
guard generation == self.ruleApplicationGeneration else { continue }
|
|
await self.convergeRuntime(.savedRules)
|
|
if generation == self.ruleApplicationGeneration { return }
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 保存前校验:固定本地冲突与重复整批拒绝并返回可解释原因。
|
|
/// 同行动覆盖和相反行动遮蔽不阻止保存;编译时按当前骨架解释完整集合。
|
|
func validateCustomRulesForPersistence(_ rules: [CustomRule]) -> (
|
|
acceptedRules: [CustomRule], rejected: [RejectedCustomRule]
|
|
) {
|
|
let base = CustomRuleValidator.hardValidation(custom: rules)
|
|
let hardRejected = base.rejected.filter {
|
|
$0.reason == .conflictsWithFixedLocalScope || $0.reason == .duplicate
|
|
}
|
|
let hardRejectedTokens = Set(hardRejected.map(\.rule.contentToken))
|
|
let accepted = rules.filter { !hardRejectedTokens.contains($0.contentToken) }
|
|
return (accepted, hardRejected)
|
|
}
|
|
|
|
/// 自定义规则安全摘要:数量 + 内容版本,不含原始域名。
|
|
func readCustomRuleSummary() -> CustomRuleSummary? {
|
|
try? customRuleStore.summary()
|
|
}
|
|
}
|