rosout, rosconsole code reading

资源链接

使用

在程序中我们可以使用ros提供的log函数输出信息,或者使用 printf/cout 系列函数直接向stdout/stderr 输出信息。根据wiki描述,这些信息会出现如下这些位置:

  • stdout
  • stderr
  • Node log file
  • /rosout topic

日志相关配置参数

roslaunch --screen

1
--screen              Force output of all local nodes to screen

默认条件下,roslaunch会把node process stdout/stderr 重定向到磁盘文件,文件名格式为 XXXX-[stdout|stderr].log。如果使用该参数,则不重定向。

该参数默认在launch文件中必选:

1
2
3
4
5
<node
name="NODE_NAME"
pkg="PACKAGE_NAME"
type="TYPE"
output="log|screen">

参考代码:ros_comm/tools/roslaunch/src/roslaunch/nodeprocess.py::LocalProcess::_configure_logging()

log4cxx config file.

roscpp环境下,ros log函数底层提供了三种不同的实现: log4cxx, google log, console output。在package编译时选择确定。

在选择使用log4cxx的情况下,可以通过 env::ROS_ROOT/config/rosconsole.config 或者 env::ROSCONSOLE_CONFIG_FILE 指定log4cxx库接受的配置文件。

初始化时,log4cxx impl 默认添加一个 ROSConsoleStdioAppender,因此日志会向 stdout/stderr 输出。node开发者可以添加多个appender。

若果使用 google log impl,或者 console output impl,日志也会向stdout/stderr输出,与log4cxx的区别是只能额外注册一个appender,并且会被RosoutAppender占用,因此node 开发者无法额外添加。

参考代码:ros_comm/tools/rosconsole/src/rosconsole/impl/

ros::init(init_options::NoRosout)

Note that until the node is fully started, messages will not be sent, so you may not see initial messages.

根据wiki我们可以在/rosout topic中查看使用log函数输出的日志,但需要node完全启动后。

该输出方式通过向 rosconsole(roscpp环境下)注册 ROSOutAppender 实现。该过程实现在 ros::start() 函数中,默认会被NodeHandle构造函数调用。

参考代码:ros_comm/clients/roscpp/src/libros/init.cpp

代码分析

  • roscosole
    提供client端使用的log函数,及相关的初始化函数。
  • rosout
    订阅/rosout topic,处理消息,写入 rosout.log。

rosmaster code reading

参考资源

代码路径入口

  • rosmaster command
    ros_comm/tools/rosmaster/scripts/rosmaster
  • rosmaster logic entry
    ros_comm/tools/rosmaster/src/rosmaster/main.py rosmaster_main()

implementation

核心逻辑入口在 Master class,实现在ros_comm/tools/rosmaster/src/rosmaster/master.py文件中。

启动流程为:

  1. 解析传入参数
  2. 初始化 ROSMasterHandler
  3. 初始化 XmlRpcNode(server),并启动
  4. 检查XmlRpcNode(server)状态,直到退出

ROSMasterHandler

最主要的业务逻辑都在这里,实现文件:ros_comm/tools/rosmaster/src/rosmaster/master_api.py

对外导出的方法、功能

EXTERNAL API
  1. shutdown
    Stop this server.
  2. getUri
    Get the XML-RPC URI of this server.
    通过xmlrpc server获取xmlrpc server的地址,有种先有鸡还是先有蛋的纠结。
  3. getPid
    Get the PID of this server.
PARAMETER SERVER ROUNTINES
  1. deleteParam
  2. setParam
  3. getParam
  4. searchParam
  5. subscribeParam
  6. unsubscribeParam
  7. hasParam
  8. getParamNames
NOTIFICATION ROUTINES
  1. param_update_task
SERVICE PROVIDER
  1. registerService
    Register the caller as a provider of the specified service.
  2. lookupService
  3. unregisterService
PUBLISH/SUBSCRIBE
  1. registerSubscriber
  2. unregisterSubscriber
  3. registerPublisher
  4. unregisterPublisher
GRAPH STATE APIS
  1. lookupNode
  2. getPublishedTopics
  3. getTopicTypes
  4. getSystemState

核心逻辑

  • 信息的存储检索
  • 信息的订阅通知

roscore code reading

代码版本:
/rosdistro: indigo
/rosversion: 1.11.21

参考资源链接

配置文件

/opt/ros/indigo/etc/ros/roscore.xml
这里配置随roscore一起启动的node。

启动的node or process

  • /rosout/rosout: a rosout logging node
  • ros master
  • ros parameter server
  • rosmaster
  • process monitor
  • parent xml-rpc server

roscore启动逻辑

1
2
$ type roscore
roscore is hashed (/opt/ros/indigo/bin/roscore)

核心代码为:

1
2
import roslaunch
roslaunch.main(['roscore', '--core'] + sys.argv[1:])

这里传入的roscore没有作用,--core参数真正指定了启动roscore。配置文件名 roscore.xml 也是在代码中写死,根据版本号等信息拼接出来完整的配置文件路径。

