小鱼 ROS 2 新书上线!点击链接查看, 新书配套视频点击链接查看。
提问前必看的发帖注意事项—— 提问前必看!不符合要求的问题拒绝回答!!
社区使用指南—如何添加标签修改密码
ROS2RUN源码流程分析,解决所有 ros2 run 报找不到问题
-
经常看到小伙伴遇到 ros2 run 时报错找不到功能包或者可执行文件的问题。今天就 ros2 run 的源码分析下,产生这种问题的原因,大家好对症下药。
怕有的同学看不到最后,把重点分析结果放到这里:
**所以当遇到找不到可执行文件的错误,第一步先 printenv AMENT_PREFIX_PATH(这句指令也可以替换为 ros2 pkg prefix 功能包名字) 看看有没有自己的功能包路径没有则检查 source 了没,install 目录下是否有这个功能包,有则继续检查第二步,打开对应路径AMENT_PREFIX_PATH/lib/package_name/ ,看看有没有生成可执行文件,自己使用的可执行文件名字是否正确。如果名字正确则检查是否具备可执行权限。
**另外需要注意两点:
- ros2 run 会对参数进行解析 add_arguments
2.执行是在子进程中进行的 subprocess.Popen(cmd)
ROS2RUN源码分析
在ROS 2中,
ros2 run
命令是一个重要的工具,它允许用户运行指定ROS包内的可执行文件。本文将对ros2 run
命令的源代码进行分析,以及其中涉及到的关键函数,帮助您更好地理解其内部工作原理。我们还会深入探讨RunCommand
类以及它所依赖的两个重要函数:get_executable_path
和run_executable
。1.
ros2 run
指向目录ros2 run
命令是通过ros2cli
包提供的,它在源码中的配置是通过entry_points
来实现的。下面是相关的配置代码:entry_points={ 'ros2cli.command': [ 'run = ros2run.command.run:RunCommand', ], }
这段代码告诉ROS 2,当用户运行
ros2 run
命令时,应该调用ros2run.command.run:RunCommand
这个类来处理。2.
RunCommand
代码分析RunCommand
类是ros2 run
命令的核心部分。下面是RunCommand
类的代码,我们将对其进行分块解析:from argparse import REMAINDER import shlex from ros2cli.command import CommandExtension from ros2pkg.api import package_name_completer from ros2pkg.api import PackageNotFound from ros2run.api import ExecutableNameCompleter from ros2run.api import get_executable_path from ros2run.api import MultipleExecutables from ros2run.api import run_executable class RunCommand(CommandExtension): """Run a package specific executable.""" # add_arguments函数用于定义命令行参数 def add_arguments(self, parser, cli_name): # --prefix参数用于指定命令的前缀 arg = parser.add_argument( '--prefix', help='Prefix command, which should go before the executable. ' 'Command must be wrapped in quotes if it contains spaces ' "(e.g. --prefix 'gdb -ex run --args').") try: from argcomplete.completers import SuppressCompleter except ImportError: pass else: arg.completer = SuppressCompleter() # package_name参数用于指定ROS包的名称 arg = parser.add_argument( 'package_name', help='Name of the ROS package') arg.completer = package_name_completer # executable_name参数用于指定可执行文件的名称 arg = parser.add_argument( 'executable_name', help='Name of the executable') arg.completer = ExecutableNameCompleter( package_name_key='package_name') # argv参数用于传递给可执行文件的额外参数 parser.add_argument( 'argv', nargs=REMAINDER, help='Pass arbitrary arguments to the executable') # main函数是命令的入口点,处理用户输入并执行相应的操作 def main(self, *, parser, args): try: # 获取可执行文件的路径 path = get_executable_path( package_name=args.package_name, executable_name=args.executable_name) except PackageNotFound: raise RuntimeError(f"Package '{args.package_name}' not found") except MultipleExecutables as e: msg = 'Multiple executables found:' for p in e.paths: msg += f'\n- {p}' raise RuntimeError(msg) if path is None: return 'No executable found' prefix = shlex.split(args.prefix) if args.prefix is not None else None # 运行可执行文件 return run_executable(path=path, argv=args.argv, prefix=prefix)
RunCommand
类负责解析命令行参数,查找可执行文件的路径,以及执行可执行文件。3. 对
RunCommand
两个重要依赖函数介绍RunCommand
类依赖于两个重要的函数,它们分别是get_executable_path
和run_executable
。下面是它们的代码以及功能介绍:get_executable_path
def get_executable_path(*, package_name, executable_name): paths = get_executable_paths(package_name=package_name) paths2base = {} for p in paths: basename = os.path.basename(p) if basename == executable_name: # 选择完全匹配的可执行文件 paths2base[p] = basename elif sys.platform == 'win32': # 检查PATHEXT中列出的扩展名以进行匹配(无扩展名) pathext = os.environ.get('PATHEXT', '').lower().split(os.pathsep) ext = os.path.splitext(basename)[1].lower() if ext in pathext and basename[:-len(ext)] == executable_name: # 选择有已知扩展名的匹配项 paths2base[p] = basename if not paths2base: return None if len(paths2base) > 1: raise MultipleExecutables(paths2base.keys()) return list(paths2base.keys())[0]
get_executable_path
函数用于查找指定ROS包内的可执行文件的路径。它会检查与executable_name
匹配的所有可执行文件,并根据操作系统和扩展名来选择最合适的可执行文件。run_executable
def run_executable(*, path, argv, prefix=None): cmd = [path] + argv # 在Windows上,Python脚本通过解释器调用 if os.name == 'nt' and path.endswith('.py'): cmd.insert(0, sys.executable) if prefix is not None: cmd = prefix + cmd process = subprocess.Popen(cmd) while process.returncode is None: try: process.communicate() except KeyboardInterrupt: # 子进程也会收到信号并应该关闭 # 因此我们继续,直到进程完成 pass if process.returncode != 0: if -process.returncode in signal.valid_signals() and os.name == 'posix': # 负值 -N 表示子进程由信号 N 终止 print(ROS2RUN_MSG_PREFIX, signal.strsignal(-process.returncode)) else: # 打印一般的失败消息 print(ROS2RUN_MSG_PREFIX, 'Process exited with failure %d' % (process.returncode)) return process.returncode
run_executable
函数用于执行指定的可执行文件。它构建命令并在子进程中运行它。在Windows上,如果可执行文件是Python脚本,它将使用Python解释器来运行。函数还能处理命令前缀(例如,gdb
),以及捕获子进程的输出。4.get_executable_paths 函数
上面获取可知性文件最重要的一步是调用 get_executable_paths 函数,根据功能包搜索路径,该函数的代码如下:
def get_executable_paths(*, package_name): prefix_path = get_prefix_path(package_name) if prefix_path is None: raise PackageNotFound(package_name) base_path = os.path.join(prefix_path, 'lib', package_name) executable_paths = [] for dirpath, dirnames, filenames in os.walk(base_path): # ignore folder starting with . dirnames[:] = [d for d in dirnames if d[0] not in ['.']] dirnames.sort() # select executable files for filename in sorted(filenames): path = os.path.join(dirpath, filename) if os.access(path, os.X_OK): executable_paths.append(path) return executable_paths
这段代码的重点是 base_path = os.path.join(prefix_path, 'lib', package_name) 可执行文件从前缀路径下找 lib ,然后找功能包的名字,之后在下面遍历找到可执行的文件。
最后的重点就是 prefix 从哪里来的,确认一个功能包的 prefix 可以使用 ros2 pkg prefix package_name 进行查找。
对 prefix 的获取抽丝剥茧,最终可以在ament_index_python/ament_index_python/constants.py
中找到是一个环境变量定义:AMENT_PREFIX_PATH。使用 ros2 pkg prefix turtlesim
和 printenv AMENT_PREFIX_PATH 打印结果应该是一致的。所以回到问题的起点,当使用 source 后,环境变量 AMENT_PREFIX_PATH 就会发生改变,将可执行文件的路径添加进去了,使用 ros2 run 的时候就会到 AMENT_PREFIX_PATH/lib/package_name/ 下搜索可执行文件。
所以当遇到找不到可执行文件的错误,第一步先 printenv AMENT_PREFIX_PATH(这句指令也可以替换为 ros2 pkg prefix 功能包名字) 看看有没有自己的功能包路径没有则检查 source 了没,install 目录下是否有这个功能包,有则继续检查第二步,打开对应路径 AMENT_PREFIX_PATH/lib/package_name/ ,看看有没有生成可执行文件,自己使用的可执行文件名字是否正确。如果名字正确则检查是否具备可执行权限。
相信通过这篇文章的学习,你再也不会遇到找不到节点,可执行文件或功能包相关的问题了。
- ros2 run 会对参数进行解析 add_arguments