#!/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域命令行" # ============================================================================= # 仿真的域(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域命令行"。 # # ⚠️ 两个必须踩过的坑(与 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) }