体检判据硬化:sysfs 读取带超时、复位命令不再写成"要密码"
两处都是「体检工具自己失效」——本仓库最贵的那类坑:判据看起来在跑,其实
结论是假的或者干脆没有结论。
scripts/lib/usb_health.py
Hub 在内核里卡死时,读它的 sysfs 属性会**永久阻塞**(实测
/sys/bus/usb/devices/1-3.4/product 读 20 秒不返回)。于是体检脚本自己挂住:
本该报出故障的那一次,恰恰什么都不输出。新增 SysfsReader —— 所有 sysfs 读取
带 1s 超时,卡死的路径只尝试一次并记入 blocked(Python 杀不掉阻塞在系统调用
里的线程,不记就会在 --watch 反复采样时攒出几十个),blocked 映射回父设备
并按 error 报出「给这个 Hub 断电重启 / 改插主板直连」。
scripts/lib/v4l_health.py
· 提示语写「到真终端里带密码跑 orisys-usb-reset」,与实际不符:sudoers 对这个
脚本本来就是 NOPASSWD 白名单(`sudo -n -l` 可见;orisys_tactile.py 的自动
复位用的也是 sudo -n)。现场按这句会以为无人值守时做不了这件事。
改成 `sudo -n /usr/local/bin/orisys-usb-reset <devpath>` 并说明免密来源。
· wedged 的处置写反了:以前是「物理重插;reset 只在没完全卡死时有效」,
而设备还在 sysfs 里正是 unbind/bind 能生效的前提,先说 reset。
· main() 增加可注入的 v4l_root/usb_root,用例不再读真机 sysfs。
测试:test_main_exit_code_is_nonzero_on_error 以前直接读真 sysfs,本机这只
相机没插(unplugged)时会变红 —— 它本该只在 wedged 下断言复位提示。改为注入
临时 sysfs(设备在总线上、无 /dev/video*),用例变成确定性的。
test_usb_health + test_v4l_health 共 28 项通过。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,8 +15,12 @@
|
||||
``sudo /usr/local/bin/orisys-usb-reset <devpath>``(unbind/bind 白名单脚本)
|
||||
复位 —— 若 unbind 成功而 bind 卡住,就会变成现在这个状态:
|
||||
``/sys/bus/usb/devices/1-3.4.1`` 还在、接口还挂在 ``uvcvideo`` 上,
|
||||
但 ``/dev/video*`` 一个都没有。**软件侧已经没救了**,只能:
|
||||
在真终端里带密码跑一次 ``orisys-usb-reset``,或拔插一次该相机。
|
||||
但 ``/dev/video*`` 一个都没有。**软件侧重连已经没救**,只能复位:
|
||||
``sudo -n /usr/local/bin/orisys-usb-reset <devpath>``。
|
||||
这条路径**不需要密码** —— sudoers 已对这一个脚本配了 NOPASSWD 白名单
|
||||
(``orisys_tactile.py`` 里的自动复位也是用 ``sudo -n`` 调的),
|
||||
所以无人值守时可以直接跑;真要输密码说明白名单被改掉了。
|
||||
复位仍不行(unbind 都不响应)就只剩拔插一次该相机。
|
||||
|
||||
3. ``可读但无帧`` —— 节点在、open 得到,但一帧都读不出来
|
||||
(日志里 ``select() timeout``)。→ 多半是同一条 USB2 总线被别的流占满;
|
||||
@@ -112,9 +116,10 @@ class Check:
|
||||
if self.state == "wedged":
|
||||
return (
|
||||
f"{self.name}:设备在总线上({self.devpath})但**没有 /dev/video* 节点** "
|
||||
"—— 内核级卡死,软件侧救不回来。到真终端里跑 "
|
||||
f"`sudo /usr/local/bin/orisys-usb-reset {self.devpath}`(要密码),"
|
||||
"或把这只相机拔插一次;然后重启触觉节点。"
|
||||
"—— 内核级卡死,软件侧重连救不回来。跑 "
|
||||
f"`sudo -n /usr/local/bin/orisys-usb-reset {self.devpath}` 复位"
|
||||
"(免密,sudoers 白名单;这条本来就是给无人值守用的),"
|
||||
"复位仍不行就拔插一次这只相机;然后重启触觉节点。"
|
||||
)
|
||||
return f"{self.name}:没有插({self.by_path} 不存在)—— 插线/换 USB 口。"
|
||||
|
||||
@@ -148,7 +153,11 @@ def check_devices(
|
||||
return out
|
||||
|
||||
|
||||
def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
def main(
|
||||
argv: Optional[Sequence[str]] = None,
|
||||
v4l_root: Path = V4L_BY_PATH,
|
||||
usb_root: Path = SYS_USB,
|
||||
) -> int:
|
||||
ap = argparse.ArgumentParser(description="V4L2 触觉相机存在性体检")
|
||||
ap.add_argument(
|
||||
"--dev",
|
||||
@@ -164,7 +173,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
for item in args.dev:
|
||||
name, _, path = str(item).partition("=")
|
||||
pairs.append((name.strip() or "v4l", path.strip()))
|
||||
checks = check_devices(pairs)
|
||||
checks = check_devices(pairs, v4l_root=v4l_root, usb_root=usb_root)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps([asdict(c) for c in checks], ensure_ascii=False, indent=2))
|
||||
|
||||
Reference in New Issue
Block a user