Ask Your Question
0

Any way to see IO sooner with OnProcessIO event in ROS2 Launch Description?

asked 2019-01-14 19:16:42 -0500

Pete B gravatar image

Background

Starting with a counter node that looks something like this:

import rclpy
import time

if __name__ == "__main__":
    rclpy.init()
    node = rclpy.create_node("counter_node")
    for n in range(10):
        print(n)
        time.sleep(1.0)

If I launch the node using ros2 run my_package counter_node I'll see it output 0, 1, 2, 3, 4, etc. . . about once per second before exiting.

If I launch the same node using ros2 launch like this:

launch_description = LaunchDescription([
    Node(package="my_package", node_executable="counter_node"),
    RegisterEventHandler(
        OnProcessIO(on_stdout=lambda info: print(info.text))
    )
])

launch_service = LaunchService()
launch_service.include_launch_description(launch_description)
launch_service.run()

Then the standard out is a little different. I see nothing for 10 seconds, and when my process ends I see all the numbers 0, 1, 2, 3, 4, etc. . . all printed at once. It looks like all the stdout is getting buffered up and printed once the node exits.

If I change my counter node to do

for n in range(10):
    print(n)
    sys.stdout.flush()  # <-- Added this
    time.sleep(1.0)

and then run it with launch_service.run(), I get the once-per-second output again

My Question

Is there a way to get the ros2 run my_package counter_node behavior with launch_service.run without modifying the counter node? I'm I overlooking something way simpler?

ros2 run is pretty easy to understand. It's using popen to launch my node, and then polling it with communicate. ros2 launch's plumbing is a bit harder for me to follow. Is there a way to reach down inside and have it poll the counter_node's IO a little more often so I can see the standard-out a little closer to when it happens without having to modify the node I'm launching? The examples above are deliberately simplified. In my actual application, it's not feasible to add sys.stdout.flush() everywhere

edit retag flag offensive close merge delete

2 Answers

Sort by ยป oldest newest most voted
1

answered 2019-01-15 12:51:43 -0500

Pete B gravatar image

updated 2019-01-15 15:07:37 -0500

This seems like a bit of a hack, but I can modify my launch file a little like this:

def generate_launch_description():
    node_env = os.environ.copy()
    node_env["PYTHONUNBUFFERED"] = "1"

    launch_description = LaunchDescription([
        Node(package="my_package", node_executable="counter_node", env=node_env),
        RegisterEventHandler(
            OnProcessIO(on_stdout=lambda info: print(info.text))
        )
    ])

According to this documentation it looks like there's supposed to be a SetEnvironmentVariable launch action, but I don't see it here. Maybe it's not implemented yet.

I'm not sure what issues I'll run into with c++ nodes, but I'll cross that bridge when I come to it

edit flag offensive delete link more

Comments

I don't think this is a hack honestly. Basically Python (in your script, not launch) looks at the file descriptor it is attached to and decides if it is a pty or not, if not then it buffer's I/O. That env var tells it explicitly to not.

William gravatar image William  ( 2019-01-15 15:25:54 -0500 )edit

The other option is for launch to create a pty and use that as the stdout/stderr file descriptors for your python script, in which case Python will check and assume it's on a terminal, and therefore will not buffer automatically. The downside is that creating many pty for each child is expensive.

William gravatar image William  ( 2019-01-15 15:28:17 -0500 )edit

A compromise might be to let users control when "tty emulation" is done, and then they may do it only for scripts that need IO feedback.

William gravatar image William  ( 2019-01-15 15:30:07 -0500 )edit

Try changing this line to True: https://github.com/ros2/launch/blob/0...

William gravatar image William  ( 2019-01-15 15:30:16 -0500 )edit

@William Things blow up down in osrf_pycommon:

. . . File ".../osrf_pycommon/process_utils/async_execute_process_asyncio/impl.py", line 68, in protocol_factory return protocol_class(None, stdout_master, stderr_master) TypeError: <lambda>() takes 0 positional arguments but 3 were given

Pete B gravatar image Pete B  ( 2019-01-15 16:35:19 -0500 )edit

Maybe we have an old asyncio implementation. . .

Pete B gravatar image Pete B  ( 2019-01-15 16:58:51 -0500 )edit

What version of Python are you using?

William gravatar image William  ( 2019-01-15 18:14:24 -0500 )edit

There's a substitution to get an env var ( https://github.com/ros2/launch/blob/0... ), but no action to set one. I'll make an issue.

William gravatar image William  ( 2019-01-15 19:30:37 -0500 )edit
1

answered 2019-01-15 00:26:41 -0500

tfoote gravatar image

There might be buffering in the launch use of subprocess but if adding flush to your program suggests that you can switch to making python's stdout process unbuffered in several ways.

https://stackoverflow.com/questions/8...

This could be a potentiality good feature to add to ros2 launch as an option especially in conjunction with the OnProcessIO handler enabled. @William can will have more insight into the launch system.

edit flag offensive delete link more

Comments

Thanks Tully - I want to try to do this without modifying the counter_node so that we can make this work for all nodes. I'll try introspecting the LaunchDescription and adding PYTHONUNBUFFERED to the node's environment. I'll follow up with William too

Pete B gravatar image Pete B  ( 2019-01-15 11:10:45 -0500 )edit

Your Answer

Please start posting anonymously - your entry will be published after you log in or create a new account.

Add Answer

Question Tools

2 followers

Stats

Asked: 2019-01-14 19:05:49 -0500

Seen: 260 times

Last updated: Jan 15 '19