结论:路段读取的可靠性瓶颈不在解码,而在「来源发现」这一步: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,宿主没有对来源返回的键集合做交集或身份校验。
延伸阅读
- 路段身份与片段切片:从分享链接到右开区间的标识链 —— 同一套标识解析与右开区间的另一视角
- 进程回放校验:用固定输入验证解析链而不是只看有没有输出 —— 「假值持续」这类问题需要回放而不是重试
- 路段下载与鉴权边界:直读 URL、内网路径与本地文件的分权 —— 直接输入路径应设独立权限与内容校验
- 车载信号为什么会说谎:TTL、源时间与 500 ms 新鲜度阈值 —— 布尔可用性结论同样需要 TTL
参考资料
- 项目 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
发表回复