鱼香ROS社区
    • 版块
    • 最新
    • 未解决
    • 已解决
    • 群组
    • 注册
    • 登录
    紧急通知:禁止一切关于政治&VPN翻墙等话题,发现相关帖子会立马删除封号
    提问前必看的发帖注意事项: 社区问答规则(小鱼个人)更新 | 高质量帖子发布指南

    ROS2RUN源码流程分析,解决所有 ros2 run 报找不到问题

    已定时 已固定 已锁定 已移动
    ROS 2相关问题
    ros2 run 源码分析
    1
    1
    2.4k
    正在加载更多帖子
    • 从旧到新
    • 从新到旧
    • 最多赞同
    回复
    • 在新帖中回复
    登录后回复
    此主题已被删除。只有拥有主题管理权限的用户可以查看。
    • 小鱼小
      小鱼 技术大佬
      最后由 小鱼 编辑

      经常看到小伙伴遇到 ros2 run 时报错找不到功能包或者可执行文件的问题。今天就 ros2 run 的源码分析下,产生这种问题的原因,大家好对症下药。

      怕有的同学看不到最后,把重点分析结果放到这里:

      **所以当遇到找不到可执行文件的错误,第一步先 printenv AMENT_PREFIX_PATH(这句指令也可以替换为 ros2 pkg prefix 功能包名字) 看看有没有自己的功能包路径没有则检查 source 了没,install 目录下是否有这个功能包,有则继续检查第二步,打开对应路径AMENT_PREFIX_PATH/lib/package_name/ ,看看有没有生成可执行文件,自己使用的可执行文件名字是否正确。如果名字正确则检查是否具备可执行权限。
      **

      另外需要注意两点:

      1. 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/ ,看看有没有生成可执行文件,自己使用的可执行文件名字是否正确。如果名字正确则检查是否具备可执行权限。

      相信通过这篇文章的学习,你再也不会遇到找不到节点,可执行文件或功能包相关的问题了。

      新书配套视频:https://www.bilibili.com/video/BV1GW42197Ck/

      1 条回复 最后回复 回复 引用 -1
      • 第一个帖子
        最后一个帖子
      皖ICP备16016415号-7
      Powered by NodeBB | 鱼香ROS