xref: /aosp_15_r20/external/pigweed/pw_emu/py/pw_emu/__main__.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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