Files
arm_dev 6baf989595 真机侧补 stop_real.sh 与域隔离:残留的 mock 栈会把仿真/真机的 TF 盖成"静止"
实测(2026-09-28)在仿真里上料失败,一路查到 IK / TF / 时钟上,全是被冤枉的。
根因是两个真机侧一直缺的东西:

1) **真机侧没有停止脚本**。仿真侧早有 stop_sim_gz.sh(R4.10/R4.11:残留会伪造出
   "代码写错了"的现场),真机侧只有 run_real.sh 里两行 pkill。而真机栈里有一族
   **独立于 ros2_control_node 存活**的进程 —— 上一轮栈的主体早没了,只剩
   real_trajectory_adapter / named_pose_server / arm_status_node /
   joint_state_publisher 四条,两条 pkill 一条都匹配不到。
   危害不是占资源而是**冒名顶替**:残留的 adapter 照样订阅 /trajectory、
   残留的 joint_state_publisher 照样发 /joint_states(mock 的静态关节角)。
   现象极具误导性:控制器 feedback 与 reference 完全一致(控制器没坏),
   TF 却显示末端一动不动。

   新增 stop_real.sh + lib/real_procs.sh:扫描 = 点名表 + `install/` 前缀兜底,
   按 ROS_DOMAIN_ID 判别真机/仿真(两边有同名的 robot_state_publisher,
   否则停真机会把正在跑的仿真杀一半),杀完复查残留并非零退出。
   run_real.sh 启动前的清理改用它,不再各写一份会漂移的模式表。
   真机侧**有意不**用仿真的"进程组"法:真机栈是前台交互跑的,没有独立进程组,
   它的 PGID 就是调用它的 shell —— 记下来再 kill 会杀掉自己的终端。

