aboutsummaryrefslogtreecommitdiff
path: root/pw_emu/py/pw_emu
diff options
context:
space:
mode:
Diffstat (limited to 'pw_emu/py/pw_emu')
-rw-r--r--pw_emu/py/pw_emu/__main__.py76
-rw-r--r--pw_emu/py/pw_emu/core.py133
-rw-r--r--pw_emu/py/pw_emu/frontend.py69
-rw-r--r--pw_emu/py/pw_emu/qemu.py28
-rw-r--r--pw_emu/py/pw_emu/renode.py5
5 files changed, 173 insertions, 138 deletions
diff --git a/pw_emu/py/pw_emu/__main__.py b/pw_emu/py/pw_emu/__main__.py
index 253edd931..cba0ac322 100644
--- a/pw_emu/py/pw_emu/__main__.py
+++ b/pw_emu/py/pw_emu/__main__.py
@@ -33,21 +33,21 @@ _TERM_CMD = ['python', '-m', 'serial', '--raw']
def _cmd_gdb_cmds(emu, args: argparse.Namespace) -> None:
- """Run gdb commands in batch mode."""
+ """Run ``gdb`` commands in batch mode."""
emu.run_gdb_cmds(args.gdb_cmd, executable=args.executable, pause=args.pause)
def _cmd_load(emu: Emulator, args: argparse.Namespace) -> None:
- """Load an executable image via gdb start executing it if pause is
- not set"""
+ """Load an executable image via ``gdb`` and start executing it if
+ ``--pause`` is not set"""
args.gdb_cmd = ['load']
_cmd_gdb_cmds(emu, args)
def _cmd_start(emu: Emulator, args: argparse.Namespace) -> None:
- """Launch the emulator and start executing, unless pause is set."""
+ """Launch the emulator and start executing, unless ``--pause`` is set."""
if args.runner:
emu.set_emu(args.runner)
@@ -103,7 +103,7 @@ def _get_miniterm(emu: Emulator, chan: str) -> Miniterm:
def _cmd_run(emu: Emulator, args: argparse.Namespace) -> None:
"""Start the emulator and connect the terminal to a channel. Stop
- the emulator when exiting the terminal"""
+ the emulator when exiting the terminal."""
emu.start(
target=args.target,
@@ -137,7 +137,7 @@ def _cmd_run(emu: Emulator, args: argparse.Namespace) -> None:
def _cmd_restart(emu: Emulator, args: argparse.Namespace) -> None:
- """Restart the emulator and start executing, unless pause is set."""
+ """Restart the emulator and start executing, unless ``--pause`` is set."""
if emu.running():
emu.stop()
@@ -145,7 +145,7 @@ def _cmd_restart(emu: Emulator, args: argparse.Namespace) -> None:
def _cmd_stop(emu: Emulator, _args: argparse.Namespace) -> None:
- """Stop the emulator"""
+ """Stop the emulator."""
emu.stop()
@@ -157,7 +157,7 @@ def _cmd_reset(emu: Emulator, _args: argparse.Namespace) -> None:
def _cmd_gdb(emu: Emulator, args: argparse.Namespace) -> None:
- """Start a gdb interactive session"""
+ """Start a ``gdb`` interactive session."""
executable = args.executable if args.executable else ""
@@ -223,7 +223,10 @@ def get_parser() -> argparse.ArgumentParser:
parser.add_argument(
'-i',
'--instance',
- help='instance to use (default: %(default)s)',
+ help=(
+ 'Run multiple instances simultaneously by assigning each instance '
+ 'an ID (default: ``%(default)s``)'
+ ),
type=str,
metavar='STRING',
default='default',
@@ -231,14 +234,17 @@ def get_parser() -> argparse.ArgumentParser:
parser.add_argument(
'-C',
'--working-dir',
- help='path to working directory (default: %(default)s)',
+ help=(
+ 'Absolute path to the working directory '
+ '(default: ``%(default)s``)'
+ ),
type=Path,
default=os.getenv('PW_EMU_WDIR'),
)
parser.add_argument(
'-c',
'--config',
- help='path config file (default: %(default)s)',
+ help='Absolute path to config file (default: ``%(default)s``)',
type=str,
default=None,
)
@@ -264,37 +270,37 @@ def get_parser() -> argparse.ArgumentParser:
'--file',
'-f',
metavar='FILE',
- help='file to load before starting',
+ help='File to load before starting',
)
subparser.add_argument(
'--runner',
'-r',
- help='emulator to use, automatically detected if not set',
+ help='The emulator to use (automatically detected if not set)',
choices=[None, 'qemu', 'renode'],
default=None,
)
subparser.add_argument(
'--args',
'-a',
- help='options to pass to the emulator',
+ help='Options to pass to the emulator',
)
subparser.add_argument(
'--pause',
'-p',
action='store_true',
- help='pause the emulator after starting it',
+ help='Pause the emulator after starting it',
)
subparser.add_argument(
'--debug',
'-d',
action='store_true',
- help='start the emulator in debug mode',
+ help='Start the emulator in debug mode',
)
subparser.add_argument(
'--foreground',
'-F',
action='store_true',
- help='start the emulator in foreground mode',
+ help='Start the emulator in foreground mode',
)
run = add_cmd('run', _cmd_run)
@@ -305,17 +311,17 @@ def get_parser() -> argparse.ArgumentParser:
run.add_argument(
'file',
metavar='FILE',
- help='file to load before starting',
+ help='File to load before starting',
)
run.add_argument(
'--args',
'-a',
- help='options to pass to the emulator',
+ help='Options to pass to the emulator',
)
run.add_argument(
'--channel',
'-n',
- help='channel to connect the terminal to',
+ help='Channel to connect the terminal to',
)
stop = add_cmd('stop', _cmd_stop)
@@ -324,19 +330,19 @@ def get_parser() -> argparse.ArgumentParser:
load.add_argument(
'executable',
metavar='FILE',
- help='file to load via gdb',
+ help='File to load via ``gdb``',
)
load.add_argument(
'--pause',
'-p',
- help='pause the emulator after loading the file',
+ help='Pause the emulator after loading the file',
action='store_true',
)
load.add_argument(
'--offset',
'-o',
metavar='ADDRESS',
- help='address to load the file at',
+ help='Address to load the file at',
)
reset = add_cmd('reset', _cmd_reset)
@@ -346,62 +352,62 @@ def get_parser() -> argparse.ArgumentParser:
'--executable',
'-e',
metavar='FILE',
- help='file to use for the debugging session',
+ help='File to use for the debugging session',
)
prop_ls = add_cmd('prop-ls', _cmd_prop_ls)
prop_ls.add_argument(
'path',
- help='path of the emulator object',
+ help='Absolute path to the emulator object',
)
prop_get = add_cmd('prop-get', _cmd_prop_get)
prop_get.add_argument(
'path',
- help='path of the emulator object',
+ help='Absolute path to the emulator object',
)
prop_get.add_argument(
'property',
- help='name of the object property',
+ help='Name of the object property',
)
prop_set = add_cmd('prop-set', _cmd_prop_set)
prop_set.add_argument(
'path',
- help='path of the emulator object',
+ help='Absolute path to the emulator object',
)
prop_set.add_argument(
'property',
- help='name of the object property',
+ help='Name of the object property',
)
prop_set.add_argument(
'value',
- help='value to set for the object property',
+ help='Value to set for the object property',
)
gdb_cmds = add_cmd('gdb-cmds', _cmd_gdb_cmds)
gdb_cmds.add_argument(
'--pause',
'-p',
- help='do not resume execution after running the commands',
+ help='Do not resume execution after running the commands',
action='store_true',
)
gdb_cmds.add_argument(
'--executable',
'-e',
metavar='FILE',
- help='executable to use while running the gdb commands',
+ help='Executable to use while running ``gdb`` commands',
)
gdb_cmds.add_argument(
'gdb_cmd',
nargs='+',
- help='gdb command to execute',
+ help='``gdb`` command to execute',
)
term = add_cmd('term', _cmd_term)
term.add_argument(
'channel',
- help='channel name',
+ help='Channel name',
)
resume = add_cmd('resume', _cmd_resume)
@@ -435,7 +441,7 @@ def main() -> int:
)
try:
- emu = Emulator(args.working_dir, args.config)
+ emu = Emulator(Path(args.working_dir), args.config)
args.func(emu, args)
except Error as err:
print(err)
diff --git a/pw_emu/py/pw_emu/core.py b/pw_emu/py/pw_emu/core.py
index 1e5c2ea9f..536fcedee 100644
--- a/pw_emu/py/pw_emu/core.py
+++ b/pw_emu/py/pw_emu/core.py
@@ -40,7 +40,7 @@ _LAUNCHER_LOG = logging.getLogger('pw_qemu.core.launcher')
def _stop_process(pid: int) -> None:
- """Gracefully stop a running process."""
+ """Gracefully stops a running process."""
try:
proc = psutil.Process(pid)
@@ -132,7 +132,7 @@ class InvalidChannelType(Error):
class WrongEmulator(Error):
- """Exception raised if an different backend is running."""
+ """Exception raised if a different backend is running."""
def __init__(self, exp: str, found: str) -> None:
super().__init__(f'wrong emulator: expected `{exp}, found {found}`')
@@ -143,6 +143,8 @@ class RunError(Error):
def __init__(self, proc: str, msg: str) -> None:
super().__init__(f'error running `{proc}`: {msg}')
+ self.proc = proc
+ self.msg = msg
class InvalidPropertyPath(Error):
@@ -160,7 +162,7 @@ class InvalidProperty(Error):
class HandlesError(Error):
- """Exception raised while trying to load emulator handles."""
+ """Exception raised if the load of an emulator handle fails."""
def __init__(self, msg: str) -> None:
super().__init__(f'error loading handles: {msg}')
@@ -244,33 +246,33 @@ class Handles:
self.procs: Dict[str, Handles.Proc] = {}
def add_channel_tcp(self, name: str, host: str, port: int) -> None:
- """Add a TCP channel."""
+ """Adds a TCP channel."""
self.channels[name] = self.TcpChannel(host, port)
def add_channel_pty(self, name: str, path: str) -> None:
- """Add a pty channel."""
+ """Adds a pty channel."""
self.channels[name] = self.PtyChannel(path)
def add_proc(self, name: str, pid: int) -> None:
- """Add a pid."""
+ """Adds a process ID."""
self.procs[name] = self.Proc(pid)
def set_target(self, target: str) -> None:
- """Set the target."""
+ """Sets the target."""
self.target = target
def set_gdb_cmd(self, cmd: List[str]) -> None:
- """Set the gdb command."""
+ """Sets the ``gdb`` command."""
self.gdb_cmd = cmd.copy()
def _stop_processes(handles: Handles, wdir: Path) -> None:
- """Stop all processes for a (partially) running emulator instance.
+ """Stops all processes for a (partially) running emulator instance.
Remove pid files as well.
"""
@@ -291,16 +293,16 @@ class Config:
target: Optional[str] = None,
emu: Optional[str] = None,
) -> None:
- """Load the emulator configuration.
+ """Loads the emulator configuration.
If no configuration file path is given, the root project
configuration is used.
- This method set ups the generic configuration (e.g. gdb).
+ This method set ups the generic configuration (e.g. ``gdb``).
- It loads emulator target files and gathers them under the 'targets' key
- for each emulator backend. The 'targets' settings in the configuration
- file takes precedence over the loaded target files.
+ It loads emulator target files and gathers them under the ``targets``
+ key for each emulator backend. The ``targets`` settings in the
+ configuration file takes precedence over the loaded target files.
"""
try:
@@ -336,7 +338,8 @@ class Config:
def set_target(self, target: str) -> None:
"""Sets the current target.
- The current target is used by the get_target method.
+ The current target is used by the
+ :py:meth:`pw_emu.core.Config.get_target` method.
"""
@@ -383,19 +386,21 @@ class Config:
optional: bool = True,
entry_type: Optional[Type] = None,
) -> Any:
- """Get a config entry.
+ """Gets a config entry.
- keys is a list of string that identifies the config entry, e.g.
- ['targets', 'test-target'] is going to look in the config dicionary for
- ['targets']['test-target'].
+ ``keys`` identifies the config entry, e.g.
+ ``['targets', 'test-target']`` looks in the config dictionary for
+ ``['targets']['test-target']``.
- If the option is not found and optional is True it returns None if
- entry_type is none or a new (empty) object of type entry_type.
+ If the option is not found and optional is ``True`` it returns ``None``
+ if ``entry_type`` is ``None`` or a new (empty) object of type
+ ``entry_type``.
- If the option is not found an optional is False it raises ConfigError.
+ If the option is not found and ``optional`` is ``False`` it raises
+ ``ConfigError``.
- If entry_type is not None it will check the option to be of
- that type. If it is not it will raise ConfigError.
+ If ``entry_type`` is not ``None`` it checks the option to be of
+ that type. If it is not it will raise ``ConfigError``.
"""
@@ -435,7 +440,7 @@ class Config:
optional: bool = True,
entry_type: Optional[Type] = None,
) -> Any:
- """Get a config option starting at ['targets'][target]."""
+ """Gets a config option starting at ``['targets'][target]``."""
if not self._target:
raise Error('target not set')
@@ -447,7 +452,7 @@ class Config:
optional: bool = True,
entry_type: Optional[Type] = None,
) -> Any:
- """Get a config option starting at [emu]."""
+ """Gets a config option starting at ``[emu]``."""
if not self._emu:
raise Error('emu not set')
@@ -459,7 +464,7 @@ class Config:
optional: bool = True,
entry_type: Optional[Type] = None,
) -> Any:
- """Get a config option starting at ['targets'][target][emu]."""
+ """Gets a config option starting at ``['targets'][target][emu]``."""
if not self._emu or not self._target:
raise Error('emu or target not set')
@@ -469,7 +474,7 @@ class Config:
class Connector(ABC):
- """Interface between a running emulator and the user visible APIs."""
+ """Interface between a running emulator and the user-visible APIs."""
def __init__(self, wdir: Path) -> None:
self._wdir = wdir
@@ -479,7 +484,7 @@ class Connector(ABC):
@staticmethod
def get(wdir: Path) -> Any:
- """Return a connector instace for a given emulator type."""
+ """Returns a connector instance for a given emulator type."""
handles = Handles.load(wdir)
config = Config(handles.config)
emu = handles.emu
@@ -496,7 +501,7 @@ class Connector(ABC):
return self._handles.emu
def get_gdb_cmd(self) -> List[str]:
- """Returns the configured gdb command."""
+ """Returns the configured ``gdb`` command."""
return self._handles.gdb_cmd
def get_config_path(self) -> Path:
@@ -519,8 +524,8 @@ class Connector(ABC):
raise InvalidChannelName(name, self._target, channels)
def get_channel_path(self, name: str) -> str:
- """Returns the channel path. Raises InvalidChannelType if this
- is not a pty channel.
+ """Returns the channel path. Raises ``InvalidChannelType`` if this
+ is not a ``pty`` channel.
"""
@@ -532,8 +537,8 @@ class Connector(ABC):
raise InvalidChannelName(name, self._target, self._channels.keys())
def get_channel_addr(self, name: str) -> tuple:
- """Returns a pair of (host, port) for the channel. Raises
- InvalidChannelType if this is not a tcp channel.
+ """Returns a pair of ``(host, port)`` for the channel. Raises
+ ``InvalidChannelType`` if this is not a ``tcp`` channel.
"""
@@ -549,11 +554,11 @@ class Connector(ABC):
name: str,
timeout: Optional[float] = None,
) -> io.RawIOBase:
- """Returns a file object for a given host exposed device.
+ """Returns a file object for a given host-exposed device.
- If timeout is None than reads and writes are blocking. If
- timeout is zero the stream is operating in non-blocking
- mode. Otherwise read and write will timeout after the given
+ If ``timeout`` is ``None`` then reads and writes are blocking. If
+ ``timeout`` is ``0`` the stream is operating in non-blocking
+ mode. Otherwise reads and writes will timeout after the given
value.
"""
@@ -579,8 +584,14 @@ class Connector(ABC):
def get_channels(self) -> List[str]:
return self._handles.channels.keys()
+ def get_logs(self) -> str:
+ """Returns the emulator logs."""
+
+ log_path = self._wdir / f'{self._handles.emu}.log'
+ return log_path.read_text()
+
def stop(self) -> None:
- """Stop the emulator."""
+ """Stops the emulator."""
_stop_processes(self._handles, self._wdir)
@@ -596,7 +607,7 @@ class Connector(ABC):
return False
def running(self) -> bool:
- """Check if the main emulator process is already running."""
+ """Checks if the main emulator process is already running."""
try:
return psutil.pid_exists(self._handles.procs[self._handles.emu].pid)
@@ -605,11 +616,11 @@ class Connector(ABC):
@abstractmethod
def reset(self) -> None:
- """Perform a software reset."""
+ """Performs a software reset."""
@abstractmethod
def cont(self) -> None:
- """Resume the emulator's execution."""
+ """Resumes the emulator's execution."""
@abstractmethod
def list_properties(self, path: str) -> List[Any]:
@@ -632,7 +643,7 @@ class Launcher(ABC):
emu: str,
config_path: Optional[Path] = None,
) -> None:
- """Initializes a Launcher instance."""
+ """Initializes a ``Launcher`` instance."""
self._wdir: Optional[Path] = None
"""Working directory"""
@@ -673,7 +684,7 @@ class Launcher(ABC):
debug: bool = False,
args: Optional[str] = None,
) -> List[str]:
- """Pre start work, returns command to start the emulator.
+ """Pre-start work, returns command to start the emulator.
The target and emulator configuration can be accessed through
:py:attr:`pw_emu.core.Launcher._config` with
@@ -685,9 +696,9 @@ class Launcher(ABC):
@abstractmethod
def _post_start(self) -> None:
- """Post start work, finalize emulator handles.
+ """Post-start work, finalize emulator handles.
- Perform any post start emulator initialization and finalize the emulator
+ Perform any post-start emulator initialization and finalize the emulator
handles information.
Typically an internal monitor channel is used to inquire information
@@ -700,7 +711,7 @@ class Launcher(ABC):
@abstractmethod
def _get_connector(self, wdir: Path) -> Connector:
- """Get a connector for this emulator type."""
+ """Gets a connector for this emulator type."""
def _path(self, name: Union[Path, str]) -> Path:
"""Returns the full path for a given emulator file."""
@@ -895,20 +906,20 @@ class Launcher(ABC):
foreground: bool = False,
args: Optional[str] = None,
) -> Connector:
- """Start the emulator for the given target.
+ """Starts the emulator for the given target.
- If file is set that the emulator will load the file before starting.
+ If ``file`` is set the emulator loads that file before starting.
- If pause is True the emulator is paused.
+ If ``pause`` is ``True`` the emulator gets paused.
- If debug is True the emulator is run in foreground with debug output
- enabled. This is useful for seeing errors, traces, etc.
+ If ``debug`` is ``True`` the emulator runs in the foreground with
+ debug output enabled. This is useful for seeing errors, traces, etc.
- If foreground is True the emulator is run in foreground otherwise it is
- started in daemon mode. This is useful when there is another process
- controlling the emulator's life cycle (e.g. cuttlefish)
+ If ``foreground`` is ``True`` the emulator is run in the foreground
+ otherwise it is started in daemon mode. This is useful when there is
+ another process controlling the emulator's life cycle, e.g. cuttlefish.
- args are passed directly to the emulator
+ ``args`` are passed directly to the emulator.
"""
@@ -946,7 +957,15 @@ class Launcher(ABC):
self._stop_procs()
raise err
- self._post_start()
+ try:
+ self._post_start()
+ except RunError as err:
+ self._handles.save(wdir)
+ connector = self._get_connector(self._wdir)
+ if not connector.running():
+ msg = err.msg + '; dumping logs:\n' + connector.get_logs()
+ raise RunError(err.proc, msg)
+ raise err
self._handles.save(wdir)
if proc:
diff --git a/pw_emu/py/pw_emu/frontend.py b/pw_emu/py/pw_emu/frontend.py
index 8ecf7d3fe..e54355d90 100644
--- a/pw_emu/py/pw_emu/frontend.py
+++ b/pw_emu/py/pw_emu/frontend.py
@@ -74,22 +74,22 @@ class Emulator:
foreground: bool = False,
args: Optional[str] = None,
) -> None:
- """Start the emulator for the given target.
+ """Starts the emulator for the given ``target``.
- If file is set the emulator will load the file before starting.
+ If ``file`` is set the emulator loads the file before starting.
- If pause is True the emulator is paused until the debugger is
+ If ``pause`` is ``True`` the emulator pauses until the debugger is
connected.
- If debug is True the emulator is run in foreground with debug
+ If ``debug`` is ``True`` the emulator runs in the foreground with debug
output enabled. This is useful for seeing errors, traces, etc.
- If foreground is True the emulator is run in foreground otherwise
- it is started in daemon mode. This is useful when there is
- another process controlling the emulator's life cycle
- (e.g. cuttlefish)
+ If ``foreground`` is ``True`` the emulator runs in the foreground,
+ otherwise it starts in daemon mode. Foreground mode is useful when
+ there is another process controlling the emulator's life cycle,
+ e.g. cuttlefish.
- args are passed directly to the emulator
+ ``args`` are passed directly to the emulator.
"""
if self._connector:
@@ -115,7 +115,7 @@ class Emulator:
return self._connector
def running(self) -> bool:
- """Check if the main emulator process is already running."""
+ """Checks if the main emulator process is already running."""
try:
return self._c().running()
@@ -133,7 +133,7 @@ class Emulator:
return self._c().stop()
def get_gdb_remote(self) -> str:
- """Return a string that can be passed to the target remote gdb
+ """Returns a string that can be passed to the target remote ``gdb``
command.
"""
@@ -150,7 +150,7 @@ class Emulator:
raise InvalidChannelType(chan_type)
def get_gdb_cmd(self) -> List[str]:
- """Returns the gdb command for current target."""
+ """Returns the ``gdb`` command for current target."""
return self._c().get_gdb_cmd()
def run_gdb_cmds(
@@ -159,14 +159,12 @@ class Emulator:
executable: Optional[str] = None,
pause: bool = False,
) -> subprocess.CompletedProcess:
- """Connect to the target and run the given commands silently
+ """Connects to the target and runs the given commands silently
in batch mode.
- The executable is optional but it may be required by some gdb
- commands.
+ ``executable`` is optional but may be required by some ``gdb`` commands.
- If pause is set do not continue execution after running the
- given commands.
+ If ``pause`` is set, execution stops after running the given commands.
"""
@@ -188,18 +186,18 @@ class Emulator:
return subprocess.run(cmd, capture_output=True)
def reset(self) -> None:
- """Perform a software reset."""
+ """Performs a software reset."""
self._c().reset()
def list_properties(self, path: str) -> List[Dict]:
"""Returns the property list for an emulator object.
- The object is identified by a full path. The path is target
- specific and the format of the path is backend specific.
+ The object is identified by a full path. The path is
+ target-specific and the format of the path is backend-specific.
- qemu path example: /machine/unattached/device[10]
+ QEMU path example: ``/machine/unattached/device[10]``
- renode path example: sysbus.uart
+ renode path example: ``sysbus.uart``
"""
return self._c().list_properties(path)
@@ -217,23 +215,23 @@ class Emulator:
def get_channel_type(self, name: str) -> str:
"""Returns the channel type
- Currently `pty` or `tcp` are the only supported types.
+ Currently ``pty`` and ``tcp`` are the only supported types.
"""
return self._c().get_channel_type(name)
def get_channel_path(self, name: str) -> str:
- """Returns the channel path. Raises InvalidChannelType if this
- is not a pty channel.
+ """Returns the channel path. Raises ``InvalidChannelType`` if this
+ is not a ``pty`` channel.
"""
return self._c().get_channel_path(name)
def get_channel_addr(self, name: str) -> tuple:
- """Returns a pair of (host, port) for the channel. Raises
- InvalidChannelType if this is not a tcp channel.
+ """Returns a pair of ``(host, port)`` for the channel. Raises
+ ``InvalidChannelType`` if this is not a TCP channel.
"""
@@ -244,10 +242,10 @@ class Emulator:
name: str,
timeout: Optional[float] = None,
) -> io.RawIOBase:
- """Returns a file object for a given host exposed device.
+ """Returns a file object for a given host-exposed device.
- If timeout is None than reads and writes are blocking. If
- timeout is zero the stream is operating in non-blocking
+ If ``timeout`` is ``None`` than reads and writes are blocking. If
+ ``timeout`` is ``0`` the stream is operating in non-blocking
mode. Otherwise read and write will timeout after the given
value.
@@ -261,12 +259,12 @@ class Emulator:
return self._c().get_channels()
def set_emu(self, emu: str) -> None:
- """Set the emulator type for this instance."""
+ """Sets the emulator type for this instance."""
self._launcher = Launcher.get(emu, self._config_path)
def cont(self) -> None:
- """Resume the emulator's execution."""
+ """Resumes the emulator's execution."""
self._c().cont()
@@ -276,11 +274,10 @@ class TemporaryEmulator(Emulator):
Manages emulator instances that run in temporary working
directories. The emulator instance is stopped and the working
- directory is cleared when the with block completes.
+ directory is cleared when the ``with`` block completes.
- It also supports interoperability with the pw emu cli, i.e.
- starting the emulator with the CLI and controlling / interacting
- with it from the API.
+ It also supports interoperability with the ``pw_emu`` cli, e.g. starting the
+ emulator with the CLI and then controlling it from the Python API.
Usage example:
diff --git a/pw_emu/py/pw_emu/qemu.py b/pw_emu/py/pw_emu/qemu.py
index 50112cda3..5d6ece5d6 100644
--- a/pw_emu/py/pw_emu/qemu.py
+++ b/pw_emu/py/pw_emu/qemu.py
@@ -31,6 +31,7 @@ from pw_emu.core import (
Launcher,
Error,
InvalidChannelType,
+ RunError,
WrongEmulator,
)
@@ -50,12 +51,17 @@ class QmpClient:
def __init__(self, stream: io.RawIOBase):
self._stream = stream
- json.loads(self._stream.readline())
- cmd = json.dumps({'execute': 'qmp_capabilities'})
- self._stream.write(cmd.encode('utf-8'))
- resp = json.loads(self._stream.readline().decode('ascii'))
- if not 'return' in resp:
- raise QmpError(f'qmp init failed: {resp.get("error")}')
+ # Perform the QMP "capabilities negotiation" handshake.
+ # https://wiki.qemu.org/Documentation/QMP#Capabilities_Negotiation
+ #
+ # When the QMP connection is established, QEMU first sends a greeting
+ # message with its version and capabilities. Then the client sends
+ # 'qmp_capabilities' to exit capabilities negotiation mode. The result
+ # is an empty 'return'.
+ #
+ # self.request() will consume both the initial greeting and the
+ # subsequent 'return' response.
+ self.request('qmp_capabilities')
def request(self, cmd: str, args: Optional[Dict[str, Any]] = None) -> Any:
"""Issue a command using the qmp interface.
@@ -285,9 +291,15 @@ class QemuLauncher(Launcher):
def _post_start(self) -> None:
assert self._qmp_init_sock is not None
- conn, _ = self._qmp_init_sock.accept()
+ try:
+ conn, _ = self._qmp_init_sock.accept()
+ except (KeyboardInterrupt, socket.timeout):
+ raise RunError('qemu', 'qmp connection failed')
self._qmp_init_sock.close()
- qmp = QmpClient(conn.makefile('rwb', buffering=0))
+ try:
+ qmp = QmpClient(conn.makefile('rwb', buffering=0))
+ except json.decoder.JSONDecodeError:
+ raise RunError('qemu', 'qmp handshake failed')
conn.close()
resp = qmp.request('query-chardev')
diff --git a/pw_emu/py/pw_emu/renode.py b/pw_emu/py/pw_emu/renode.py
index 1e3ccb548..75113246b 100644
--- a/pw_emu/py/pw_emu/renode.py
+++ b/pw_emu/py/pw_emu/renode.py
@@ -22,10 +22,11 @@ from typing import Optional, List, Any
from pw_emu.core import (
Connector,
+ Error,
Handles,
InvalidChannelType,
Launcher,
- Error,
+ RunError,
WrongEmulator,
)
@@ -155,7 +156,7 @@ class RenodeLauncher(Launcher):
if not connected:
msg = 'failed to connect to robot channel'
msg += f'({robot.host}:{robot.port}): {err}'
- raise RenodeRobotError(msg)
+ raise RunError('renode', msg)
sock.close()