Skip to content

Commit

Permalink
Add ros_launch action (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
fred-labs authored Jun 27, 2024
1 parent 7356b31 commit ad1ed88
Show file tree
Hide file tree
Showing 20 changed files with 621 additions and 9 deletions.
27 changes: 25 additions & 2 deletions docs/libraries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,12 @@ For debugging purposes, log a string using the available log mechanism.

Run a process. Reports `running` while the process has not finished.

- ``command: string``: Command to execute
If ``wait_for_shutdown`` is ``false`` and the process is still running on scenario shutdown, ``shutdown_signal`` is sent. If the process does not shutdown within shutdown_timeout, ``signal.sigkill`` is sent.

- ``command: string``: Command to execute
- ``wait_for_shutdown: bool``: Wait for the process to be finished. If false, the action immediately finishes (default: ``true``)
- ``shutdown_signal: signal``: (Only used if ``wait_for_shutdown`` is ``false``) Signal that is sent if a process is still running on scenario shutdown (default: ``signal!sigterm``)
- ``shutdown_timeout: time``: (Only used if ``wait_for_shutdown`` is ``false``) time to wait between ``shutdown_signal`` and SIGKILL getting sent, if process is still running on scenario shutdown (default: ``10s``)

OS
--
Expand All @@ -112,7 +116,15 @@ Actions

Report success if a file exists.

- ``file_name: string``: File name to check
- ``file_name: string``: File to check


``check_file_not_exists()``
"""""""""""""""""""""""""""

Report success if a file does not exist.

- ``file_name: string``: File to check


Robotics
Expand Down Expand Up @@ -270,6 +282,17 @@ Record a ROS bag, stored in directory ``output_dir`` defined by command-line par
- ``hidden_topics: bool``: Whether to record hidden topics (default: ``false``)
- ``storage: string``: Storage type to use (empty string: use ROS bag record default)

``ros_launch()``
""""""""""""""""

Execute a ROS launch file.

- ``package_name: string``: Package that contains the launch file
- ``launch_file: string``: Launch file name
- ``arguments: list of ros_argument``: ROS arguments (get forwarded as key:=value pairs)
- ``wait_for_shutdown: bool``: If true, the action waits until the execution is finished (default: ``true``)
- ``shutdown_timeout: time``: (Only used ``if wait_for_shutdown`` is ``false``) Time to wait between ``SIGINT`` and ``SIGKILL`` getting sent, if process is still running on scenario shutdown (default: ``10s``)

``service_call()``
""""""""""""""""""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ def __init__(self, name, file_name):

def update(self) -> py_trees.common.Status:
if os.path.isfile(self.file_name):
self.feedback_message = f"File '{self.file_name}' exists" # pylint: disable= attribute-defined-outside-init
return py_trees.common.Status.SUCCESS
else:
self.feedback_message = f"File '{self.file_name}' does not exist" # pylint: disable= attribute-defined-outside-init
return py_trees.common.Status.FAILURE
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright (C) 2024 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0

import os
import py_trees


class CheckFileNotExists(py_trees.behaviour.Behaviour):
"""
Check that a file does not exist
"""

def __init__(self, name, file_name):
super().__init__(name)
self.file_name = file_name

def update(self) -> py_trees.common.Status:
if os.path.isfile(self.file_name):
self.feedback_message = f"File '{self.file_name}' exists" # pylint: disable= attribute-defined-outside-init
return py_trees.common.Status.FAILURE
else:
self.feedback_message = f"File '{self.file_name}' does not exist" # pylint: disable= attribute-defined-outside-init
return py_trees.common.Status.SUCCESS
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ import osc.standard.base
action check_file_exists:
# report success if a file exists
file_name: string # file to check

action check_file_not_exists:
# report success if a file does not exist
file_name: string # file to check
1 change: 1 addition & 0 deletions libs/scenario_execution_os/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
entry_points={
'scenario_execution.actions': [
'check_file_exists = scenario_execution_os.actions.check_file_exists:CheckFileExists',
'check_file_not_exists = scenario_execution_os.actions.check_file_not_exists:CheckFileNotExists',
],
'scenario_execution.osc_libraries': [
'os = '
Expand Down
77 changes: 77 additions & 0 deletions libs/scenario_execution_os/test/test_check_file_not_exists.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Copyright (C) 2024 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0

import unittest
import tempfile
from scenario_execution import ScenarioExecution
from scenario_execution.model.osc2_parser import OpenScenario2Parser
from scenario_execution.model.model_to_py_tree import create_py_tree
from scenario_execution.utils.logging import Logger

from antlr4.InputStream import InputStream


class TestCheckData(unittest.TestCase):
# pylint: disable=missing-function-docstring,missing-class-docstring

def setUp(self) -> None:
self.parser = OpenScenario2Parser(Logger('test', False))
self.scenario_execution = ScenarioExecution(debug=False, log_model=False, live_tree=False,
scenario_file="test.osc", output_dir=None)
self.tmp_file = tempfile.NamedTemporaryFile()
print(self.tmp_file.name)

def test_success(self):
scenario_content = """
import osc.os
scenario test:
do parallel:
serial:
check_file_not_exists('UNKNOWN_FILE')
emit end
time_out: serial:
wait elapsed(1s)
emit fail
"""

parsed_tree = self.parser.parse_input_stream(InputStream(scenario_content))
model = self.parser.create_internal_model(parsed_tree, "test.osc", False)
scenarios = create_py_tree(model, self.parser.logger, False)
self.scenario_execution.scenarios = scenarios
self.scenario_execution.run()
self.assertTrue(self.scenario_execution.process_results())

def test_fail(self):
scenario_content = """
import osc.os
scenario test:
do parallel:
serial:
check_file_not_exists('""" + self.tmp_file.name + """')
emit end
time_out: serial:
wait elapsed(1s)
emit fail
"""

parsed_tree = self.parser.parse_input_stream(InputStream(scenario_content))
model = self.parser.create_internal_model(parsed_tree, "test.osc", False)
scenarios = create_py_tree(model, self.parser.logger, False)
self.scenario_execution.scenarios = scenarios
self.scenario_execution.run()
self.assertFalse(self.scenario_execution.process_results())
33 changes: 27 additions & 6 deletions scenario_execution/scenario_execution/actions/run_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,20 @@
import subprocess # nosec B404
from threading import Thread
from collections import deque
import signal


class RunProcess(py_trees.behaviour.Behaviour):
"""
Class to execute an process. It finishes when the process finishes
Args:
command[str]: command to execute
Class to execute an process.
"""

def __init__(self, name, command=None):
def __init__(self, name, command=None, wait_for_shutdown=True, shutdown_timeout=10, shutdown_signal=("", signal.SIGTERM)):
super().__init__(name)
self.command = command.split(" ") if isinstance(command, str) else command
self.wait_for_shutdown = wait_for_shutdown
self.shutdown_timeout = shutdown_timeout
self.shutdown_signal = shutdown_signal[1]
self.executed = False
self.process = None
self.log_stdout_thread = None
Expand Down Expand Up @@ -78,6 +79,7 @@ def log_output(out, log_fct, buffer):
self.log_stderr_thread.start()

if self.process is None:
self.process = None
return py_trees.common.Status.FAILURE

ret = self.process.poll()
Expand Down Expand Up @@ -106,7 +108,10 @@ def check_running_process(self):
return:
py_trees.common.Status
"""
return py_trees.common.Status.RUNNING
if self.wait_for_shutdown:
return py_trees.common.Status.RUNNING
else:
return py_trees.common.Status.SUCCESS

def on_process_finished(self, ret):
"""
Expand All @@ -133,3 +138,19 @@ def set_command(self, command):

def get_command(self):
return self.command

def shutdown(self):
if self.process is None:
return

ret = self.process.poll()
if ret is None:
# kill running process
self.logger.info(f'Sending {signal.Signals(self.shutdown_signal).name} to process...')
self.process.send_signal(self.shutdown_signal)
self.process.wait(self.shutdown_timeout)
if self.process.poll() is None:
self.logger.info('Sending SIGKILL to process...')
self.process.send_signal(signal.SIGKILL)
self.process.wait()
self.logger.info('Process finished.')
15 changes: 14 additions & 1 deletion scenario_execution/scenario_execution/lib_osc/helpers.osc
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import osc.standard.base

enum signal: [
sighup = 1,
sigint = 2,
sigkill = 9,
sigusr1 = 10,
sigusr2 = 12,
sigterm = 15
]

action log:
# Print out a message
msg: string # Message to print

action run_process:
# Run an external process
# Run an external process. If wait_for_shutdown is false and the process is still running on scenario shutdown, shutdown_signal is sent. If the process does not shutdown within shutdown_timeout, SIGKILL is sent.
command: string # Command to execute
wait_for_shutdown: bool = true # wait for the process to be finished. If false, the action immediately finishes.
shutdown_signal: signal = signal!sigterm # (only used if wait_for_shutdown is false) signal that is sent if a process is still running on scenario shutdown
shutdown_timeout: time = 10s # (only used if wait_for_shutdown is false) time to wait between shutdown_signal and SIGKILL getting sent if process is still running on scenario shutdown

struct random:
def seed(seed_value: int = 0) is external scenario_execution.external_methods.random.seed()
Expand Down
39 changes: 39 additions & 0 deletions scenario_execution/test/test_run_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from scenario_execution.model.model_to_py_tree import create_py_tree
from scenario_execution.utils.logging import Logger
from antlr4.InputStream import InputStream
from datetime import datetime


class TestScenarioExecutionSuccess(unittest.TestCase):
Expand Down Expand Up @@ -102,3 +103,41 @@ def test_multi_element_command(self):
self.scenario_execution.scenarios = scenarios
self.scenario_execution.run()
self.assertTrue(self.scenario_execution.process_results())

def test_wait_for_shutdown_false(self):
scenario_content = """
import osc.standard.base
import osc.helpers
scenario test_run_process:
do parallel:
serial:
run_process('sleep 15', wait_for_shutdown: false)
emit end
time_out: serial:
wait elapsed(10s)
time_out_shutdown: emit fail
"""
parsed_tree = self.parser.parse_input_stream(InputStream(scenario_content))
model = self.parser.create_internal_model(parsed_tree, "test.osc", False)
scenarios = create_py_tree(model, self.parser.logger, False)
self.scenario_execution.scenarios = scenarios

start = datetime.now()
self.scenario_execution.run()
end = datetime.now()
duration = (end-start).total_seconds()
self.assertLessEqual(duration, 10.)
self.assertTrue(self.scenario_execution.process_results())

def test_signal_parsing(self):
scenario_content = """
import osc.standard.base
import osc.helpers
scenario test_run_process:
do run_process('sleep 15', wait_for_shutdown: false, shutdown_signal: signal!sigint)
"""
parsed_tree = self.parser.parse_input_stream(InputStream(scenario_content))
model = self.parser.create_internal_model(parsed_tree, "test.osc", False)
_ = create_py_tree(model, self.parser.logger, False)
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright (C) 2024 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0

from enum import Enum

from scenario_execution.actions.run_process import RunProcess
import signal


class RosLaunchActionState(Enum):
"""
States for executing a ros bag recording
"""
WAITING_FOR_TOPICS = 1
RECORDING = 2
FAILURE = 5


class RosLaunch(RunProcess):
"""
Class to execute ros bag recording
"""

def __init__(self, name, package_name: str, launch_file: str, arguments: list, wait_for_shutdown: bool, shutdown_timeout: float):
super().__init__(name, None, wait_for_shutdown, shutdown_timeout, shutdown_signal=("", signal.SIGINT))
self.package_name = package_name
self.launch_file = launch_file
self.arguments = arguments
self.wait_for_shutdown = wait_for_shutdown
self.command = None

def setup(self, **kwargs):
self.command = ["ros2", "launch", self.package_name, self.launch_file]

for arg in self.arguments:
if not arg["key"] or not arg["value"]:
raise ValueError(f'Invalid ros argument key:{arg["key"]}, value:{arg["value"]}')
self.command.append(f'{arg["key"]}:={arg["value"]}')

self.logger.info(f'Command: {" ".join(self.command)}')
Loading

0 comments on commit ad1ed88

Please sign in to comment.