如果没有预先执行roscore,我们用roslaunch *.launch 启动node也会自动roscore相关的进程组件。分析代码,可以把处理逻辑总结为:roslaunch启动 *.launch 文件中配置的node,启动前先检查环境,如果没有启动roscore则先启动,如果已经启动了,则判断命令行参数中有没有 --core,如果有则报错,如果没有继续执行 *.launch 中的操作。

roslaunch command demo:

1
$ roslaunch package_name file.launch

roslaunch 启动逻辑

核心代码逻辑位于 ros_comm/tools/roslaunch/src/roslaunch/parent.py ROSLaunchParent::start()

start infrastructure

load ros launch file

代码文件 ros_comm/tools/roslaunch/src/roslaunch/config.py

首先加载 roscore.xml 文件,然后加载命令行中执行的 launch file。参考代码注释:

1
2
3
# load the roscore file first. we currently have
# last-declaration wins rules. roscore is just a
# roslaunch file with special load semantics

start process monitor

代码文件 ros_comm/tools/roslaunch/src/roslaunch/pmon.py

ProcessMonitor 是一个继承自Python::Thread的类。其中另一个类Process 代表launch启动的一个进程,封装了进程的一些启动参数,是否是关键节点,如果异常退出是否重启等信息。然后将 Process instance 注册到 ProcessMonitor instance 进行监控。

start roslaunch runner and XMLRPC server

代码文件 ros_comm/tools/roslaunch/src/roslaunch/server.py

这里启动了一个xmlrpc server,实现在class ROSLaunchParentNode,并注册了一些方法。启动后立即创建一个client去连接server,确认启动成功。

initialize the actual runner and launch

代码文件 ros_comm/tools/roslaunch/src/roslaunch/launch.py

这里创建一个ROSLaunchRunner instance,这个实例负责启动 launch file 中配置的node。在调用 launch() 启动时,会检测roscore环境是否启动,如果没有则启动:

1
2
3
4
5
6
7
8
9
10
11
12
13
def _setup(self):
"""
Setup the state of the ROS network, including the parameter
server state and core services
"""

...

# start up the core: master + core nodes defined in core.xml
# 启动 ros master
self._launch_master()
# 启动 roscore 包含的 nodes
self._launch_core_nodes()

在启动时,XmlLoader对象用于解析launch file和roscore.xml,并对 core nodes 和 normal nodes分组管理,接口中有参数指定要解析的xml配置文件是否属于roscore的,由调用方负责传入,如果是,其中包含的nodes放入 core nodes中管理。
启动 core nodes 时,如果node已经存在,则跳过。启动 normal nodes 时,如果node已经存在,则杀死已存在的node,再启动新node。

ros master 则是通过rosmaster --core ...方式启动。

Summary

  • roscore代表着启动整套基础环境。
  • parameter server并不是一个单独存在的Server,而是rosmaster的一部分。

request processing in Django

最近有个需求,提供一个http代理,提供后端分布式文件存储系统的下载、上传两个功能,文件大小极限值在100GB+。考虑到其他一些限制,最终选择使用 Nginx + uWSGI + Django的方案。

这里仅仅是记录了开发过程中对Django默认配置下,WSGIHandler处理http请求过程的分析。重点关注了请求数据、返回数据的解析、处理过程。因为要避免大量数据载入内存导致异常,以及大量数据落盘导致的开销。
文章中的代码片段并没有遵守python语法,为了描述的简洁混杂了c++的语法。

django code version: 1.11.3

Request

默认构造一个WSGIRequest对象。ref
解析请求参数过程使用了lazy loading方式,GET & POST字段都是property。
GET是每次都解析 self.environ[‘QUERY_STRING’] 参数。
POST或FILES是其中一个首次使用时(即_post, _files字段为空),调用父类的 _load_post_and_files() 函数,该函数解析post方式的数据,并设置 _post, _files 两个字段,此时POST & FILES都处于直接可用的状态。

_load_post_and_files() 函数分析

忽略异常判断逻辑。
代码只支持 'multipart/form-data' & 'application/x-www-form-urlencoded' 两种POST数据编码方式。使用其他方式不会报错,仅仅是 POST & FILES 为空。
根据WSGIRequest::__init__(),post entity-body数据流保存在_stream字段,并未读取。

'application/x-www-form-urlencoded' 数据解析

一次性读取出所有的entity-body数据,并赋值给_body字段,然后解码。
这种方式是只能传递form参数,不能上传文件。
同时这种方式可以通过 settings.DATA_UPLOAD_MAX_MEMORY_SIZE 设置数据大小上限。

'multipart/form-data' 数据解析

首先判断数据源,如果有_body字段则使用该字段的数据,并封装出来readable object,即有read()函数。
如果没有则使用self,self提供了read()函数读取self._stream数据。
然后使用 parse_file_upload() 函数解析数据,该函数同时解析出来 POST & FILES参数,不要被函数名误导。该函数使用django.http.multipartparser.MultiPartParser类解析。

此处上传数据有两种:请求字段,文件。
对于上传的普通字段,可以通过 settings.DATA_UPLOAD_MAX_NUMBER_FIELDS 限制上传的字段个数,不包含文件个数,以及通过 settings.DATA_UPLOAD_MAX_MEMORY_SIZE 限制字段值长度和的上限,不包括文件大小。

