mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-14 02:04:13 +00:00
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。
166 lines
7.2 KiB
Swift
166 lines
7.2 KiB
Swift
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
|
||
}
|
||
}
|
||
}
|