Files
ShadowsocksX-NG/ShadowsocksX-NG2/Domain/RuntimeLog.swift
T
Qiu Yuzhou c1aba5a882 fix(system-proxy): make helper XPC connection reachable and crash-free
GUI 侧 XPC 回调闭包(invalidationHandler 与 remoteObjectProxy 错误回调)在
@MainActor 方法里形成、传给非 @Sendable 的 Foundation 参数被推断为主 actor
隔离,Foundation 在 XPC 内部队列上调起时触发动态隔离断言直接崩溃;显式标注
@Sendable(nonisolated),闭包只捕获 Sendable 应答闸门。

连接系统域 LaunchDaemon 的 MachService 必须带 options: .privileged(否则只
在 per-user launchd 域查找,永远失败走错误回调);并把 resume() 提到首次远
程调用之前,超时兜底任务在应答到达后取消。

收敛/清除失败补 RuntimeLog systemProxyWriteFailed(detail:)(诊断报告白名单
按既有承诺脱敏收录),XPC/ helper 层失败原因不再只能靠崩溃报告逆向。

listener delegate 与客户端校验移入 Domain 共享编译单元(与引擎同模式),
main.swift 改为显式注入引擎;新增真实 XPC 装配层 round-trip 测试:匿名监听
器挂真实 delegate(注入假特权写入器)+ listenerEndpoint 连接,覆盖接口签
名、客户端签名校验、Data 传输与 XPC 队列应答路径,无需 launchd。
2026-09-30 13:40:16 +08:00

