标签: 日志诊断

  • URLFile 缓存完整性:查询串不入键与接受 200 全量响应

    结论:hash_url() 只对去掉 ? 之后的 URL 计算 MD5 作为缓存键,因此签名查询串变化仍共用同一缓存,可能读到另一版本甚至另一授权上下文的字节。read() 默认把请求范围扩到 1,000,000 字节对齐块,但即使某块实际响应短于要求,也不核对块大小、Content-Range 或最终输出长度,返回后位置仍设为目标终点;get_multi_range() 接受 HTTP 200/206 且不核对单段偏移。以下为固定快照 c8fb906 的静态阅读与隔离探针,未访问远端、未运行会删除本机缓存的上游测试、未读取真实路段。

    函数链

    url_file.py 是离线日志/视频的远端字节入口,其文件接口与常规文件类似,但每一层都缺少内容校验。下表列出关键函数。

    请求层与文件层的语义也要分开看:pool_manager()/_request() 复用 urllib3 池并配置特定重试与超时,但超时是请求级配置,不提供整个多块读取操作的总 deadline;seek() 只更新内部位置,SEEK_END 依赖已知长度,既不限制负偏移也不限制越过 EOF,异常要等后续读取才暴露。

    函数 实现 数据契约与风险
    hash_url() 去掉 ? 后的 URL 计算 MD5,作为块与长度缓存键 同一路径但查询参数不同(如带签名 URL)会共享缓存;MD5 这里只是文件名,不是内容完整性校验
    pool_manager() / _request() 复用 urllib3 池,配置特定 409/429/503/504 重试及 connect/read 超时;fork 后重置池引用 超时是请求级配置,不提供整个多块读取操作的总 deadline
    get_length_online() / get_length() HEAD 的 2xx Content-Length 转整数;缓存长度单独落盘 无 Content-Length 按 0 处理,非 2xx 返回 -1;本地长度缓存不随远端 ETag/内容版本自动失效
    read() 默认把范围扩到 1,000,000 字节对齐块;不存在时从远端读并原子写入缓存,命中时直接读文件,再裁切请求片段 即使某块实际响应短于要求,也未核对块大小、Content-Range 或最终输出长度;请求零长度也会进入块处理循环
    read_aux() 以当前位置构造单段 Range,调用 get_multi_range(),按返回字节数推进位置 仅依赖收到的字节,不校验其与请求区间的对应关系
    get_multi_range() 构造含端点的 Range 头;接受 HTTP 200/206;非 multipart 直接返回单个字节串,multipart 按 boundary 切分 不核对单段 Content-Range、起止偏移或长度;multipart 用 .rstrip(b"rn") 可能删掉合法尾部字节
    prune_cache() manifest.txt 存每个新块的写入时刻,按条目数乘块大小估计容量,再删最旧条目 缓存命中不刷新时间,实际是近似创建时间淘汰,不是访问级 LRU;真实块大小与并发写入未纳入容量/事务保证
    seek() 更新内部位置;SEEK_END 依赖已知长度 不限制负偏移或越过 EOF,后续读取才可能暴露异常

    Range 响应隔离验证

    四项原方法探针从归档提取 get_multi_range()/read_aux(),注入固定 HTTP 响应,不加载 urllib3 或写入缓存。

    假响应与请求 实际结果 合约缺口
    请求 bytes=5-7,返回 200 和完整 10 字节文件 原样返回 10 字节;read_aux(ll=3) 从位置 5 前进到 15 200 全文件没有按 Range 裁切,也不校验 Content-Range 或请求长度
    请求 5 字节,multipart 单段载荷为 abcrn 结果为 abc .rstrip(b"rn") 把合法载荷尾部也删掉
    请求两个 Range,206 非 multipart 只返回 AB 返回 [b"AB"] 非 multipart 分支没有检查响应段数与请求数
    请求一个 Range,返回 404 抛 URLFileException 状态拒绝正常,但不证明其他状态的字节正确

    这些输入在原函数分支下可复现,但不是对真实存储服务会如此响应的断言。生产修复需按请求逐段核对状态、Content-Range、实际长度和顺序,不能只依赖状态码或 multipart 段数。

    缓存读取隔离验证

    三项原方法探针用临时目录运行 read()、read_aux()、hash_url() 和原子写;为降低开销,只把全局 CHUNK_SIZE 从生产的 1,000,000 改为 4 字节,远端响应仍由内存假方法提供。

    受控输入 实际结果 边界
    请求 4 字节,假来源仅给 ab 返回 2 字节,缓存也保存 ab,位置却推进到请求终点 4 缓存存在性代替内容长度/范围校验,短块可成为持续错误输入
    read(ll=0) 返回空字节,但仍取一次 4 字节块并写缓存 零长度请求没有在 I/O 前早退
    相同 URL 路径,仅签名查询串变化 新请求读到前一查询串的 old! 缓存,没有调用假远端 缓存键不含查询串或远端内容版本

    这些结果验证原函数在缩尺块长下的控制流,不能证明真实 1 MB 块的性能、并发缓存写入、远端授权差异或实际模型回放输出。

    对模型与硬件回归的影响

    FileReader() 将 HTTP(S) 交给 URLFile;LogReader 与视频 FfmpegDecoder.get_iterator() 因此可能消费这些缓存块。若服务端忽略 Range 返回 200 全文件,非零偏移读可取得错误片段;若旧缓存与新 URL 查询串共享键,回放可能混合版本。因此 HEAD 成功、缓存命中、Cap’n Proto 可解析或 FFmpeg 能输出帧,都不能单独证明输入字节与固定参考数据一致。

    现有 test_caching.py 检查默认池配置、正常缓存与非缓存读的一致性、缺失文件恢复和淘汰;其中部分测试访问公网,还有测试会删除下载缓存根目录,故本次没有运行该上游套件。隔离探针现已覆盖忽略 Range 的 200 响应、multipart 尾部 CRLF、短缓存块、零长度读取和查询串共用键;并发清单覆盖、真实云端版本和内容哈希仍未动态验证。

    分块缓存的键、长度与时序都缺少内容身份:hash_url() 用 URL 路径的 MD5 作键,长度缓存单独落盘且不随远端 ETag/内容版本失效,prune_cache() 的容量估计只按条目数乘块大小,不含真实块大小、_length 文件与并发写入。因此「缓存命中」只说明本地存在文件,不说明该文件与当前远端内容一致。

    面向插件化重构,远端数据源应携带固定内容摘要/大小、ETag 或版本 ID,并将身份和版本纳入缓存键;每次 Range 响应检查状态、Content-Range、长度及顺序,完整文件下载后校验 SHA-256。缓存清单应可并发更新且按实际字节计量,明确过期/撤销与隔离域。自制硬件的模型回归必须保存完整输入清单,而不是仅保存可变 URL。

    常见问题

    带签名的 URL 会共用缓存吗?

    会。hash_url() 只对去掉 ? 之后的 URL 取 MD5,查询串不入键;若响应内容由查询参数决定,可能读到另一版本甚至另一授权上下文的字节。这里的 MD5 只是文件名,不是内容完整性校验。

    缓存命中的块长度可信吗?

    不可信。缩尺探针里请求 4 字节、假来源只给 2 字节时,read() 返回 2 字节、缓存也保存 ab,但位置推进到请求终点 4;再次读取会直接复用这个短块。

    服务端忽略 Range、返回 200 全文件会怎样?

    请求 bytes=5-7 却拿到完整 10 字节时,原函数原样返回 10 字节,read_aux(ll=3) 从位置 5 前进到 15,既不按 Range 裁切也不校验 Content-Range。

    怎样防止回放混合版本?

    让远端数据源携带固定内容摘要/大小、ETag 或版本 ID,并把身份与版本纳入缓存键;每次 Range 响应检查状态、Content-Range、长度及顺序,完整下载后校验 SHA-256。模型回归应保存完整输入清单,而不是仅保存可变 URL。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/URLFILE_RANGE_CACHE_INTEGRITY_DEEP_DIVE.md(HTTP Range、分块缓存与回放完整性)
    • 项目 02 —— openpilot-research/LOGREADER_DECODE_ITERATION_LIFECYCLE_DEEP_DIVE.md(消费缓存块的日志解码器,作为本文的补充阅读)

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 开发日志工具的三个权限等级:转储、音频导出与状态刺激

    结论:四个开发脚本的权限差异很大,不应统称「开发插件」。dump.py 与 filter_log_message.py 主要读取消息/日志,extract_audio.py 会导出含乘员信息的音频,而 cycle_alerts.py 会主动发布核心状态主题(selfdriveState、deviceState、pandaStates)。至少两个反直觉的事实:--json 对 Cap’n Proto 原始字节调用 json.loads 通常无法解析,真正转 JSON 的是 --dump-json;cycle_alerts.py 先声明九个告警却随即重赋值为只剩两项,实际只循环 cameraMalfunction 与 cameraFrameRate。远端 --addr 不能当作权限、脱敏或租户隔离策略。

    四个脚本,三种权限等级

    把数据流和副作用放在一起看,权限差异一目了然:前三个工具的输出主要是「读出来的内容」,第四个则会争用发布入口。

    工具 数据流 实际输出或副作用
    dump.py raw_live_logreader() 订阅指定地址主题,默认为 SERVICE_LIST 全部服务;每条原始字节先按 log.Event 解码 可打印 Event、十六进制或字段;--addr 非环回时底层切换 ZMQ 并重置消息上下文
    filter_log_message.py 有路段参数时用排序后的 LogReader 查 logMessage、errorLogMessage、operatingSystemLog;无路段时实时订阅前者与 OS 日志 打印 JSON 结构化日志或 OS 日志;实时模式未订阅 errorLogMessage
    extract_audio.py ReadMode.AUTO_INTERACTIVE 选路段来源,LogReader.filter("rawAudioData") 得载荷,拼成 int16 样本 经 write_wav_file() 导出单声道 WAV,或经 play_audio() 本机播放
    cycle_alerts.py 构造 Honda 参数与随机化 SubMaster 状态,用 Events.create_alerts()、AlertManager 生成告警 每 200 个控制周期发布 selfdriveState、deviceState、pandaStates,循环不停

    dump.py 与 filter_log_message.py 的失效点

    dump.py 即使指定 --no-print 仍逐条解析 Event;--pipe 用 str(msg) 写标准输出,得到的是 Python 字节表示文本而不是原始二进制流;--json 对原始字节调用 json.loads 通常失败。--values 只检查路径第一段存在,后续 getattr 缺失可直接抛错。顶层 codecs.register_error("strict", ...) 还会影响进程内使用该错误处理器的解码行为,插件封装时不应默许这种全局修改。

    filter_log_message.py 的坑集中在时间与字段两处。等级过滤只作用于结构化日志,不影响 OS 日志;离线非 --absolute 的时间从第一条读取到的消息起算,而不是第一条匹配日志,且多个路段共享该时间原点、段间不重排序;实时模式则用原始 logMonoTime 纳秒换算秒数,两者语义不同。print_logmessage() 只捕获 JSON 解析异常,若 JSON 缺 levelnum、filename 或 msg 会抛键错误。

    工具 易踩边界 原因
    dump.py --json 通常解析失败 对 Cap’n Proto 原始字节调用 json.loads;转换应使用 --dump-json
    dump.py 解码行为被全局改写 顶层注册了 "strict" 错误处理器
    filter_log_message.py 相对时间原点不是首条匹配日志 离线时间从第一条读取到的消息起算,多路段共享
    filter_log_message.py 实时模式拿不到错误日志 实时订阅未包含 errorLogMessage

    两个工具都会打印原始事件或日志内容,可能包含标识符、位置、进程路径及异常栈,因此远端 --addr 不能作为生产环境的权限、脱敏或租户隔离策略。

    extract_audio.py:WAV 连续性与隐私

    extract_audio.py 把所有 rawAudioData.data 按 NumPy 原生端序的 int16 视为连续样本,再原样写成 WAV,未显式转换字节序。它只取第一条的 sampleRate,不检查之后的采样率变化、通道数、消息时间戳或缺包间隔,因此导出文件不承诺与原车时间轴严格连续。它先把所有消息和样本拼成内存数组,长路段可能占用较大内存。

    隐私侧还有一个关键前提:rawAudioData 缺省 should_log=False,loggerd 只有在启动时读取 RecordAudio 为真才订阅该主题。所以没有音频消息不等于麦克风硬件故障;而一旦录制,导出的语音可能含高度敏感的乘员信息。使用上,输出文件已存在时会交互确认,--play 会使用本机音频设备;只传 --play 或 -o 而不传路段名,主程序仍执行 args.route_or_segment_name.strip(),会报 AttributeError。

    cycle_alerts.py:发布核心状态的边界

    cycle_alerts.py 先声明九个可听告警,但随后把 alerts 重赋值为仅 cameraMalfunction 与 cameraFrameRate 两项,本快照实际只循环这两项。它随机改写 SubMaster 内的设备状态、模型速度、进程列表和主题 alive/valid/freq_ok,没有对这些订阅数据调用 sm.update(),由此产生的告警文本是随机夹具结果,不是车辆健康检查。

    发送消息由 new_message() 构造,默认 Event valid=False,脚本没有覆写。即便如此,在实际设备上争用 selfdriveState、deviceState、pandaStates 的发布入口仍会干扰 UI 和状态消费者,不能作为行车调试插件。脚本没有发送 sendcan 或直接控制 ECU,但这不等于发布核心状态没有风险。

    硬件与插件化约束

    建议把原始消息/日志检索、音频导出、状态刺激拆成三个权限等级,而不是统称「开发插件」:观测插件仅获指定主题、时间范围及字段脱敏后的只读视图;音频导出需要单独授权、留审计并限定本地文件生命周期;状态刺激仅允许仿真进程或隔离测试总线,不能发布到生产 deviceState/pandaStates。源文档明确这些是重构要求,不是当前工具已有的隔离保证。

    常见问题

    dump.py 的 –json 为什么常常解析失败?

    因为它对 Cap’n Proto 原始字节直接调用 json.loads(msg),通常无法作为 JSON 解析。源文档指出真正把 Event 转为 JSON 文本的是 --dump-json;--pipe 得到的也只是 Python 字节表示文本,不是原始二进制流。

    导出的音频能当作和原车同步的时间轴证据吗?

    不能。extract_audio.py 只取第一条的 sampleRate,不检查之后的采样率变化、通道数、消息时间戳或缺包间隔,而且是按 NumPy 原生端序把 int16 视为连续样本原样写 WAV,因此导出文件不承诺与原车时间轴严格连续。

    cycle_alerts.py 会循环几个告警?

    本快照实际只循环两个:cameraMalfunction 与 cameraFrameRate。脚本先声明九个可听告警,随后把 alerts 重赋值为这两项,且告警文本来自随机夹具结果,不是车辆健康检查。

    这些开发工具能当作「只读」吗?

    不能一概而论。前三个工具主要读取数据,但远端 --addr 不是权限或脱敏策略;cycle_alerts.py 则主动发布三个核心状态主题,会干扰 UI 与状态消费者。音频导出还涉及高度敏感的乘员信息,应单独授权与审计。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DEV_LOG_AUDIO_ALERT_TOOL_BOUNDARIES_DEEP_DIVE.md(取用了四个开发脚本的数据流与副作用、转储与日志过滤失效点、音频导出连续性与隐私门、告警刺激的真实范围)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • LogReader 生命周期:reset 不清缓存与跨片段不归并

    结论:LogReader.reset() 只重算路径清单、并不清 __lrs;原类探针确认先读 first、把 identifier 改成 second 后调用 reset(),迭代仍返回 first 的缓存而未读取第二文件。sort_by_time=True 只在该文件内部排序,多个文件按 logreader_identifiers 列表顺序拼接、不做跨片段归并:假事件探针在两个文件内分别给出 30,10 与 20,启用排序后整体输出 10,30,20。以下为固定快照 c8fb906 的静态阅读与隔离探针,未读取真实路段、未执行进程回放。

    关键函数与数据契约

    解码围绕「单文件一次读入 + 惰性片段加载 + 事件包装」展开。下表列出关键函数、源码事实与插件化含义。

    函数 源码事实 风险与插件化含义
    save_log() 把每个事件重新序列化并 b"".join() 成完整缓冲;按输出后缀可选 bz2/zstd 压缩,再直接 open(..., "wb") 写目标 序列化异常发生在打开目标前;写入异常可能留下被截断的旧目标;无隐私净化、哈希核验、原子替换或断电持久化
    decompress_stream() / _LogFileReader.__init__() FileReader 一次 read() 整个文件;先按 .bz2/.zst 扩展名,否则按 BZh9/zstd 魔数决定解压 后缀优先于魔数;错后缀会进入对应解压器并失败;压缩输入、解压输出与事件列表可能同时占内存
    损坏事件分支 遍历 Cap’n Proto 事件时若抛 capnp.KjException,发出 RuntimeWarning,保留异常前已加入的事件 消费者仍可得到合法前缀,但没有返回「日志已截断」的结构化状态
    CachedEventReader which() 首次计算后缓存 union 名称,普通字段访问交给原事件;__reduce__ 用于跨进程 只是读取加速包装,不验证事件 schema、有效位或字段语义
    _LogFileReader.__iter__() 默认逐个返回包装事件;only_union_types=True 时调用 which(),判别失败则跳过 「仅 union 类型」会静默丢掉无法判别的事件,不是修复坏日志;sort_by_time 仅文件内排序
    LogReader._get_lr() / __iter__() 按索引首次构造 _LogFileReader 后保存在 __lrs;多文件按标识列表顺序拼接 已加载事件列表长期留存;无跨片段归并
    LogReader.reset() 重新计算 logreader_identifiers,没有清 __lrs 改标识后迭代仍返回旧缓存;源码内仅构造函数调用它
    run_across_segments() / from_bytes() 前者用进程池 imap 按片段调用并聚合;后者以字节缓冲构建单文件读取器 存在序列化/内存成本;from_bytes(b'') 会触发空缓冲回退,不是可用空日志

    解码与迭代的边界

    「能解析」并不等于「完整」。坏 union 过滤会静默丢事件;dat=b'' 因 if not dat 被当作未提供而尝试打开路径。下表的受控输入在原函数分支下可复现。

    输入 / 操作 实际结果 边界
    压缩后缀与实际格式不一致 后缀优先,错后缀进入对应解压器并失败 compress=False 即使后缀是 .zst 也写原始字节
    dat=b'' 被当作未提供,尝试打开路径 不是可用的空日志表示
    坏 union + only_union_types=True 判别失败的事件被跳过 静默丢失,不是修复
    两个文件分别给 30,10 与 20 启用排序后整体输出 10,30,20 确认没有跨片段时间归并
    reset() 后同索引 返回旧缓存,未读取新文件 路径清单已变但事件缓存未失效

    损坏事件分支保留异常前已加入的事件并发 RuntimeWarning,但没有返回「日志已截断」的结构化状态;消费者仍可得到合法前缀,因此完整性检查必须比较预期段、时间跨度、消息数与内容摘要。同理,only_union_types=True 会静默丢掉无法判别的事件,这不是修复坏日志。

    故障传播与现有测试

    六项导出隔离探针用原 save_log()、假消息和真实 bz2/zstandard 库验证:未知后缀直写拼接字节;bz2/zstd 后缀能解压回原字节;compress=False 可产出后缀与实际格式不一致的文件;序列化失败时现有目标保持原状;模拟写入失败时现有目标被 wb 截断并仅留下两字节。它未测试真实 Cap’n Proto 消息、断电持久性或大文件内存峰值。

    该导出函数的调用方包括 run_process_on_route.py、进程回放测试与 PlotJuggler 桥;PlotJuggler 明确用 compress=False 写临时 .rlog。已有 test_logreader.py 验证重复迭代只构造一次 _LogFileReader,并覆盖文件内排序与坏 union 过滤。

    并行与容错侧还有两点:run_across_segments() 用进程池 imap 按片段调用函数并聚合结果,from_bytes() 以字节缓冲构建单文件读取器;并行处理函数与事件包装存在序列化/内存成本,from_bytes(b'') 还会触发空缓冲回退。CachedEventReader 与 which() 缓存只是读取加速,不验证事件 schema、有效位或字段语义。日志的输入与输出都不是内容摘要证明——「可解析」既不代表完整,也不代表与固定参考一致。

    LogReader.first() 与 filter() 都会触发迭代,第一次调用可能下载/解压所需文件;重复调用使用缓存,但仍遍历事件。若读取的是远端来源,HTTP HEAD 探测成功与实际 FileReader.read() 完成也在不同阶段;任何解压/解析失败需要被单独归档,而不能被上层概括成「该路线不存在」。

    硬件与插件化验收

    回放输入插件应交付固定有序片段清单、内容摘要、压缩格式、schema 版本、消息条数和时间覆盖;解析器输出「完整/截断/坏 union/解压失败」的显式状态。单段读入应设内存与解压比上限,必要时改流式处理;在跨片段时间归并前校验时间域与单调性。

    reset/热重载必须同时使路径清单与已载入事件缓存失效,或把不可变快照做成新实例,不能仅更新标识列表。日志导出应由受信任边界执行字段级隐私审查、目标路径约束、临时文件与原子发布,再记录文件摘要;读取方应按魔数或受信任元数据核对真实压缩格式,不只看扩展名。

    常见问题

    reset 之后会读到新的文件吗?

    不会自动生效。reset() 只重算 logreader_identifiers,不清 __lrs;原类探针确认先读 first、改 identifier 为 second 后调用 reset(),实际迭代仍返回 first 的缓存。

    sort_by_time=True 会跨文件排序吗?

    不会。它只在该文件内部按 logMonoTime 排序;多个文件按标识列表顺序拼接,探针给出的整体输出 10,30,20 即说明没有跨片段归并。

    only_union_types 能修好坏日志吗?

    不能。它在迭代时调用 which(),凡 union 判别失败的事件都被跳过,是静默丢弃而不是修复;完整性检查必须比较预期段、时间跨度、消息数与内容摘要。

    save_log 是原子写吗?

    不是。序列化异常发生在打开目标之前;写入异常则可能留下被截断的旧目标。该路径没有隐私净化、哈希核验、原子替换或断电持久化。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/LOGREADER_DECODE_ITERATION_LIFECYCLE_DEEP_DIVE.md(单文件解码、惰性片段加载与迭代契约)
    • 项目 02 —— openpilot-research/LOG_SOURCE_AVAILABILITY_CACHE_DEEP_DIVE.md(日志来源发现与可用性,作为本文的补充阅读)
    • 项目 02 —— openpilot-research/URLFILE_RANGE_CACHE_INTEGRITY_DEEP_DIVE.md(远端字节与缓存完整性,支撑「HEAD 成功不等于内容一致」)

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 崩溃采集的三道门:100 MB 过滤、0o100640 模式与 12 小时暂缓

    结论:崩溃采集不是「一律上报」,而是一条跨进程链路,且至少被三道门约束:报告门(构建来源为官方 origin、本地已注册、非 PC 三者同时成立)、扫描过滤门(只接受精确 mode 0o100640 的 .crash,超过 100 MB 直接丢弃)、上传门(计费网络暂缓最近 12 小时的 crash/、HTTP 401/403/412 不再重试)。要特别记住两点:100 MB 阈值不是隐私过滤,完整 crash 副本可能含路径、进程环境与内存相关信息;而「已进入本地处理」也不等于云端已保存。

    入口:报告门只在进程启动时采样一次

    在固定源码快照里,tombstoned 被配置成非 PC 的常驻进程。它的 main() 启动时计算一次 should_report,要求构建元数据的官方 origin、is_registered_device() 与「非 PC」三项都成立。这里的「已注册」只依据本地 DongleId 非空且不为 UnregisteredDevice,不是每次崩溃都向后端重新鉴权。

    两个时序后果必须分开看。第一,should_report 不在循环中刷新,运行期间身份或来源发生变化不会立即改变本实例的处理门。第二,启动时 clear_apport_folder() 会遍历 /var/crash/* 尝试删除,之后才建立初始快照——这意味着重启前尚未处理的 crash 文件可能直接丢失,源文档给出的动机是避免重复报告。

    扫描与本地处理:三道门各自不管什么

    采集循环每五秒用 get_tombstones() 扫描至多 1000 个目录项,用「路径 + ctime」的集合差异找出新条目。下面这张表把各道门的条件和边界摊开——每一道的「不管什么」比它「管什么」更容易被误读。

    门 / 环节 条件 边界与后果
    报告门 官方 origin 且 is_registered_device() 且非 PC,仅在启动时计算一次 运行中改变返回值不会改变本实例的删除决策
    文件模式门 .crash 必须是精确 mode 0o100640 名字以 tombstone 开头的条目不受该 mode 门约束,但报告门打开时只记录「unknown crash type」,不进入解析与复制
    大小门 超过 100 MB 记错后返回 该分支不复制也不删除原文件;大小阈值不是隐私过滤
    目录常量 TOMBSTONE_DIR="/data/tombstones/" 已定义 该常量未被 get_tombstones() 使用,实际扫描的是 /var/crash/;不能据常量名推断 Android tombstone 也被处理

    解析与落盘阶段的关键行为见下表。摘要与完整副本走的是两条路径,清洗只作用于摘要,不改写完整文件。

    阶段 源码行为
    摘要 提取 ExecutablePath、Signal,经 apport-retrace -s 获取栈,优先找 openpilot/selfdrive 栈帧;去掉形似指针的参数并剥除圆括号内容后写 cloudlog.error。外部命令设 30 秒超时
    完整报告 用日期、提交哈希与清洗后路径组成文件名,复制完整原始 crash 文件到 Paths.log_root()/crash/,随后尝试删除 /var/crash 原件
    未启用报告 对新检测到的条目尝试删除;不会保留「本地可查但不上传」的独立模式
    处理异常 异常写 cloudlog,下一轮仍把当前集合设为基线;未变化的同一文件通常不会因本次失败自动重试

    需要强调的是:摘要去掉指针、剥掉圆括号内容,只是让日志更短,并不等于对 dump 做了脱敏审计。完整 crash 可能包含路径、进程环境和内存相关信息,不能因为 cloudlog 摘要删了指针就把副本视为匿名数据。

    上游处理与下游上传是两段独立判断

    Uploader 把 crash/ 放进 immediate_folders,next_file_to_upload() 会优先选该目录的未标记文件。但「优先」仍受网络与计费策略约束:在计费连接上,最近 12 小时的 crash/ 与 boot/ 文件会暂缓;无网络时主循环不上传。因此 tombstoned 的「reporting」日志只表示进入本地处理,不代表云端已收到文件。

    Uploader.upload() 把 HTTP 200/201/401/403/412 都列为停止重试的状态,并尝试写 user.upload xattr。其中 401/403 或 412 的情形要单独记忆:处理完成不等于服务端保存成功。回收侧 deleter_step() 在空间不足时优先删普通目录,把 boot/crash 列为最后删除,但这不是永久保留——存储紧张时崩溃副本最终仍可能被删掉。

    对自制硬件与插件平台的要求

    源文档给出的重构要求可以归纳为一组「必须分开授权」的清单:

    1. 崩溃诊断插件默认只读、驻车或后台运行;原始 dump 的收集、读取和外传分别授权,并提供本地留存但禁止上传的隐私模式,避免把「是否官方 origin」当成唯一隐私开关。
    2. 在受信任核心记录采集决策与数据流:设备身份状态、用户/车队同意、文件分类、脱敏结果、上传结果与删除原因。不要把文件名清洗或摘要去指针等同于 dump 脱敏。
    3. 重构时把「启动清空、扫描上限、失败后不自动重试、上报门固定不变」列为可复现的行为差异,并用模拟 crash、权限错误、断电、磁盘满和计费网络来测试新策略。
    4. 崩溃报告管线不应向 carControl、sendcan 或 panda 安全模式写入;诊断过载或上传阻塞不得影响实时控制与驾驶员告警。

    此外要承认覆盖局限:system/tombstoned.py 没有就近的专门单元测试,工作区六项隔离探针只覆盖扫描主循环与本地文件处理。1000 条目录项上限、启动残留清理与实际文件竞争仍只有静态证据,具体 crash 内容与服务端保留策略仍需在目标硬件和服务协议上验证。

    常见问题

    100 MB 的文件大小上限是隐私过滤吗?

    不是。源文档明确指出超过 100 MB 的文件只是记错后返回,该分支不复制也不删除原文件,并特别标注「大小阈值不是隐私过滤」。完整 crash 副本可能包含路径、进程环境或内存相关信息,不能因为摘要清洗了指针就把副本当匿名数据。

    为什么「本地已上报」不等于云端收到了文件?

    因为本地与远端是两段独立判断。tombstoned 的 reporting 日志只表示进入本地处理;上传侧还受网络与计费策略约束,计费连接上最近 12 小时的 crash 文件会暂缓、无网络时不上传,而 401/403 或 412 属于停止重试状态,处理完成不等于服务端保存成功。

    重启后没处理的崩溃文件会保留吗?

    可能不会。启动时 clear_apport_folder() 会遍历 /var/crash/* 尝试删除,之后才建立初始快照,源文档的动机是避免重复报告。这意味着重启前尚未处理的 crash 文件可能直接丢失。

    崩溃报告会影响实时控制或驾驶员告警吗?

    按源文档的重构要求不应影响。管线不应向 carControl、sendcan 或 panda 安全模式写入,诊断过载或上传阻塞也不得干扰实时控制与驾驶员告警。但这些是重构要求,不是当前实现已验证的隔离保证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CRASH_COLLECTION_PRIVACY_DEEP_DIVE.md(取用了崩溃采集的报告门、扫描与大小过滤、完整副本复制、上传与回收语义及插件化要求)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • C++ Replay 引擎:五段窗口缓存与消息时序发布

    结论:C++ Replay 引擎不是「把日志快一点放一遍」,而是一条把历史路段重新发布到消息总线、SubMaster 与 VisionIPC 的离线回放链:Route 选段 → SegmentManager 五段窗口缓存 → LogReader 解析 Cap’n Proto → Replay 按单调时间节拍发布 → CameraServer 解码并推送帧。源文档将其定义为交互式开发工具,其中 startStream() 会把日志里的 CarParams 写入本机 CarParams 与 CarParamsPersistent,属于有持久副作用的路径,不应在连接真实车辆控制栈的环境中直接启动。本文为静态阅读,未下载路段、未运行 GUI、未编译 C++。

    一、五个组件各管一段

    Replay::load() 调用 SegmentManager::load();Route::loadSegments() 选择本地目录、服务端或 auto_source,只保留带 rlog/qlog 的段。路段号用有序 map 管理,但文件元数据本身并不是数据完整性证明。

    LogReader 解析 Cap’n Proto,按 union 过滤,把完整 HEVC EncodeIndex 复制成带 segmentNum 的帧事件,并按单调时间排序;FrameReader 用 FFmpeg 建立包索引,随机帧从最近关键帧重解码。Replay 则负责 setupServices()、seekTo()、pause()、publishEvents(),消息 allow/block 与加载过滤联动。

    Timeline 独立读取每段 qlog,按 selfdriveState/书签构建使能、告警与书签区间,seekToFlag() 在目标时间前 2 秒跳转。qlog 缺失时该段时间轴可为空,即使 rlog 正常;这两条输入链不能互相替代。

    二、五段窗口缓存与失败重试

    Replay::seekTo() 将目标秒数映射到 60 秒段并通知缓存线程;manageSegmentCache() 默认维护五段窗口,先向前加载,再补前段,失败对象最多三次尝试并退避。Segment::Segment() 并行启动日志、可选相机加载,任一文件失败会使该段失败。

    组件 核心入口 责任与行为边界
    Route parseRoute()、loadFromLocal()、loadFromServer() 解析路段范围、枚举或查询文件 URL;rlog 优先,缺失才回退 qlog;窄路相机可回退 qcamera。
    SegmentManager manageSegmentCache()、mergeSegments() 异步装载与驱逐窗口、失败重试、按时间合并事件,并原子交换只读快照;isSegmentLoaded() 不代表媒体质量或日志完整性。
    LogReader load()、migrateOldEvents() 调用 Python 下载/解压;旧 controlsState 可补生成 selfdriveState;解析异常会保留已读事件,非空即返回成功。
    FrameReader loadFromFile()、get() FFmpeg 建包索引,随机帧从最近关键帧重解码;未找到帧时记录错误,不合成替代帧。
    Timeline buildTimeline()、find() 独立读取每段 qlog;seekToFlag() 在目标时间前 2 秒跳转。

    合并阶段 mergeSegments() 会跳过各段首部的 INIT_DATA,通过共享 EventData 快照保留事件与对应段对象的生命周期。这也意味着段边界处的初始化数据不会作为普通事件重复发布。

    三、时序发布、seek 与失败边界

    streamThread() 用 upper_bound 从当前 (mono_time, which) 之后继续发布,因此相同时间且相同消息类型的多条事件在 seek/中断续播边界可能一起被跳过——源文档标注这是静态推导,尚无构造样本实测。publishEvents() 按回放速度计算睡眠,落后超过一秒、跨段大跳变或变速时重置节拍。

    入口 行为与边界
    seekTo() 只接受路段 map 内存在的目标段;onSeekedTo 由段加载完成触发,不表示目标帧已解码、订阅者已收到或算法已收敛。
    startStream() 设置时间原点、写入回放 CarParams、启动可选 VisionIPC 和时间轴;缺失 CarParams 时只警告并继续。
    publishMessage() 检测多发布者错误后禁用该服务 socket,避免继续冲突;不提供自动恢复。
    CameraServer::pushFrame() 分辨率变化时重建 VisionIPC 缓冲,相关订阅端需能处理重连/尺寸变化。
    waitForSent() 等待队列发送完毕,但不证明下游消费完成。

    因此 seekTo() 的成功、onSeekedTo 的触发、以及 waitForSent() 的返回,三者语义各不相同,不能互相代替。

    四、对硬件与插件重构的含义

    另一个易被忽略的点是帧事件与普通事件走两条发布路径:普通事件由 publishMessage() 发往 PubMaster 或注入 SubMaster,帧事件由 publishFrame() 送往 CameraServer::pushFrame(),再由 cameraThread() 处理。帧用 segmentId 取视频内帧、用 frameId 标注 VisionIPC 缓冲,并携带 SOF/EOF 时间戳;未找到帧时记录错误,不合成替代帧。因此「帧未发布」与「消息未发布」是两类不同的失败,日志回放时不能只看消息侧是否完整。

    Replay 可当作离线验证适配器,不能当作实车控制安全门。建议插件边界分为 RouteSource(路段元数据/文件定位)、EventTransform(纯函数消息转换)、FrameSource(解码/时间戳)和 ReplaySink(隔离的消息/VisionIPC 输出);每个插件需声明 schema、时间基准、输入完整性及失败策略。车端正式控制路径仍由 panda/opendbc 安全钩子与单一控制发布者约束,不允许回放插件绕过。

    回归实验必须先记录每段实际输入文件、哈希、消息数、首末时间戳、视频包数及帧 ID 对照,才能比较两次结果。源码现有 test_route_ranges() 仅覆盖路段范围解析;本次未运行测试,也未发现覆盖上述发布时序和视频失败路径的同目录专门测试,本结论只针对当前归档及该目录。

    常见问题

    Replay 引擎会把日志里的车辆参数写到本机吗?

    会。源文档指出 startStream() 把日志中的 CarParams 写入本机 CarParams 和 CarParamsPersistent,因此它是有持久副作用的开发工具,不应在连接真实车辆控制栈的环境中直接启动,应隔离 Params、消息命名空间与 VisionIPC。

    为什么回放时「相同时间戳的事件」可能被跳过?

    因为事件比较先按单调时间、再按消息 union 编号排序,streamThread() 的 upper_bound 从严格大于上次键的位置继续。相同时间且相同消息类型的多条事件在 seek/中断续播边界可能一起被跳过;源文档标注这是静态推导,尚无构造样本实测。

    段文件存在就等于日志完整吗?

    不等于。isSegmentLoaded() 只表示快照中有段对象;LogReader::load() 对损坏日志可保留前缀事件,非空即返回成功,故不能以 load 成功代表日志完整。结合 rlog/qlog 与 qcamera 回退,必须记录每段实际输入文件与哈希才能比较两次结果。

    Replay 能当作实车控制的安全门吗?

    不能。源文档把 Replay 定位为离线验证适配器,车端正式控制路径仍由 panda/opendbc 安全钩子与单一控制发布者约束,不允许回放插件绕过。建议先用确定性日志/视频样本验证 seek、断段、损坏文件与多发布者冲突,再进行封闭台架测试。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/REPLAY_ENGINE_DEEP_DIVE.md(取用了端到端调用链、五段窗口缓存、时序与失败边界,以及插件化重构含义)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 开发诊断脚本:事件计数、日志体积归因与 8 核 CPU 采样边界

    结论:项目 02 的五个开发诊断脚本(count_events、qlog_size、mem_usage、watch_timings、live_cpu_and_temp)只产出启发式观测,不是系统健康证明:事件计数是消息出现次数而非独立故障次数,日志体积饼图是给定压缩顺序下的边际贡献而非可相加的存储配额,8 核 CPU 采样对超过 7 的核号会数组越界、对零累计时间差会除零。任何监测结果只能形成告警或实验准入证据,不能绕过 panda 安全钩子直接获得车辆控制权。本文为固定源码快照 c8fb906 的静态阅读,未使用真实路段、未启动订阅、未连接车端。

    五个脚本各自算什么

    这五个脚本被放在同一目录下,但输入域分成两类:三个作用于离线路段日志,两个订阅实时消息。count_events.py 用 LogReader(route, ReadMode.QLOG) 读取路段,经 migrate_all() 兼容迁移后统计 onroadEvents 各名称出现次数,并记录去掉三类踏板/覆盖事件后的集合变化、selfdriveState.alertType 变化和各相机状态条数。qlog_size.py 的 make_pie() 先压缩全体序列,再逐主题移除并重新压缩,把「总压缩长度减去移除后长度」作为该主题体积。mem_usage.py 从 LogReader 收集 procLog 与 deviceState,交 print_report() 汇总内存。

    组件 输入与计算 结果及边界
    count_events.py LogReader 读路段,migrate_all() 后按名称统计事件 是消息出现次数,不是独立故障次数
    qlog_size.make_pie() 全体压缩后逐主题移除再压缩,取差额 边际贡献,不可相加;分别打印 real total 与 breakdown total
    mem_usage.py 收集 procLog/deviceState,调用 print_report() 末帧系统内存与各进程 RSS/PSS 均值峰值;非泄漏或 GPU 用量直接测量
    watch_timings.ServiceTiming.add() 保留 logMonoTime 列表、最近 100 个间隔与 valid 间隔超过 lag_threshold / frequency 记滞后
    live_cpu_and_temp.py SubMaster 订阅 deviceState/procLog 按每核累计 busy/total 差算 CPU;只有更新了 procLog 才打印

    把「脚本跑出数字」等同于「系统健康」是最常见的误用。count_events.py 在每条消息上先用旧的 start_time 计算相对时间再更新起点,若第一条消息恰为要输出的事件或告警,时间可以是负无穷;它遇到第一次点火关闭的 pandaStates 就停止遍历,此后的消息均不参与持续时间与频率统计。相机期望帧数为 int(frequency * duration / decimation),短路段若得到 0,输出百分比时会除零。再加上 migrate_all() 会把整个输入转成列表、按 logMonoTime 排序且相机状态迁移缺省关闭,因此「缺少相机状态」不能只凭该脚本的计数判定相机物理故障。

    日志体积归因为什么不可当配额

    qlog_size.py 默认由 LogReader 读取 rlog,并打印标题为 qlog 的重压缩结果;没有 --as-qlog 时,这个标题并不代表输入或输出是实际 qlog。开关启用后按当前 SERVICE_LIST 的 decimation 过滤,initData/sentinel 总保留、未注册主题丢弃——这是用现在的配置模拟抽样,不是重建历史录制时的 qlog。loggerd 实际在每个已订阅服务上连续递增计数器并按模数选取,日志中还有编码器等特殊写入路径。

    make_pie() 将所有消息和每个主题排除后的其余消息重复序列化、拼接、压缩,时间与内存开销随主题数增长;压缩上下文使各差额不具可加性,空输入或总大小为零也没有有效饼图的保护。因此不能把饼图比例当作日志服务的精确配额,更不能据此直接关闭高频安全证据主题。

    内存与 CPU 采样:缺测、越界与残差标签

    print_report() 没有 procLog 时只打印提示;有数据时仅用最后一帧是否存在任一非零 PSS 来选全报告指标,不同帧/进程的缺测可能混入零值。进程名来自命令行首项,可能合并同名进程;均值只在出现的样本上取,不能当作连续驻留时间加权平均。kernel/ION/GPU 一行是从已用内存扣除缓存和进程估算的残差标签,不是 GPU 计数器。

    live_cpu_and_temp.py 固定为 8 核数组,cpuNum 超过 7 会越界,累计时间差为零会除零;首份 procLog 相对全零基线得到的是累计历史占比,不是当前瞬时负载。进程 CPU 以 PID 找上份采样却未校验 PID 复用,--mem 按名称写字典时同名进程会覆盖而非求和;温度与 RAM 由最近一次 deviceState 更新保留,打印时没有新鲜度提示。

    实时消息间隔:能看见的与看不见的

    monitor_services() 关闭 conflate 并逐轮排空队列,ServiceTiming 将全部时间戳和滞后事件长期保留,长时间运行内存会增长。valid 展示的是最近最多 100 条 Event 的全部有效性,即使 Ctrl-C 的频率统计使用全程数据也是如此。它只观察收到的相邻消息时间戳,既没有第一条消息前或最后一条消息后的静默时长,也无法从一个大间隔区分发布端迟到、订阅端丢包、队列覆盖或时钟问题。若订阅了已注册但频率为 0 Hz 的服务,收到消息后 1.0 / frequency 会触发除零异常,而不是跳过滞后判断。

    观测插件化时的最小契约

    这些建议不是当前工具的已实现能力。观测插件应声明数据源(路段或实时、rlog 或 qlog、服务版本、时间域)、样本区间和缺测状态;CPU 与温度要随每份 deviceState 标注年龄,日志体积归因要保留「估算」而非「账单」语义。对新硬件,核数、PSS 可用性、传感器温度字段和相机期望帧率应由平台能力描述,不写死在诊断界面。原始路段、车辆标识与进程命令行可能含敏感信息,插件仅应获得最小只读数据范围。

    观测项 必须声明的元数据 不能据此推断
    事件计数 路段来源、迁移版本、遍历终止条件 独立故障次数、相机物理健康
    体积归因 压缩顺序、压缩级别、是否 qlog 模拟 可相加的存储配额
    CPU/温度 核数、样本年龄、PID 复用策略 瞬时负载、GPU 用量、驻留均值
    消息间隔 时间域、队列策略、有效位窗口 丢包与迟到的成因归属

    常见问题

    事件计数能当作故障次数吗?

    不能。count_events.py 统计的是 onroadEvents 各名称在消息里出现的次数,不是独立故障次数;脚本遇到第一次点火关闭的 pandaStates 就停止遍历,此后的消息都不参与统计,所以它不是一份完整的故障台账。

    日志体积饼图能直接用来做存储配额吗?

    不能。make_pie() 在给定压缩顺序与压缩级别下求每个主题的边际贡献,压缩上下文使各差额不具可加性,脚本本身也分别打印 real total 与 breakdown total。它适合定位「谁更占地方」,不适合当作精确配额,更不能据此关闭高频安全证据主题。

    为什么 8 核 CPU 采样会异常退出?

    live_cpu_and_temp.py 固定为 8 核数组,cpuNum 超过 7 会越界,累计时间差为零会除零。另外首份 procLog 相对全零基线得到的是累计历史占比而非瞬时负载,参数缺失或核数与平台不符时应先改由平台能力描述。

    消息时间间隔变大就一定是丢包吗?

    不一定。watch_timings 只观察收到的相邻消息时间戳,无法从一个大间隔区分发布端迟到、订阅端丢包、队列覆盖或时钟问题,也看不到第一条消息前与最后一条消息后的静默时长。判定成因需要结合队列策略与其他进程日志。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DEV_METRICS_LOG_SIZE_RESOURCE_TIMING_TOOLS_DEEP_DIVE.md(取用了五个脚本的输入输出、事件与体积失效边界、内存/CPU 越界除零,以及观测插件化建议)。
    • 项目 02 —— openpilot-research/MESSAGE_LOG_CONTRACTS_DEEP_DIVE.md(仅用于确认 loggerd 写入相位与 decimation 的抽样位置)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 路段标识与切片:route/-1 与 route—1 为什么结果不同

    结论:路段标识不是单一校验器,而是正则族、构造器与切片函数三套契约的叠加。RouteName 只用 assert 检查两段长度 16/20;SegmentRange.seg_idxs 对有界正索引遵循 Python 的右开区间(1:4 返回片段 1、2、3),而负索引与开放区间会隐式查询远端 maxqlog。因此 route/-1 与 route---1 走的是不同分支:前者可被接受为「没有片段号」,后者在路段长度断言处失败。

    标识与切片函数

    入口 实现语义 边界
    RouteName.__init__() 选择先出现的 | 或 / 分隔设备 ID 与日志 ID,并转为 device|log 规范名。 只用 assert 检查两段长度 16/20,未验证十六进制设备 ID 或时间字段格式;不是可信身份认证。
    SegmentName.__init__() 从三种分隔形式中提取片段号,可保留本地数据目录;allow_route_name=True 用 -1 表示没有片段号。 斜杠分段的 int() 可接受负片段号;-- 加负号形成三个连字符时,rsplit('--', 1) 留下多余 - 并导致路段长度断言失败。
    SegmentRange.__init__() 用 RE.SEGMENT_RANGE 对整个字符串匹配,保存正则组。 selector 仅允许 q/r/a,但「匹配成功」不保证所选日志实际存在。
    SegmentRange.seg_idxs 单个非负片段直接返回;有界正索引切片用 range(end+1)[slice];负索引或无上界切片先查询最大片段号。 整条路线、负索引和开放区间依赖远端元数据;单个过大的正片段号不会在此处查存在性。

    有界切片遵循右开区间,例如 1:4 返回片段 1、2、3;该函数先构造完整 list 再切片,不是按需生成,大的上界会增加内存。对于单个负索引,先加 maxqlog + 1,没有进一步验证结果仍非负。隔离探针把 maxqlog 固定为 3,确认 -1 → [3]、-10 → [-6],而单个正数 50 → [50] 也不在此处检查真实上界。

    共享正则与解析器边界

    RE 是共享的文本模式集合,不是统一的路段身份验证器。同一个字符串经由不同入口,结论可能相反。

    匹配方式本身就是差异来源:RE.EXPLORER_FILE 要求本地文件名主体与扩展均为小写,大写 RLOG.zst 不匹配;Bootlog.__init__() 用 re.search 可从前后带其他文本的字符串提取路段,而 Athena createClip() 用 re.fullmatch。搜索匹配不能充当整串授权或来源证明,导入器也必须明确如何处理非预期命名。

    模式或消费者 实测契约 后续设计含义
    RE.TIMESTAMP 2023-99-99--25-61-61 符合字符形状;模式不检查日历值。 区分日志 ID 语法和有效日期。
    RE.ROUTE_NAME 与 RouteName 正则允许下划线分隔,但构造器只识别 | 或 /;反过来,构造器接受 16 个 Z 和 20 个 x,正则拒绝。 不同入口不能互相代替校验结果;应定义唯一规范格式。
    RE.SEGMENT_NAME 与 SegmentName 模式片段号只接受非负数字;构造器接受 route/-1,但 route---1 不能正常解析。 切片哨兵值与真实文件/目录名应分开类型。
    RE.BOOTLOG_NAME 它只是 ROUTE_NAME 别名;Bootlog.__init__() 的 re.search 可从前后带其他文本的字符串提取路段,Athena createClip() 则用 re.fullmatch。 搜索匹配不能充当整串授权或来源证明。
    RE.EXPLORER_FILE 本地文件名需小写字母主体及小写字母数字扩展;大写 RLOG.zst 不匹配。 导入器要明确如何处理非预期命名。

    本地与远端路段发现

    Route.__init__() 按是否给 data_dir 选择本地或远端,再取排序后最后一个片段号作为 max_seg_number;若远端返回空片段列表,取 [-1] 会失败。_get_segments_remote() 请求 v1/route/<canonical>/files,按 URL path 末四级拼出片段名;_get_segments_local() 识别 explorer 单文件、device|route--segment 目录和 device|route/segment 目录三种形态,本地缺段保留为 None。

    一个值得注意的边界:远端 API 若为请求路段返回另一设备 ID 的合法 URL,该 URL 会被解析成另一路段的 Segment,但仍进入当前 Route.log_paths();本层不核对返回 URL 的 dongle_id/log_id 是否与请求相同。此外 Segment.url 的 @cache 仅按路段名缓存,不把令牌/会话放进键,也没有 TTL;Segment.events 再 GET <url>/events.json,调用 raise_for_status(),但没有显式超时。

    归类与缓存还有几处覆盖规则:_get_segments_remote() 把 URL path 末四级拼成片段名并按文件名归类 rlog、qlog 和三路相机,同类别多 URL 时后出现的值覆盖先前值,没有 .zst 与 .bz2 的显式优先级;本地则以目录遍历顺序决定同类重名格式的首选,Route 对路径的整理不打开日志,也不检查压缩格式、schema 或文件哈希。隔离探针还确认:本地 0、2 段会保留第 1 段的 None 空位;远端同一段的 rlog.zst、rlog.bz2 由后者覆盖;空远端文件列表在读取最大段号时抛 IndexError。上述结论由十六项隔离探针核对,但未联网查询、未运行真实 LogReader 或回放。换言之,「字符串能匹配」与「文件真实存在且属于该路段」是两回事;服务端是否实际返回旧权限下的数据、请求库默认超时与网络资源行为,均不由探针证明。

    对自制硬件与插件化的要求

    1. 把「语法标识」「已授权数据集」「实际文件内容」分成三层:规范设备/日志/片段 ID、来源与选择器、固定文件哈希及 schema 分别记录。
    2. 远端返回每个文件 URL 时,先核对其中的设备 ID、日志 ID 和片段号属于所请求的路段。
    3. 离线回放不应为负索引或开放切片隐式访问 API;先解析成明确有序片段清单,再逐段验证日志/视频/相机同步。
    4. 不能用标识字符串长度或 API 返回 URL 作为真实性证明;跨来源补段、qlog/rlog 选择和文件格式须在报告中显式呈现。

    常见问题

    route/-1 和 route---1 为什么结果不同?

    route/-1 走斜杠分段,int() 可接受负片段号;route---1 是 -- 加负号形成三个连字符,rsplit('--', 1) 会留下多余 -,导致路段长度断言失败。两者与真实路段是否存在无关。

    有界切片 1:4 会选中哪些片段?

    遵循 Python 右开区间,返回片段 1、2、3。该函数先构造完整 range 列表再切片,不是按需生成,大的上界会增加内存。

    为什么负索引会访问网络?

    因为 SegmentRange.seg_idxs 对负索引或无上界切片会先调用 get_max_seg_number_cached() 查询远端 maxqlog。整条路线、负索引和开放区间都不是纯离线解析,依赖远端元数据。

    路段标识能当身份认证吗?

    不能。RouteName.__init__() 只用 assert 检查两段长度 16/20,未验证十六进制设备 ID 或时间字段;而且正则与构造器的接受集不同。标识只表示语法形状,不构成可信身份认证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/ROUTE_SEGMENT_IDENTITY_SLICE_DEEP_DIVE.md(取用了标识与切片函数、共享正则与解析器边界、本地与远端路段发现,以及插件化要求)。
    • 项目 02 —— openpilot-research/REPLAY_INPUT_IO_DEEP_DIVE.md(仅用于确认回放输入与实际文件读取的衔接位置)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 路段来源发现的缓存陷阱:HEAD 2xx 与无 TTL 假值

    结论:路段读取的可靠性瓶颈不在解码,而在「来源发现」这一步:internal_source_available() 与 file_exists() 用无期限 functools.cache 保存布尔结论,端点恢复或文件删除后本进程仍会复用旧值;HTTP HEAD 只要状态为 2xx(哪怕缺 Content-Length)就判存在;auto_source() 的返回顺序取决于首次找到该段的来源,而非段号。以下基于本地 openpilot c8fb906 源码归档的静态阅读与隔离探针,未访问网络、未运行日志回放。

    标识解析:分享链接与直接输入分流

    入口 LogReader._parse_identifier() 先把 useradmin/connect 分享链接改写成路段标识,再把 HTTP(S)、cd:/ 或当前存在的本地文件视作直接输入,其余按 SegmentRange 走多来源发现。分流规则里有几处只做「字符串处理」却不做「身份校验」的地方。

    输入 原函数结果 解释
    useradmin ?onebox=<route>&onebox=other parse_qs() 解码后取第一个 onebox 值;缺键抛 KeyError 这是参数提取,不是分享链接真实性验证
    https://example.invalid/?ref=useradmin.comma.ai&onebox=<route> 仍触发 useradmin 分支并提取路段 代码检查整串是否包含域名字样,而非 urlparse(...).hostname;不能把此分支当作可信来源认证
    connect 路段 /119/120/q 秒数整除 60 后输出 /1:3/q 结束片段再加一,形成右开区间;跨边界会包含结束秒所在片段
    connect 路段 /180/0 输出 /3:1;非数字秒数抛 ValueError 此层不检查时间先后或范围非空,后续切片可能给出空清单
    不存在的 HTTP(S) URL、cd:/、本地路径 前两者仍判直接输入;本地路径只有 Path.exists() 为真才直读 直读分类不证明远端可达或文件内容完整;本地新路径会落到路段解析

    来源序列与回退

    auto_source() 默认按 internal → comma API → openpilotci → car segments 的顺序收集所需段号。RLOG 只查 rlog,AUTO 先查 rlog 再为缺段找 qlog;部分来源的结果被合并进字典,异常被记录后继续下一个来源。回退因此可以混合来源和日志类型。涉及具体函数时:eval_source() 对每个段号按候选 URL 顺序调用 file_exists()、首个存在即选中,候选优先级只反映可访问性和列表顺序,不反映版本、完整性或内容覆盖。

    一个容易忽略的实现细节是返回顺序:auto_source() 返回 list(valid_files.values()),而不按段号排序。隔离探针用内存假来源确认,请求片段 [0,1,2] 而来源依次提供 2、0、1 时,返回路径顺序为 [2,0,1],即「晚段先找到」会让输出顺序偏离段号。

    来源回退是按轮累积的:每轮保留已找到的片段,只为仍缺的段继续尝试下一个来源,因此最终清单可能同时包含 rlog 与 qlog;AUTO 会在 rlog 缺失时再为该段补查 qlog。

    无 TTL 缓存:假值与真值都会持续

    internal_source_available() 与 file_exists() 都使用无期限 functools.cache。举例来说,某段 rlog 的 HEAD 初次返回非 2xx,这个结果会在同一 Python 进程内保持为假;稍后远端上传完成,即使重新调用 LogReader.reset(),来源探测仍可复用旧假值——因为 reset() 只重建标识列表,不清这两个函数的缓存。反过来,旧真值在远端删除或权限撤销后,也会让发现阶段继续选中,直到真正读取时才失败。

    file_exists() 以 HEAD 状态为 2xx 且长度不等于 -1 判存在;2xx 且缺 Content-Length 时长度默认为 0,仍判存在。它既不验证可读取,也不核对内容摘要。这些结论是缓存键和调用链推导,未做网络时序故障注入。

    混合来源与额外段号

    混合来源隔离探针进一步显示宿主边界的缺口:AUTO 模式在缺 1 时只对 [1] 继续查询 qlog,但最终结果仍按发现顺序为 [rlog-2, rlog-0, qlog-1];若来源返回 {0: path0, 99: path99},即使只请求片段 0,auto_source() 也返回额外的 99。这里没有对来源返回的键集合做交集或身份校验。也正因如此,comma_api_source() 若被要求一个超出该路线 max_seg_number 的段号,列表索引会抛异常,字典推导式不能返回其他已找到的片段,上层只能改尝试下一来源。

    三套缓存键规则的差异

    归档里同时存在三套缓存命名规则,只有前一套是遗留辅助函数,另外两套才是活跃路径;把它们混为一谈会得出错误的「碰撞」结论。

    命名空间 键规则 备注
    遗留 tools/lib/cache.py 导入时读 CACHE_ROOT,否则 ~/.commacache;把 / 直接替换为 _,远端键忽略 scheme、端口、查询串 归档内检索只找到定义、无调用方;隔离探针确认 /a/b 与 /a_b、带不同查询串的两个 URL 会得到相同键
    URLFile.hash_url() 用各自 URL 哈希 活跃下载缓存路径
    file_downloader.cache_file_path() 用各自 URL 哈希,目录来自 Paths.download_cache_root() 的 COMMA_CACHE/OPENPILOT_PREFIX 规则 不用旧辅助函数的 CACHE_ROOT

    三种键规则都不提供内容哈希验证。迁移插件缓存时应只保留明确使用的一套命名空间,并测试规范化和版本键,不要把未调用的辅助函数误当运行事实。

    来源拼接本身也不做校验:openpilotci.get_url() 把 route 名中的 | 换成 /,再直接把 segment_num/filename 拼到固定容器 URL,函数自身不校验或 URL 编码路径片段;FileReader() 则把 cd:/ 先映射内部端点、本地路径包装 DiskFile、远端用 URLFile。再次强调,发现阶段的 HEAD 成功与读取阶段的成功是两个独立事件。

    常见问题

    为什么远端文件已上传,重试仍然失败?

    因为 internal_source_available() 和 file_exists() 使用无期限 functools.cache,旧假值会保留;LogReader.reset() 只重建标识列表,不清这两个函数的缓存。

    HEAD 返回 2xx 就代表文件可用吗?

    不代表。2xx 且缺 Content-Length 时长度默认为 0、仍判存在;它既不验证可读取,也不核对内容摘要。发现阶段成功与读取阶段成功是两个独立事件。

    auto_source() 的返回列表是按段号排序的吗?

    不是。顺序取决于首次找到该段的来源;探针确认请求 [0,1,2] 而来源依次给出 2、0、1 时,返回顺序就是 [2,0,1]。需要按段号处理时应显式排序。

    来源插件返回了额外段号会被过滤吗?

    不会。探针确认即使只请求片段 0,auto_source() 也会返回来源附带的额外段号 99,宿主没有对来源返回的键集合做交集或身份校验。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/LOG_SOURCE_AVAILABILITY_CACHE_DEEP_DIVE.md(来源发现、存在性缓存与回退契约,含源码行号索引)
    • 项目 02 —— openpilot-research/probes/test_logreader_identifier_dispatch_contract.py(分享链接与直接输入分流的五项隔离探针)
    • 项目 02 —— openpilot-research/probes/test_logreader_auto_source_contract.py(混合来源顺序与额外段号的隔离探针)

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11