经常看到小伙伴遇到 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/ ,看看有没有生成可执行文件,自己使用的可执行文件名字是否正确。如果名字正确则检查是否具备可执行权限。
相信通过这篇文章的学习,你再也不会遇到找不到节点,可执行文件或功能包相关的问题了。