对于上传的文件,MultiPartParser class本身没有具体的逻辑,而是提供了一套处理机制,由提供的实现 class FileUploadHandler 的类完成处理。可以提供多个handler,通过参数 settings.FILE_UPLOAD_HANDLERS 配置,有默认值,参考文档:link。这些handler会对每一个Request初始化一次,然后使用同一个handler instance处理request内所有的文件上传信息,根据代码Django遵循了POST数据内每一个 form field or file chunk 都是顺序连续出现的,不会有交叉。
MultiPartParser循环读取上传文件的每个chunk,然后循环调用每个handlers处理当前chunk。每个handlers最后返回一个Filelike object,因此理论上上传一个文件,server端会产生len(handlers)个文件对象,但是我们通过FILES[FILENAME]只会返回一个object,这是因为FILES是 django.utils.datastructures.MultiValueDict 类型,接口类似dict,但是逻辑不同,参考代码:link
GET & POST 类型为 class QueryDict(MultiValueDict),集成增加了 mutable 开关。

看到这里我们知道file upload handler的设置是全局的,如果想针对某个接口做定制化的配置并不能直接达到目的。那么如果在业务代码收到request object时设置 http.HttpRequest::upload_handlers 字段也是可以的,只要代码中的middlewares,或其他部分没有使用过POST或FILES参数。
在某一个时间点上是可以做到的,但是随着项目迭代这种约定很容易被破坏。

参考链接:

  1. Django 1.11 Doc: Modifying upload handlers on the fly
  2. Django 1.11 Doc: Writing custom upload handlers

Process

接下来通过

1
response = WSGIHandler::BaseHandler.get_response(request)

进入处理流程。

get_response() 主要逻辑就是调用 middleware_chain。settings.MIDDLEWARE 是按顺序执行的,不要被初始化时的 reversed(settings.MIDDLEWARE) 操作混淆。
初始化middleware_chain时首先会插入 BaseHandler::_get_response(), 该函数在调用链中最后执行,也就是在该函数里走入url dispatch逻辑,并获取业务代码产生的response object。

settings.ROOT_URLCONF 配置了 url dispatch patterns。

Response

python manage.py runserver 方式启动

server实现为 django.core.servers.basehttp.WSGIServer 类,该类继承自python标准库的类:

1
wsgiref.simple_server.WSGIServer(server_address, RequestHandlerClass)

manual: link

处理逻辑入口为RequestHandlerClass::handle()函数,Django的继承实现类为 django.core.servers.basehttp.WSGIRequestHandler

处理框架在 class wsgiref.handlers.BaesHandler 里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class BaseHandler:
"""Manage the invocation of a WSGI application"""

...

def run(self, application):
"""Invoke the application"""
# Note to self: don't move the close()! Asynchronous servers shouldn't
# call close() from finish_response(), so if you close() anywhere but
# the double-error branch here, you'll break asynchronous servers by
# prematurely closing. Async servers must return from 'run()' without
# closing if there might still be output to iterate over.
try:
self.setup_environ()
self.result = application(self.environ, self.start_response)
self.finish_response()
except:
try:
self.handle_error()
except:
# If we get an error handling an error, just give up already!
self.close()
raise # ...and let the actual server figure it out.

def finish_response(self):
"""Send any iterable data, then close self and the iterable

Subclasses intended for use in asynchronous servers will
want to redefine this method, such that it sets up callbacks
in the event loop to iterate over the data, and to call
'self.close()' once the response is finished.
"""
try:
if not self.result_is_file() or not self.sendfile():
for data in self.result:
self.write(data)
self.finish_content()
finally:
self.close()

self.result 即Django框架返回的 response object,必须是一个generator,可以迭代。
Django response有 stream HTTP response & normal HTTP response 两种返回值,在内部已经统一封装成Iterator,对外无区别。

uWSGI 方式启动

todo

difference between 'manage.py runserver' and uswgi

python manage.py runserver 启动方式

该命令执行的 django/core/management/commands/runserver.py 中的代码,通过调用链:

1
2
3
4
5
6
7
execute()
-> BaseCommand::execute()
-> handle()
-> run()
-> inner_run()
-> get_handler()
-> django.core.servers.basehttp.get_internal_wsgi_application()

获取一个 WSGI application object。

get_internal_wsgi_application() 默认调用 django.core.wsgi.get_wsgi_application(),该函数首先读取settings.WSGI_APPLICATION参数获取一个wsgi application object对象。

WSGI_APPLICATION参数设置方式参考:

By default, it’s set to .wsgi.application, which points to the application callable in /wsgi.py.

如果该参数没有设置,则调用 django.core.wsgi.get_wsgi_application(),返回django.core.handers.wsgi.WSGIHandler类的实例。

使用uwsgi启动

当使用 django-admin startproject ProjectName 创建新app后,会生成 ProjectName/wsgi.py 文件,该文件默认也使用 django.core.wsgi.get_wsgi_application() 获取一个application object。

refference:
http://uwsgi-docs.readthedocs.io/en/latest/Python.html

因此二者默认情况下是一样的。但是个性化配置方式不同。