1# Copyright 2023 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Command line interface for the Pigweed emulators frontend""" 15 16import argparse 17import json 18import os 19from pathlib import Path 20import signal 21import subprocess 22import sys 23import threading 24 25from typing import Any 26 27from pw_emu.core import Error 28from pw_emu.frontend import Emulator 29from serial import serial_for_url, SerialException 30from serial.tools.miniterm import Miniterm, key_description 31 32_TERM_CMD = ['python', '-m', 'serial', '--raw'] 33 34 35def _cmd_gdb_cmds(emu, args: argparse.Namespace) -> None: 36 """Run ``gdb`` commands in batch mode.""" 37 38 emu.run_gdb_cmds(args.gdb_cmd, executable=args.executable, pause=args.pause) 39 40 41def _cmd_load(emu: Emulator, args: argparse.Namespace) -> None: 42 """Load an executable image via ``gdb`` and start executing it if 43 ``--pause`` is not set""" 44 45 args.gdb_cmd = ['load'] 46 _cmd_gdb_cmds(emu, args) 47 48 49def _cmd_start(emu: Emulator, args: argparse.Namespace) -> None: 50 """Launch the emulator and start executing, unless ``--pause`` is set.""" 51 52 if args.runner: 53 emu.set_emu(args.runner) 54 55 emu.start( 56 target=args.target, 57 file=args.file, 58 pause=args.pause, 59 args=args.args, 60 debug=args.debug, 61 foreground=args.foreground, 62 ) 63 64 65def _get_miniterm(emu: Emulator, chan: str) -> Miniterm: 66 chan_type = emu.get_channel_type(chan) 67 if chan_type == 'tcp': 68 host, port = emu.get_channel_addr(chan) 69 url = f'socket://[{host}]:{port}' 70 elif chan_type == 'pty': 71 url = emu.get_channel_path(chan) 72 else: 73 raise Error(f'unknown channel type `{chan_type}`') 74 ser = serial_for_url(url) 75 ser.timeout = 1 76 miniterm = Miniterm(ser) 77 miniterm.raw = True 78 miniterm.set_tx_encoding('UTF-8') 79 miniterm.set_rx_encoding('UTF-8') 80 81 quit_key = key_description(miniterm.exit_character) 82 menu_key = key_description(miniterm.menu_character) 83 help_key = key_description('\x08') 84 help_desc = f'Help: {menu_key} followed by {help_key} ---' 85 86 print(f'--- Miniterm on {chan} ---') 87 print(f'--- Quit: {quit_key} | Menu: {menu_key} | {help_desc}') 88 89 # On POSIX systems miniterm uses TIOCSTI to "cancel" the TX thread 90 # (reading from the console, sending to the serial) which is 91 # disabled on Linux kernels > 6.2 see 92 # https://github.com/pyserial/pyserial/issues/243 93 # 94 # On Windows the cancel method does not seem to work either with 95 # recent win10 versions. 96 # 97 # Workaround by terminating the process for exceptions in the read 98 # and write threads. 99 threading.excepthook = lambda args: signal.raise_signal(signal.SIGTERM) 100 101 return miniterm 102 103 104def _cmd_run(emu: Emulator, args: argparse.Namespace) -> None: 105 """Start the emulator and connect the terminal to a channel. Stop 106 the emulator when exiting the terminal.""" 107 108 emu.start( 109 target=args.target, 110 file=args.file, 111 pause=True, 112 args=args.args, 113 ) 114 115 ctrl_chans = ['gdb', 'monitor', 'qmp', 'robot'] 116 if not args.channel: 117 for chan in emu.get_channels(): 118 if chan not in ctrl_chans: 119 args.channel = chan 120 break 121 if not args.channel: 122 raise Error(f'only control channels {ctrl_chans} found') 123 124 try: 125 miniterm = _get_miniterm(emu, args.channel) 126 emu.cont() 127 miniterm.start() 128 miniterm.join(True) 129 print('--- exit ---') 130 miniterm.stop() 131 miniterm.join() 132 miniterm.close() 133 except SerialException as err: 134 raise Error(f'error connecting to channel `{args.channel}`: {err}') 135 finally: 136 emu.stop() 137 138 139def _cmd_restart(emu: Emulator, args: argparse.Namespace) -> None: 140 """Restart the emulator and start executing, unless ``--pause`` is set.""" 141 142 if emu.running(): 143 emu.stop() 144 _cmd_start(emu, args) 145 146 147def _cmd_stop(emu: Emulator, _args: argparse.Namespace) -> None: 148 """Stop the emulator.""" 149 150 emu.stop() 151 152 153def _cmd_reset(emu: Emulator, _args: argparse.Namespace) -> None: 154 """Perform a software reset.""" 155 156 emu.reset() 157 158 159def _cmd_gdb(emu: Emulator, args: argparse.Namespace) -> None: 160 """Start a ``gdb`` interactive session.""" 161 162 executable = args.executable if args.executable else "" 163 164 signal.signal(signal.SIGINT, signal.SIG_IGN) 165 try: 166 cmd = emu.get_gdb_cmd() + [ 167 '-ex', 168 f'target remote {emu.get_gdb_remote()}', 169 executable, 170 ] 171 subprocess.run(cmd) 172 finally: 173 signal.signal(signal.SIGINT, signal.SIG_DFL) 174 175 176def _cmd_prop_ls(emu: Emulator, args: argparse.Namespace) -> None: 177 """List emulator object properties.""" 178 179 props = emu.list_properties(args.path) 180 print(json.dumps(props, indent=4)) 181 182 183def _cmd_prop_get(emu: Emulator, args: argparse.Namespace) -> None: 184 """Show the emulator's object properties.""" 185 186 print(emu.get_property(args.path, args.property)) 187 188 189def _cmd_prop_set(emu: Emulator, args: argparse.Namespace) -> None: 190 """Set emulator's object properties.""" 191 192 emu.set_property(args.path, args.property, args.value) 193 194 195def _cmd_term(emu: Emulator, args: argparse.Namespace) -> None: 196 """Connect with an interactive terminal to an emulator channel""" 197 198 try: 199 miniterm = _get_miniterm(emu, args.channel) 200 miniterm.start() 201 miniterm.join(True) 202 print('--- exit ---') 203 miniterm.stop() 204 miniterm.join() 205 miniterm.close() 206 except SerialException as err: 207 raise Error(f'error connecting to channel `{args.channel}`: {err}') 208 209 210def _cmd_resume(emu: Emulator, _args: argparse.Namespace) -> None: 211 """Resume the execution of a paused emulator.""" 212 213 emu.cont() 214 215 216def get_parser() -> argparse.ArgumentParser: 217 """Command line parser""" 218 219 parser = argparse.ArgumentParser( 220 description=__doc__, 221 formatter_class=argparse.RawDescriptionHelpFormatter, 222 ) 223 parser.add_argument( 224 '-i', 225 '--instance', 226 help=( 227 'Run multiple instances simultaneously by assigning each instance ' 228 'an ID (default: ``%(default)s``)' 229 ), 230 type=str, 231 metavar='STRING', 232 default='default', 233 ) 234 parser.add_argument( 235 '-C', 236 '--working-dir', 237 help=( 238 'Absolute path to the working directory ' 239 '(default: ``%(default)s``)' 240 ), 241 type=Path, 242 default=os.getenv('PW_EMU_WDIR'), 243 ) 244 parser.add_argument( 245 '-c', 246 '--config', 247 help='Absolute path to config file (default: ``%(default)s``)', 248 type=str, 249 default=None, 250 ) 251 252 subparsers = parser.add_subparsers(dest='command', required=True) 253 254 def add_cmd(name: str, func: Any) -> argparse.ArgumentParser: 255 subparser = subparsers.add_parser( 256 name, description=func.__doc__, help=func.__doc__ 257 ) 258 subparser.set_defaults(func=func) 259 return subparser 260 261 start = add_cmd('start', _cmd_start) 262 restart = add_cmd('restart', _cmd_restart) 263 264 for subparser in [start, restart]: 265 subparser.add_argument( 266 'target', 267 type=str, 268 ) 269 subparser.add_argument( 270 '--file', 271 '-f', 272 metavar='FILE', 273 help='File to load before starting', 274 ) 275 subparser.add_argument( 276 '--runner', 277 '-r', 278 help='The emulator to use (automatically detected if not set)', 279 choices=[None, 'qemu', 'renode'], 280 default=None, 281 ) 282 subparser.add_argument( 283 '--args', 284 '-a', 285 help='Options to pass to the emulator', 286 ) 287 subparser.add_argument( 288 '--pause', 289 '-p', 290 action='store_true', 291 help='Pause the emulator after starting it', 292 ) 293 subparser.add_argument( 294 '--debug', 295 '-d', 296 action='store_true', 297 help='Start the emulator in debug mode', 298 ) 299 subparser.add_argument( 300 '--foreground', 301 '-F', 302 action='store_true', 303 help='Start the emulator in foreground mode', 304 ) 305 306 run = add_cmd('run', _cmd_run) 307 run.add_argument( 308 'target', 309 type=str, 310 ) 311 run.add_argument( 312 'file', 313 metavar='FILE', 314 help='File to load before starting', 315 ) 316 run.add_argument( 317 '--args', 318 '-a', 319 help='Options to pass to the emulator', 320 ) 321 run.add_argument( 322 '--channel', 323 '-n', 324 help='Channel to connect the terminal to', 325 ) 326 327 stop = add_cmd('stop', _cmd_stop) 328 329 load = add_cmd('load', _cmd_load) 330 load.add_argument( 331 'executable', 332 metavar='FILE', 333 help='File to load via ``gdb``', 334 ) 335 load.add_argument( 336 '--pause', 337 '-p', 338 help='Pause the emulator after loading the file', 339 action='store_true', 340 ) 341 load.add_argument( 342 '--offset', 343 '-o', 344 metavar='ADDRESS', 345 help='Address to load the file at', 346 ) 347 348 reset = add_cmd('reset', _cmd_reset) 349 350 gdb = add_cmd('gdb', _cmd_gdb) 351 gdb.add_argument( 352 '--executable', 353 '-e', 354 metavar='FILE', 355 help='File to use for the debugging session', 356 ) 357 358 prop_ls = add_cmd('prop-ls', _cmd_prop_ls) 359 prop_ls.add_argument( 360 'path', 361 help='Absolute path to the emulator object', 362 ) 363 364 prop_get = add_cmd('prop-get', _cmd_prop_get) 365 prop_get.add_argument( 366 'path', 367 help='Absolute path to the emulator object', 368 ) 369 prop_get.add_argument( 370 'property', 371 help='Name of the object property', 372 ) 373 374 prop_set = add_cmd('prop-set', _cmd_prop_set) 375 prop_set.add_argument( 376 'path', 377 help='Absolute path to the emulator object', 378 ) 379 prop_set.add_argument( 380 'property', 381 help='Name of the object property', 382 ) 383 prop_set.add_argument( 384 'value', 385 help='Value to set for the object property', 386 ) 387 388 gdb_cmds = add_cmd('gdb-cmds', _cmd_gdb_cmds) 389 gdb_cmds.add_argument( 390 '--pause', 391 '-p', 392 help='Do not resume execution after running the commands', 393 action='store_true', 394 ) 395 gdb_cmds.add_argument( 396 '--executable', 397 '-e', 398 metavar='FILE', 399 help='Executable to use while running ``gdb`` commands', 400 ) 401 gdb_cmds.add_argument( 402 'gdb_cmd', 403 nargs='+', 404 help='``gdb`` command to execute', 405 ) 406 407 term = add_cmd('term', _cmd_term) 408 term.add_argument( 409 'channel', 410 help='Channel name', 411 ) 412 413 resume = add_cmd('resume', _cmd_resume) 414 415 parser.epilog = f"""commands usage: 416 {start.format_usage().strip()} 417 {restart.format_usage().strip()} 418 {stop.format_usage().strip()} 419 {run.format_usage().strip()} 420 {load.format_usage().strip()} 421 {reset.format_usage().strip()} 422 {gdb.format_usage().strip()} 423 {prop_ls.format_usage().strip()} 424 {prop_get.format_usage().strip()} 425 {prop_set.format_usage().strip()} 426 {gdb_cmds.format_usage().strip()} 427 {term.format_usage().strip()} 428 {resume.format_usage().strip()} 429 """ 430 431 return parser 432 433 434def main() -> int: 435 """Emulators frontend command line interface.""" 436 437 args = get_parser().parse_args() 438 if not args.working_dir: 439 args.working_dir = ( 440 f'{os.getenv("PW_PROJECT_ROOT")}/.pw_emu/{args.instance}' 441 ) 442 443 try: 444 emu = Emulator(Path(args.working_dir), args.config) 445 args.func(emu, args) 446 except Error as err: 447 print(err) 448 return 1 449 450 return 0 451 452 453if __name__ == '__main__': 454 sys.exit(main()) 455