1.. role:: python(code) 2 :language: python 3 :class: highlight 4 5.. _seed-0108: 6 7======================== 80108: Emulators Frontend 9======================== 10.. seed:: 11 :number: 108 12 :name: Emulators Frontend 13 :status: Accepted 14 :proposal_date: 2023-06-24 15 :cl: 158190 16 :authors: Octavian Purdila 17 :facilitator: Armando Montanez 18 19------- 20Summary 21------- 22This SEED proposes a new Pigweed module that allows users to define emulator 23targets, start, control and interact with multiple running emulator instances, 24either through a command line interface or programmatically through Python APIs. 25 26----------- 27Definitions 28----------- 29An **emulator** is a program that allows users to run unmodified images compiled 30for :ref:`target <docs-targets>` on the host machine. The **host** is the machine that 31runs the Pigweed environment. 32 33An emulated **machine** or **board** is an emulator abstraction that typically 34has a correspondence in the real world - a product, an evaluation board, a 35hardware platform. 36 37An emulated machine can be extended / tweaked through runtime configuration 38options: add sensors on an i2c bus, connect a disk drive to a disk controller, 39etc. 40 41An emulator may use an **object model**, a hierarchical arrangement of emulator 42**objects** which are emulated devices (e.g. SPI controller) or internal 43emulator structures. 44 45An object can be accessed through an **object path** and can have 46**properties**. Device properties controls how the device is emulated 47(e.g. enables or disables certain features, defines memory sizes, etc.). 48 49A **channel** is a communication abstraction between the emulator and host 50processes. Examples of channels that an emulator can expose to the host: 51 52* An emulated UART could be exposed on the host as a `PTY 53 <https://en.wikipedia.org/wiki/Pseudoterminal>`_ or a socket. 54 55* A flash device could be exposed on the host as file. 56 57* A network device could be exposed on the host as a tun/tap interface. 58 59* A remote gdb interface could be exposed to the host as socket. 60 61A **monitor** is a control channel that allows the user to interactively or 62programmatically control the emulator: pause execution, inspect the emulator 63internal state, connect new devices, etc. 64 65---------- 66Motivation 67---------- 68While it is already possible to use emulators directly, there is a significant 69learning curve for using a specific emulator. Even for the same emulator each 70emulated machine (board) has its own peculiarities and it often requires tweaks 71to customize it to a specific project's needs through command line options or 72scripts (either native emulator scripts, if supported, or through helper shell 73scripts). 74 75Once started, the user is responsible for managing the emulator life-cycle, 76potentially for multiple instances. They also have to interact with it through 77various channels (monitor, debugger, serial ports) that requires some level of 78host resource management. Especially in the case of using multiple emulator 79instances manually managing host resources are burdensome. 80 81A frequent example is the default debugger ``localhost:1234`` port that can 82conflict with multiple emulator instances or with other debuggers running on the 83host. Another example: serials exposed over PTY have the pts number in 84``/dev/pts/`` allocated dynamically and it requires the user to retrieve it 85somehow. 86 87This gets even more complex when using different operating systems where some 88type of host resources are not available (e.g. no PTYs on Windows) or with 89limited functionality (e.g. UNIX sockets are supported on Windows > Win10 but 90only for stream sockets and there is no Python support available yet). 91 92Using emulators in CI is also difficult, in part because host resource 93management is getting more complicated due scaling (e.g. more chances of TCP 94port conflicts) and restrictions in the execution environment. But also because 95of a lack of high level APIs to control emulators and access their channels. 96 97-------- 98Proposal 99-------- 100Add a new Pigweed module that: 101 102* Allows users to define emulation :ref:`targets <docs-targets>` that 103 encapsulate the emulated machine configuration, the tools configuration and 104 the host channels configuration. 105 106* Provides a command line interface that manages multiple emulator instances and 107 provides interactive access to the emulator's host channels. 108 109* Provides a Python API to control emulator instances and access the emulator's 110 host channels. 111 112* Supports multiple emulators, QEMU and renode as a starting point. 113 114* Expose channels for gdb, monitor and user selected devices through 115 configurable host resources, sockets and PTYs as a starting point. 116 117The following sections will add more details about the configuration, the 118command line interface, the API for controlling and accessing emulators and the 119API for adding support for more emulators. 120 121 122Configuration 123============= 124The emulators configuration is part of the Pigweed root configuration file 125(``pigweed.json``) and reside in the ``pw:pw_emu`` namespace. 126 127Projects can define emulation targets in the Pigweed root configuration file and 128can also import predefined targets from other files. The pw_emu module provides 129a set of targets as examples and to promote reusability. 130 131For example, the following top level ``pigweed.json`` configuration includes a 132target fragment from the ``pw_emu/qemu-lm3s6965evb.json`` file: 133 134.. code-block:: 135 136 { 137 "pw": { 138 "pw_emu": { 139 "target_files": [ 140 "pw_emu/qemu-lm3s6965evb.json" 141 ] 142 } 143 } 144 } 145 146 147``pw_emu/qemu-lm3s6965evb.json`` defines the ``qemu-lm3s6965evb`` target 148that uses qemu as the emulator and lm3s6965evb as the machine, with the 149``serial0`` chardev exposed as ``serial0``: 150 151.. code-block:: 152 153 { 154 "targets": { 155 "qemu-lm3s6965evb": { 156 "gdb": "arm-none-eabi-gdb", 157 "qemu": { 158 "executable": "qemu-system-arm", 159 "machine": "lm3s6965evb", 160 "channels": { 161 "chardevs": { 162 "serial0": { 163 "id": "serial0" 164 } 165 } 166 } 167 } 168 } 169 } 170 } 171 172This target emulates a stm32f405 SoC and is compatible with the 173:ref:`target-lm3s6965evb-qemu` Pigweed build target. 174 175The configuration defines a ``serial0`` channel to be the QEMU **chardev** with 176the ``serial0`` id. The default type of the channel is used, which is TCP and 177which is supported by all platforms. The user can change the type by adding a 178``type`` key set to the desired type (e.g. ``pty``). 179 180The following configuration fragment defines a target that uses renode: 181 182.. code-block:: 183 184 { 185 "targets": { 186 "renode-stm32f4_discovery": { 187 "gdb": "arm-none-eabi-gdb", 188 "renode": { 189 "executable": "renode", 190 "machine": "platforms/boards/stm32f4_discovery-kit.repl", 191 "channels": { 192 "terminals": { 193 "serial0": { 194 "device-path": "sysbus.uart0", 195 "type": "pty" 196 } 197 } 198 } 199 } 200 } 201 } 202 } 203 204Note that ``machine`` is used to identify which renode script to use to load the 205plaform description from and ``terminals`` to define which UART devices to 206expose to the host. Also note the ``serial0`` channel is set to be exposed as a 207PTY on the host. 208 209The following channel types are currently supported: 210 211* ``pty``: supported on Mac and Linux; renode only supports PTYs for 212 ``terminals`` channels. 213 214* ``tcp``: supported on all platforms and for all channels; it is also the 215 default type if no channel type is configured. 216 217The channel configuration can be set at multiple levels: emulator, target, or 218specific channel. The channel configuration takes precedence, then the target 219channel configuration then the emulator channel configuration. 220 221The following expressions are replaced in the configuration strings: 222 223* ``$pw_emu_wdir{relative-path}``: replaces statement with an absolute path by 224 concatenating the emulator's working directory with the given relative path. 225 226* ``$pw_emu_channel_port{channel-name}``: replaces the statement with the port 227 number for the given channel name; the channel type should be ``tcp``. 228 229* ``$pw_emu_channel_host{channel-name}``: replaces the statement with the host 230 for the given channel name; the channel type should be ``tcp``. 231 232* ``$pw_emu_channel_path{channel-name}``: replaces the statement with the path 233 for the given channel name; the channel type should be ``pty``. 234 235Besides running QEMU and renode as the main emulator, the target configuration 236allows users to start other programs before or after starting the main emulator 237process. This allows extending the emulated target with simulation or emulation 238outside of the main emulator. For example, for BLE emulation the main emulator 239could emulate just the serial port while the HCI emulation done is in an 240external program (e.g. `bumble <https://google.github.io/bumble>`_, `netsim 241<https://android.googlesource.com/platform/tools/netsim>`_). 242 243 244Command line interface 245====================== 246The command line interfaces enables users to control emulator instances and 247access their channels interactively. 248 249.. code-block:: text 250 251 usage: pw emu [-h] [-i STRING] [-w WDIR] {command} ... 252 253 Pigweed Emulators Frontend 254 255 start Launch the emulator and start executing, unless pause 256 is set. 257 restart Restart the emulator and start executing, unless pause 258 is set. 259 run Start the emulator and connect the terminal to a 260 channel. Stop the emulator when exiting the terminal 261 stop Stop the emulator 262 load Load an executable image via gdb. If pause is not set 263 start executing it. 264 reset Perform a software reset. 265 gdb Start a gdb interactive session 266 prop-ls List emulator object properties. 267 prop-get Show the emulator's object properties. 268 prop-set Show emulator's object properties. 269 gdb-cmds Run gdb commands in batch mode. 270 term Connect with an interactive terminal to an emulator 271 channel 272 273 optional arguments: 274 -h, --help show this help message and exit 275 -i STRING, --instance STRING 276 instance to use (default: default) 277 -w WDIR, --wdir WDIR path to working directory (default: None) 278 279 commands usage: 280 usage: pw emu start [-h] [--file FILE] [--runner {None,qemu,renode}] 281 [--args ARGS] [--pause] [--debug] [--foreground] 282 {qemu-lm3s6965evb,qemu-stm32vldiscovery,qemu-netduinoplus2} 283 usage: pw emu restart [-h] [--file FILE] [--runner {None,qemu,renode}] 284 [--args ARGS] [--pause] [--debug] [--foreground] 285 {qemu-lm3s6965evb,qemu-stm32vldiscovery,qemu-netduinoplus2} 286 usage: pw emu stop [-h] 287 usage: pw emu run [-h] [--args ARGS] [--channel CHANNEL] 288 {qemu-lm3s6965evb,qemu-stm32vldiscovery,qemu-netduinoplus2} FILE 289 usage: pw emu load [-h] [--pause] FILE 290 usage: pw emu reset [-h] 291 usage: pw emu gdb [-h] [--executable FILE] 292 usage: pw emu prop-ls [-h] path 293 usage: pw emu prop-get [-h] path property 294 usage: pw emu prop-set [-h] path property value 295 usage: pw emu gdb-cmds [-h] [--pause] [--executable FILE] gdb-command [gdb-command ...] 296 usage: pw emu term [-h] channel 297 298For example, the ``run`` command is useful for quickly running ELF binaries on an 299emulated target and seeing / interacting with a serial channel. It starts an 300emulator, loads an images, connects to a channel and starts executing. 301 302.. code-block:: 303 304 $ pw emu run qemu-netduinoplus2 out/stm32f429i_disc1_debug/obj/pw_snapshot/test/cpp_compile_test.elf 305 306 --- Miniterm on serial0 --- 307 --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H --- 308 INF [==========] Running all tests. 309 INF [ RUN ] Status.CompileTest 310 INF [ OK ] Status.CompileTest 311 INF [==========] Done running all tests. 312 INF [ PASSED ] 1 test(s). 313 --- exit --- 314 315Multiple emulator instances can be run and each emulator instance is identified 316by its working directory. The default working directory for ``pw emu`` is 317``$PW_PROJECT_ROOT/.pw_emu/<instance-id>`` where ``<instance-id>`` is a command 318line option that defaults to ``default``. 319 320For more complex usage patterns, the ``start`` command can be used which will 321launch an emulator instance in the background. Then, the user can debug the 322image with the ``gdb`` command, connect to a channel (e.g. serial port) with the 323``term`` command, reset the emulator with the ``reset`` command, inspect or 324change emulator properties with the ``prop-ls``, ``prop-get``, ``prop-set`` and 325finally stop the emulator instance with ``stop``. 326 327 328Python APIs 329=========== 330The pw_emu module offers Python APIs to launch, control and interact with an 331emulator instance. 332 333The following is an example of using these APIs which implements a simplified 334version of the ``run`` pw_emu CLI command: 335 336.. code-block:: python 337 338 # start an emulator instance and load the image to execute 339 # pause the emulator after loading the image 340 emu = Emulator(args.wdir) 341 emu.start(args.target, args.file, pause=True) 342 343 # determine the channel type and create a pyserial compatible URL 344 chan_type = emu.get_channel_type(args.chan) 345 if chan_type == 'tcp': 346 host, port = emu.get_channel_addr(chan) 347 url = f'socket://{host}:{port}' 348 elif chan_type == 'pty': 349 url = emu.get_channel_path(chan) 350 else: 351 raise Error(f'unknown channel type `{chan_type}`') 352 353 # open the serial port and create a miniterm instance 354 serial = serial_for_url(url) 355 serial.timeout = 1 356 miniterm = Miniterm(serial) 357 miniterm.raw = True 358 miniterm.set_tx_encoding('UTF-8') 359 miniterm.set_rx_encoding('UTF-8') 360 361 # now that we are connected to the channel we can unpause 362 # this approach will prevent and data loses 363 emu.cont() 364 365 miniterm.start() 366 try: 367 miniterm.join(True) 368 except KeyBoardInterrupt: 369 pass 370 miniterm.stop() 371 miniterm.join() 372 miniterm.close() 373 374For convenience, a ``TemporaryEmulator`` class is also provided. 375 376It manages emulator instances that run in temporary working directories. The 377emulator instance is stopped and the working directory is cleared when the with 378block completes. 379 380It also supports interoperability with the pw emu cli, i.e. starting the 381emulator with the CLI and controlling / interacting with it from the API. 382 383Usage example: 384 385.. code-block:: python 386 387 # programmatically start and load an executable then access it 388 with TemporaryEmulator() as emu: 389 emu.start(target, file) 390 with emu.get_channel_stream(chan) as stream: 391 ... 392 393 394 # or start it form the command line then access it programmatically 395 with TemporaryEmulator() as emu: 396 build.bazel( 397 ctx, 398 "run", 399 exec_path, 400 "--run_under=pw emu start <target> --file " 401 ) 402 403 with emu.get_channel_stream(chan) as stream: 404 ... 405 406 407Intended API shape 408------------------ 409This is not an API reference, just an example of the probable shape of the final 410API. 411 412:python:`class Emulator` is used to launch, control and interact with an 413emulator instance: 414 415.. code-block:: python 416 417 def start( 418 self, 419 target: str, 420 file: os.PathLike | None = None, 421 pause: bool = False, 422 debug: bool = False, 423 foreground: bool = False, 424 args: str | None = None, 425 ) -> None: 426 427|nbsp| 428 Start the emulator for the given target. 429 430 If file is set that the emulator will load the file before starting. 431 432 If pause is True the emulator is paused until the debugger is connected. 433 434 If debug is True the emulator is run in foreground with debug output 435 enabled. This is useful for seeing errors, traces, etc. 436 437 If foreground is True the emulator is run in foreground otherwise it is 438 started in daemon mode. This is useful when there is another process 439 controlling the emulator's life cycle (e.g. cuttlefish) 440 441 args are passed directly to the emulator 442 443:python:`def running(self) -> bool:` 444 Check if the main emulator process is already running. 445 446:python:`def stop(self) -> None` 447 Stop the emulator 448 449:python:`def get_gdb_remote(self) -> str:` 450 Return a string that can be passed to the target remote gdb command. 451 452:python:`def get_gdb(self) -> str | None:` 453 Returns the gdb command for current target. 454 455.. code-block:: python 456 457 def run_gdb_cmds( 458 commands : list[str], 459 executable: Path | None = None, 460 pause: bool = False 461 ) -> subprocess.CompletedProcess: 462 463|nbsp| 464 Connect to the target and run the given commands silently 465 in batch mode. 466 467 The executable is optional but it may be required by some gdb 468 commands. 469 470 If pause is set do not continue execution after running the 471 given commands. 472 473:python:`def reset() -> None` 474 Performs a software reset 475 476:python:`def list_properties(self, path: str) -> List[dict]` 477 Returns the property list for an emulator object. 478 479 The object is identified by a full path. The path is target specific and 480 the format of the path is emulator specific. 481 482 QEMU path example: /machine/unattached/device[10] 483 484 renode path example: sysbus.uart 485 486:python:`def set_property(path: str, prop: str, value: str) -> None` 487 Sets the value of an emulator's object property. 488 489:python:`def get_property(self, path: str, prop: str) -> None` 490 Returns the value of an emulator's object property. 491 492:python:`def get_channel_type(self, name: str) -> str` 493 Returns the channel type. 494 495 Currently ``pty`` or ``tcp`` are the only supported types. 496 497:python:`def get_channel_path(self, name: str) -> str:` 498 Returns the channel path. Raises InvalidChannelType if this is not a PTY 499 channel. 500 501:python:`def get_channel_addr(self, name: str) -> tuple:` 502 Returns a pair of (host, port) for the channel. Raises InvalidChannelType 503 if this is not a tcp channel. 504 505.. code-block:: python 506 507 def get_channel_stream( 508 name: str, 509 timeout: float | None = None 510 ) -> io.RawIOBase: 511 512|nbsp| 513 Returns a file object for a given host exposed device. 514 515 If timeout is None than reads and writes are blocking. If timeout is zero the 516 stream is operating in non-blocking mode. Otherwise read and write will 517 timeout after the given value. 518 519:python:`def get_channels(self) -> List[str]:` 520 Returns the list of available channels. 521 522:python:`def cont(self) -> None:` 523 Resume the emulator's execution 524 525--------------------- 526Problem investigation 527--------------------- 528Pigweed is missing a tool for basic emulators control and as shown in the 529motivation section directly using emulators directly is difficult. 530 531While emulation is not a goal for every project, it is appealing for some due 532to the low cost and scalability. Offering a customizable emulators frontend in 533Pigweed will make this even better for downstream projects as the investment to 534get started with emulation will be lower - significantly lower for projects 535looking for basic usage. 536 537There are two main use-cases that this proposal is addressing: 538 539* Easier and robust interactive debugging and testing on emulators. 540 541* Basic APIs for controlling and accessing emulators to help with emulator 542 based testing (and trivial CI deployment - as long as the Pigweed bootstrap 543 process can run in CI). 544 545The proposal focuses on a set of fairly small number of commands and APIs in 546order to minimize complexity and gather feedback from users before adding more 547features. 548 549Since the state of emulated boards may different between emulators, to enable 550users access to more emulated targets, the goal of the module is to support 551multiple emulators from the start. 552 553Two emulators were selected for the initial implementation: QEMU and 554renode. Both run on all Pigweed currently supported hosts (Linux, Mac, Windows) 555and there is good overlap in terms of APIs to configure, start, control and 556access host exposed channels to start with the two for the initial 557implementation. These emulators also have good support for embedded targets 558(with QEMU more focused on MMU class machines and renode fully focused on 559microcontrollers) and are widely used in this space for emulation purposes. 560 561 562Prior art 563========= 564While there are several emulators frontends available, their main focus is on 565graphical interfaces (`aqemu <https://sourceforge.net/projects/aqemu/>`_, 566`GNOME Boxes <https://wiki.gnome.org/Apps/Boxes>`_, 567`QtEmu <https://gitlab.com/qtemu/gui>`_, 568`qt-virt-manager <https://f1ash.github.io/qt-virt-manager/>`_, 569`virt-manager <https://virt-manager.org/>`_) and virtualization ( 570`virsh <https://www.libvirt.org/>`_, 571`guestfish <https://libguestfs.org/>`_). 572`qemu-init <https://github.com/mm1ke/qemu-init>`_ is a qemu CLI frontend but since 573it is written in bash it does not work on Windows nor is easy to retrofit it to 574add Python APIs for automation. 575 576.. inclusive-language: disable 577 578The QEMU project has a few `Python modules 579<https://github.com/qemu/qemu/tree/master/python/qemu>`_ that are used 580internally for testing and debugging QEMU. :python:`qemu.machine.QEMUMachine` 581implements a QEMU frontend that can start a QEMU process and can interact with 582it. However, it is clearly marked for internal use only, it is not published on 583pypi or with the QEMU binaries. It is also not as configurable for pw_emu's 584use-cases (e.g. does not support running the QEMU process in the background, 585does not multiple serial ports, does not support configuring how to expose the 586serial port, etc.). The :python:`qemu.qmp` module is `published on pypi 587<https://pypi.org/project/qemu.qmp/>`_ and can be potentially used by `pw_emu` 588to interact with the emulator over the QMP channel. 589 590.. inclusive-language: enable 591 592--------------- 593Detailed design 594--------------- 595The implementation supports QEMU and renode as emulators and works on 596Linux, Mac and Windows. 597 598Multiple instances are supported in order to enable developers who work on 599multiple downstream Pigweed projects to work unhindered and also to run 600multiple test instances in parallel on the same machine. 601 602Each instance is identified by a system absolute path that is also used to store 603state about the running instance such as pid files for running processes, 604current emulator and target, etc. This directory also contains information about 605how to access the emulator channels (e.g. socket ports, PTY paths) 606 607.. mermaid:: 608 609 graph TD; 610 TemporaryEmulator & pw_emu_cli[pw emu cli] <--> Emulator 611 Emulator <--> Launcher & Connector 612 Launcher <--> Handles 613 Connector <--- Handles 614 Launcher <--> Config 615 Handles --Save--> WD --Load--> Handles 616 WD[Working Directory] 617 618The implementation uses the following classes: 619 620* :py:class:`pw_emu.Emulator`: the user visible APIs 621 622* :py:class:`pw_emu.core.Launcher`: an abstract class that starts an emulator 623 instance for a given configuration and target 624 625* :py:class:`pw_emu.core.Connector`: an abstract class that is the interface 626 between a running emulator and the user visible APIs 627 628* :py:class:`pw_emu.core.Handles`: class that stores specific information about 629 a running emulator instance such as ports to reach emulator channels; it is 630 populated by :py:class:`pw_emu.core.Launcher` and saved in the working 631 directory and used by :py:class:`pw_emu.core.Connector` to access the emulator 632 channels, process pids, etc. 633 634* :py:class:`pw_emu.core.Config`: loads the pw_emu configuration and provides 635 helper methods to get and validate configuration options 636 637 638Documentation update 639==================== 640The following documentation should probably be updated to use ``pw emu`` instead 641of direct QEMU invocation: :ref:`module-pw_rust`, 642:ref:`target-lm3s6965evb-qemu`. The referenced QEMU targets are defined in 643fragment configuration files in the pw_emu module and included in the top level 644pigweed.json file. 645 646------------ 647Alternatives 648------------ 649UNIX sockets were investigated as an alternative to TCP for the host exposed 650channels. UNIX sockets main advantages over TCP is that it does not require 651dynamic port allocation which simplifies the bootstrap of the emulator (no need 652to query the emulator to determine which ports were allocated). Unfortunately, 653while Windows supports UNIX sockets since Win10, Python still does not support 654them on win32 platforms. renode also does not support UNIX sockets. 655 656-------------- 657Open questions 658-------------- 659 660Renode dynamic ports 661==================== 662While renode allows passing 0 for ports to allocate a dynamic port, it does not 663have APIs to retrieve the allocated port. Until support for such a feature is 664added upstream, the following technique can be used to allocate a port 665dynamically: 666 667.. code-block:: python 668 669 sock = socket.socket(socket.SOCK_INET, socket.SOCK_STREAM) 670 sock.bind(('', 0)) 671 _, port = socket.getsockname() 672 sock.close() 673 674There is a race condition that allows another program to fetch the same port, 675but it should work in most light use cases until the issue is properly resolved 676upstream. 677 678qemu_gcc target 679=============== 680It should still be possible to call QEMU directly as described in 681:ref:`target-lm3s6965evb-qemu` however, once ``pw_emu`` is implemented it is 682probably better to define a lm3s6965evb emulation target and update the 683documentation to use ``pw emu`` instead of the direct QEMU invocation. 684 685 686.. |nbsp| unicode:: 0xA0 687 :trim: 688