mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-11 23:25:06 +00:00
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 阶段。
134 lines
4.6 KiB
Swift
134 lines
4.6 KiB
Swift
import Foundation
|
||
|
||
// MARK: - 防火墙呈现、wrapper 观测与系统代理出口事实
|
||
|
||
extension ProxyRuntimeController {
|
||
func presentFirewallStatus(for document: SslocalRuntimeDocument) async {
|
||
let generation = flowGeneration
|
||
let preparation = runtimePreparationGeneration
|
||
guard document.listen.listenerMode.exposesNetworkInterfaces else {
|
||
state = .running
|
||
return
|
||
}
|
||
let blocked = await blockedFirewallExecutable()
|
||
guard generation == flowGeneration, preparation == runtimePreparationGeneration else { return }
|
||
if let blocked {
|
||
presentFirewallBlocked(blocked)
|
||
return
|
||
}
|
||
state = .running
|
||
observeFirewall(generation: flowGeneration)
|
||
}
|
||
|
||
private func blockedFirewallExecutable() async -> URL? {
|
||
for executableURL in firewallExecutableURLs {
|
||
let status = await firewallStatus(for: executableURL)
|
||
if status == .blocked { return executableURL }
|
||
}
|
||
return nil
|
||
}
|
||
|
||
private func presentFirewallBlocked(_ executableURL: URL) {
|
||
let name = executableURL.lastPathComponent
|
||
state = .firewallBlocked(FirewallBlockedFacts(executableName: name))
|
||
}
|
||
|
||
private func observeFirewall(generation: Int) {
|
||
firewallObservationTask?.cancel()
|
||
let interval = firewallPollIntervalNanoseconds
|
||
firewallObservationTask = Task { @MainActor [weak self] in
|
||
while !Task.isCancelled {
|
||
try? await Task.sleep(nanoseconds: interval)
|
||
guard !Task.isCancelled, let self, generation == flowGeneration else { return }
|
||
if let blocked = await blockedFirewallExecutable() {
|
||
// await 恢复后代际可能已被 execute 推进(含取消本任务的 stop 路径):
|
||
// 过期的防火墙事实不得盖到最新状态呈现上。
|
||
guard generation == flowGeneration else { return }
|
||
presentFirewallBlocked(blocked)
|
||
firewallObservationTask = nil
|
||
return
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func cancelFirewallObservation() {
|
||
firewallObservationTask?.cancel()
|
||
firewallObservationTask = nil
|
||
}
|
||
|
||
private func firewallStatus(for executableURL: URL) async -> FirewallBlockStatus {
|
||
let checker = firewallChecker
|
||
return await Task.detached(priority: .utility) {
|
||
checker.status(for: executableURL)
|
||
}.value
|
||
}
|
||
|
||
func probeAsync(
|
||
host: String, port: Int, timeout: TimeInterval
|
||
) async -> EndpointHealthProbe.Outcome {
|
||
let probe = probe
|
||
return await Task.detached(priority: .utility) {
|
||
probe.probe(host: host, port: port, timeout: timeout)
|
||
}.value
|
||
}
|
||
|
||
/// 显式停止协议次序(D2):注销(SIGTERM wrapper → wrapper 停 sslocal 并
|
||
/// 等待)完成后才允许后续删文件动作。最多等 5 秒,超时也继续(launchd 会
|
||
/// 兜底结束进程)。
|
||
func waitForWrapperExit() async {
|
||
let deadline = Date().addingTimeInterval(5)
|
||
while wrapperState() != .notRunning && Date() < deadline {
|
||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||
}
|
||
}
|
||
|
||
func wrapperState() -> WrapperProcessState {
|
||
guard
|
||
let data = try? Data(contentsOf: runtimeFileStore.pidFileURL),
|
||
let text = String(data: data, encoding: .utf8)?.trimmingCharacters(
|
||
in: .whitespacesAndNewlines),
|
||
let pid = Int32(text)
|
||
else { return .notRunning }
|
||
guard sendSignal(pid, 0) == 0 else { return .notRunning }
|
||
return .running(pid: pid)
|
||
}
|
||
|
||
// MARK: - 目录同步
|
||
|
||
/// Re-expands one command-start snapshot; a concurrently published catalog
|
||
/// is intentionally observed by the next command, not halfway through this one.
|
||
func reexpand(in catalog: ConfigurationCatalog) -> ActivationEffect? {
|
||
let effect = machine.catalogDidCommit(
|
||
catalog, credentials: credentials, plugins: plugins,
|
||
options: settings.runtimeDocumentOptions)
|
||
activeTargetID = machine.activeTargetID
|
||
switch effect {
|
||
case .deployed(let configuration):
|
||
skippedServers = configuration.skippedServers
|
||
case .clearedAndStopped:
|
||
skippedServers = []
|
||
case nil:
|
||
break
|
||
}
|
||
return effect
|
||
}
|
||
|
||
func describe(_ error: Error) -> String {
|
||
String(describing: error)
|
||
}
|
||
|
||
// MARK: - 系统代理出口事实(issue #60/#71)
|
||
|
||
/// 出口可用 = agent 入站健康(回环入口可用,含防火墙仅阻主机态的情形)
|
||
/// 且模式有出口;直连不依赖活动服务器目标。观察机以闭包读取同一事实源。
|
||
var systemProxyExitAvailable: Bool {
|
||
switch state {
|
||
case .running, .firewallBlocked:
|
||
return proxyMode == .direct || activeTargetID != nil
|
||
case .off, .starting, .launchFailed, .requiresApproval, .serviceFailed:
|
||
return false
|
||
}
|
||
}
|
||
}
|