aboutsummaryrefslogtreecommitdiff
path: root/pw_emu/design.rst
blob: 67e2207299acb6a2ee81edeb0aaeb0fc821dc1f3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
.. _module-pw_emu-design:

======
Design
======
.. pigweed-module-subpage::
   :name: pw_emu
   :tagline: Emulators frontend

Multiple instances are supported in order to enable developers who work on
multiple downstream Pigweed projects to work unhindered and also to run multiple
test instances in parallel on the same machine.

Each instance is identified by a system absolute path that is also used to store
state about the running instance such as pid files for running processes,
current emulator and target, etc. This directory also contains information about
how to access the emulator channels (e.g. socket ports, pty paths)

.. mermaid::

   graph TD;
       TemporaryEmulator & pw_emu_cli[pw emu cli] <--> Emulator
       Emulator <--> Launcher & Connector
       Launcher  <--> Handles
       Connector <--> Handles
       Launcher <--> Config
       Handles --Save--> WD --Load--> Handles
       WD[Working Directory]


The implementation uses the following classes:

* :py:class:`pw_emu.frontend.Emulator`: the user visible API

* :py:class:`pw_emu.core.Launcher`: an abstract class that starts an
  emulator instance for a given configuration and target

* :py:class:`pw_emu.core.Connector`: an abstract class that is the
  interface between a running emulator and the user visible APIs

* :py:class:`pw_emu.core.Handles`: class that stores specific
  information about a running emulator instance such as ports to reach emulator
  channels; it is populated by :py:class:`pw_emu.core.Launcher` and
  saved in the working directory and used by
  :py:class:`pw_emu.core.Connector` to access the emulator channels,
  process pids, etc.

* :py:class:`pw_emu.core.Config`: loads the pw_emu configuration and provides
  helper methods to get and validate configuration options

-------------------
Emulator properties
-------------------
The implementation exposes the ability to list, read and write emulator
properties. The frontend does not abstract properties in a way that is emulator
or even emulator target independent, other than mandating that each property is
identified by a path. Note that the format of the path is also emulator specific
and not standardized.

----
QEMU
----
The QEMU frontend is using `QMP <https://wiki.qemu.org/Documentation/QMP>`_ to
communicate with the running QEMU process and implement emulator specific
functionality like reset, list or reading properties, etc.

QMP is exposed to the host through two channels: a temporary one to establish
the initial connection that is used to read the dynamic configuration (e.g. TCP
ports, pty paths) and a permanent one that can be used thought the life of the
QEMU processes. The frontend is configuring QEMU to expose QMP to a
``localhost`` TCP port reserved by the frontend and then waiting for QEMU to
establish the connection on that port. Once the connection is established the
frontend will read the configuration of the permanent QMP channel (which can be
either a TCP port or a PTY path) and save it as a channel named ``qmp`` in the
:py:class:`pw_emu.core.Handles` object.

------
renode
------
The renode frontend is using `renode's robot port
<https://renode.readthedocs.io/en/latest/introduction/testing.html>`_ to
interact with the renode process. Although the robot interface is designed for
testing and not as a control interface, it is more robust and better suited to
be used as a machine interface than the alternative ``monitor`` interface which
is user oriented, ANSI colored, echoed, log mixed, telnet interface.

Bugs
====
While renode allows passing 0 for ports to allocate a dynamic port, it does not
have APIs to retrieve the allocated port. Until support for such a feature is
added upstream, the implementation is using the following technique to allocate
a port dynamically:

.. code-block:: python

   sock = socket.socket(socket.SOCK_INET, socket.SOCK_STREAM)
   sock.bind(('', 0))
   _, port = socket.getsockname()
   sock.close()

There is a race condition that allows another program to fetch the same port,
but it should work in most light use cases until the issue is properly resolved
upstream.