import Foundation import os /// 运行时日志基线:事件是封闭枚举,脱敏由构造保证——永不 /// 携带密码、插件参数、Keychain 值、完整订阅 URL 及其 token 或运行时 JSON /// 内容;测试 URL 属于非敏感信息,可完整记录。服务器地址与备注默认不进 /// 普通日志(端点探测失败点名的 host:port 是本机监听端点,属于需要呈现的 /// 事实字段,不受此限)。日志查看与诊断导出均以本事件流为数据源。 enum RuntimeLogEvent: Equatable, CustomStringConvertible, Sendable { /// 契约文件已原子写入(只携带数量元数据,非内容)。 case contractWritten(serverCount: Int) /// 契约内容与磁盘一致,跳过写入(幂等)。 case contractUnchanged /// 系统代理期望值与系统当前应用值语义等价,跳过写入(零授权)。 case systemProxyUnchanged /// 特权系统代理 helper 的 SMAppService 注册失败(不含内容)。 case systemProxyHelperRegisterFailed(detail: String) /// 系统代理写入/清除请求失败(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 拉起后限时内未完成本地监听绑定(「3 秒未建立监听 → error /// 日志」;监管继续,GUI 健康门负责呈现,端点为本机监听端点不受脱敏限制)。 case listenNotEstablished(detail: String) case contractMissing case contractInvalid case reloadForwarded case reloadDeferred case reloadRestarted /// 本机代理端点启动后未就绪(点名端点与端口)。 case endpointProbeFailed(host: String, port: Int, detail: String) /// 激活失败或活动目标清除(原因枚举已点名且不含秘密)。 case activationFailed(reason: String) /// 用户显式触发的脱敏诊断导出已完成(不携带导出路径)。 case diagnosticsExported /// 偏好设置持久化读取失败,已回落出厂默认(不静默改端口——事件进 /// 诊断日志由查看器呈现;完整日志查看器属于独立的诊断界面。 case settingsUnreadable(detail: String) /// 启动激活重试事实(键盘启动器场景诊断):attempt 从 1 起;active 为该拍 /// 检查时的应用激活状态;policyRegular 为进程本地策略是否已 regular(本地 /// 为真而 Dock 图标缺失说明系统侧注册滞后);frontmost 为当时前台应用 /// bundle id(无则 "-");menuBarOwner 为菜单栏实际归属应用(active=true 而 /// menuBarOwner 非本应用即激活假象,重试继续);onActiveSpace 为主窗口是否 /// 在当前空间;windowVisible 为该拍窗口是否可见(首拍 false 说明链在窗口 /// 呈现完成前启动、已终结待 re-arm)。正常启动只有 attempt=1 一条且各信号 /// 一致。 case launchActivationRetry( attempt: Int, active: Bool, policyRegular: Bool, frontmost: String, menuBarOwner: String, onActiveSpace: Bool, windowVisible: Bool) /// 窗口锚点附着事实(ADR 0017 激活协调):锚点上交宿主窗口时记录当时的 /// 可见/key 状态——用于定位启动呈现路径下附着与可见化的先后次序(失败 /// 场景启动期无任何激活事件时由此排查附着是否发生、时机如何)。 case windowAnchorAttached(visible: Bool, key: Bool) case connectivityMeasurement(id: UUID, entry: ConnectivityTrace) /// stderr 行式输出(统一行格式,见 `LogLine`);wrapper 侧收敛进 /// agent.log。连通性测量内容只经应用内日志查看器显式查看,不落 stderr /// 行与诊断报告。 func standardErrorLine(timestamp: Date = Date()) -> String? { if case .connectivityMeasurement = self { return nil } return LogLine(event: self, timestamp: timestamp, source: RuntimeLog.lineSource).rendered + "\n" } var description: String { switch self { case .connectivityMeasurement(let id, let entry): return "connectivity \(id.uuidString) \(entry.description)" 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 .contractInvalid: return "supervision invalid, preserved" 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 .settingsUnreadable(let detail): return "settings unreadable, falling back to factory defaults: \(detail)" case .launchActivationRetry( let attempt, let active, let policyRegular, let frontmost, let menuBarOwner, let onActiveSpace, let windowVisible): return "launch activation retry #\(attempt) active=\(active)" + " policy=\(policyRegular ? "regular" : "accessory")" + " frontmost=\(frontmost) menubar=\(menuBarOwner)" + " space=\(onActiveSpace ? "on" : "off") visible=\(windowVisible)" case .windowAnchorAttached(let visible, let key): return "window anchor attached visible=\(visible) key=\(key)" } } } /// 系统日志路由(统一日志,Console.app 可按 subsystem/category 过滤): /// level 三分(失败类 error、里程碑 default、冗长测量 info)、category 五分 /// (runtime / supervision / system-proxy / connectivity / diagnostics; /// sslocal 输出 intake 的自由文本行走独立的 category `sslocal`,不经本枚举)。 /// 去向、privacy 与第三方库取舍见 ADR-0045。 /// 拆分规则:`text` + `privateDetail` == `description`;自由错误 detail 与 /// 连通性测量内容(含探测目标 IP)拆出由发射缝标 private,公开文本由 /// description 尾部裁切派生,不手抄前缀。穷举 switch:新事件漏路由无法编译。 extension RuntimeLogEvent { var systemLogRoute: SystemLogRoute { switch self { case .contractWritten, .contractUnchanged, .runtimeFilesDeleted: return SystemLogRoute( level: .default, category: "runtime", text: description, privateDetail: nil) case .contractMissing, .contractInvalid, .activationFailed: return SystemLogRoute( level: .error, category: "runtime", text: description, privateDetail: nil) case .runtimePersistFailed(let detail): return SystemLogRoute( level: .error, category: "runtime", text: String(description.dropLast(detail.count)), privateDetail: detail) case .settingsUnreadable(let detail): return SystemLogRoute( level: .error, category: "runtime", text: String(description.dropLast(detail.count)), privateDetail: detail) case .agentRegistered, .agentUnregistered, .sslocalSpawned, .sslocalStopRequested, .reloadForwarded, .reloadDeferred, .reloadRestarted: return SystemLogRoute( level: .default, category: "supervision", text: description, privateDetail: nil) case .sslocalSpawnFailed, .sslocalExitedUnexpectedly, .listenNotEstablished: return SystemLogRoute( level: .error, category: "supervision", text: description, privateDetail: nil) case .agentRegisterFailed(let detail): return SystemLogRoute( level: .error, category: "supervision", text: String(description.dropLast(detail.count)), privateDetail: detail) case .agentUnregisterFailed(let detail): return SystemLogRoute( level: .error, category: "supervision", text: String(description.dropLast(detail.count)), privateDetail: detail) case .systemProxyUnchanged: return SystemLogRoute( level: .default, category: "system-proxy", text: description, privateDetail: nil) case .systemProxyHelperRegisterFailed(let detail): return SystemLogRoute( level: .error, category: "system-proxy", text: String(description.dropLast(detail.count)), privateDetail: detail) case .systemProxyWriteFailed(let detail): return SystemLogRoute( level: .error, category: "system-proxy", text: String(description.dropLast(detail.count)), privateDetail: detail) case .endpointProbeFailed(_, _, let detail): return SystemLogRoute( level: .error, category: "connectivity", text: String(description.dropLast(detail.count)), privateDetail: detail) case .connectivityMeasurement(_, let entry): // 测量内容含探测目标 IP,整段标 private(普通日志不进服务器地址)。 return SystemLogRoute( level: .info, category: "connectivity", text: String(description.dropLast(entry.description.count)), privateDetail: entry.description) case .diagnosticsExported: return SystemLogRoute( level: .default, category: "diagnostics", text: description, privateDetail: nil) case .launchActivationRetry, .windowAnchorAttached: // 前台应用 bundle id 等用户环境事实:进应用内日志查看器与统一日志, // 不进共享诊断报告(白名单见诊断 module)。 return SystemLogRoute( level: .default, category: "window", text: description, privateDetail: nil) } } } /// 事件接收缝:GUI 进程把事件接入 RuntimeEventStore(内存环形 /// 缓冲),供主窗口日志查看器实时呈现与诊断导出取用;wrapper 进程不注册 /// (stderr 收敛进 agent.log)。接收方拿到的是封闭枚举原值,渲染发生在 /// 呈现边缘(诊断报告的白名单清洗只可能作用于原值)。 protocol RuntimeEventSink: Sendable { func append(event: RuntimeLogEvent, timestamp: Date) } /// 敏感信息脱敏工具(「敏感信息」定义的读取面)。 enum Redactor { /// 敏感远程 URL 只保留 scheme+host:路径、查询与片段全部略去,完整订阅 /// URL 与 URL token 永不入日志。解析失败一律归并为占位符。 /// 测试 URL 无需脱敏,不使用此方法。 static func remoteURL(_ url: String) -> String { guard let parsed = URL(string: url), let host = parsed.host(), !host.isEmpty else { return "" } return "\(parsed.scheme ?? "https")://\(host)/…" } } /// 事件发射缝:wrapper 与 GUI 共用 stderr 行式输出(wrapper 侧收敛进 /// agent.log)与系统日志输出(统一 subsystem,Console.app 按 category 过滤, /// 拆出的 detail 段标 private);GUI 启动时另注册接收缝接入事件缓冲。 enum RuntimeLog { private static let sinkHolder = SinkHolder() private static let lineSourceLock = OSAllocatedUnfairLock(initialState: LogLineSource.gui) /// stderr 行的来源标记:GUI 进程默认 `gui`,wrapper 启动时置 `wrapper`, /// 同一枚举事件的两个进程由此在行内可分辨。启动期一次性设置。 static var lineSource: LogLineSource { lineSourceLock.withLock { $0 } } static func setLineSource(_ source: LogLineSource) { lineSourceLock.withLock { $0 = source } } /// 注册/替换事件接收缝;传 nil 移除接收缝(测试隔离用)。 static func setSink(_ sink: (any RuntimeEventSink)?) { sinkHolder.set(sink) } static func emit(_ event: RuntimeLogEvent) { let now = Date() if let line = event.standardErrorLine(timestamp: now) { FileHandle.standardError.write(Data(line.utf8)) } SystemLog.emit(event.systemLogRoute) sinkHolder.current?.append(event: event, timestamp: now) } /// 可变静态的锁保护持有者: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 } } } /// Structured latency observations for explicit local log viewing. These events /// are excluded from shared diagnostic reports, including their target IPs. struct ConnectivityTrace: Equatable, Sendable, CustomStringConvertible { enum Phase: String, Sendable { case startup, google, cloudflare, candidate, selected, sample, summary } enum Status: String, Sendable { case success, network, timeout, http, configuration, cancelled, insufficientSamples } enum FailureStage: String, Sendable { case none, startup, dns, selection, request, send, headers case localTCP = "local_tcp" case firstByte = "first_byte" } var phase: Phase var status: Status var index: Int = 0 var address: String? var milliseconds: Double? var localTCPMilliseconds: Double? var sendMilliseconds: Double? var firstByteMilliseconds: Double? var headersMilliseconds: Double? var failureStage: FailureStage = .none var httpStatus: Int? var description: String { var fields = ["phase=\(phase.rawValue)", "result=\(status.rawValue)"] if index > 0 { fields.append("index=\(index)") } if let address { fields.append("ip=\(address)") } if let httpStatus { fields.append("http=\(httpStatus)") } if failureStage != .none { fields.append("failure_stage=\(failureStage.rawValue)") } let times: [(String, Double?)] = [ ("total_ms", milliseconds), ("local_tcp_ms", localTCPMilliseconds), ("send_ms", sendMilliseconds), ("wait_first_byte_ms", firstByteMilliseconds), ("first_byte_to_headers_ms", headersMilliseconds), ] for (name, value) in times { if let value { fields.append( "\(name)=\(String(format: "%.3f", locale: Locale(identifier: "en_US_POSIX"), value))") } } return fields.joined(separator: " ") } }