Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/Application/ProxyRuntime/ProxyRuntimeController+SettingsSync.swift
T
Qiu Yuzhou fb428f12bc refactor(app): 按分层重组 App/ 源码目录
App/ 根目录原有 73 个散落源文件,已无法从目录读出任何分组信息。

按「目录即分层」重组为 Composition / Application / Presentation / UI /
PlatformEffects / Resources 六个目录,UI 层再按子视图分目录(Workspace、
Home、Servers(含 Plugin)、Subscriptions、Rules、Settings、Diagnostics、
StatusMenu)。纯移动:类型名、模块名与 import 均未改动,App/ 根层不再有
散落文件。

配套改动:

- project.yml:Core 测试镜像的 excludes 由裸文件名改为 `**/` 递归形式。
  XcodeGen 的 exclude 不跨 `/`,裸名只匹配 App 根层;MainApp.swift 下移后
  旧写法会静默失效,把 @main 组合根编进测试镜像。info.path 与
  entitlements.path 指向 App/Resources/;并排除 `**/*.md`——App/ 下任何
  非源码文件都会被 XcodeGen 收进 Resources 阶段并封进签名后的 bundle。
- SkeletonTests 的架构守卫由「默认扫描全部 App/ + 7 个豁免文件 + 1 个豁免
  前缀 + 4 个豁免目录」改为正面圈定 App/UI/,豁免登记全部删除;新增运行时
  适配器放进 Application/ 即自动落在扫描范围外,无需再登记。
- README 工程结构说明同步;根 .gitignore 的生成物路径随 Resources/ 归位。

验证:clean build 0 error;task test 1022 用例全通过(含两个架构守卫);
task format 无改动(证明纯移动);task lint 与移动前同为 21 条内容型
warning;packaging-gate.sh ALL CHECKS PASSED,确认 plist/entitlements 归位
后签名与 entitlements 语义不变。

注意:迁移时须先删旧的生成物再 xcodegen generate,否则遗留的 App/Info.plist
会被当作普通资源扫进 Resources 阶段。
2026-10-05 19:56:18 +08:00

109 lines
4.6 KiB
Swift

import Foundation
// MARK: - 设置变更与目录提交同步
extension ProxyRuntimeController {
/// A published mapping invalidates prepared documents before asynchronous convergence.
func pluginMappingsDidPublish() {
runtimePreparationGeneration += 1
}
/// 目录提交协调器的生产适配入口(issue #40):以刚提交的内存快照重展开,
/// 不回读磁盘(磁盘仍是重启与跨进程恢复的权威来源)。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
}
let result = await deploy(configuration.document)
if case .superseded = result {
return .revalidated
}
return Self.syncOutcome(
agentState: state, systemProxyState: systemProxyState,
skippedServers: configuration.skippedServers)
case .clearedAndStopped(let failure):
await handleCleared(failure)
return .clearedAndStopped(failure)
case nil:
guard settings.agentEnabled else {
return .revalidated
}
// 无活动目标:已在以空列表监听则不重走健康门(目录提交不该让状态闪回
// starting);否则补齐空监听。
if state != .off, lastDocument?.servers.isEmpty == true {
return Self.syncOutcome(
agentState: state, systemProxyState: systemProxyState, skippedServers: [])
}
await deployListeningWithoutTarget()
return Self.syncOutcome(
agentState: state, systemProxyState: systemProxyState, skippedServers: [])
}
}
/// 部署后的控制器状态 → 结构化收敛结果:健康通过(含防火墙受阻的运行态)
/// 视为已收敛;系统代理写入失败携带点名细节,待应用不算目录收敛失败。
private static func syncOutcome(
agentState: AgentRunState, systemProxyState: SystemProxyControlState,
skippedServers: [SkippedServer]
) -> RuntimeSyncOutcome {
if case .failed(let facts) = systemProxyState {
return .failed(failure: .systemProxy(facts))
}
switch agentState {
case .running, .firewallBlocked:
return .converged(skippedServers: skippedServers)
case .launchFailed(let facts):
return .failed(failure: .launch(facts))
case .serviceFailed(let facts):
return .failed(failure: .service(facts))
case .requiresApproval, .off, .starting:
return .failed(failure: nil)
}
}
/// Persists a fully validated settings snapshot and, when the agent is
/// running, re-derives the same runtime path with the new snapshot. Runtime
/// status remains observable through the controller's status stream.
func updateSettings(_ proposed: ProxySettings) async throws {
let catalog = catalogSnapshotReader.catalogSnapshot
try settingsStore.save(proposed)
settings = proposed
listenSettingsUnreadable = false
settingsUnreadable = false
guard state != .off else { return }
await convergeToReexpanded(catalog)
}
/// Applies the post-import 2.0 runtime boundary without touching
/// SystemConfiguration. Importing data leaves the user's system proxy
/// dictionary untouched; any currently running 2.0 runtime is stopped and
/// the existing 2.0 target/settings are reloaded.
func legacyImportDidCommit() async {
cancelFirewallObservation()
flowGeneration += 1
_ = await execute(.stop, document: nil)
state = .off
lastDocument = nil
skippedServers = []
lastActivationFailure = nil
// 导入边界不触碰 SystemConfiguration(既有不变量),系统代理状态面同
// 样不动:它呈现的是系统设置的真实作用,不由导入改写。
if let restored = try? settingsStore.load() {
settings = restored
settingsUnreadable = false
listenSettingsUnreadable = false
proxyMode = Self.makeProxyMode(from: restored)
}
let persistedTarget = try? activationFileStore.loadActiveTargetID()
machine = ActivationStateMachine(activeTargetID: persistedTarget ?? nil)
activeTargetID = machine.activeTargetID
}
}