2) **真机侧不强制 ROS_DOMAIN_ID**。sim_env.sh 早就强制 77 并写明"隔离不能靠
   『环境里恰好没设』",真机侧没有对应保护:先 source 过 sim_env.sh 的 shell 里
   再起真机栈,真机栈就落到**仿真的域**上,节点名撞车、/joint_states 与 /tf
   各有两个来源互相盖。补 lib/ros_domain.sh(真机 0 / 覆盖用 REAL_ROS_DOMAIN_ID),
   env.sh、run_real.sh、run_sim.sh 三处共用一份实现 —— 成对强制,否则一侧的
   强制只是把运气挪到了另一侧。

   run_sim.sh 特别容易漏:它名为 sim,用的却是**真机的 install/**、节点名与真机
   完全一样,而且当时不管域;这次也钉到真机域 0。run_sim_gz.sh 文件头那句
   "两者互不影响"是错的,一并改掉。

验证:stop_real.sh --list 在有仿真在跑时为空(REAL_PROC_INCLUDE_SIM=1 能看到
并按"域 77"标出);伪装一个 install/arm_control/lib/arm_control/arm_status_node
的孤儿进程能被点名。新增 6 项单测钉住域的四种输入 + 一个不许误报。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 12:25:16 +08:00

140 lines
6.9 KiB
Bash
Executable File
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.
#!/usr/bin/env bash
# =============================================================================
# scripts/lib/real_procs.sh —— 真机进程扫描(被 stop_real.sh / run_real.sh 共用)
#
# 为什么要有这个文件
# ----------------
# 仿真侧早就有 lib/sim_procs.sh(点名表 + install_sim/ 兜底 + 杀完复查),
# 真机侧**一直没有**对应物:只有 run_real.sh 里两行 pkill,
# pkill -f '/controller_manager/ros2_control_node'
# pkill -f 'arm_bringup.*stack.launch'
# 而真机栈里还有一族**独立于这两条存活**的进程。2026-09-28 实测抓到的一次:
# 上一轮栈的主体早就没了,只剩
# real_trajectory_adapter / named_pose_server / arm_status_node /
# joint_state_publisher
# 四条(全在 install/arm_control/lib/arm_control/ 下),两条 pkill 一条都匹配不到。
#
# 危害不是"占内存",而是**冒名顶替**:
# · 残留的 adapter 照样订阅 /trajectory、
# · 残留的 named_pose_server 照样提供 /set_named_pose、
# · 残留的 joint_state_publisher 照样发 /joint_states(mock 值的**静态**关节角),
# 于是新一轮的请求被 DDS 负载均衡到"残留的那个",/joint_states 与 /tf 有两份
# 来源互相盖。现象极具误导性:控制器 feedback 与 reference 完全一致(控制器没坏),
# 但 TF 显示末端**一动不动** —— 顺着现象查会一路查到 IK / TF / 时钟上。
#
# 所以这里做三件事:
# 1. run_real.sh 与 stop_real.sh 共用同一份扫描实现(不再各写一份会漂移的表);
# 2. 扫描 = 点名表 + `install/` 前缀兜底 —— 凡命令行含 ``<仓库>/install/``
# 的进程都算真机进程,以后再加新节点,不改这个文件也能被扫到;
# 3. **按 ROS_DOMAIN_ID 区分真机与仿真**:仿真强制 77(见 sim_env.sh),
# 真机默认 0。只按名字扫的话,停真机会顺手把正在跑的仿真的
# robot_state_publisher 一起杀掉(两边同名、都在 conda/ROS 下)。
#
# 用法(在 `set -uo pipefail` 之后 source):
# source "$(dirname "$0")/lib/real_procs.sh"
# mapfile -t targets < <(real_proc_targets) # 只输出 PID(已排除自身/祖先/仿真)
# real_proc_scan # "PID<TAB>域<TAB>命令行"
# =============================================================================
# 仿真的域(sim_env.sh 强制那个值)。真机扫到它 = 那是仿真进程,不是残留。
REAL_SIM_DOMAIN=77
_real_repo_root() {
cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd
}
# 兜底模式:真机安装空间。用绝对路径前缀,避免误伤 install_sim/(仿真那边)。
# 注意 "install_sim/" **不含** 子串 "install/"(_sim 把它截断了),所以这一条
# 天然不会扫到仿真进程。
#
# ⚠️ 路径要按 awk 正则转义:路径里可能有 . + - 之类字符,不转义的话
# "." 会匹配任意字符(匹配面变大 = 误杀无关进程)。
_real_install_dir() {
printf '%s/install/' "$(_real_repo_root)"
}
_real_regex_escape() {
printf '%s' "$1" | sed -e 's/[][\.^$*+?(){}|\\]/\\&/g'
}
# 点名表:列**不在 install/ 下**、但属于真机栈的进程。
# 它们的可执行文件来自 conda 环境或 ROS 发行版(/opt/ros/humble/lib/...),
# 命令行里没有仓库的 install/ 字样,兜底规则扫不到。
#
# ros2_control_node 控制器本体(它占着 RDK,RDK 对同一台臂是独占的)
# controller_manager.*spawner 孤儿 spawner,会永远等一个不存在的 controller_manager
# joint_state_publisher **mock 专用**节点;在真机上出现 = 上一轮 mock 栈的残留,
# 它发的静态关节角会盖掉真实值
# robot_state_publisher 发 TF 与 /robot_description;残留会让"改了描述却不生效"
# static_transform_publisher 发 latched 的 /tf_static,残留会一直把旧外参按在新树上
REAL_PROC_NAMED='ros2_control_node|controller_manager.*spawner|joint_state_publisher|robot_state_publisher|static_transform_publisher'
real_proc_patterns() {
printf '%s|%s' "$REAL_PROC_NAMED" "$(_real_regex_escape "$(_real_install_dir)")"
}
# 读某个进程的 ROS_DOMAIN_ID。读不到(僵尸/无权限/没设)输出空。
# `|| true`:调用方可能是 `set -e` 的脚本(run_real.sh),而 tr/sed 在
# /proc 读不到时会非零返回 —— 不兜住的话整个启动脚本会被这一行带停。
real_proc_domain() {
local pid="$1" v=""
v="$(tr '\0' '\n' < "/proc/${pid}/environ" 2>/dev/null \
| sed -n 's/^ROS_DOMAIN_ID=//p' | head -1)" || true
printf '%s' "${v:-}"
}
# 是不是仿真进程(域 == REAL_SIM_DOMAIN)。
# 读不到 environ 时**不当**成仿真:宁可停真机时多杀一个"域未知但确实在
# install/ 下"的进程,也不要漏掉真机残留(漏掉的代价见文件头)。
real_proc_is_sim() {
[ "$(real_proc_domain "$1")" = "${REAL_SIM_DOMAIN}" ]
}
# 扫描:输出 "PID<TAB>域<TAB>命令行"。
#
# ⚠️ 两个必须踩过的坑(与 sim_procs.sh 同源,别再犯):
# 1. `pkill -f <模式>` 会匹配到自己所在的 shell(它的命令行里就含那个模式),
# 表现成"命令立刻返回、什么都没杀"。
# 2. 模式串用**环境变量**传给 awk,不用 `-v pat=...`:用了 -v 的话 awk 自己的
# 命令行里就含整串模式,于是它匹配到自己,复查时多报一个幽灵 PID。
real_proc_scan() {
export PAT="$(real_proc_patterns)"
export ME="${REAL_PROC_SELF:-$$}"
ps -eo pid=,ppid=,args= | awk '
BEGIN { pat = ENVIRON["PAT"]; me = ENVIRON["ME"] }
$0 ~ pat {
if (NF < 3) next # 没有 args 字段(僵尸/内核线程)
pid = $1; ppid = $2
args = substr($0, index($0, $3))
if (ppid == me) next # 本脚本的直接子进程(如 $(...) 的 fork)
if (args ~ /real_procs\.sh/) next # 本库自己(含 grep/sed 的读数)
if (args ~ /stop_real\.sh/) next
if (args ~ /run_real\.sh/) next
if (args ~ /^\s*awk/) next
print pid "\t" args
}'
}
# 排除自己与全部祖先,跳过仿真进程,只输出 PID。
# 设 REAL_PROC_INCLUDE_SIM=1 可以连仿真进程一起列出(自查用,停止流程不用)。
real_proc_targets() {
local exclude=() p=$$ pid rest _d skip e
while [ "${p:-0}" -gt 1 ] 2>/dev/null; do
exclude+=("$p")
p=$(ps -o ppid= -p "$p" 2>/dev/null | tr -d ' ')
[ -z "$p" ] && break
done
while IFS=$'\t' read -r pid rest; do
[ -z "${pid:-}" ] && continue
skip=0
for e in "${exclude[@]}"; do
[ "$pid" = "$e" ] && skip=1 && break
done
[ "$skip" = "1" ] && continue
if [ "${REAL_PROC_INCLUDE_SIM:-0}" != "1" ] && real_proc_is_sim "$pid"; then
continue
fi
echo "$pid"
done < <(real_proc_scan)
}