diff options
Diffstat (limited to 'pw_system/py/pw_system/console.py')
-rw-r--r-- | pw_system/py/pw_system/console.py | 193 |
1 files changed, 125 insertions, 68 deletions
diff --git a/pw_system/py/pw_system/console.py b/pw_system/py/pw_system/console.py index 49950884a..6f6405cac 100644 --- a/pw_system/py/pw_system/console.py +++ b/pw_system/py/pw_system/console.py @@ -38,6 +38,7 @@ from inspect import cleandoc import logging from pathlib import Path import sys +import time from types import ModuleType from typing import ( Any, @@ -49,7 +50,6 @@ from typing import ( Optional, Union, ) -import socket import serial import IPython # type: ignore @@ -60,8 +60,11 @@ from pw_console.log_store import LogStore from pw_console.plugins.bandwidth_toolbar import BandwidthToolbar from pw_console.pyserial_wrapper import SerialWithLogging from pw_console.python_logging import create_temp_log_file, JsonLogFormatter +from pw_console.socket_client import SocketClient, SocketClientWithLogging +from pw_hdlc import rpc from pw_rpc.console_tools.console import flattened_rpc_completions from pw_system.device import Device +from pw_system.device_tracing import DeviceWithTracing from pw_tokenizer.detokenize import AutoUpdatingDetokenizer # Default proto imports: @@ -69,20 +72,21 @@ from pw_log.proto import log_pb2 from pw_metric_proto import metric_service_pb2 from pw_thread_protos import thread_snapshot_service_pb2 from pw_unit_test_proto import unit_test_pb2 +from pw_file import file_pb2 +from pw_trace_protos import trace_service_pb2 +from pw_transfer import transfer_pb2 _LOG = logging.getLogger('tools') _DEVICE_LOG = logging.getLogger('rpc_device') _SERIAL_DEBUG = logging.getLogger('pw_console.serial_debug_logger') _ROOT_LOG = logging.getLogger() -PW_RPC_MAX_PACKET_SIZE = 256 -SOCKET_SERVER = 'localhost' -SOCKET_PORT = 33000 MKFIFO_MODE = 0o666 -def _parse_args(): - """Parses and returns the command line arguments.""" +def get_parser() -> argparse.ArgumentParser: + """Gets argument parser with console arguments.""" + parser = argparse.ArgumentParser( prog="python -m pw_system.console", description=__doc__ ) @@ -157,8 +161,13 @@ def _parse_args(): '-s', '--socket-addr', type=str, - help='use socket to connect to server, type default for\ - localhost:33000, or manually input the server address:port', + help=( + 'Socket address used to connect to server. Type "default" to use ' + 'localhost:33000, pass the server address and port as ' + 'address:port, or prefix the path to a forwarded socket with ' + f'"{SocketClient.FILE_SOCKET_SERVER}:" as ' + f'{SocketClient.FILE_SOCKET_SERVER}:path_to_file.' + ), ) parser.add_argument( "--token-databases", @@ -179,6 +188,13 @@ def _parse_args(): help='glob pattern for .proto files.', ) parser.add_argument( + '-f', + '--ticks_per_second', + type=int, + dest='ticks_per_second', + help=('The clock rate of the trace events.'), + ) + parser.add_argument( '-v', '--verbose', action='store_true', @@ -191,7 +207,7 @@ def _parse_args(): help='Use IPython instead of pw_console.', ) - # TODO(b/248257406) Use argparse.BooleanOptionalAction when Python 3.8 is + # TODO: b/248257406 - Use argparse.BooleanOptionalAction when Python 3.8 is # no longer supported. parser.add_argument( '--rpc-logging', @@ -207,6 +223,38 @@ def _parse_args(): help="Don't use pw_rpc based logging.", ) + # TODO: b/248257406 - Use argparse.BooleanOptionalAction when Python 3.8 is + # no longer supported. + parser.add_argument( + '--hdlc-encoding', + action='store_true', + default=True, + help='Use HDLC encoding on transfer interfaces.', + ) + + parser.add_argument( + '--no-hdlc-encoding', + action='store_false', + dest='hdlc_encoding', + help="Don't use HDLC encoding on transfer interface.", + ) + + parser.add_argument( + '--channel-id', + type=int, + default=rpc.DEFAULT_CHANNEL_ID, + help="Channel ID used in RPC communications.", + ) + + return parser + + +def _parse_args(args: Optional[argparse.Namespace] = None): + """Parses and returns the command line arguments.""" + if args is not None: + return args + + parser = get_parser() return parser.parse_args() @@ -269,8 +317,10 @@ def _start_python_terminal( # pylint: disable=too-many-arguments if use_ipython: print(welcome_message) - IPython.terminal.embed.InteractiveShellEmbed().mainloop( - local_ns=local_variables, module=argparse.Namespace() + IPython.start_ipython( + argv=[], + display_banner=False, + user_ns=local_variables, ) return @@ -307,32 +357,12 @@ def _start_python_terminal( # pylint: disable=too-many-arguments interactive_console.embed() -class SocketClientImpl: - def __init__(self, config: str): - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - socket_server = '' - socket_port = 0 - - if config == 'default': - socket_server = SOCKET_SERVER - socket_port = SOCKET_PORT - else: - socket_server, socket_port_str = config.split(':') - socket_port = int(socket_port_str) - self.socket.connect((socket_server, socket_port)) - - def write(self, data: bytes): - self.socket.sendall(data) - - def read(self, num_bytes: int = PW_RPC_MAX_PACKET_SIZE): - return self.socket.recv(num_bytes) - - # pylint: disable=too-many-arguments,too-many-locals def console( device: str, baudrate: int, proto_globs: Collection[str], + ticks_per_second: Optional[int], token_databases: Collection[Path], socket_addr: str, logfile: str, @@ -347,6 +377,8 @@ def console( merge_device_and_host_logs: bool = False, rpc_logging: bool = True, use_ipython: bool = False, + channel_id: int = rpc.DEFAULT_CHANNEL_ID, + hdlc_encoding: bool = True, ) -> int: """Starts an interactive RPC console for HDLC.""" # argparse.FileType doesn't correctly handle '-' for binary files. @@ -418,7 +450,11 @@ def console( detokenizer = None if token_databases: - detokenizer = AutoUpdatingDetokenizer(*token_databases) + token_databases_with_domains = [] * len(token_databases) + for token_database in token_databases: + token_databases_with_domains.append(str(token_database) + "#trace") + + detokenizer = AutoUpdatingDetokenizer(*token_databases_with_domains) detokenizer.show_errors = True protos: List[Union[ModuleType, Path]] = list(_expand_globs(proto_globs)) @@ -435,6 +471,9 @@ def console( protos.extend(compiled_protos) protos.append(metric_service_pb2) protos.append(thread_snapshot_service_pb2) + protos.append(file_pb2) + protos.append(trace_service_pb2) + protos.append(transfer_pb2) if not protos: _LOG.critical( @@ -449,12 +488,9 @@ def console( ', '.join(proto_globs), ) - serial_impl = serial.Serial - if serial_debug: - serial_impl = SerialWithLogging - timestamp_decoder = None if socket_addr is None: + serial_impl = SerialWithLogging if serial_debug else serial.Serial serial_device = serial_impl( device, baudrate, @@ -464,7 +500,7 @@ def console( # https://pythonhosted.org/pyserial/pyserial_api.html#serial.Serial timeout=0.1, ) - read = lambda: serial_device.read(8192) + reader = rpc.SerialReader(serial_device, 8192) write = serial_device.write # Overwrite decoder for serial device. @@ -474,47 +510,68 @@ def console( timestamp_decoder = milliseconds_to_string else: + socket_impl = SocketClientWithLogging if serial_debug else SocketClient + + def disconnect_handler(socket_device: SocketClient) -> None: + """Attempts to reconnect on disconnected socket.""" + _LOG.error('Socket disconnected. Will retry to connect.') + while True: + try: + socket_device.connect() + break + except: # pylint: disable=bare-except + # Ignore errors and retry to reconnect. + time.sleep(1) + _LOG.info('Successfully reconnected') + try: - socket_device = SocketClientImpl(socket_addr) - read = socket_device.read + socket_device = socket_impl( + socket_addr, on_disconnect=disconnect_handler + ) + reader = rpc.SelectableReader(socket_device) write = socket_device.write except ValueError: _LOG.exception('Failed to initialize socket at %s', socket_addr) return 1 - device_client = Device( - 1, - read, - write, - protos, - detokenizer, - timestamp_decoder=timestamp_decoder, - rpc_timeout_s=5, - use_rpc_logging=rpc_logging, - ) - - _start_python_terminal( - device=device_client, - device_log_store=device_log_store, - root_log_store=root_log_store, - serial_debug_log_store=serial_debug_log_store, - log_file=logfile, - host_logfile=host_logfile, - device_logfile=device_logfile, - json_logfile=json_logfile, - serial_debug=serial_debug, - config_file_path=config_file, - use_ipython=use_ipython, - ) + with reader: + device_client = DeviceWithTracing( + ticks_per_second, + channel_id, + reader, + write, + protos, + detokenizer=detokenizer, + timestamp_decoder=timestamp_decoder, + rpc_timeout_s=5, + use_rpc_logging=rpc_logging, + use_hdlc_encoding=hdlc_encoding, + ) + with device_client: + _start_python_terminal( + device=device_client, + device_log_store=device_log_store, + root_log_store=root_log_store, + serial_debug_log_store=serial_debug_log_store, + log_file=logfile, + host_logfile=host_logfile, + device_logfile=device_logfile, + json_logfile=json_logfile, + serial_debug=serial_debug, + config_file_path=config_file, + use_ipython=use_ipython, + ) return 0 -def main() -> int: - return console(**vars(_parse_args())) +def main(args: Optional[argparse.Namespace] = None) -> int: + return console(**vars(_parse_args(args))) -def main_with_compiled_protos(compiled_protos): - return console(**vars(_parse_args()), compiled_protos=compiled_protos) +def main_with_compiled_protos( + compiled_protos, args: Optional[argparse.Namespace] = None +): + return console(**vars(_parse_args(args)), compiled_protos=compiled_protos) if __name__ == '__main__': |