166 lines
7.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// 运行时日志基线(spec #21 D5):事件是封闭枚举,脱敏由构造保证——永不
/// 携带密码、插件参数、Keychain 值、完整订阅 URL、URL token 或运行时 JSON
/// 内容;服务器地址与备注默认不进普通日志(端点探测失败点名的 host:port
/// 是本机监听端点,属 D8 要求呈现的事实字段,不受此限)。日志查看与诊断
/// 导出由 #34 在本事件流上接出。
enum RuntimeLogEvent: Equatable, CustomStringConvertible, Sendable {
/// 契约文件已原子写入(只携带数量元数据,非内容)。
case contractWritten(serverCount: Int)
/// 契约内容与磁盘一致,跳过写入(幂等)。
case contractUnchanged
/// 系统代理期望值与系统当前应用值语义等价,跳过写入(零授权,issue #70)。
case systemProxyUnchanged
/// 特权系统代理 helper 的 SMAppService 注册失败(issue #71;不含内容)。
case systemProxyHelperRegisterFailed(detail: String)
/// 系统代理写入/清除请求失败(issue #71;detail 为 XPC 或 helper 错误文本)。
case systemProxyWriteFailed(detail: String)
/// 运行时文件已清理。
case runtimeFilesDeleted
/// 激活状态等运行时元数据落盘失败(不含内容)。
case runtimePersistFailed(detail: String)
case agentRegistered
case agentRegisterFailed(detail: String)
case agentUnregistered
case agentUnregisterFailed(detail: String)
/// wrapper 侧 sslocal 监管事件。
case sslocalSpawned(pid: Int32)
case sslocalSpawnFailed
case sslocalExitedUnexpectedly(status: Int32)
case sslocalStopRequested
/// sslocal 拉起后限时内未完成本地监听绑定(D10「3 秒未建立监听 → error
/// 日志」;监管继续,GUI 健康门负责呈现,端点为本机监听端点不受脱敏限制)。
case listenNotEstablished(detail: String)
case contractMissing
case contractInvalidRemoved
case reloadForwarded
case reloadDeferred
case reloadRestarted
/// 本机代理端点启动后未就绪(点名端点与端口,D8)。
case endpointProbeFailed(host: String, port: Int, detail: String)
/// 激活失败或活动目标清除(原因枚举已点名且不含秘密)。
case activationFailed(reason: String)
/// 用户显式触发的脱敏诊断导出已完成(不携带导出路径)。
case diagnosticsExported
/// 监听设置持久化读取失败,已回落出厂默认(D8:不静默改端口——事件进
/// 诊断日志供 #34 查看器呈现,完整呈现面在 #33)。
case listenSettingsUnreadable(detail: String)
var description: String {
switch self {
case .contractWritten(let serverCount):
return "contract written (servers=\(serverCount))"
case .contractUnchanged:
return "contract unchanged, write skipped"
case .systemProxyUnchanged:
return "system proxy values unchanged, write skipped"
case .systemProxyHelperRegisterFailed(let detail):
return "system proxy helper register failed: \(detail)"
case .systemProxyWriteFailed(let detail):
return "system proxy write failed: \(detail)"
case .runtimeFilesDeleted:
return "runtime files deleted"
case .runtimePersistFailed(let detail):
return "runtime metadata persist failed: \(detail)"
case .agentRegistered:
return "launch agent registered"
case .agentRegisterFailed(let detail):
return "launch agent register failed: \(detail)"
case .agentUnregistered:
return "launch agent unregistered"
case .agentUnregisterFailed(let detail):
return "launch agent unregister failed: \(detail)"
case .sslocalSpawned(let pid):
return "sslocal spawned (pid=\(pid))"
case .sslocalSpawnFailed:
return "sslocal spawn failed"
case .sslocalExitedUnexpectedly(let status):
return "sslocal exited unexpectedly (status=\(status))"
case .sslocalStopRequested:
return "sslocal stop requested"
case .listenNotEstablished(let detail):
return "listen not established within deadline: \(detail)"
case .contractMissing:
return "contract missing"
case .contractInvalidRemoved:
return "contract invalid, removed"
case .reloadForwarded:
return "reload forwarded to sslocal"
case .reloadDeferred:
return "reload deferred until listeners are ready"
case .reloadRestarted:
return "reload: listen change, restarting sslocal"
case .endpointProbeFailed(let host, let port, let detail):
return "endpoint \(host):\(port) not ready: \(detail)"
case .activationFailed(let reason):
return "activation failed: \(reason)"
case .diagnosticsExported:
return "diagnostics report exported (redacted)"
case .listenSettingsUnreadable(let detail):
return "listen settings unreadable, falling back to factory defaults: \(detail)"
}
}
}
/// 事件接收缝(issue #34):GUI 进程把事件接入 RuntimeEventStore(内存环形
/// 缓冲),供主窗口日志查看器实时呈现与诊断导出取用;wrapper 进程不注册,
/// 仅走 stderr → agent.log 收敛。接收方拿到的是封闭枚举原值,渲染发生在
/// 呈现边缘(诊断报告的白名单清洗只可能作用于原值,issue #43)。
protocol RuntimeEventSink: Sendable {
func append(event: RuntimeLogEvent, timestamp: Date)
}
/// 敏感信息脱敏工具(D5「敏感信息」定义的读取面)。
enum Redactor {
/// 非空远程 URL 只保留 scheme+host:路径、查询与片段全部略去,完整订阅
/// URL 与 URL token 永不入日志。解析失败一律归并为占位符。
static func remoteURL(_ url: String) -> String {
guard let parsed = URL(string: url), let host = parsed.host(), !host.isEmpty else {
return "<redacted-url>"
}
return "\(parsed.scheme ?? "https")://\(host)/…"
}
/// 运行时文档只允许暴露数量与模式元数据。
static func documentSummary(_ document: SslocalRuntimeDocument) -> String {
let protocols = document.locals.map(\.inboundProtocol).joined(separator: ",")
return "servers=\(document.servers.count) protocols=\(protocols) mode=\(document.socksMode)"
}
}
/// 事件发射缝:wrapper 与 GUI 共用 stderr 行式输出(wrapper 侧由 launchd/
/// agent.log 收敛,GUI 侧进系统日志);GUI 启动时另注册接收缝接入事件缓冲。
enum RuntimeLog {
private static let sinkHolder = SinkHolder()
/// 注册/替换事件接收缝;传 nil 恢复纯 stderr(测试隔离用)。
static func setSink(_ sink: (any RuntimeEventSink)?) {
sinkHolder.set(sink)
}
static func emit(_ event: RuntimeLogEvent) {
let line = "ssxng: \(event.description)\n"
FileHandle.standardError.write(Data(line.utf8))
sinkHolder.current?.append(event: event, timestamp: Date())
}
/// 可变静态的锁保护持有者:emit 可能来自任意线程。
private final class SinkHolder: @unchecked Sendable {
private let lock = NSLock()
private var sink: (any RuntimeEventSink)?
var current: (any RuntimeEventSink)? {
lock.lock()
defer { lock.unlock() }
return sink
}
func set(_ value: (any RuntimeEventSink)?) {
lock.lock()
defer { lock.unlock() }
sink = value
}
}
}