Files
Qiu Yuzhou 94ef128db8 feat(system-proxy): emit privileged helper events to unified log
The root system-proxy helper logged nothing and launchd discards its
stdout/stderr, leaving helper launches, connection refusals, GUI-side
timeouts and root-side failures invisible. Share the minimal unified-log
emission unit (SystemLog) across GUI, wrapper and helper, and add a
dedicated closed helper event enum (lifecycle minimum: listener ready,
connection accepted/rejected with peer pid, typed apply/clear results)
with ADR-0045 privacy splitting. The emission seam is injected at the
helper engine and listener for test doubles. Helper events are
Console-only: no stderr, no agent.log, no diagnostic view or report
surface (ADR-0047).
2026-10-20 15:59:38 +08:00

99 lines
3.8 KiB
Swift
Raw Permalink 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
import os
/// 脱敏后行的去向缝:intake 只产出行(`LogLine`),行最终落在哪里由
/// adapter 决定。
protocol SslocalLineDestination: Sendable {
func receive(_ row: LogLine)
}
/// adapter:wrapper 的 stderr。`redirectStandardStreams` 以 freopen 把 stderr
/// 收敛进 agent.log,行按统一行格式渲染(时间戳/级别/来源与诊断查看器同一
/// 口径)。handle 可注入(生产默认标准错误),测试用管道替身断言序列化,
/// 不碰进程全局 stderr。
struct SslocalStandardErrorDestination: SslocalLineDestination {
private let handle: FileHandle
init(handle: FileHandle = .standardError) {
self.handle = handle
}
func receive(_ row: LogLine) {
handle.write(Data((row.rendered + "\n").utf8))
}
}
/// adapter:统一日志(category `sslocal`,与 RuntimeLogEvent 同 subsystem)。
/// 上游输出是自由文本,不进封闭枚举事件流;整行标 private——上游默认日志
/// 可能含服务器地址(ADR-0014),正文阅读继续走 agent.log 查看面。Console
/// 获得时间戳、category、level 与行数关联;stderr/agent.log 通路没有这些
/// 可过滤维度。级别由 intake 探测,这里只投影到统一日志五级。
struct SslocalUnifiedLogDestination: SslocalLineDestination {
private let logger = Logger(subsystem: SystemLog.subsystem, category: "sslocal")
func receive(_ row: LogLine) {
// 统一日志写入受测试缝控制(SSXNG_SYSTEM_LOG=0 关闭),agent.log 去向不受影响。
guard SystemLog.systemLogEnabled else { return }
logger.log(level: OSLogType(row.level), "\(row.text, privacy: .private)")
}
}
/// sslocal 输出 intake:chunk-safe 行分帧 + 密码脱敏 + 超长行丢弃,把脱敏后的
/// 行送到去向缝。FileHandle 回调通过锁串行访问缓冲区,避免分块输入绕过隐藏
/// 规则。`write(Data())` 表示输入结束,冲刷尾部半行。
///
/// 这是上游自由文本进入日志去向的唯一入口:脱敏规则只在这一处,不再随去向
/// 复制;上游认证失败日志包含客户端提交的密码,按完整行隐藏密码后再分发。
/// 行模型(时间戳、级别探测、来源 `sslocal`)也在此构造,各 adapter 只做投影。
final class SslocalOutputIntake: @unchecked Sendable {
private let lock = NSLock()
private var buffer = Data()
private var discardingOversizedLine = false
private let destinations: [any SslocalLineDestination]
private let now: () -> Date
init(destinations: [any SslocalLineDestination], now: @escaping () -> Date = { Date() }) {
self.destinations = destinations
self.now = now
}
func write(_ data: Data) {
lock.lock()
defer { lock.unlock() }
if discardingOversizedLine {
guard let newline = data.firstIndex(of: 10) else { return }
discardingOversizedLine = false
buffer.append(data.suffix(from: data.index(after: newline)))
} else {
buffer.append(data)
}
while let newline = buffer.firstIndex(of: 10) {
emitLine(String(data: buffer.prefix(upTo: newline), encoding: .utf8))
buffer.removeSubrange(...newline)
}
if data.isEmpty && !buffer.isEmpty {
emitLine(String(data: buffer, encoding: .utf8))
buffer.removeAll()
} else if buffer.count > 65_536 {
buffer.removeAll()
discardingOversizedLine = true
emitLine("log line truncated")
}
}
private func emitLine(_ text: String?) {
guard let text else { return }
let sanitized = text.replacingOccurrences(
of: "password: [^\\r\\n]*",
with: "password: [redacted]", options: .regularExpression)
let row = LogLine(
timestamp: now(),
level: .detect(in: sanitized),
source: .sslocal,
text: sanitized)
for destination in destinations {
destination.receive(row)
}
}
}