From 353f6094dd48769fa26fd641bdac83283611b49d Mon Sep 17 00:00:00 2001 From: Christophe Bedard Date: Fri, 21 Apr 2023 16:14:45 -0700 Subject: [PATCH] Create start/pause/resume/stop sub-commands for 'ros2 trace' Signed-off-by: Christophe Bedard --- README.md | 12 + ros2trace/ros2trace/command/trace.py | 20 +- ros2trace/ros2trace/verb/__init__.py | 44 ++++ ros2trace/ros2trace/verb/pause.py | 27 +++ ros2trace/ros2trace/verb/resume.py | 27 +++ ros2trace/ros2trace/verb/start.py | 27 +++ ros2trace/ros2trace/verb/stop.py | 27 +++ ros2trace/setup.py | 9 + .../test/test_ros2trace/test_trace.py | 174 +++++++++++---- .../tracetools_trace/tools/args.py | 35 ++- .../tracetools_trace/tools/lttng.py | 22 ++ tracetools_trace/tracetools_trace/trace.py | 208 +++++++++++++++--- 12 files changed, 550 insertions(+), 82 deletions(-) create mode 100644 ros2trace/ros2trace/verb/__init__.py create mode 100644 ros2trace/ros2trace/verb/pause.py create mode 100644 ros2trace/ros2trace/verb/resume.py create mode 100644 ros2trace/ros2trace/verb/start.py create mode 100644 ros2trace/ros2trace/verb/stop.py diff --git a/README.md b/README.md index bef468d7..39185ab0 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,18 @@ By default, it will enable all ROS 2 tracepoints. The trace will be written to `~/.ros/tracing/session-YYYYMMDDHHMMSS`. Run the command with `-h` for more information. +The `ros2 trace` command requires user interaction to start and then stop tracing. +To trace without user interaction (e.g., in scripts), or for finer-grained tracing control, the following sub-commands can be used: + +``` +$ ros2 trace start session_name # Configure tracing session and start tracing +$ ros2 trace pause session_name # Pause tracing after starting +$ ros2 trace resume session_name # Resume tracing after pausing +$ ros2 trace stop session_name # Stop tracing after starting or resuming +``` + +Run each command with `-h` for more information. + You must [install the kernel tracer](#building) if you want to enable kernel events (using the `-k`/`--kernel-events` option). If you have installed the kernel tracer, use kernel tracing, and still encounter an error here, make sure to [add your user to the `tracing` group](#tracing). diff --git a/ros2trace/ros2trace/command/trace.py b/ros2trace/ros2trace/command/trace.py index b0f0cb8e..dbe6aefa 100644 --- a/ros2trace/ros2trace/command/trace.py +++ b/ros2trace/ros2trace/command/trace.py @@ -14,16 +14,28 @@ """Module for trace command extension implementation.""" +from ros2cli.command import add_subparsers_on_demand from ros2cli.command import CommandExtension from tracetools_trace.tools import args from tracetools_trace.trace import trace class TraceCommand(CommandExtension): - """Trace ROS nodes to get information on their execution.""" + """Trace ROS 2 nodes to get information on their execution. The main 'trace' command requires user interaction; to trace non-interactively, use the 'start'/'stop'/'pause'/'resume' sub-commands.""" # noqa: E501 - def add_arguments(self, parser, cli_name): + def add_arguments(self, parser, cli_name) -> None: + self._subparser = parser args.add_arguments(parser) - def main(self, *, parser, args): - return trace(args) + # Add arguments and sub-commands of verbs + add_subparsers_on_demand(parser, cli_name, '_verb', 'ros2trace.verb', required=False) + + def main(self, *, parser, args) -> int: + if not hasattr(args, '_verb'): + # In case no verb was passed, do interactive tracing + return trace(args) + + extension = getattr(args, '_verb') + + # Call the verb's main method + return extension.main(args=args) diff --git a/ros2trace/ros2trace/verb/__init__.py b/ros2trace/ros2trace/verb/__init__.py new file mode 100644 index 00000000..21e690b7 --- /dev/null +++ b/ros2trace/ros2trace/verb/__init__.py @@ -0,0 +1,44 @@ +# Copyright 2017 Open Source Robotics Foundation, Inc. +# +# 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. + +from ros2cli.plugin_system import PLUGIN_SYSTEM_VERSION +from ros2cli.plugin_system import satisfies_version + + +class VerbExtension: + """ + The extension point for 'trace' verb extensions. + + The following properties must be defined: + * `NAME` (will be set to the entry point name) + + The following methods must be defined: + * `main` + + The following methods can be defined: + * `add_arguments` + """ + + NAME = None + EXTENSION_POINT_VERSION = '0.1' + + def __init__(self): + super(VerbExtension, self).__init__() + satisfies_version(PLUGIN_SYSTEM_VERSION, '^0.1') + + def add_arguments(self, parser, cli_name): + pass + + def main(self, *, args): + raise NotImplementedError() diff --git a/ros2trace/ros2trace/verb/pause.py b/ros2trace/ros2trace/verb/pause.py new file mode 100644 index 00000000..fa2cd7b2 --- /dev/null +++ b/ros2trace/ros2trace/verb/pause.py @@ -0,0 +1,27 @@ +# Copyright 2023 Apex.AI, Inc. +# +# 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. + +from ros2trace.verb import VerbExtension +from tracetools_trace.tools import args +from tracetools_trace.trace import pause + + +class PauseVerb(VerbExtension): + """Pause tracing after starting.""" + + def add_arguments(self, parser, cli_name) -> None: + args.add_arguments_session_name(parser) + + def main(self, *, args) -> int: + return pause(args) diff --git a/ros2trace/ros2trace/verb/resume.py b/ros2trace/ros2trace/verb/resume.py new file mode 100644 index 00000000..38a9c58b --- /dev/null +++ b/ros2trace/ros2trace/verb/resume.py @@ -0,0 +1,27 @@ +# Copyright 2023 Apex.AI, Inc. +# +# 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. + +from ros2trace.verb import VerbExtension +from tracetools_trace.tools import args +from tracetools_trace.trace import resume + + +class ResumeVerb(VerbExtension): + """Resume tracing after pausing.""" + + def add_arguments(self, parser, cli_name) -> None: + args.add_arguments_session_name(parser) + + def main(self, *, args) -> int: + return resume(args) diff --git a/ros2trace/ros2trace/verb/start.py b/ros2trace/ros2trace/verb/start.py new file mode 100644 index 00000000..640b627c --- /dev/null +++ b/ros2trace/ros2trace/verb/start.py @@ -0,0 +1,27 @@ +# Copyright 2023 Apex.AI, Inc. +# +# 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. + +from ros2trace.verb import VerbExtension +from tracetools_trace.tools import args +from tracetools_trace.trace import start + + +class StartVerb(VerbExtension): + """Configure tracing session and start tracing.""" + + def add_arguments(self, parser, cli_name) -> None: + args.add_arguments_noninteractive(parser) + + def main(self, *, args) -> int: + return start(args) diff --git a/ros2trace/ros2trace/verb/stop.py b/ros2trace/ros2trace/verb/stop.py new file mode 100644 index 00000000..1b8274a7 --- /dev/null +++ b/ros2trace/ros2trace/verb/stop.py @@ -0,0 +1,27 @@ +# Copyright 2023 Apex.AI, Inc. +# +# 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. + +from ros2trace.verb import VerbExtension +from tracetools_trace.tools import args +from tracetools_trace.trace import stop + + +class StopVerb(VerbExtension): + """Stop tracing after starting or resuming.""" + + def add_arguments(self, parser, cli_name) -> None: + args.add_arguments_session_name(parser) + + def main(self, *, args) -> int: + return stop(args) diff --git a/ros2trace/setup.py b/ros2trace/setup.py index a671fc48..39cc019a 100644 --- a/ros2trace/setup.py +++ b/ros2trace/setup.py @@ -37,5 +37,14 @@ 'ros2cli.command': [ f'trace = {package_name}.command.trace:TraceCommand', ], + 'ros2cli.extension_point': [ + f'{package_name}.verb = {package_name}.verb:VerbExtension', + ], + f'{package_name}.verb': [ + f'pause = {package_name}.verb.pause:PauseVerb', + f'resume = {package_name}.verb.resume:ResumeVerb', + f'start = {package_name}.verb.start:StartVerb', + f'stop = {package_name}.verb.stop:StopVerb', + ], } ) diff --git a/test_ros2trace/test/test_ros2trace/test_trace.py b/test_ros2trace/test/test_ros2trace/test_trace.py index de8e35a4..43a3e9a4 100644 --- a/test_ros2trace/test/test_ros2trace/test_trace.py +++ b/test_ros2trace/test/test_ros2trace/test_trace.py @@ -63,22 +63,30 @@ def tearDown(self) -> None: # Even if running 'ros2 trace' fails, we do not want any lingering tracing session self.assertNoTracingSession() - def assertNoTracingSession(self) -> None: + def get_tracing_sessions(self) -> Tuple[bool, str]: output = self.run_lttng_list() # If there is no session daemon, then there are no tracing sessions no_session_daemon_available = 'No session daemon is available' in output if no_session_daemon_available: - return + return False, output # Starting from LTTng 2.13, 'tracing session' was replaced with 'recording session' # (see lttng-tools e971184) - no_tracing_sessions = any( + has_tracing_sessions = not any( f'Currently no available {name} session' in output for name in ('tracing', 'recording') ) - if not no_tracing_sessions: + return has_tracing_sessions, output + + def assertTracingSession(self) -> None: + has_tracing_sessions, output = self.get_tracing_sessions() + self.assertTrue(has_tracing_sessions, 'no tracing sessions exist:\n' + output) + + def assertNoTracingSession(self) -> None: + has_tracing_sessions, output = self.get_tracing_sessions() + if has_tracing_sessions: # Destroy tracing sessions if there are any, this way we can continue running tests and # avoid possible interference between them self.run_lttng_destroy_all() - self.assertTrue(no_tracing_sessions, 'tracing session(s) exist:\n' + output) + self.assertFalse(has_tracing_sessions, 'tracing session(s) exist:\n' + output) def assertTraceExist(self, trace_dir: str) -> None: self.assertTrue(os.path.isdir(trace_dir), f'trace directory does not exist: {trace_dir}') @@ -90,7 +98,7 @@ def assertTraceContains( self, trace_dir: str, expected_trace_data: List[Tuple[str, str]], - ) -> None: + ) -> int: self.assertTraceExist(trace_dir) from tracetools_read.trace import get_trace_events events = get_trace_events(trace_dir) @@ -99,6 +107,7 @@ def assertTraceContains( any(trace_data in event.items() for event in events), f'{trace_data} not found in events: {events}', ) + return len(events) def create_test_tmpdir(self, test_name: str) -> str: prefix = self.__class__.__name__ + '__' + test_name @@ -126,20 +135,18 @@ def run_lttng_destroy_all(self): output = process.stdout + process.stderr self.assertEqual(0, process.returncode, f"'lttng destroy' command failed: {output}") - def run_trace_command_start( + def run_command( self, args: List[str], *, env: Optional[Dict[str, str]] = None, - wait_for_start: bool = False, ) -> subprocess.Popen: - args = ['ros2', 'trace', *args] print('=>running:', args) process_env = os.environ.copy() process_env['PYTHONUNBUFFERED'] = '1' if env: process_env.update(env) - process = subprocess.Popen( + return subprocess.Popen( args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, @@ -147,6 +154,26 @@ def run_trace_command_start( encoding='utf-8', env=process_env, ) + + def wait_and_print_command_output( + self, + process: subprocess.Popen, + ) -> int: + stdout, stderr = process.communicate() + stdout = stdout.strip(' \r\n\t') + stderr = stderr.strip(' \r\n\t') + print('=>stdout:\n' + stdout) + print('=>stderr:\n' + stderr) + return process.wait() + + def run_trace_command_start( + self, + args: List[str], + *, + env: Optional[Dict[str, str]] = None, + wait_for_start: bool = False, + ) -> subprocess.Popen: + process = self.run_command(['ros2', 'trace', *args], env=env) # Write to stdin to start tracing assert process.stdin process.stdin.write('\n') @@ -167,12 +194,7 @@ def run_trace_command_stop( assert process.stdin process.stdin.write('\n') process.stdin.flush() - stdout, stderr = process.communicate() - stdout = stdout.strip(' \r\n\t') - stderr = stderr.strip(' \r\n\t') - print('=>stdout:\n' + stdout) - print('=>stderr:\n' + stderr) - return process.wait() + return self.wait_and_print_command_output(process) def run_trace_command( self, @@ -183,6 +205,34 @@ def run_trace_command( process = self.run_trace_command_start(args, env=env) return self.run_trace_command_stop(process) + def run_trace_subcommand( + self, + args: List[str], + *, + env: Optional[Dict[str, str]] = None, + ) -> int: + process = self.run_command(['ros2', 'trace', *args], env=env) + return self.wait_and_print_command_output(process) + + def run_nodes(self) -> None: + nodes = [ + Node( + package='test_tracetools', + executable='test_ping', + output='screen', + ), + Node( + package='test_tracetools', + executable='test_pong', + output='screen', + ), + ] + ld = LaunchDescription(nodes) + ls = LaunchService() + ls.include_launch_description(ld) + exit_code = ls.run() + self.assertEqual(0, exit_code) + def test_default(self) -> None: tmpdir = self.create_test_tmpdir('test_default') @@ -205,31 +255,12 @@ def test_default(self) -> None: def test_default_tracing(self) -> None: tmpdir = self.create_test_tmpdir('test_default_tracing') - def run_nodes(): - nodes = [ - Node( - package='test_tracetools', - executable='test_ping', - output='screen', - ), - Node( - package='test_tracetools', - executable='test_pong', - output='screen', - ), - ] - ld = LaunchDescription(nodes) - ls = LaunchService() - ls.include_launch_description(ld) - exit_code = ls.run() - self.assertEqual(0, exit_code) - # Test with the default session name process = self.run_trace_command_start( ['--path', tmpdir, '--ust', tracepoints.rcl_subscription_init], wait_for_start=True, ) - run_nodes() + self.run_nodes() ret = self.run_trace_command_stop(process) self.assertEqual(0, ret) # Check that the trace directory was created @@ -256,7 +287,7 @@ def run_nodes(): ], wait_for_start=True, ) - run_nodes() + self.run_nodes() ret = self.run_trace_command_stop(process) self.assertEqual(0, ret) self.assertTraceContains( @@ -380,3 +411,72 @@ def test_append_trace(self) -> None: self.assertTraceExist(trace_dir) shutil.rmtree(tmpdir) + + def test_pause_resume_stop_bad_session_name(self) -> None: + for subcommand in ('pause', 'resume', 'stop'): + # Session name doesn't exist + ret = self.run_trace_subcommand([subcommand, 'some_nonexistent_session_name']) + self.assertEqual(1, ret, f'subcommand: {subcommand}') + # Session name not provided + ret = self.run_trace_subcommand([subcommand]) + self.assertEqual(2, ret, f'subcommand: {subcommand}') + + @unittest.skipIf(not are_tracepoints_included(), 'tracepoints are required') + def test_start_pause_resume_stop(self) -> None: + tmpdir = self.create_test_tmpdir('test_start_pause_resume_stop') + + # Start tracing and run nodes + ret = self.run_trace_subcommand( + ['start', 'test_start_pause_resume_stop', '--path', tmpdir], + ) + self.assertEqual(0, ret) + trace_dir = os.path.join(tmpdir, 'test_start_pause_resume_stop') + self.assertTraceExist(trace_dir) + self.assertTracingSession() + self.run_nodes() + + # Pause tracing and check trace + ret = self.run_trace_subcommand(['pause', 'test_start_pause_resume_stop']) + self.assertEqual(0, ret) + self.assertTracingSession() + expected_trace_data = [ + ('topic_name', '/ping'), + ('topic_name', '/pong'), + ] + num_events = self.assertTraceContains(trace_dir, expected_trace_data) + + # Pausing again should give an error but not affect anything + ret = self.run_trace_subcommand(['pause', 'test_start_pause_resume_stop']) + self.assertEqual(1, ret) + self.assertTracingSession() + new_num_events = self.assertTraceContains(trace_dir, expected_trace_data) + self.assertEqual(num_events, new_num_events, 'unexpected new events in trace') + + # When not tracing, run nodes again and check that trace didn't change + self.run_nodes() + new_num_events = self.assertTraceContains(trace_dir, expected_trace_data) + self.assertEqual(num_events, new_num_events, 'unexpected new events in trace') + + # Resume tracing and run nodes again + ret = self.run_trace_subcommand(['resume', 'test_start_pause_resume_stop']) + self.assertEqual(0, ret) + self.assertTracingSession() + self.run_nodes() + + # Resuming tracing again should give an error but not affect anything + ret = self.run_trace_subcommand(['resume', 'test_start_pause_resume_stop']) + self.assertEqual(1, ret) + self.assertTracingSession() + + # Stop tracing and check that trace changed + ret = self.run_trace_subcommand(['stop', 'test_start_pause_resume_stop']) + self.assertEqual(0, ret) + self.assertNoTracingSession() + new_num_events = self.assertTraceContains(trace_dir, expected_trace_data) + self.assertGreater(new_num_events, num_events, 'no new events in trace') + + # Stopping tracing again should give an error but not affect anything + ret = self.run_trace_subcommand(['stop', 'test_start_pause_resume_stop']) + self.assertEqual(1, ret) + + shutil.rmtree(tmpdir) diff --git a/tracetools_trace/tracetools_trace/tools/args.py b/tracetools_trace/tracetools_trace/tools/args.py index 05e41a59..c27074c1 100644 --- a/tracetools_trace/tracetools_trace/tools/args.py +++ b/tracetools_trace/tracetools_trace/tools/args.py @@ -38,17 +38,14 @@ def __init__(self, arg): def parse_args() -> argparse.Namespace: - """Parse args for tracing.""" - parser = argparse.ArgumentParser(description='Setup and launch an LTTng tracing session.') + """Parse arguments for interactive tracing session configuration.""" + parser = argparse.ArgumentParser( + description='Trace ROS 2 nodes to get information on their execution') add_arguments(parser) return parser.parse_args() -def add_arguments(parser: argparse.ArgumentParser) -> None: - parser.add_argument( - '-s', '--session-name', dest='session_name', - default=path.append_timestamp('session'), - help='the name of the tracing session (default: session-YYYYMMDDHHMMSS)') +def _add_arguments_configure(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-p', '--path', dest='path', help='path of the base directory for trace data (default: ' @@ -79,3 +76,27 @@ def add_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument( '-a', '--append-trace', dest='append_trace', action='store_true', help='append to trace if it already exists, otherwise error out (default: %(default)s)') + + +def _add_arguments_default_session_name(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + '-s', '--session-name', dest='session_name', + default=path.append_timestamp('session'), + help='the name of the tracing session (default: session-YYYYMMDDHHMMSS)') + + +def add_arguments(parser: argparse.ArgumentParser) -> None: + """Add arguments to parser for interactive tracing session configuration.""" + _add_arguments_default_session_name(parser) + _add_arguments_configure(parser) + + +def add_arguments_noninteractive(parser: argparse.ArgumentParser) -> None: + """Add arguments to parser for non-interactive tracing session configuration.""" + add_arguments_session_name(parser) + _add_arguments_configure(parser) + + +def add_arguments_session_name(parser: argparse.ArgumentParser) -> None: + """Add mandatory session name argument to parser.""" + parser.add_argument('session_name', help='the name of the tracing session') diff --git a/tracetools_trace/tracetools_trace/tools/lttng.py b/tracetools_trace/tracetools_trace/tools/lttng.py index 3c6b08ce..8e109ce8 100644 --- a/tracetools_trace/tracetools_trace/tools/lttng.py +++ b/tracetools_trace/tracetools_trace/tools/lttng.py @@ -84,6 +84,28 @@ def lttng_fini(**kwargs) -> None: _lttng.destroy(**kwargs) +def lttng_start(**kwargs) -> None: + """ + Start tracing. + + Raises RuntimeError on failure. + + :param session_name: the name of the session + """ + _lttng.start(**kwargs) + + +def lttng_stop(**kwargs) -> None: + """ + Stop tracing. + + Raises RuntimeError on failure. + + :param session_name: the name of the session + """ + _lttng.stop(**kwargs) + + def is_lttng_installed( *, minimum_version: Optional[str] = None, diff --git a/tracetools_trace/tracetools_trace/trace.py b/tracetools_trace/tracetools_trace/trace.py index 1d0f8148..725e27b8 100644 --- a/tracetools_trace/tracetools_trace/trace.py +++ b/tracetools_trace/tracetools_trace/trace.py @@ -19,8 +19,10 @@ import argparse import os import sys +from typing import Callable from typing import List from typing import Optional +from typing import Tuple from tracetools_trace.tools import args from tracetools_trace.tools import lttng @@ -29,36 +31,18 @@ from tracetools_trace.tools import signals -def init( +def _assert_lttng_installed() -> None: + if not lttng.is_lttng_installed(): + sys.exit(2) + + +def _display_info( *, - session_name: str, - base_path: Optional[str], - append_trace: bool, ros_events: List[str], kernel_events: List[str], context_fields: List[str], - display_list: bool = False, -) -> bool: - """ - Init and start tracing. - - Raises RuntimeError on failure, in which case the tracing session might still exist. - - :param session_name: the name of the session - :param base_path: the path to the directory in which to create the tracing session directory, - or `None` for default - :param append_trace: whether to append to the trace directory if it already exists, otherwise - an error is reported - :param ros_events: list of ROS events to enable - :param kernel_events: list of kernel events to enable - :param context_fields: list of context fields to enable - :param display_list: whether to display list(s) of enabled events and context names - :return: True if successful, False otherwise - """ - # Check if LTTng is installed right away before printing anything - if not lttng.is_lttng_installed(): - sys.exit(2) - + display_list: bool, +) -> None: ust_enabled = len(ros_events) > 0 kernel_enabled = len(kernel_events) > 0 if ust_enabled: @@ -78,12 +62,64 @@ def init( if display_list: print_names_list(context_fields) + +def _resolve_session_path( + *, + session_name: str, + base_path: Optional[str], +) -> Tuple[str, str]: if not base_path: base_path = path.get_tracing_directory() full_session_path = os.path.join(base_path, session_name) print(f'writing tracing session to: {full_session_path}') + return base_path, full_session_path + + +def init( + *, + session_name: str, + base_path: Optional[str], + append_trace: bool, + ros_events: List[str], + kernel_events: List[str], + context_fields: List[str], + display_list: bool, + interactive: bool, +) -> bool: + """ + Init and start tracing. + + Can be interactive by requiring user interaction to start tracing. If non-interactive, tracing + starts right away. + + Raises RuntimeError on failure, in which case the tracing session might still exist. - input('press enter to start...') + :param session_name: the name of the session + :param base_path: the path to the directory in which to create the tracing session directory, + or `None` for default + :param append_trace: whether to append to the trace directory if it already exists, otherwise + an error is reported + :param ros_events: list of ROS events to enable + :param kernel_events: list of kernel events to enable + :param context_fields: list of context fields to enable + :param display_list: whether to display list(s) of enabled events and context names + :param interactive: whether to require user interaction to start tracing + :return: True if successful, False otherwise + """ + _display_info( + ros_events=ros_events, + kernel_events=kernel_events, + context_fields=context_fields, + display_list=display_list, + ) + + base_path, full_session_path = _resolve_session_path( + session_name=session_name, + base_path=base_path, + ) + + if interactive: + input('press enter to start...') trace_directory = lttng.lttng_init( session_name=session_name, base_path=base_path, @@ -106,6 +142,8 @@ def fini( """ Stop and finalize tracing. + Needs user interaction to stop tracing. Stops tracing automatically on SIGINT. + :param session_name: the name of the session """ def _run() -> None: @@ -130,14 +168,45 @@ def cleanup( lttng.lttng_fini(session_name=session_name, ignore_error=True) +def _do_work_and_report_error( + work: Callable[[], int], + session_name: str, + *, + do_cleanup: bool, +) -> int: + """ + Perform some work, reporting any error and cleaning up. + + This will call the work function and catch `RuntimeError`, in which case the error will be + printed, and the session will be cleaned up if needed. + + :param work: the work function to be called which may raise `RuntimeError` + :param session_name: the session name + :param do_cleanup: whether to clean the session up on error + :return: the return code of the work function, or 1 if an error was reported + """ + _assert_lttng_installed() + try: + return work() + except RuntimeError as e: + print(f'error: {str(e)}', file=sys.stderr) + if do_cleanup: + cleanup(session_name=session_name) + return 1 + + def trace(args: argparse.Namespace) -> int: """ Trace. - :param args: the arguments parsed using `tracetools_trace.tools.args` + Needs user interaction to start tracing and then stop tracing. + + On failure, the tracing session will not exist. + + :param args: the arguments parsed using `tracetools_trace.tools.args.add_arguments` :return: the return code (0 if successful, 1 otherwise) """ - try: + def work() -> int: if not init( session_name=args.session_name, base_path=args.path, @@ -146,15 +215,86 @@ def trace(args: argparse.Namespace) -> int: kernel_events=args.events_kernel, context_fields=args.context_fields, display_list=args.list, + interactive=True, ): return 1 fini(session_name=args.session_name) return 0 - except RuntimeError as e: - print(f'error: {str(e)}', file=sys.stderr) - # Make sure to clean up tracing session - cleanup(session_name=args.session_name) - return 1 + return _do_work_and_report_error(work, args.session_name, do_cleanup=True) + + +def start(args: argparse.Namespace) -> int: + """ + Configure tracing session and start tracing. + + On failure, the tracing session will not exist. + + :param args: the arguments parsed using + `tracetools_trace.tools.args.add_arguments_noninteractive` + :return: the return code (0 if successful, 1 otherwise) + """ + def work() -> int: + return int( + not init( + session_name=args.session_name, + base_path=args.path, + append_trace=args.append_trace, + ros_events=args.events_ust, + kernel_events=args.events_kernel, + context_fields=args.context_fields, + display_list=args.list, + interactive=False, + ) + ) + return _do_work_and_report_error(work, args.session_name, do_cleanup=True) + + +def stop(args: argparse.Namespace) -> int: + """ + Stop tracing. + + On failure, the tracing session might still exist. + + :param args: the arguments parsed using + `tracetools_trace.tools.args.add_arguments_session_name` + :return: the return code (0 if successful, 1 otherwise) + """ + def work() -> int: + lttng.lttng_fini(session_name=args.session_name) + return 0 + return _do_work_and_report_error(work, args.session_name, do_cleanup=False) + + +def pause(args: argparse.Namespace) -> int: + """ + Pause tracing after starting or resuming. + + On failure, the tracing session might still exist. + + :param args: the arguments parsed using + `tracetools_trace.tools.args.add_arguments_session_name` + :return: the return code (0 if successful, 1 otherwise) + """ + def work() -> int: + lttng.lttng_stop(session_name=args.session_name) + return 0 + return _do_work_and_report_error(work, args.session_name, do_cleanup=False) + + +def resume(args: argparse.Namespace) -> int: + """ + Resume tracing after pausing. + + On failure, the tracing session might still exist. + + :param args: the arguments parsed using + `tracetools_trace.tools.args.add_arguments_session_name` + :return: the return code (0 if successful, 1 otherwise) + """ + def work() -> int: + lttng.lttng_start(session_name=args.session_name) + return 0 + return _do_work_and_report_error(work, args.session_name, do_cleanup=False) def main() -> int: