mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 10:54:50 +00:00
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).
99 lines
3.8 KiB
Swift
99 lines
3.8 KiB
Swift
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)
|
||
}
|
||
}
|
||
}
|