@samitouri / QOSamiQemu / commits / e1e49b35b3

python: delete qemu.qmp

Start relying on the external python-qemu-qmp dependency instead, to prevent desync between the internal and external libraries. This library is now entirely independent; to contribute changes, see https://gitlab.com/qemu-project/python-qemu-qmp/ Reviewed-by: Thomas Huth <thuth@redhat.com> Message-ID: <20260218213416.674483-19-jsnow@redhat.com> Signed-off-by: John Snow <jsnow@redhat.com>

John Snow committed Feb 18, 2026 at 16:34 UTC e1e49b35b3c3cf6b7c68b6b1df18a477ac3183e5
16 files changed +6 -5533
python/qemu/qmp/__init__.py deleted
-60
@@ -1,60 +0,0 @@
1 -"""
2 -QEMU Monitor Protocol (QMP) development library & tooling.
3 -
4 -This package provides a fairly low-level class for communicating
5 -asynchronously with QMP protocol servers, as implemented by QEMU, the
6 -QEMU Guest Agent, and the QEMU Storage Daemon.
7 -
8 -`QMPClient` provides the main functionality of this package. All errors
9 -raised by this library derive from `QMPError`, see `qmp.error` for
10 -additional detail. See `qmp.events` for an in-depth tutorial on
11 -managing QMP events.
12 -"""
13 -
14 -# Copyright (C) 2020-2022 John Snow for Red Hat, Inc.
15 -#
16 -# Authors:
17 -# John Snow <jsnow@redhat.com>
18 -#
19 -# Based on earlier work by Luiz Capitulino <lcapitulino@redhat.com>.
20 -#
21 -# This work is licensed under the terms of the GNU LGPL, version 2 or
22 -# later. See the COPYING file in the top-level directory.
23 -
24 -import logging
25 -
26 -from .error import QMPError
27 -from .events import EventListener
28 -from .message import Message
29 -from .protocol import (
30 - ConnectError,
31 - Runstate,
32 - SocketAddrT,
33 - StateError,
34 -)
35 -from .qmp_client import ExecInterruptedError, ExecuteError, QMPClient
36 -
37 -
38 -# Suppress logging unless an application engages it.
39 -logging.getLogger('qemu.qmp').addHandler(logging.NullHandler())
40 -
41 -
42 -# IMPORTANT: When modifying this list, update the Sphinx overview docs.
43 -# Anything visible in the qemu.qmp namespace should be on the overview page.
44 -__all__ = (
45 - # Classes, most to least important
46 - 'QMPClient',
47 - 'Message',
48 - 'EventListener',
49 - 'Runstate',
50 -
51 - # Exceptions, most generic to most explicit
52 - 'QMPError',
53 - 'StateError',
54 - 'ConnectError',
55 - 'ExecuteError',
56 - 'ExecInterruptedError',
57 -
58 - # Type aliases
59 - 'SocketAddrT',
60 -)
python/qemu/qmp/error.py deleted
-53
@@ -1,53 +0,0 @@
1 -"""
2 -QMP Error Classes
3 -
4 -This package seeks to provide semantic error classes that are intended
5 -to be used directly by clients when they would like to handle particular
6 -semantic failures (e.g. "failed to connect") without needing to know the
7 -enumeration of possible reasons for that failure.
8 -
9 -QMPError serves as the ancestor for all exceptions raised by this
10 -package, and is suitable for use in handling semantic errors from this
11 -library. In most cases, individual public methods will attempt to catch
12 -and re-encapsulate various exceptions to provide a semantic
13 -error-handling interface.
14 -
15 -.. admonition:: QMP Exception Hierarchy Reference
16 -
17 - | `Exception`
18 - | +-- `QMPError`
19 - | +-- `ConnectError`
20 - | +-- `StateError`
21 - | +-- `ExecInterruptedError`
22 - | +-- `ExecuteError`
23 - | +-- `ListenerError`
24 - | +-- `ProtocolError`
25 - | +-- `DeserializationError`
26 - | +-- `UnexpectedTypeError`
27 - | +-- `ServerParseError`
28 - | +-- `BadReplyError`
29 - | +-- `GreetingError`
30 - | +-- `NegotiationError`
31 -"""
32 -
33 -
34 -class QMPError(Exception):
35 - """Abstract error class for all errors originating from this package."""
36 -
37 -
38 -class ProtocolError(QMPError):
39 - """
40 - Abstract error class for protocol failures.
41 -
42 - Semantically, these errors are generally the fault of either the
43 - protocol server or as a result of a bug in this library.
44 -
45 - :param error_message: Human-readable string describing the error.
46 - """
47 - def __init__(self, error_message: str, *args: object):
48 - super().__init__(error_message, *args)
49 - #: Human-readable error message, without any prefix.
50 - self.error_message: str = error_message
51 -
52 - def __str__(self) -> str:
53 - return self.error_message
python/qemu/qmp/events.py deleted
-751
@@ -1,751 +0,0 @@
1 -"""
2 -QMP Events and EventListeners
3 -
4 -Asynchronous QMP uses `EventListener` objects to listen for events. An
5 -`EventListener` is a FIFO event queue that can be pre-filtered to listen
6 -for only specific events. Each `EventListener` instance receives its own
7 -copy of events that it hears, so events may be consumed without fear or
8 -worry for depriving other listeners of events they need to hear.
9 -
10 -
11 -EventListener Tutorial
12 -----------------------
13 -
14 -In all of the following examples, we assume that we have a `QMPClient`
15 -instantiated named ``qmp`` that is already connected. For example:
16 -
17 -.. code:: python
18 -
19 - from qemu.qmp import QMPClient
20 -
21 - qmp = QMPClient('example-vm')
22 - await qmp.connect('127.0.0.1', 1234)
23 -
24 -
25 -`listener()` context blocks with one name
26 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
27 -
28 -The most basic usage is by using the `listener()` context manager to
29 -construct them:
30 -
31 -.. code:: python
32 -
33 - with qmp.listener('STOP') as listener:
34 - await qmp.execute('stop')
35 - await listener.get()
36 -
37 -The listener is active only for the duration of the ‘with’ block. This
38 -instance listens only for ‘STOP’ events.
39 -
40 -
41 -`listener()` context blocks with two or more names
42 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
43 -
44 -Multiple events can be selected for by providing any ``Iterable[str]``:
45 -
46 -.. code:: python
47 -
48 - with qmp.listener(('STOP', 'RESUME')) as listener:
49 - await qmp.execute('stop')
50 - event = await listener.get()
51 - assert event['event'] == 'STOP'
52 -
53 - await qmp.execute('cont')
54 - event = await listener.get()
55 - assert event['event'] == 'RESUME'
56 -
57 -
58 -`listener()` context blocks with no names
59 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
60 -
61 -By omitting names entirely, you can listen to ALL events.
62 -
63 -.. code:: python
64 -
65 - with qmp.listener() as listener:
66 - await qmp.execute('stop')
67 - event = await listener.get()
68 - assert event['event'] == 'STOP'
69 -
70 -This isn’t a very good use case for this feature: In a non-trivial
71 -running system, we may not know what event will arrive next. Grabbing
72 -the top of a FIFO queue returning multiple kinds of events may be prone
73 -to error.
74 -
75 -
76 -Using async iterators to retrieve events
77 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
78 -
79 -If you’d like to simply watch what events happen to arrive, you can use
80 -the listener as an async iterator:
81 -
82 -.. code:: python
83 -
84 - with qmp.listener() as listener:
85 - async for event in listener:
86 - print(f"Event arrived: {event['event']}")
87 -
88 -This is analogous to the following code:
89 -
90 -.. code:: python
91 -
92 - with qmp.listener() as listener:
93 - while True:
94 - event = listener.get()
95 - print(f"Event arrived: {event['event']}")
96 -
97 -This event stream will never end, so these blocks will never
98 -terminate. Even if the QMP connection errors out prematurely, this
99 -listener will go silent without raising an error.
100 -
101 -
102 -Using asyncio.Task to concurrently retrieve events
103 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
104 -
105 -Since a listener’s event stream will never terminate, it is not likely
106 -useful to use that form in a script. For longer-running clients, we can
107 -create event handlers by using `asyncio.Task` to create concurrent
108 -coroutines:
109 -
110 -.. code:: python
111 -
112 - async def print_events(listener):
113 - try:
114 - async for event in listener:
115 - print(f"Event arrived: {event['event']}")
116 - except asyncio.CancelledError:
117 - return
118 -
119 - with qmp.listener() as listener:
120 - task = asyncio.Task(print_events(listener))
121 - await qmp.execute('stop')
122 - await qmp.execute('cont')
123 - task.cancel()
124 - await task
125 -
126 -However, there is no guarantee that these events will be received by the
127 -time we leave this context block. Once the context block is exited, the
128 -listener will cease to hear any new events, and becomes inert.
129 -
130 -Be mindful of the timing: the above example will *probably*– but does
131 -not *guarantee*– that both STOP/RESUMED events will be printed. The
132 -example below outlines how to use listeners outside of a context block.
133 -
134 -
135 -Using `register_listener()` and `remove_listener()`
136 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
137 -
138 -To create a listener with a longer lifetime, beyond the scope of a
139 -single block, create a listener and then call `register_listener()`:
140 -
141 -.. code:: python
142 -
143 - class MyClient:
144 - def __init__(self, qmp):
145 - self.qmp = qmp
146 - self.listener = EventListener()
147 -
148 - async def print_events(self):
149 - try:
150 - async for event in self.listener:
151 - print(f"Event arrived: {event['event']}")
152 - except asyncio.CancelledError:
153 - return
154 -
155 - async def run(self):
156 - self.task = asyncio.Task(self.print_events)
157 - self.qmp.register_listener(self.listener)
158 - await qmp.execute('stop')
159 - await qmp.execute('cont')
160 -
161 - async def stop(self):
162 - self.task.cancel()
163 - await self.task
164 - self.qmp.remove_listener(self.listener)
165 -
166 -The listener can be deactivated by using `remove_listener()`. When it is
167 -removed, any possible pending events are cleared and it can be
168 -re-registered at a later time.
169 -
170 -
171 -Using the built-in all events listener
172 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
173 -
174 -The `QMPClient` object creates its own default listener named
175 -:py:obj:`~Events.events` that can be used for the same purpose without
176 -having to create your own:
177 -
178 -.. code:: python
179 -
180 - async def print_events(listener):
181 - try:
182 - async for event in listener:
183 - print(f"Event arrived: {event['event']}")
184 - except asyncio.CancelledError:
185 - return
186 -
187 - task = asyncio.Task(print_events(qmp.events))
188 -
189 - await qmp.execute('stop')
190 - await qmp.execute('cont')
191 -
192 - task.cancel()
193 - await task
194 -
195 -
196 -Using both .get() and async iterators
197 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
198 -
199 -The async iterator and `get()` methods pull events from the same FIFO
200 -queue. If you mix the usage of both, be aware: Events are emitted
201 -precisely once per listener.
202 -
203 -If multiple contexts try to pull events from the same listener instance,
204 -events are still emitted only precisely once.
205 -
206 -This restriction can be lifted by creating additional listeners.
207 -
208 -
209 -Creating multiple listeners
210 -~~~~~~~~~~~~~~~~~~~~~~~~~~~
211 -
212 -Additional `EventListener` objects can be created at-will. Each one
213 -receives its own copy of events, with separate FIFO event queues.
214 -
215 -.. code:: python
216 -
217 - my_listener = EventListener()
218 - qmp.register_listener(my_listener)
219 -
220 - await qmp.execute('stop')
221 - copy1 = await my_listener.get()
222 - copy2 = await qmp.events.get()
223 -
224 - assert copy1 == copy2
225 -
226 -In this example, we await an event from both a user-created
227 -`EventListener` and the built-in events listener. Both receive the same
228 -event.
229 -
230 -
231 -Clearing listeners
232 -~~~~~~~~~~~~~~~~~~
233 -
234 -`EventListener` objects can be cleared, clearing all events seen thus far:
235 -
236 -.. code:: python
237 -
238 - await qmp.execute('stop')
239 - discarded = qmp.events.clear()
240 - await qmp.execute('cont')
241 - event = await qmp.events.get()
242 - assert event['event'] == 'RESUME'
243 - assert discarded[0]['event'] == 'STOP'
244 -
245 -`EventListener` objects are FIFO queues. If events are not consumed,
246 -they will remain in the queue until they are witnessed or discarded via
247 -`clear()`. FIFO queues will be drained automatically upon leaving a
248 -context block, or when calling `remove_listener()`.
249 -
250 -Any events removed from the queue in this fashion will be returned by
251 -the clear call.
252 -
253 -
254 -Accessing listener history
255 -~~~~~~~~~~~~~~~~~~~~~~~~~~
256 -
257 -`EventListener` objects record their history. Even after being cleared,
258 -you can obtain a record of all events seen so far:
259 -
260 -.. code:: python
261 -
262 - await qmp.execute('stop')
263 - await qmp.execute('cont')
264 - qmp.events.clear()
265 -
266 - assert len(qmp.events.history) == 2
267 - assert qmp.events.history[0]['event'] == 'STOP'
268 - assert qmp.events.history[1]['event'] == 'RESUME'
269 -
270 -The history is updated immediately and does not require the event to be
271 -witnessed first.
272 -
273 -
274 -Using event filters
275 -~~~~~~~~~~~~~~~~~~~
276 -
277 -`EventListener` objects can be given complex filtering criteria if names
278 -are not sufficient:
279 -
280 -.. code:: python
281 -
282 - def job1_filter(event) -> bool:
283 - event_data = event.get('data', {})
284 - event_job_id = event_data.get('id')
285 - return event_job_id == "job1"
286 -
287 - with qmp.listener('JOB_STATUS_CHANGE', job1_filter) as listener:
288 - await qmp.execute('blockdev-backup', arguments={'job-id': 'job1', ...})
289 - async for event in listener:
290 - if event['data']['status'] == 'concluded':
291 - break
292 -
293 -These filters might be most useful when parameterized. `EventListener`
294 -objects expect a function that takes only a single argument (the raw
295 -event, as a `Message`) and returns a bool; True if the event should be
296 -accepted into the stream. You can create a function that adapts this
297 -signature to accept configuration parameters:
298 -
299 -.. code:: python
300 -
301 - def job_filter(job_id: str) -> EventFilter:
302 - def filter(event: Message) -> bool:
303 - return event['data']['id'] == job_id
304 - return filter
305 -
306 - with qmp.listener('JOB_STATUS_CHANGE', job_filter('job2')) as listener:
307 - await qmp.execute('blockdev-backup', arguments={'job-id': 'job2', ...})
308 - async for event in listener:
309 - if event['data']['status'] == 'concluded':
310 - break
311 -
312 -
313 -Activating an existing listener with `listen()`
314 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
315 -
316 -Listeners with complex, long configurations can also be created manually
317 -and activated temporarily by using `listen()` instead of `listener()`:
318 -
319 -.. code:: python
320 -
321 - listener = EventListener(('BLOCK_JOB_COMPLETED', 'BLOCK_JOB_CANCELLED',
322 - 'BLOCK_JOB_ERROR', 'BLOCK_JOB_READY',
323 - 'BLOCK_JOB_PENDING', 'JOB_STATUS_CHANGE'))
324 -
325 - with qmp.listen(listener):
326 - await qmp.execute('blockdev-backup', arguments={'job-id': 'job3', ...})
327 - async for event in listener:
328 - print(event)
329 - if event['event'] == 'BLOCK_JOB_COMPLETED':
330 - break
331 -
332 -Any events that are not witnessed by the time the block is left will be
333 -cleared from the queue; entering the block is an implicit
334 -`register_listener()` and leaving the block is an implicit
335 -`remove_listener()`.
336 -
337 -
338 -Activating multiple existing listeners with `listen()`
339 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
340 -
341 -While `listener()` is only capable of creating a single listener,
342 -`listen()` is capable of activating multiple listeners simultaneously:
343 -
344 -.. code:: python
345 -
346 - def job_filter(job_id: str) -> EventFilter:
347 - def filter(event: Message) -> bool:
348 - return event['data']['id'] == job_id
349 - return filter
350 -
351 - jobA = EventListener('JOB_STATUS_CHANGE', job_filter('jobA'))
352 - jobB = EventListener('JOB_STATUS_CHANGE', job_filter('jobB'))
353 -
354 - with qmp.listen(jobA, jobB):
355 - qmp.execute('blockdev-create', arguments={'job-id': 'jobA', ...})
356 - qmp.execute('blockdev-create', arguments={'job-id': 'jobB', ...})
357 -
358 - async for event in jobA.get():
359 - if event['data']['status'] == 'concluded':
360 - break
361 - async for event in jobB.get():
362 - if event['data']['status'] == 'concluded':
363 - break
364 -
365 -
366 -Note that in the above example, we explicitly wait on jobA to conclude
367 -first, and then wait for jobB to do the same. All we have guaranteed is
368 -that the code that waits for jobA will not accidentally consume the
369 -event intended for the jobB waiter.
370 -
371 -
372 -Extending the `EventListener` class
373 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
374 -
375 -In the case that a more specialized `EventListener` is desired to
376 -provide either more functionality or more compact syntax for specialized
377 -cases, it can be extended.
378 -
379 -One of the key methods to extend or override is
380 -:py:meth:`~EventListener.accept()`. The default implementation checks an
381 -incoming message for:
382 -
383 -1. A qualifying name, if any :py:obj:`~EventListener.names` were
384 - specified at initialization time
385 -2. That :py:obj:`~EventListener.event_filter()` returns True.
386 -
387 -This can be modified however you see fit to change the criteria for
388 -inclusion in the stream.
389 -
390 -For convenience, a ``JobListener`` class could be created that simply
391 -bakes in configuration so it does not need to be repeated:
392 -
393 -.. code:: python
394 -
395 - class JobListener(EventListener):
396 - def __init__(self, job_id: str):
397 - super().__init__(('BLOCK_JOB_COMPLETED', 'BLOCK_JOB_CANCELLED',
398 - 'BLOCK_JOB_ERROR', 'BLOCK_JOB_READY',
399 - 'BLOCK_JOB_PENDING', 'JOB_STATUS_CHANGE'))
400 - self.job_id = job_id
401 -
402 - def accept(self, event) -> bool:
403 - if not super().accept(event):
404 - return False
405 - if event['event'] in ('BLOCK_JOB_PENDING', 'JOB_STATUS_CHANGE'):
406 - return event['data']['id'] == job_id
407 - return event['data']['device'] == job_id
408 -
409 -From here on out, you can conjure up a custom-purpose listener that
410 -listens only for job-related events for a specific job-id easily:
411 -
412 -.. code:: python
413 -
414 - listener = JobListener('job4')
415 - with qmp.listener(listener):
416 - await qmp.execute('blockdev-backup', arguments={'job-id': 'job4', ...})
417 - async for event in listener:
418 - print(event)
419 - if event['event'] == 'BLOCK_JOB_COMPLETED':
420 - break
421 -
422 -
423 -Experimental Interfaces & Design Issues
424 ----------------------------------------
425 -
426 -These interfaces are not ones I am sure I will keep or otherwise modify
427 -heavily.
428 -
429 -qmp.listen()’s type signature
430 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
431 -
432 -`listen()` does not return anything, because it was assumed the caller
433 -already had a handle to the listener. However, for
434 -``qmp.listen(EventListener())`` forms, the caller will not have saved a
435 -handle to the listener.
436 -
437 -Because this function can accept *many* listeners, I found it hard to
438 -accurately type in a way where it could be used in both “one” or “many”
439 -forms conveniently and in a statically type-safe manner.
440 -
441 -Ultimately, I removed the return altogether, but perhaps with more time
442 -I can work out a way to re-add it.
443 -
444 -
445 -API Reference
446 --------------
447 -
448 -"""
449 -
450 -import asyncio
451 -from contextlib import contextmanager
452 -import logging
453 -from typing import (
454 - AsyncIterator,
455 - Callable,
456 - Iterable,
457 - Iterator,
458 - List,
459 - Optional,
460 - Set,
461 - Tuple,
462 - Union,
463 -)
464 -
465 -from .error import QMPError
466 -from .message import Message
467 -
468 -
469 -EventNames = Union[str, Iterable[str], None]
470 -EventFilter = Callable[[Message], bool]
471 -
472 -
473 -class ListenerError(QMPError):
474 - """
475 - Generic error class for `EventListener`-related problems.
476 - """
477 -
478 -
479 -class EventListener:
480 - """
481 - Selectively listens for events with runtime configurable filtering.
482 -
483 - This class is designed to be directly usable for the most common cases,
484 - but it can be extended to provide more rigorous control.
485 -
486 - :param names:
487 - One or more names of events to listen for.
488 - When not provided, listen for ALL events.
489 - :param event_filter:
490 - An optional event filtering function.
491 - When names are also provided, this acts as a secondary filter.
492 -
493 - When ``names`` and ``event_filter`` are both provided, the names
494 - will be filtered first, and then the filter function will be called
495 - second. The event filter function can assume that the format of the
496 - event is a known format.
497 - """
498 - def __init__(
499 - self,
500 - names: EventNames = None,
501 - event_filter: Optional[EventFilter] = None,
502 - ):
503 - # Queue of 'heard' events yet to be witnessed by a caller.
504 - self._queue: 'asyncio.Queue[Message]' = asyncio.Queue()
505 -
506 - # Intended as a historical record, NOT a processing queue or backlog.
507 - self._history: List[Message] = []
508 -
509 - #: Primary event filter, based on one or more event names.
510 - self.names: Set[str] = set()
511 - if isinstance(names, str):
512 - self.names.add(names)
513 - elif names is not None:
514 - self.names.update(names)
515 -
516 - #: Optional, secondary event filter.
517 - self.event_filter: Optional[EventFilter] = event_filter
518 -
519 - def __repr__(self) -> str:
520 - args: List[str] = []
521 - if self.names:
522 - args.append(f"names={self.names!r}")
523 - if self.event_filter:
524 - args.append(f"event_filter={self.event_filter!r}")
525 -
526 - if self._queue.qsize():
527 - state = f"<pending={self._queue.qsize()}>"
528 - else:
529 - state = ''
530 -
531 - argstr = ", ".join(args)
532 - return f"{type(self).__name__}{state}({argstr})"
533 -
534 - @property
535 - def history(self) -> Tuple[Message, ...]:
536 - """
537 - A read-only history of all events seen so far.
538 -
539 - This represents *every* event, including those not yet witnessed
540 - via `get()` or ``async for``. It persists between `clear()`
541 - calls and is immutable.
542 - """
543 - return tuple(self._history)
544 -
545 - def accept(self, event: Message) -> bool:
546 - """
547 - Determine if this listener accepts this event.
548 -
549 - This method determines which events will appear in the stream.
550 - The default implementation simply checks the event against the
551 - list of names and the event_filter to decide if this
552 - `EventListener` accepts a given event. It can be
553 - overridden/extended to provide custom listener behavior.
554 -
555 - User code is not expected to need to invoke this method.
556 -
557 - :param event: The event under consideration.
558 - :return: `True`, if this listener accepts this event.
559 - """
560 - name_ok = (not self.names) or (event['event'] in self.names)
561 - return name_ok and (
562 - (not self.event_filter) or self.event_filter(event)
563 - )
564 -
565 - async def put(self, event: Message) -> None:
566 - """
567 - Conditionally put a new event into the FIFO queue.
568 -
569 - This method is not designed to be invoked from user code, and it
570 - should not need to be overridden. It is a public interface so
571 - that `QMPClient` has an interface by which it can inform
572 - registered listeners of new events.
573 -
574 - The event will be put into the queue if
575 - :py:meth:`~EventListener.accept()` returns `True`.
576 -
577 - :param event: The new event to put into the FIFO queue.
578 - """
579 - if not self.accept(event):
580 - return
581 -
582 - self._history.append(event)
583 - await self._queue.put(event)
584 -
585 - async def get(self) -> Message:
586 - """
587 - Wait for the very next event in this stream.
588 -
589 - If one is already available, return that one.
590 - """
591 - return await self._queue.get()
592 -
593 - def empty(self) -> bool:
594 - """
595 - Return `True` if there are no pending events.
596 - """
597 - return self._queue.empty()
598 -
599 - def clear(self) -> List[Message]:
600 - """
601 - Clear this listener of all pending events.
602 -
603 - Called when an `EventListener` is being unregistered, this clears the
604 - pending FIFO queue synchronously. It can be also be used to
605 - manually clear any pending events, if desired.
606 -
607 - :return: The cleared events, if any.
608 -
609 - .. warning::
610 - Take care when discarding events. Cleared events will be
611 - silently tossed on the floor. All events that were ever
612 - accepted by this listener are visible in `history()`.
613 - """
614 - events = []
615 - while True:
616 - try:
617 - events.append(self._queue.get_nowait())
618 - except asyncio.QueueEmpty:
619 - break
620 -
621 - return events
622 -
623 - def __aiter__(self) -> AsyncIterator[Message]:
624 - return self
625 -
626 - async def __anext__(self) -> Message:
627 - """
628 - Enables the `EventListener` to function as an async iterator.
629 -
630 - It may be used like this:
631 -
632 - .. code:: python
633 -
634 - async for event in listener:
635 - print(event)
636 -
637 - These iterators will never terminate of their own accord; you
638 - must provide break conditions or otherwise prepare to run them
639 - in an `asyncio.Task` that can be cancelled.
640 - """
641 - return await self.get()
642 -
643 -
644 -class Events:
645 - """
646 - Events is a mix-in class that adds event functionality to the QMP class.
647 -
648 - It's designed specifically as a mix-in for `QMPClient`, and it
649 - relies upon the class it is being mixed into having a 'logger'
650 - property.
651 - """
652 - def __init__(self) -> None:
653 - self._listeners: List[EventListener] = []
654 -
655 - #: Default, all-events `EventListener`. See `qmp.events` for more info.
656 - self.events: EventListener = EventListener()
657 - self.register_listener(self.events)
658 -
659 - # Parent class needs to have a logger
660 - self.logger: logging.Logger
661 -
662 - async def _event_dispatch(self, msg: Message) -> None:
663 - """
664 - Given a new event, propagate it to all of the active listeners.
665 -
666 - :param msg: The event to propagate.
667 - """
668 - for listener in self._listeners:
669 - await listener.put(msg)
670 -
671 - def register_listener(self, listener: EventListener) -> None:
672 - """
673 - Register and activate an `EventListener`.
674 -
675 - :param listener: The listener to activate.
676 - :raise ListenerError: If the given listener is already registered.
677 - """
678 - if listener in self._listeners:
679 - raise ListenerError("Attempted to re-register existing listener")
680 - self.logger.debug("Registering %s.", str(listener))
681 - self._listeners.append(listener)
682 -
683 - def remove_listener(self, listener: EventListener) -> None:
684 - """
685 - Unregister and deactivate an `EventListener`.
686 -
687 - The removed listener will have its pending events cleared via
688 - `clear()`. The listener can be re-registered later when
689 - desired.
690 -
691 - :param listener: The listener to deactivate.
692 - :raise ListenerError: If the given listener is not registered.
693 - """
694 - if listener == self.events:
695 - raise ListenerError("Cannot remove the default listener.")
696 - self.logger.debug("Removing %s.", str(listener))
697 - listener.clear()
698 - self._listeners.remove(listener)
699 -
700 - @contextmanager
701 - def listen(self, *listeners: EventListener) -> Iterator[None]:
702 - r"""
703 - Context manager: Temporarily listen with an `EventListener`.
704 -
705 - Accepts one or more `EventListener` objects and registers them,
706 - activating them for the duration of the context block.
707 -
708 - `EventListener` objects will have any pending events in their
709 - FIFO queue cleared upon exiting the context block, when they are
710 - deactivated.
711 -
712 - :param \*listeners: One or more EventListeners to activate.
713 - :raise ListenerError: If the given listener(s) are already active.
714 - """
715 - _added = []
716 -
717 - try:
718 - for listener in listeners:
719 - self.register_listener(listener)
720 - _added.append(listener)
721 -
722 - yield
723 -
724 - finally:
725 - for listener in _added:
726 - self.remove_listener(listener)
727 -
728 - @contextmanager
729 - def listener(
730 - self,
731 - names: EventNames = (),
732 - event_filter: Optional[EventFilter] = None
733 - ) -> Iterator[EventListener]:
734 - """
735 - Context manager: Temporarily listen with a new `EventListener`.
736 -
737 - Creates an `EventListener` object and registers it, activating
738 - it for the duration of the context block.
739 -
740 - :param names:
741 - One or more names of events to listen for.
742 - When not provided, listen for ALL events.
743 - :param event_filter:
744 - An optional event filtering function.
745 - When names are also provided, this acts as a secondary filter.
746 -
747 - :return: The newly created and active `EventListener`.
748 - """
749 - listener = EventListener(names, event_filter)
750 - with self.listen(listener):
751 - yield listener
python/qemu/qmp/legacy.py deleted
-339
@@ -1,339 +0,0 @@
1 -"""
2 -(Legacy) Sync QMP Wrapper
3 -
4 -This module provides the `QEMUMonitorProtocol` class, which is a
5 -synchronous wrapper around `QMPClient`.
6 -
7 -Its design closely resembles that of the original QEMUMonitorProtocol
8 -class, originally written by Luiz Capitulino. It is provided here for
9 -compatibility with scripts inside the QEMU source tree that expect the
10 -old interface.
11 -"""
12 -
13 -#
14 -# Copyright (C) 2009-2022 Red Hat Inc.
15 -#
16 -# Authors:
17 -# Luiz Capitulino <lcapitulino@redhat.com>
18 -# John Snow <jsnow@redhat.com>
19 -#
20 -# This work is licensed under the terms of the GNU GPL, version 2. See
21 -# the COPYING file in the top-level directory.
22 -#
23 -
24 -import asyncio
25 -import socket
26 -from types import TracebackType
27 -from typing import (
28 - Any,
29 - Awaitable,
30 - Dict,
31 - List,
32 - Optional,
33 - Type,
34 - TypeVar,
35 - Union,
36 -)
37 -
38 -from .error import QMPError
39 -from .protocol import Runstate, SocketAddrT
40 -from .qmp_client import QMPClient
41 -from .util import get_or_create_event_loop
42 -
43 -
44 -#: QMPMessage is an entire QMP message of any kind.
45 -QMPMessage = Dict[str, Any]
46 -
47 -#: QMPReturnValue is the 'return' value of a command.
48 -QMPReturnValue = object
49 -
50 -#: QMPObject is any object in a QMP message.
51 -QMPObject = Dict[str, object]
52 -
53 -# QMPMessage can be outgoing commands or incoming events/returns.
54 -# QMPReturnValue is usually a dict/json object, but due to QAPI's
55 -# 'command-returns-exceptions', it can actually be anything.
56 -#
57 -# {'return': {}} is a QMPMessage,
58 -# {} is the QMPReturnValue.
59 -
60 -
61 -class QMPBadPortError(QMPError):
62 - """
63 - Unable to parse socket address: Port was non-numerical.
64 - """
65 -
66 -
67 -class QEMUMonitorProtocol:
68 - """
69 - Provide an API to connect to QEMU via QEMU Monitor Protocol (QMP)
70 - and then allow to handle commands and events.
71 -
72 - :param address: QEMU address, can be a unix socket path (string), a tuple
73 - in the form ( address, port ) for a TCP connection, or an
74 - existing `socket.socket` object.
75 - :param server: Act as the socket server. (See 'accept')
76 - Not applicable when passing a socket directly.
77 - :param nickname: Optional nickname used for logging.
78 - """
79 -
80 - def __init__(self,
81 - address: Union[SocketAddrT, socket.socket],
82 - server: bool = False,
83 - nickname: Optional[str] = None):
84 -
85 - if server and isinstance(address, socket.socket):
86 - raise ValueError(
87 - "server argument should be False when passing a socket")
88 -
89 - self._qmp = QMPClient(nickname)
90 - self._address = address
91 - self._timeout: Optional[float] = None
92 -
93 - # This is a sync shim intended for use in fully synchronous
94 - # programs. Create and set an event loop if necessary.
95 - self._aloop = get_or_create_event_loop()
96 -
97 - if server:
98 - assert not isinstance(self._address, socket.socket)
99 - self._sync(self._qmp.start_server(self._address))
100 -
101 - _T = TypeVar('_T')
102 -
103 - def _sync(
104 - self, future: Awaitable[_T], timeout: Optional[float] = None
105 - ) -> _T:
106 - return self._aloop.run_until_complete(
107 - asyncio.wait_for(future, timeout=timeout)
108 - )
109 -
110 - def _get_greeting(self) -> Optional[QMPMessage]:
111 - if self._qmp.greeting is not None:
112 - # pylint: disable=protected-access
113 - return self._qmp.greeting._asdict()
114 - return None
115 -
116 - def __enter__(self: _T) -> _T:
117 - # Implement context manager enter function.
118 - return self
119 -
120 - def __exit__(self,
121 - exc_type: Optional[Type[BaseException]],
122 - exc_val: Optional[BaseException],
123 - exc_tb: Optional[TracebackType]) -> None:
124 - # Implement context manager exit function.
125 - self.close()
126 -
127 - @classmethod
128 - def parse_address(cls, address: str) -> SocketAddrT:
129 - """
130 - Parse a string into a QMP address.
131 -
132 - Figure out if the argument is in the port:host form.
133 - If it's not, it's probably a file path.
134 - """
135 - components = address.split(':')
136 - if len(components) == 2:
137 - try:
138 - port = int(components[1])
139 - except ValueError:
140 - msg = f"Bad port: '{components[1]}' in '{address}'."
141 - raise QMPBadPortError(msg) from None
142 - return (components[0], port)
143 -
144 - # Treat as filepath.
145 - return address
146 -
147 - def connect(self, negotiate: bool = True) -> Optional[QMPMessage]:
148 - """
149 - Connect to the QMP Monitor and perform capabilities negotiation.
150 -
151 - :return: QMP greeting dict, or None if negotiate is false
152 - :raise ConnectError: on connection errors
153 - """
154 - self._qmp.await_greeting = negotiate
155 - self._qmp.negotiate = negotiate
156 -
157 - self._sync(
158 - self._qmp.connect(self._address)
159 - )
160 - return self._get_greeting()
161 -
162 - def accept(self, timeout: Optional[float] = 15.0) -> QMPMessage:
163 - """
164 - Await connection from QMP Monitor and perform capabilities negotiation.
165 -
166 - :param timeout:
167 - timeout in seconds (nonnegative float number, or None).
168 - If None, there is no timeout, and this may block forever.
169 -
170 - :return: QMP greeting dict
171 - :raise ConnectError: on connection errors
172 - """
173 - self._qmp.await_greeting = True
174 - self._qmp.negotiate = True
175 -
176 - self._sync(self._qmp.accept(), timeout)
177 -
178 - ret = self._get_greeting()
179 - assert ret is not None
180 - return ret
181 -
182 - def cmd_obj(self, qmp_cmd: QMPMessage) -> QMPMessage:
183 - """
184 - Send a QMP command to the QMP Monitor.
185 -
186 - :param qmp_cmd: QMP command to be sent as a Python dict
187 - :return: QMP response as a Python dict
188 - """
189 - return dict(
190 - self._sync(
191 - # pylint: disable=protected-access
192 -
193 - # _raw() isn't a public API, because turning off
194 - # automatic ID assignment is discouraged. For
195 - # compatibility with iotests *only*, do it anyway.
196 - self._qmp._raw(qmp_cmd, assign_id=False),
197 - self._timeout
198 - )
199 - )
200 -
201 - def cmd_raw(self, name: str,
202 - args: Optional[Dict[str, object]] = None) -> QMPMessage:
203 - """
204 - Build a QMP command and send it to the QMP Monitor.
205 -
206 - :param name: command name (string)
207 - :param args: command arguments (dict)
208 - """
209 - qmp_cmd: QMPMessage = {'execute': name}
210 - if args:
211 - qmp_cmd['arguments'] = args
212 - return self.cmd_obj(qmp_cmd)
213 -
214 - def cmd(self, cmd: str, **kwds: object) -> QMPReturnValue:
215 - """
216 - Build and send a QMP command to the monitor, report errors if any
217 - """
218 - return self._sync(
219 - self._qmp.execute(cmd, kwds),
220 - self._timeout
221 - )
222 -
223 - def pull_event(self,
224 - wait: Union[bool, float] = False) -> Optional[QMPMessage]:
225 - """
226 - Pulls a single event.
227 -
228 - :param wait:
229 - If False or 0, do not wait. Return None if no events ready.
230 - If True, wait forever until the next event.
231 - Otherwise, wait for the specified number of seconds.
232 -
233 - :raise asyncio.TimeoutError:
234 - When a timeout is requested and the timeout period elapses.
235 -
236 - :return: The first available QMP event, or None.
237 - """
238 - # Kick the event loop to allow events to accumulate
239 - self._sync(asyncio.sleep(0))
240 -
241 - if not wait:
242 - # wait is False/0: "do not wait, do not except."
243 - if self._qmp.events.empty():
244 - return None
245 -
246 - # If wait is 'True', wait forever. If wait is False/0, the events
247 - # queue must not be empty; but it still needs some real amount
248 - # of time to complete.
249 - timeout = None
250 - if wait and isinstance(wait, float):
251 - timeout = wait
252 -
253 - return dict(
254 - self._sync(
255 - self._qmp.events.get(),
256 - timeout
257 - )
258 - )
259 -
260 - def get_events(self, wait: Union[bool, float] = False) -> List[QMPMessage]:
261 - """
262 - Get a list of QMP events and clear all pending events.
263 -
264 - :param wait:
265 - If False or 0, do not wait. Return None if no events ready.
266 - If True, wait until we have at least one event.
267 - Otherwise, wait for up to the specified number of seconds for at
268 - least one event.
269 -
270 - :raise asyncio.TimeoutError:
271 - When a timeout is requested and the timeout period elapses.
272 -
273 - :return: A list of QMP events.
274 - """
275 - events = [dict(x) for x in self._qmp.events.clear()]
276 - if events:
277 - return events
278 -
279 - event = self.pull_event(wait)
280 - return [event] if event is not None else []
281 -
282 - def clear_events(self) -> None:
283 - """Clear current list of pending events."""
284 - self._qmp.events.clear()
285 -
286 - def close(self) -> None:
287 - """Close the connection."""
288 - self._sync(
289 - self._qmp.disconnect()
290 - )
291 -
292 - def settimeout(self, timeout: Optional[float]) -> None:
293 - """
294 - Set the timeout for QMP RPC execution.
295 -
296 - This timeout affects the `cmd`, `cmd_obj`, and `cmd_raw` methods.
297 - The `accept`, `pull_event` and `get_events` methods have their
298 - own configurable timeouts.
299 -
300 - :param timeout:
301 - timeout in seconds, or None.
302 - None will wait indefinitely.
303 - """
304 - self._timeout = timeout
305 -
306 - def send_fd_scm(self, fd: int) -> None:
307 - """
308 - Send a file descriptor to the remote via SCM_RIGHTS.
309 - """
310 - self._qmp.send_fd_scm(fd)
311 -
312 - def __del__(self) -> None:
313 - if self._qmp.runstate != Runstate.IDLE:
314 - self._qmp.logger.warning(
315 - "QEMUMonitorProtocol object garbage collected without a prior "
316 - "call to close()"
317 - )
318 -
319 - if not self._aloop.is_running():
320 - if self._qmp.runstate != Runstate.IDLE:
321 - # If the user neglected to close the QMP session and we
322 - # are not currently running in an asyncio context, we
323 - # have the opportunity to close the QMP session. If we
324 - # do not do this, the error messages presented over
325 - # dangling async resources may not make any sense to the
326 - # user.
327 - self.close()
328 -
329 - if self._qmp.runstate != Runstate.IDLE:
330 - # If QMP is still not quiesced, it means that the garbage
331 - # collector ran from a context within the event loop and we
332 - # are simply too late to take any corrective action. Raise
333 - # our own error to give meaningful feedback to the user in
334 - # order to prevent pages of asyncio stacktrace jargon.
335 - raise QMPError(
336 - "QEMUMonitorProtocol.close() was not called before object was "
337 - "garbage collected, and could not be closed due to GC running "
338 - "in the event loop"
339 - )
python/qemu/qmp/message.py deleted
-217
@@ -1,217 +0,0 @@
1 -"""
2 -QMP Message Format
3 -
4 -This module provides the `Message` class, which represents a single QMP
5 -message sent to or from the server.
6 -"""
7 -
8 -import json
9 -from json import JSONDecodeError
10 -from typing import (
11 - Dict,
12 - Iterator,
13 - Mapping,
14 - MutableMapping,
15 - Optional,
16 - Union,
17 -)
18 -
19 -from .error import ProtocolError
20 -
21 -
22 -class Message(MutableMapping[str, object]):
23 - """
24 - Represents a single QMP protocol message.
25 -
26 - QMP uses JSON objects as its basic communicative unit; so this
27 - Python object is a :py:obj:`~collections.abc.MutableMapping`. It may
28 - be instantiated from either another mapping (like a `dict`), or from
29 - raw `bytes` that still need to be deserialized.
30 -
31 - Once instantiated, it may be treated like any other
32 - :py:obj:`~collections.abc.MutableMapping`::
33 -
34 - >>> msg = Message(b'{"hello": "world"}')
35 - >>> assert msg['hello'] == 'world'
36 - >>> msg['id'] = 'foobar'
37 - >>> print(msg)
38 - {
39 - "hello": "world",
40 - "id": "foobar"
41 - }
42 -
43 - It can be converted to `bytes`::
44 -
45 - >>> msg = Message({"hello": "world"})
46 - >>> print(bytes(msg))
47 - b'{"hello":"world","id":"foobar"}'
48 -
49 - Or back into a garden-variety `dict`::
50 -
51 - >>> dict(msg)
52 - {'hello': 'world'}
53 -
54 - Or pretty-printed::
55 -
56 - >>> print(str(msg))
57 - {
58 - "hello": "world"
59 - }
60 -
61 - :param value: Initial value, if any.
62 - :param eager:
63 - When `True`, attempt to serialize or deserialize the initial value
64 - immediately, so that conversion exceptions are raised during
65 - the call to ``__init__()``.
66 -
67 - """
68 - # pylint: disable=too-many-ancestors
69 -
70 - def __init__(self,
71 - value: Union[bytes, Mapping[str, object]] = b'{}', *,
72 - eager: bool = True):
73 - self._data: Optional[bytes] = None
74 - self._obj: Optional[Dict[str, object]] = None
75 -
76 - if isinstance(value, bytes):
77 - self._data = value
78 - if eager:
79 - self._obj = self._deserialize(self._data)
80 - else:
81 - self._obj = dict(value)
82 - if eager:
83 - self._data = self._serialize(self._obj)
84 -
85 - # Methods necessary to implement the MutableMapping interface, see:
86 - # https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping
87 -
88 - # We get pop, popitem, clear, update, setdefault, __contains__,
89 - # keys, items, values, get, __eq__ and __ne__ for free.
90 -
91 - def __getitem__(self, key: str) -> object:
92 - return self._object[key]
93 -
94 - def __setitem__(self, key: str, value: object) -> None:
95 - self._object[key] = value
96 - self._data = None
97 -
98 - def __delitem__(self, key: str) -> None:
99 - del self._object[key]
100 - self._data = None
101 -
102 - def __iter__(self) -> Iterator[str]:
103 - return iter(self._object)
104 -
105 - def __len__(self) -> int:
106 - return len(self._object)
107 -
108 - # Dunder methods not related to MutableMapping:
109 -
110 - def __repr__(self) -> str:
111 - if self._obj is not None:
112 - return f"Message({self._object!r})"
113 - return f"Message({bytes(self)!r})"
114 -
115 - def __str__(self) -> str:
116 - """Pretty-printed representation of this QMP message."""
117 - return json.dumps(self._object, indent=2)
118 -
119 - def __bytes__(self) -> bytes:
120 - """bytes representing this QMP message."""
121 - if self._data is None:
122 - self._data = self._serialize(self._obj or {})
123 - return self._data
124 -
125 - # Conversion Methods
126 -
127 - @property
128 - def _object(self) -> Dict[str, object]:
129 - """
130 - A `dict` representing this QMP message.
131 -
132 - Generated on-demand, if required. This property is private
133 - because it returns an object that could be used to invalidate
134 - the internal state of the `Message` object.
135 - """
136 - if self._obj is None:
137 - self._obj = self._deserialize(self._data or b'{}')
138 - return self._obj
139 -
140 - @classmethod
141 - def _serialize(cls, value: object) -> bytes:
142 - """
143 - Serialize a JSON object as `bytes`.
144 -
145 - :raise ValueError: When the object cannot be serialized.
146 - :raise TypeError: When the object cannot be serialized.
147 -
148 - :return: `bytes` ready to be sent over the wire.
149 - """
150 - return json.dumps(value, separators=(',', ':')).encode('utf-8')
151 -
152 - @classmethod
153 - def _deserialize(cls, data: bytes) -> Dict[str, object]:
154 - """
155 - Deserialize JSON `bytes` into a native Python `dict`.
156 -
157 - :raise DeserializationError:
158 - If JSON deserialization fails for any reason.
159 - :raise UnexpectedTypeError:
160 - If the data does not represent a JSON object.
161 -
162 - :return: A `dict` representing this QMP message.
163 - """
164 - try:
165 - obj = json.loads(data)
166 - except JSONDecodeError as err:
167 - emsg = "Failed to deserialize QMP message."
168 - raise DeserializationError(emsg, data) from err
169 - if not isinstance(obj, dict):
170 - raise UnexpectedTypeError(
171 - "QMP message is not a JSON object.",
172 - obj
173 - )
174 - return obj
175 -
176 -
177 -class DeserializationError(ProtocolError):
178 - """
179 - A QMP message was not understood as JSON.
180 -
181 - When this Exception is raised, ``__cause__`` will be set to the
182 - `json.JSONDecodeError` Exception, which can be interrogated for
183 - further details.
184 -
185 - :param error_message: Human-readable string describing the error.
186 - :param raw: The raw `bytes` that prompted the failure.
187 - """
188 - def __init__(self, error_message: str, raw: bytes):
189 - super().__init__(error_message, raw)
190 - #: The raw `bytes` that were not understood as JSON.
191 - self.raw: bytes = raw
192 -
193 - def __str__(self) -> str:
194 - return "\n".join((
195 - super().__str__(),
196 - f" raw bytes were: {str(self.raw)}",
197 - ))
198 -
199 -
200 -class UnexpectedTypeError(ProtocolError):
201 - """
202 - A QMP message was JSON, but not a JSON object.
203 -
204 - :param error_message: Human-readable string describing the error.
205 - :param value: The deserialized JSON value that wasn't an object.
206 - """
207 - def __init__(self, error_message: str, value: object):
208 - super().__init__(error_message, value)
209 - #: The JSON value that was expected to be an object.
210 - self.value: object = value
211 -
212 - def __str__(self) -> str:
213 - strval = json.dumps(self.value, indent=2)
214 - return "\n".join((
215 - super().__str__(),
216 - f" json value was: {strval}",
217 - ))
python/qemu/qmp/models.py deleted
-146
@@ -1,146 +0,0 @@
1 -"""
2 -QMP Data Models
3 -
4 -This module provides simplistic data classes that represent the few
5 -structures that the QMP spec mandates; they are used to verify incoming
6 -data to make sure it conforms to spec.
7 -"""
8 -# pylint: disable=too-few-public-methods
9 -
10 -from collections import abc
11 -import copy
12 -from typing import (
13 - Any,
14 - Dict,
15 - Mapping,
16 - Optional,
17 - Sequence,
18 -)
19 -
20 -
21 -class Model:
22 - """
23 - Abstract data model, representing some QMP object of some kind.
24 -
25 - :param raw: The raw object to be validated.
26 - :raise KeyError: If any required fields are absent.
27 - :raise TypeError: If any required fields have the wrong type.
28 - """
29 - def __init__(self, raw: Mapping[str, Any]):
30 - self._raw = raw
31 -
32 - def _check_key(self, key: str) -> None:
33 - if key not in self._raw:
34 - raise KeyError(f"'{self._name}' object requires '{key}' member")
35 -
36 - def _check_value(self, key: str, type_: type, typestr: str) -> None:
37 - assert key in self._raw
38 - if not isinstance(self._raw[key], type_):
39 - raise TypeError(
40 - f"'{self._name}' member '{key}' must be a {typestr}"
41 - )
42 -
43 - def _check_member(self, key: str, type_: type, typestr: str) -> None:
44 - self._check_key(key)
45 - self._check_value(key, type_, typestr)
46 -
47 - @property
48 - def _name(self) -> str:
49 - return type(self).__name__
50 -
51 - def __repr__(self) -> str:
52 - return f"{self._name}({self._raw!r})"
53 -
54 -
55 -class Greeting(Model):
56 - """
57 - Defined in `interop/qmp-spec`, "Server Greeting" section.
58 -
59 - :param raw: The raw Greeting object.
60 - :raise KeyError: If any required fields are absent.
61 - :raise TypeError: If any required fields have the wrong type.
62 - """
63 - def __init__(self, raw: Mapping[str, Any]):
64 - super().__init__(raw)
65 - #: 'QMP' member
66 - self.QMP: QMPGreeting # pylint: disable=invalid-name
67 -
68 - self._check_member('QMP', abc.Mapping, "JSON object")
69 - self.QMP = QMPGreeting(self._raw['QMP'])
70 -
71 - def _asdict(self) -> Dict[str, object]:
72 - """
73 - For compatibility with the iotests sync QMP wrapper.
74 -
75 - The legacy QMP interface needs Greetings as a garden-variety Dict.
76 -
77 - This interface is private in the hopes that it will be able to
78 - be dropped again in the near-future. Caller beware!
79 - """
80 - return dict(copy.deepcopy(self._raw))
81 -
82 -
83 -class QMPGreeting(Model):
84 - """
85 - Defined in `interop/qmp-spec`, "Server Greeting" section.
86 -
87 - :param raw: The raw QMPGreeting object.
88 - :raise KeyError: If any required fields are absent.
89 - :raise TypeError: If any required fields have the wrong type.
90 - """
91 - def __init__(self, raw: Mapping[str, Any]):
92 - super().__init__(raw)
93 - #: 'version' member
94 - self.version: Mapping[str, object]
95 - #: 'capabilities' member
96 - self.capabilities: Sequence[object]
97 -
98 - self._check_member('version', abc.Mapping, "JSON object")
99 - self.version = self._raw['version']
100 -
101 - self._check_member('capabilities', abc.Sequence, "JSON array")
102 - self.capabilities = self._raw['capabilities']
103 -
104 -
105 -class ErrorResponse(Model):
106 - """
107 - Defined in `interop/qmp-spec`, "Error" section.
108 -
109 - :param raw: The raw ErrorResponse object.
110 - :raise KeyError: If any required fields are absent.
111 - :raise TypeError: If any required fields have the wrong type.
112 - """
113 - def __init__(self, raw: Mapping[str, Any]):
114 - super().__init__(raw)
115 - #: 'error' member
116 - self.error: ErrorInfo
117 - #: 'id' member
118 - self.id: Optional[object] = None # pylint: disable=invalid-name
119 -
120 - self._check_member('error', abc.Mapping, "JSON object")
121 - self.error = ErrorInfo(self._raw['error'])
122 -
123 - if 'id' in raw:
124 - self.id = raw['id']
125 -
126 -
127 -class ErrorInfo(Model):
128 - """
129 - Defined in `interop/qmp-spec`, "Error" section.
130 -
131 - :param raw: The raw ErrorInfo object.
132 - :raise KeyError: If any required fields are absent.
133 - :raise TypeError: If any required fields have the wrong type.
134 - """
135 - def __init__(self, raw: Mapping[str, Any]):
136 - super().__init__(raw)
137 - #: 'class' member, with an underscore to avoid conflicts in Python.
138 - self.class_: str
139 - #: 'desc' member
140 - self.desc: str
141 -
142 - self._check_member('class', str, "string")
143 - self.class_ = self._raw['class']
144 -
145 - self._check_member('desc', str, "string")
146 - self.desc = self._raw['desc']
python/qemu/qmp/protocol.py deleted
-1101
@@ -1,1101 +0,0 @@
1 -"""
2 -Generic Asynchronous Message-based Protocol Support
3 -
4 -This module provides a generic framework for sending and receiving
5 -messages over an asyncio stream. `AsyncProtocol` is an abstract class
6 -that implements the core mechanisms of a simple send/receive protocol,
7 -and is designed to be extended.
8 -
9 -In this package, it is used as the implementation for the `QMPClient`
10 -class.
11 -"""
12 -
13 -# It's all the docstrings ... ! It's long for a good reason ^_^;
14 -# pylint: disable=too-many-lines
15 -
16 -import asyncio
17 -from asyncio import StreamReader, StreamWriter
18 -from contextlib import asynccontextmanager
19 -from enum import Enum
20 -from functools import wraps
21 -from inspect import iscoroutinefunction
22 -import logging
23 -import socket
24 -from ssl import SSLContext
25 -from typing import (
26 - Any,
27 - AsyncGenerator,
28 - Awaitable,
29 - Callable,
30 - Generic,
31 - List,
32 - Optional,
33 - Tuple,
34 - TypeVar,
35 - Union,
36 - cast,
37 -)
38 -
39 -from .error import QMPError
40 -from .util import (
41 - bottom_half,
42 - exception_summary,
43 - flush,
44 - pretty_traceback,
45 - upper_half,
46 -)
47 -
48 -
49 -T = TypeVar('T')
50 -_U = TypeVar('_U')
51 -_TaskFN = Callable[[], Awaitable[None]] # aka ``async def func() -> None``
52 -
53 -InternetAddrT = Tuple[str, int]
54 -UnixAddrT = str
55 -SocketAddrT = Union[UnixAddrT, InternetAddrT]
56 -
57 -# Maximum allowable size of read buffer, default
58 -_DEFAULT_READBUFLEN = 64 * 1024
59 -
60 -
61 -class Runstate(Enum):
62 - """Protocol session runstate."""
63 -
64 - #: Fully quiesced and disconnected.
65 - IDLE = 0
66 - #: In the process of connecting or establishing a session.
67 - CONNECTING = 1
68 - #: Fully connected and active session.
69 - RUNNING = 2
70 - #: In the process of disconnecting.
71 - #: Runstate may be returned to `IDLE` by calling `disconnect()`.
72 - DISCONNECTING = 3
73 -
74 -
75 -class ConnectError(QMPError):
76 - """
77 - Raised when the initial connection process has failed.
78 -
79 - This Exception always wraps a "root cause" exception that can be
80 - interrogated for additional information.
81 -
82 - For example, when connecting to a non-existent socket::
83 -
84 - await qmp.connect('not_found.sock')
85 - # ConnectError: Failed to establish connection:
86 - # [Errno 2] No such file or directory
87 -
88 - :param error_message: Human-readable string describing the error.
89 - :param exc: The root-cause exception.
90 - """
91 - def __init__(self, error_message: str, exc: Exception):
92 - super().__init__(error_message, exc)
93 - #: Human-readable error string
94 - self.error_message: str = error_message
95 - #: Wrapped root cause exception
96 - self.exc: Exception = exc
97 -
98 - def __str__(self) -> str:
99 - cause = str(self.exc)
100 - if not cause:
101 - # If there's no error string, use the exception name.
102 - cause = exception_summary(self.exc)
103 - return f"{self.error_message}: {cause}"
104 -
105 -
106 -class StateError(QMPError):
107 - """
108 - An API command (connect, execute, etc) was issued at an inappropriate time.
109 -
110 - This error is raised when a command like
111 - :py:meth:`~AsyncProtocol.connect()` is called when the client is
112 - already connected.
113 -
114 - :param error_message: Human-readable string describing the state violation.
115 - :param state: The actual `Runstate` seen at the time of the violation.
116 - :param required: The `Runstate` required to process this command.
117 - """
118 - def __init__(self, error_message: str,
119 - state: Runstate, required: Runstate):
120 - super().__init__(error_message, state, required)
121 - self.error_message = error_message
122 - self.state = state
123 - self.required = required
124 -
125 - def __str__(self) -> str:
126 - return self.error_message
127 -
128 -
129 -F = TypeVar('F', bound=Callable[..., Any]) # pylint: disable=invalid-name
130 -
131 -
132 -# Don't Panic.
133 -def require(required_state: Runstate) -> Callable[[F], F]:
134 - """
135 - Decorator: protect a method so it can only be run in a certain `Runstate`.
136 -
137 - :param required_state: The `Runstate` required to invoke this method.
138 - :raise StateError: When the required `Runstate` is not met.
139 - """
140 - def _check(proto: 'AsyncProtocol[Any]') -> None:
141 - name = type(proto).__name__
142 - if proto.runstate == required_state:
143 - return
144 -
145 - if proto.runstate == Runstate.CONNECTING:
146 - emsg = f"{name} is currently connecting."
147 - elif proto.runstate == Runstate.DISCONNECTING:
148 - emsg = (f"{name} is disconnecting."
149 - " Call disconnect() to return to IDLE state.")
150 - elif proto.runstate == Runstate.RUNNING:
151 - emsg = f"{name} is already connected and running."
152 - elif proto.runstate == Runstate.IDLE:
153 - emsg = f"{name} is disconnected and idle."
154 - else:
155 - assert False
156 -
157 - raise StateError(emsg, proto.runstate, required_state)
158 -
159 - def _decorator(func: F) -> F:
160 - # _decorator is the decorator that is built by calling the
161 - # require() decorator factory; e.g.:
162 - #
163 - # @require(Runstate.IDLE) def foo(): ...
164 - # will replace 'foo' with the result of '_decorator(foo)'.
165 -
166 - @wraps(func)
167 - def _wrapper(proto: 'AsyncProtocol[Any]',
168 - *args: Any, **kwargs: Any) -> Any:
169 - _check(proto)
170 - return func(proto, *args, **kwargs)
171 -
172 - @wraps(func)
173 - async def _async_wrapper(proto: 'AsyncProtocol[Any]',
174 - *args: Any, **kwargs: Any) -> Any:
175 - _check(proto)
176 - return await func(proto, *args, **kwargs)
177 -
178 - # Return the decorated method; F => Decorated[F]
179 - # Use an async version when applicable, which
180 - # preserves async signature generation in sphinx.
181 - if iscoroutinefunction(func):
182 - return cast(F, _async_wrapper)
183 - return cast(F, _wrapper)
184 -
185 - # Return the decorator instance from the decorator factory. Phew!
186 - return _decorator
187 -
188 -
189 -class AsyncProtocol(Generic[T]):
190 - """
191 - AsyncProtocol implements a generic async message-based protocol.
192 -
193 - This protocol assumes the basic unit of information transfer between
194 - client and server is a "message", the details of which are left up
195 - to the implementation. It assumes the sending and receiving of these
196 - messages is full-duplex and not necessarily correlated; i.e. it
197 - supports asynchronous inbound messages.
198 -
199 - It is designed to be extended by a specific protocol which provides
200 - the implementations for how to read and send messages. These must be
201 - defined in `_do_recv()` and `_do_send()`, respectively.
202 -
203 - Other callbacks have a default implementation, but are intended to be
204 - either extended or overridden:
205 -
206 - - `_establish_session`:
207 - The base implementation starts the reader/writer tasks.
208 - A protocol implementation can override this call, inserting
209 - actions to be taken prior to starting the reader/writer tasks
210 - before the super() call; actions needing to occur afterwards
211 - can be written after the super() call.
212 - - `_on_message`:
213 - Actions to be performed when a message is received.
214 - - `_cb_outbound`:
215 - Logging/Filtering hook for all outbound messages.
216 - - `_cb_inbound`:
217 - Logging/Filtering hook for all inbound messages.
218 - This hook runs *before* `_on_message()`.
219 -
220 - :param name:
221 - Name used for logging messages, if any. By default, messages
222 - will log to 'qemu.qmp.protocol', but each individual connection
223 - can be given its own logger by giving it a name; messages will
224 - then log to 'qemu.qmp.protocol.${name}'.
225 - :param readbuflen:
226 - The maximum read buffer length of the underlying StreamReader
227 - instance.
228 - """
229 - # pylint: disable=too-many-instance-attributes
230 -
231 - #: Logger object for debugging messages from this connection.
232 - logger = logging.getLogger(__name__)
233 -
234 - # -------------------------
235 - # Section: Public interface
236 - # -------------------------
237 -
238 - def __init__(
239 - self, name: Optional[str] = None,
240 - readbuflen: int = _DEFAULT_READBUFLEN
241 - ) -> None:
242 - self._name: Optional[str]
243 - self.name = name
244 - self.readbuflen = readbuflen
245 -
246 - # stream I/O
247 - self._reader: Optional[StreamReader] = None
248 - self._writer: Optional[StreamWriter] = None
249 -
250 - # Outbound Message queue
251 - self._outgoing: asyncio.Queue[T]
252 -
253 - # Special, long-running tasks:
254 - self._reader_task: Optional[asyncio.Future[None]] = None
255 - self._writer_task: Optional[asyncio.Future[None]] = None
256 -
257 - # Aggregate of the above two tasks, used for Exception management.
258 - self._bh_tasks: Optional[asyncio.Future[Tuple[None, None]]] = None
259 -
260 - #: Disconnect task. The disconnect implementation runs in a task
261 - #: so that asynchronous disconnects (initiated by the
262 - #: reader/writer) are allowed to wait for the reader/writers to
263 - #: exit.
264 - self._dc_task: Optional[asyncio.Future[None]] = None
265 -
266 - self._runstate = Runstate.IDLE
267 - self._runstate_changed: Optional[asyncio.Event] = None
268 -
269 - # Server state for start_server() and _incoming()
270 - self._server: Optional[asyncio.AbstractServer] = None
271 - self._accepted: Optional[asyncio.Event] = None
272 -
273 - def __repr__(self) -> str:
274 - cls_name = type(self).__name__
275 - tokens = []
276 - if self.name is not None:
277 - tokens.append(f"name={self.name!r}")
278 - tokens.append(f"runstate={self.runstate.name}")
279 - return f"<{cls_name} {' '.join(tokens)}>"
280 -
281 - @property
282 - def name(self) -> Optional[str]:
283 - """
284 - The nickname for this connection, if any.
285 -
286 - This name is used for differentiating instances in debug output.
287 - """
288 - return self._name
289 -
290 - @name.setter
291 - def name(self, name: Optional[str]) -> None:
292 - logger = logging.getLogger(__name__)
293 - if name:
294 - self.logger = logger.getChild(name)
295 - else:
296 - self.logger = logger
297 - self._name = name
298 -
299 - @property # @upper_half
300 - def runstate(self) -> Runstate:
301 - """The current `Runstate` of the connection."""
302 - return self._runstate
303 -
304 - @upper_half
305 - async def runstate_changed(self) -> Runstate:
306 - """
307 - Wait for the `runstate` to change, then return that `Runstate`.
308 - """
309 - await self._runstate_event.wait()
310 - return self.runstate
311 -
312 - @upper_half
313 - @require(Runstate.IDLE)
314 - async def start_server_and_accept(
315 - self, address: SocketAddrT,
316 - ssl: Optional[SSLContext] = None
317 - ) -> None:
318 - """
319 - Accept a connection and begin processing message queues.
320 -
321 - If this call fails, `runstate` is guaranteed to be set back to
322 - `IDLE`. This method is precisely equivalent to calling
323 - `start_server()` followed by :py:meth:`~AsyncProtocol.accept()`.
324 -
325 - :param address:
326 - Address to listen on; UNIX socket path or TCP address/port.
327 - :param ssl: SSL context to use, if any.
328 -
329 - :raise StateError: When the `Runstate` is not `IDLE`.
330 - :raise ConnectError:
331 - When a connection or session cannot be established.
332 -
333 - This exception will wrap a more concrete one. In most cases,
334 - the wrapped exception will be `OSError` or `EOFError`. If a
335 - protocol-level failure occurs while establishing a new
336 - session, the wrapped error may also be a `QMPError`.
337 -
338 - """
339 - await self.start_server(address, ssl)
340 - await self.accept()
341 - assert self.runstate == Runstate.RUNNING
342 -
343 - @upper_half
344 - @require(Runstate.IDLE)
345 - async def start_server(self, address: SocketAddrT,
346 - ssl: Optional[SSLContext] = None) -> None:
347 - """
348 - Start listening for an incoming connection, but do not wait for a peer.
349 -
350 - This method starts listening for an incoming connection, but
351 - does not block waiting for a peer. This call will return
352 - immediately after binding and listening on a socket. A later
353 - call to :py:meth:`~AsyncProtocol.accept()` must be made in order
354 - to finalize the incoming connection.
355 -
356 - :param address:
357 - Address to listen on; UNIX socket path or TCP address/port.
358 - :param ssl: SSL context to use, if any.
359 -
360 - :raise StateError: When the `Runstate` is not `IDLE`.
361 - :raise ConnectError:
362 - When the server could not start listening on this address.
363 -
364 - This exception will wrap a more concrete one. In most cases,
365 - the wrapped exception will be `OSError`.
366 - """
367 - async with self._session_guard('Failed to establish connection'):
368 - await self._do_start_server(address, ssl)
369 - assert self.runstate == Runstate.CONNECTING
370 -
371 - @upper_half
372 - @require(Runstate.CONNECTING)
373 - async def accept(self) -> None:
374 - """
375 - Accept an incoming connection and begin processing message queues.
376 -
377 - Used after a previous call to `start_server()` to accept an
378 - incoming connection. If this call fails, `runstate` is
379 - guaranteed to be set back to `IDLE`.
380 -
381 - :raise StateError: When the `Runstate` is not `CONNECTING`.
382 - :raise QMPError: When `start_server()` was not called first.
383 - :raise ConnectError:
384 - When a connection or session cannot be established.
385 -
386 - This exception will wrap a more concrete one. In most cases,
387 - the wrapped exception will be `OSError` or `EOFError`. If a
388 - protocol-level failure occurs while establishing a new
389 - session, the wrapped error may also be an `QMPError`.
390 - """
391 - if self._accepted is None:
392 - raise QMPError("Cannot call accept() before start_server().")
393 - async with self._session_guard('Failed to establish connection'):
394 - await self._do_accept()
395 - async with self._session_guard('Failed to establish session'):
396 - await self._establish_session()
397 - assert self.runstate == Runstate.RUNNING
398 -
399 - @upper_half
400 - @require(Runstate.IDLE)
401 - async def connect(self, address: Union[SocketAddrT, socket.socket],
402 - ssl: Optional[SSLContext] = None) -> None:
403 - """
404 - Connect to the server and begin processing message queues.
405 -
406 - If this call fails, `runstate` is guaranteed to be set back to `IDLE`.
407 -
408 - :param address:
409 - Address to connect to; UNIX socket path or TCP address/port.
410 - :param ssl: SSL context to use, if any.
411 -
412 - :raise StateError: When the `Runstate` is not `IDLE`.
413 - :raise ConnectError:
414 - When a connection or session cannot be established.
415 -
416 - This exception will wrap a more concrete one. In most cases,
417 - the wrapped exception will be `OSError` or `EOFError`. If a
418 - protocol-level failure occurs while establishing a new
419 - session, the wrapped error may also be an `QMPError`.
420 - """
421 - async with self._session_guard('Failed to establish connection'):
422 - await self._do_connect(address, ssl)
423 - async with self._session_guard('Failed to establish session'):
424 - await self._establish_session()
425 - assert self.runstate == Runstate.RUNNING
426 -
427 - @upper_half
428 - async def disconnect(self) -> None:
429 - """
430 - Disconnect and wait for all tasks to fully stop.
431 -
432 - If there was an exception that caused the reader/writers to
433 - terminate prematurely, it will be raised here.
434 -
435 - :raise Exception:
436 - When the reader or writer terminate unexpectedly. You can
437 - expect to see `EOFError` if the server hangs up, or
438 - `OSError` for connection-related issues. If there was a QMP
439 - protocol-level problem, `ProtocolError` will be seen.
440 - """
441 - self.logger.debug("disconnect() called.")
442 - self._schedule_disconnect()
443 - await self._wait_disconnect()
444 -
445 - # --------------------------
446 - # Section: Session machinery
447 - # --------------------------
448 -
449 - @asynccontextmanager
450 - async def _session_guard(self, emsg: str) -> AsyncGenerator[None, None]:
451 - """
452 - Async guard function used to roll back to `IDLE` on any error.
453 -
454 - On any Exception, the state machine will be reset back to
455 - `IDLE`. Most Exceptions will be wrapped with `ConnectError`, but
456 - `BaseException` events will be left alone (This includes
457 - asyncio.CancelledError, even prior to Python 3.8).
458 -
459 - :param error_message:
460 - Human-readable string describing what connection phase failed.
461 -
462 - :raise BaseException:
463 - When `BaseException` occurs in the guarded block.
464 - :raise ConnectError:
465 - When any other error is encountered in the guarded block.
466 - """
467 - try:
468 - # Caller's code runs here.
469 - yield
470 - except BaseException as err:
471 - self.logger.error("%s: %s", emsg, exception_summary(err))
472 - self.logger.debug("%s:\n%s\n", emsg, pretty_traceback())
473 - try:
474 - # Reset the runstate back to IDLE.
475 - await self.disconnect()
476 - except:
477 - # We don't expect any Exceptions from the disconnect function
478 - # here, because we failed to connect in the first place.
479 - # The disconnect() function is intended to perform
480 - # only cannot-fail cleanup here, but you never know.
481 - emsg = (
482 - "Unexpected bottom half exception. "
483 - "This is a bug in the QMP library. "
484 - "Please report it to <qemu-devel@nongnu.org> and "
485 - "CC: John Snow <jsnow@redhat.com>."
486 - )
487 - self.logger.critical("%s:\n%s\n", emsg, pretty_traceback())
488 - raise
489 -
490 - # CancelledError is an Exception with special semantic meaning;
491 - # We do NOT want to wrap it up under ConnectError.
492 - # NB: CancelledError is not a BaseException before Python 3.8
493 - if isinstance(err, asyncio.CancelledError):
494 - raise
495 -
496 - # Any other kind of error can be treated as some kind of connection
497 - # failure broadly. Inspect the 'exc' field to explore the root
498 - # cause in greater detail.
499 - if isinstance(err, Exception):
500 - raise ConnectError(emsg, err) from err
501 -
502 - # Raise BaseExceptions un-wrapped, they're more important.
503 - raise
504 -
505 - @property
506 - def _runstate_event(self) -> asyncio.Event:
507 - # asyncio.Event() objects should not be created prior to entrance into
508 - # an event loop, so we can ensure we create it in the correct context.
509 - # Create it on-demand *only* at the behest of an 'async def' method.
510 - if not self._runstate_changed:
511 - self._runstate_changed = asyncio.Event()
512 - return self._runstate_changed
513 -
514 - @upper_half
515 - @bottom_half
516 - def _set_state(self, state: Runstate) -> None:
517 - """
518 - Change the `Runstate` of the protocol connection.
519 -
520 - Signals the `runstate_changed` event.
521 - """
522 - if state == self._runstate:
523 - return
524 -
525 - self.logger.debug("Transitioning from '%s' to '%s'.",
526 - str(self._runstate), str(state))
527 - self._runstate = state
528 - self._runstate_event.set()
529 - self._runstate_event.clear()
530 -
531 - @bottom_half
532 - async def _stop_server(self) -> None:
533 - """
534 - Stop listening for / accepting new incoming connections.
535 - """
536 - if self._server is None:
537 - return
538 -
539 - try:
540 - self.logger.debug("Stopping server.")
541 - self._server.close()
542 - self.logger.debug("Server stopped.")
543 - finally:
544 - self._server = None
545 -
546 - @bottom_half # However, it does not run from the R/W tasks.
547 - async def _incoming(self,
548 - reader: asyncio.StreamReader,
549 - writer: asyncio.StreamWriter) -> None:
550 - """
551 - Accept an incoming connection and signal the upper_half.
552 -
553 - This method does the minimum necessary to accept a single
554 - incoming connection. It signals back to the upper_half ASAP so
555 - that any errors during session initialization can occur
556 - naturally in the caller's stack.
557 -
558 - :param reader: Incoming `asyncio.StreamReader`
559 - :param writer: Incoming `asyncio.StreamWriter`
560 - """
561 - peer = writer.get_extra_info('peername', 'Unknown peer')
562 - self.logger.debug("Incoming connection from %s", peer)
563 -
564 - if self._reader or self._writer:
565 - # Sadly, we can have more than one pending connection
566 - # because of https://bugs.python.org/issue46715
567 - # Close any extra connections we don't actually want.
568 - self.logger.warning("Extraneous connection inadvertently accepted")
569 - writer.close()
570 - return
571 -
572 - # A connection has been accepted; stop listening for new ones.
573 - assert self._accepted is not None
574 - await self._stop_server()
575 - self._reader, self._writer = (reader, writer)
576 - self._accepted.set()
577 -
578 - @upper_half
579 - async def _do_start_server(self, address: SocketAddrT,
580 - ssl: Optional[SSLContext] = None) -> None:
581 - """
582 - Start listening for an incoming connection, but do not wait for a peer.
583 -
584 - This method starts listening for an incoming connection, but does not
585 - block waiting for a peer. This call will return immediately after
586 - binding and listening to a socket. A later call to accept() must be
587 - made in order to finalize the incoming connection.
588 -
589 - :param address:
590 - Address to listen on; UNIX socket path or TCP address/port.
591 - :param ssl: SSL context to use, if any.
592 -
593 - :raise OSError: For stream-related errors.
594 - """
595 - assert self.runstate == Runstate.IDLE
596 - self._set_state(Runstate.CONNECTING)
597 -
598 - self.logger.debug("Awaiting connection on %s ...", address)
599 - self._accepted = asyncio.Event()
600 -
601 - if isinstance(address, tuple):
602 - coro = asyncio.start_server(
603 - self._incoming,
604 - host=address[0],
605 - port=address[1],
606 - ssl=ssl,
607 - backlog=1,
608 - limit=self.readbuflen,
609 - )
610 - else:
611 - coro = asyncio.start_unix_server(
612 - self._incoming,
613 - path=address,
614 - ssl=ssl,
615 - backlog=1,
616 - limit=self.readbuflen,
617 - )
618 -
619 - # Allow runstate watchers to witness 'CONNECTING' state; some
620 - # failures in the streaming layer are synchronous and will not
621 - # otherwise yield.
622 - await asyncio.sleep(0)
623 -
624 - # This will start the server (bind(2), listen(2)). It will also
625 - # call accept(2) if we yield, but we don't block on that here.
626 - self._server = await coro
627 - self.logger.debug("Server listening on %s", address)
628 -
629 - @upper_half
630 - async def _do_accept(self) -> None:
631 - """
632 - Wait for and accept an incoming connection.
633 -
634 - Requires that we have not yet accepted an incoming connection
635 - from the upper_half, but it's OK if the server is no longer
636 - running because the bottom_half has already accepted the
637 - connection.
638 - """
639 - assert self._accepted is not None
640 - await self._accepted.wait()
641 - assert self._server is None
642 - self._accepted = None
643 -
644 - self.logger.debug("Connection accepted.")
645 -
646 - @upper_half
647 - async def _do_connect(self, address: Union[SocketAddrT, socket.socket],
648 - ssl: Optional[SSLContext] = None) -> None:
649 - """
650 - Acting as the transport client, initiate a connection to a server.
651 -
652 - :param address:
653 - Address to connect to; UNIX socket path or TCP address/port.
654 - :param ssl: SSL context to use, if any.
655 -
656 - :raise OSError: For stream-related errors.
657 - """
658 - assert self.runstate == Runstate.IDLE
659 - self._set_state(Runstate.CONNECTING)
660 -
661 - # Allow runstate watchers to witness 'CONNECTING' state; some
662 - # failures in the streaming layer are synchronous and will not
663 - # otherwise yield.
664 - await asyncio.sleep(0)
665 -
666 - if isinstance(address, socket.socket):
667 - self.logger.debug("Connecting with existing socket: "
668 - "fd=%d, family=%r, type=%r",
669 - address.fileno(), address.family, address.type)
670 - connect = asyncio.open_connection(
671 - limit=self.readbuflen,
672 - ssl=ssl,
673 - sock=address,
674 - )
675 - elif isinstance(address, tuple):
676 - self.logger.debug("Connecting to %s ...", address)
677 - connect = asyncio.open_connection(
678 - address[0],
679 - address[1],
680 - ssl=ssl,
681 - limit=self.readbuflen,
682 - )
683 - else:
684 - self.logger.debug("Connecting to file://%s ...", address)
685 - connect = asyncio.open_unix_connection(
686 - path=address,
687 - ssl=ssl,
688 - limit=self.readbuflen,
689 - )
690 -
691 - self._reader, self._writer = await connect
692 - self.logger.debug("Connected.")
693 -
694 - @upper_half
695 - async def _establish_session(self) -> None:
696 - """
697 - Establish a new session.
698 -
699 - Starts the readers/writer tasks; subclasses may perform their
700 - own negotiations here. The Runstate will be RUNNING upon
701 - successful conclusion.
702 - """
703 - assert self.runstate == Runstate.CONNECTING
704 -
705 - self._outgoing = asyncio.Queue()
706 -
707 - reader_coro = self._bh_loop_forever(self._bh_recv_message, 'Reader')
708 - writer_coro = self._bh_loop_forever(self._bh_send_message, 'Writer')
709 -
710 - self._reader_task = asyncio.create_task(reader_coro)
711 - self._writer_task = asyncio.create_task(writer_coro)
712 -
713 - self._bh_tasks = asyncio.gather(
714 - self._reader_task,
715 - self._writer_task,
716 - )
717 -
718 - self._set_state(Runstate.RUNNING)
719 - await asyncio.sleep(0) # Allow runstate_event to process
720 -
721 - @upper_half
722 - @bottom_half
723 - def _schedule_disconnect(self) -> None:
724 - """
725 - Initiate a disconnect; idempotent.
726 -
727 - This method is used both in the upper-half as a direct
728 - consequence of `disconnect()`, and in the bottom-half in the
729 - case of unhandled exceptions in the reader/writer tasks.
730 -
731 - It can be invoked no matter what the `runstate` is.
732 - """
733 - if not self._dc_task:
734 - self._set_state(Runstate.DISCONNECTING)
735 - self.logger.debug("Scheduling disconnect.")
736 - self._dc_task = asyncio.create_task(self._bh_disconnect())
737 -
738 - @upper_half
739 - async def _wait_disconnect(self) -> None:
740 - """
741 - Waits for a previously scheduled disconnect to finish.
742 -
743 - This method will gather any bottom half exceptions and re-raise
744 - the one that occurred first; presuming it to be the root cause
745 - of any subsequent Exceptions. It is intended to be used in the
746 - upper half of the call chain.
747 -
748 - :raise Exception:
749 - Arbitrary exception re-raised on behalf of the reader/writer.
750 - """
751 - assert self.runstate == Runstate.DISCONNECTING
752 - assert self._dc_task
753 -
754 - aws: List[Awaitable[object]] = [self._dc_task]
755 - if self._bh_tasks:
756 - aws.insert(0, self._bh_tasks)
757 - all_defined_tasks = asyncio.gather(*aws)
758 -
759 - # Ensure disconnect is done; Exception (if any) is not raised here:
760 - await asyncio.wait((self._dc_task,))
761 -
762 - try:
763 - await all_defined_tasks # Raise Exceptions from the bottom half.
764 - finally:
765 - self._cleanup()
766 - self._set_state(Runstate.IDLE)
767 -
768 - @upper_half
769 - def _cleanup(self) -> None:
770 - """
771 - Fully reset this object to a clean state and return to `IDLE`.
772 - """
773 - def _paranoid_task_erase(task: Optional['asyncio.Future[_U]']
774 - ) -> Optional['asyncio.Future[_U]']:
775 - # Help to erase a task, ENSURING it is fully quiesced first.
776 - assert (task is None) or task.done()
777 - return None if (task and task.done()) else task
778 -
779 - assert self.runstate == Runstate.DISCONNECTING
780 - self._dc_task = _paranoid_task_erase(self._dc_task)
781 - self._reader_task = _paranoid_task_erase(self._reader_task)
782 - self._writer_task = _paranoid_task_erase(self._writer_task)
783 - self._bh_tasks = _paranoid_task_erase(self._bh_tasks)
784 -
785 - self._reader = None
786 - self._writer = None
787 - self._accepted = None
788 -
789 - # NB: _runstate_changed cannot be cleared because we still need it to
790 - # send the final runstate changed event ...!
791 -
792 - # ----------------------------
793 - # Section: Bottom Half methods
794 - # ----------------------------
795 -
796 - @bottom_half
797 - async def _bh_disconnect(self) -> None:
798 - """
799 - Disconnect and cancel all outstanding tasks.
800 -
801 - It is designed to be called from its task context,
802 - :py:obj:`~AsyncProtocol._dc_task`. By running in its own task,
803 - it is free to wait on any pending actions that may still need to
804 - occur in either the reader or writer tasks.
805 - """
806 - assert self.runstate == Runstate.DISCONNECTING
807 -
808 - def _done(task: Optional['asyncio.Future[Any]']) -> bool:
809 - return task is not None and task.done()
810 -
811 - # If the server is running, stop it.
812 - await self._stop_server()
813 -
814 - # Are we already in an error pathway? If either of the tasks are
815 - # already done, or if we have no tasks but a reader/writer; we
816 - # must be.
817 - #
818 - # NB: We can't use _bh_tasks to check for premature task
819 - # completion, because it may not yet have had a chance to run
820 - # and gather itself.
821 - tasks = tuple(filter(None, (self._writer_task, self._reader_task)))
822 - error_pathway = _done(self._reader_task) or _done(self._writer_task)
823 - if not tasks:
824 - error_pathway |= bool(self._reader) or bool(self._writer)
825 -
826 - try:
827 - # Try to flush the writer, if possible.
828 - # This *may* cause an error and force us over into the error path.
829 - if not error_pathway:
830 - await self._bh_flush_writer()
831 - except BaseException as err:
832 - error_pathway = True
833 - emsg = "Failed to flush the writer"
834 - self.logger.error("%s: %s", emsg, exception_summary(err))
835 - self.logger.debug("%s:\n%s\n", emsg, pretty_traceback())
836 - raise
837 - finally:
838 - # Cancel any still-running tasks (Won't raise):
839 - if self._writer_task is not None and not self._writer_task.done():
840 - self.logger.debug("Cancelling writer task.")
841 - self._writer_task.cancel()
842 - if self._reader_task is not None and not self._reader_task.done():
843 - self.logger.debug("Cancelling reader task.")
844 - self._reader_task.cancel()
845 -
846 - # Close out the tasks entirely (Won't raise):
847 - if tasks:
848 - self.logger.debug("Waiting for tasks to complete ...")
849 - await asyncio.wait(tasks)
850 -
851 - # Lastly, close the stream itself. (*May raise*!):
852 - await self._bh_close_stream(error_pathway)
853 - self.logger.debug("Disconnected.")
854 -
855 - @bottom_half
856 - async def _bh_flush_writer(self) -> None:
857 - if not self._writer_task:
858 - return
859 -
860 - self.logger.debug("Draining the outbound queue ...")
861 - await self._outgoing.join()
862 - if self._writer is not None:
863 - self.logger.debug("Flushing the StreamWriter ...")
864 - await flush(self._writer)
865 -
866 - @bottom_half
867 - async def _bh_close_stream(self, error_pathway: bool = False) -> None:
868 - # NB: Closing the writer also implicitly closes the reader.
869 - if not self._writer:
870 - return
871 -
872 - if not self._writer.is_closing():
873 - self.logger.debug("Closing StreamWriter.")
874 - self._writer.close()
875 -
876 - self.logger.debug("Waiting for StreamWriter to close ...")
877 - try:
878 - await self._writer.wait_closed()
879 - except Exception: # pylint: disable=broad-except
880 - # It's hard to tell if the Stream is already closed or
881 - # not. Even if one of the tasks has failed, it may have
882 - # failed for a higher-layered protocol reason. The
883 - # stream could still be open and perfectly fine.
884 - # I don't know how to discern its health here.
885 -
886 - if error_pathway:
887 - # We already know that *something* went wrong. Let's
888 - # just trust that the Exception we already have is the
889 - # better one to present to the user, even if we don't
890 - # genuinely *know* the relationship between the two.
891 - self.logger.debug(
892 - "Discarding Exception from wait_closed:\n%s\n",
893 - pretty_traceback(),
894 - )
895 - else:
896 - # Oops, this is a brand-new error!
897 - raise
898 - finally:
899 - self.logger.debug("StreamWriter closed.")
900 -
901 - @bottom_half
902 - async def _bh_loop_forever(self, async_fn: _TaskFN, name: str) -> None:
903 - """
904 - Run one of the bottom-half methods in a loop forever.
905 -
906 - If the bottom half ever raises any exception, schedule a
907 - disconnect that will terminate the entire loop.
908 -
909 - :param async_fn: The bottom-half method to run in a loop.
910 - :param name: The name of this task, used for logging.
911 - """
912 - try:
913 - while True:
914 - await async_fn()
915 - except asyncio.CancelledError:
916 - # We have been cancelled by _bh_disconnect, exit gracefully.
917 - self.logger.debug("Task.%s: cancelled.", name)
918 - return
919 - except BaseException as err:
920 - self.logger.log(
921 - logging.INFO if isinstance(err, EOFError) else logging.ERROR,
922 - "Task.%s: %s",
923 - name, exception_summary(err)
924 - )
925 - self.logger.debug("Task.%s: failure:\n%s\n",
926 - name, pretty_traceback())
927 - self._schedule_disconnect()
928 - raise
929 - finally:
930 - self.logger.debug("Task.%s: exiting.", name)
931 -
932 - @bottom_half
933 - async def _bh_send_message(self) -> None:
934 - """
935 - Wait for an outgoing message, then send it.
936 -
937 - Designed to be run in `_bh_loop_forever()`.
938 - """
939 - msg = await self._outgoing.get()
940 - try:
941 - await self._send(msg)
942 - finally:
943 - self._outgoing.task_done()
944 -
945 - @bottom_half
946 - async def _bh_recv_message(self) -> None:
947 - """
948 - Wait for an incoming message and call `_on_message` to route it.
949 -
950 - Designed to be run in `_bh_loop_forever()`.
951 - """
952 - msg = await self._recv()
953 - await self._on_message(msg)
954 -
955 - # --------------------
956 - # Section: Message I/O
957 - # --------------------
958 -
959 - @upper_half
960 - @bottom_half
961 - def _cb_outbound(self, msg: T) -> T:
962 - """
963 - Callback: outbound message hook.
964 -
965 - This is intended for subclasses to be able to add arbitrary
966 - hooks to filter or manipulate outgoing messages. The base
967 - implementation does nothing but log the message without any
968 - manipulation of the message.
969 -
970 - :param msg: raw outbound message
971 - :return: final outbound message
972 - """
973 - self.logger.debug("--> %s", str(msg))
974 - return msg
975 -
976 - @upper_half
977 - @bottom_half
978 - def _cb_inbound(self, msg: T) -> T:
979 - """
980 - Callback: inbound message hook.
981 -
982 - This is intended for subclasses to be able to add arbitrary
983 - hooks to filter or manipulate incoming messages. The base
984 - implementation does nothing but log the message without any
985 - manipulation of the message.
986 -
987 - This method does not "handle" incoming messages; it is a filter.
988 - The actual "endpoint" for incoming messages is `_on_message()`.
989 -
990 - :param msg: raw inbound message
991 - :return: processed inbound message
992 - """
993 - self.logger.debug("<-- %s", str(msg))
994 - return msg
995 -
996 - @upper_half
997 - @bottom_half
998 - async def _readline(self) -> bytes:
999 - """
1000 - Wait for a newline from the incoming reader.
1001 -
1002 - This method is provided as a convenience for upper-layer
1003 - protocols, as many are line-based.
1004 -
1005 - This method *may* return a sequence of bytes without a trailing
1006 - newline if EOF occurs, but *some* bytes were received. In this
1007 - case, the next call will raise `EOFError`. It is assumed that
1008 - the layer 5 protocol will decide if there is anything meaningful
1009 - to be done with a partial message.
1010 -
1011 - :raise OSError: For stream-related errors.
1012 - :raise EOFError:
1013 - If the reader stream is at EOF and there are no bytes to return.
1014 - :return: bytes, including the newline.
1015 - """
1016 - assert self._reader is not None
1017 - msg_bytes = await self._reader.readline()
1018 -
1019 - if not msg_bytes:
1020 - if self._reader.at_eof():
1021 - raise EOFError
1022 -
1023 - return msg_bytes
1024 -
1025 - @upper_half
1026 - @bottom_half
1027 - async def _do_recv(self) -> T:
1028 - """
1029 - Abstract: Read from the stream and return a message.
1030 -
1031 - Very low-level; intended to only be called by `_recv()`.
1032 - """
1033 - raise NotImplementedError
1034 -
1035 - @upper_half
1036 - @bottom_half
1037 - async def _recv(self) -> T:
1038 - """
1039 - Read an arbitrary protocol message.
1040 -
1041 - .. warning::
1042 - This method is intended primarily for `_bh_recv_message()`
1043 - to use in an asynchronous task loop. Using it outside of
1044 - this loop will "steal" messages from the normal routing
1045 - mechanism. It is safe to use prior to `_establish_session()`,
1046 - but should not be used otherwise.
1047 -
1048 - This method uses `_do_recv()` to retrieve the raw message, and
1049 - then transforms it using `_cb_inbound()`.
1050 -
1051 - :return: A single (filtered, processed) protocol message.
1052 - """
1053 - message = await self._do_recv()
1054 - return self._cb_inbound(message)
1055 -
1056 - @upper_half
1057 - @bottom_half
1058 - def _do_send(self, msg: T) -> None:
1059 - """
1060 - Abstract: Write a message to the stream.
1061 -
1062 - Very low-level; intended to only be called by `_send()`.
1063 - """
1064 - raise NotImplementedError
1065 -
1066 - @upper_half
1067 - @bottom_half
1068 - async def _send(self, msg: T) -> None:
1069 - """
1070 - Send an arbitrary protocol message.
1071 -
1072 - This method will transform any outgoing messages according to
1073 - `_cb_outbound()`.
1074 -
1075 - .. warning::
1076 - Like `_recv()`, this method is intended to be called by
1077 - the writer task loop that processes outgoing
1078 - messages. Calling it directly may circumvent logic
1079 - implemented by the caller meant to correlate outgoing and
1080 - incoming messages.
1081 -
1082 - :raise OSError: For problems with the underlying stream.
1083 - """
1084 - msg = self._cb_outbound(msg)
1085 - self._do_send(msg)
1086 -
1087 - @bottom_half
1088 - async def _on_message(self, msg: T) -> None:
1089 - """
1090 - Called to handle the receipt of a new message.
1091 -
1092 - .. caution::
1093 - This is executed from within the reader loop, so be advised
1094 - that waiting on either the reader or writer task will lead
1095 - to deadlock. Additionally, any unhandled exceptions will
1096 - directly cause the loop to halt, so logic may be best-kept
1097 - to a minimum if at all possible.
1098 -
1099 - :param msg: The incoming message, already logged/filtered.
1100 - """
1101 - # Nothing to do in the abstract case.
python/qemu/qmp/py.typed
python/qemu/qmp/qmp_client.py deleted
-732
@@ -1,732 +0,0 @@
1 -"""
2 -QMP Protocol Implementation
3 -
4 -This module provides the `QMPClient` class, which can be used to connect
5 -and send commands to a QMP server such as QEMU. The QMP class can be
6 -used to either connect to a listening server, or used to listen and
7 -accept an incoming connection from that server.
8 -"""
9 -
10 -import asyncio
11 -import logging
12 -import socket
13 -import struct
14 -from typing import (
15 - Dict,
16 - List,
17 - Mapping,
18 - Optional,
19 - Union,
20 - cast,
21 -)
22 -
23 -from .error import ProtocolError, QMPError
24 -from .events import Events
25 -from .message import Message
26 -from .models import ErrorResponse, Greeting
27 -from .protocol import AsyncProtocol, Runstate, require
28 -from .util import (
29 - bottom_half,
30 - exception_summary,
31 - pretty_traceback,
32 - upper_half,
33 -)
34 -
35 -
36 -class _WrappedProtocolError(ProtocolError):
37 - """
38 - Abstract exception class for Protocol errors that wrap an Exception.
39 -
40 - :param error_message: Human-readable string describing the error.
41 - :param exc: The root-cause exception.
42 - """
43 - def __init__(self, error_message: str, exc: Exception):
44 - super().__init__(error_message, exc)
45 - self.exc = exc
46 -
47 - def __str__(self) -> str:
48 - return f"{self.error_message}: {self.exc!s}"
49 -
50 -
51 -class GreetingError(_WrappedProtocolError):
52 - """
53 - An exception occurred during the Greeting phase.
54 -
55 - :param error_message: Human-readable string describing the error.
56 - :param exc: The root-cause exception.
57 - """
58 -
59 -
60 -class NegotiationError(_WrappedProtocolError):
61 - """
62 - An exception occurred during the Negotiation phase.
63 -
64 - :param error_message: Human-readable string describing the error.
65 - :param exc: The root-cause exception.
66 - """
67 -
68 -
69 -class ExecuteError(QMPError):
70 - """
71 - Exception raised by `QMPClient.execute()` on RPC failure.
72 -
73 - This exception is raised when the server received, interpreted, and
74 - replied to a command successfully; but the command itself returned a
75 - failure status.
76 -
77 - For example::
78 -
79 - await qmp.execute('block-dirty-bitmap-add',
80 - {'node': 'foo', 'name': 'my_bitmap'})
81 - # qemu.qmp.qmp_client.ExecuteError:
82 - # Cannot find device='foo' nor node-name='foo'
83 -
84 - :param error_response: The RPC error response object.
85 - :param sent: The sent RPC message that caused the failure.
86 - :param received: The raw RPC error reply received.
87 - """
88 - def __init__(self, error_response: ErrorResponse,
89 - sent: Message, received: Message):
90 - super().__init__(error_response, sent, received)
91 - #: The sent `Message` that caused the failure
92 - self.sent: Message = sent
93 - #: The received `Message` that indicated failure
94 - self.received: Message = received
95 - #: The parsed error response
96 - self.error: ErrorResponse = error_response
97 -
98 - @property
99 - def error_class(self) -> str:
100 - """The QMP error class"""
101 - return self.error.error.class_
102 -
103 - def __str__(self) -> str:
104 - return self.error.error.desc
105 -
106 -
107 -class ExecInterruptedError(QMPError):
108 - """
109 - Exception raised by `execute()` (et al) when an RPC is interrupted.
110 -
111 - This error is raised when an `execute()` statement could not be
112 - completed. This can occur because the connection itself was
113 - terminated before a reply was received. The true cause of the
114 - interruption will be available via `disconnect()`.
115 -
116 - The QMP protocol does not make it possible to know if a command
117 - succeeded or failed after such an event; the client will need to
118 - query the server to determine the state of the server on a
119 - case-by-case basis.
120 -
121 - For example, ECONNRESET might look like this::
122 -
123 - try:
124 - await qmp.execute('query-block')
125 - # ExecInterruptedError: Disconnected
126 - except ExecInterruptedError:
127 - await qmp.disconnect()
128 - # ConnectionResetError: [Errno 104] Connection reset by peer
129 - """
130 -
131 -
132 -class _MsgProtocolError(ProtocolError):
133 - """
134 - Abstract error class for protocol errors that have a `Message` object.
135 -
136 - This Exception class is used for protocol errors where the `Message`
137 - was mechanically understood, but was found to be inappropriate or
138 - malformed.
139 -
140 - :param error_message: Human-readable string describing the error.
141 - :param msg: The QMP `Message` that caused the error.
142 - """
143 - def __init__(self, error_message: str, msg: Message, *args: object):
144 - super().__init__(error_message, msg, *args)
145 - #: The received `Message` that caused the error.
146 - self.msg: Message = msg
147 -
148 - def __str__(self) -> str:
149 - return "\n".join([
150 - super().__str__(),
151 - f" Message was: {str(self.msg)}\n",
152 - ])
153 -
154 -
155 -class ServerParseError(_MsgProtocolError):
156 - """
157 - The Server sent a `Message` indicating parsing failure.
158 -
159 - i.e. A reply has arrived from the server, but it is missing the "ID"
160 - field, indicating a parsing error.
161 -
162 - :param error_message: Human-readable string describing the error.
163 - :param msg: The QMP `Message` that caused the error.
164 - """
165 -
166 -
167 -class BadReplyError(_MsgProtocolError):
168 - """
169 - An execution reply was successfully routed, but not understood.
170 -
171 - If a QMP message is received with an 'id' field to allow it to be
172 - routed, but is otherwise malformed, this exception will be raised.
173 -
174 - A reply message is malformed if it is missing either the 'return' or
175 - 'error' keys, or if the 'error' value has missing keys or members of
176 - the wrong type.
177 -
178 - :param error_message: Human-readable string describing the error.
179 - :param msg: The malformed reply that was received.
180 - :param sent: The message that was sent that prompted the error.
181 - """
182 - def __init__(self, error_message: str, msg: Message, sent: Message):
183 - super().__init__(error_message, msg, sent)
184 - #: The sent `Message` that caused the failure
185 - self.sent = sent
186 -
187 -
188 -class QMPClient(AsyncProtocol[Message], Events):
189 - """Implements a QMP client connection.
190 -
191 - `QMPClient` can be used to either connect or listen to a QMP server,
192 - but always acts as the QMP client.
193 -
194 - :param name:
195 - Optional nickname for the connection, used to differentiate
196 - instances when logging.
197 -
198 - :param readbuflen:
199 - The maximum buffer length for reads and writes to and from the QMP
200 - server, in bytes. Default is 10MB. If `QMPClient` is used to
201 - connect to a guest agent to transfer files via ``guest-file-read``/
202 - ``guest-file-write``, increasing this value may be required.
203 -
204 - Basic script-style usage looks like this::
205 -
206 - import asyncio
207 - from qemu.qmp import QMPClient
208 -
209 - async def main():
210 - qmp = QMPClient('my_virtual_machine_name')
211 - await qmp.connect(('127.0.0.1', 1234))
212 - ...
213 - res = await qmp.execute('query-block')
214 - ...
215 - await qmp.disconnect()
216 -
217 - asyncio.run(main())
218 -
219 - A more advanced example that starts to take advantage of asyncio
220 - might look like this::
221 -
222 - class Client:
223 - def __init__(self, name: str):
224 - self.qmp = QMPClient(name)
225 -
226 - async def watch_events(self):
227 - try:
228 - async for event in self.qmp.events:
229 - print(f"Event: {event['event']}")
230 - except asyncio.CancelledError:
231 - return
232 -
233 - async def run(self, address='/tmp/qemu.socket'):
234 - await self.qmp.connect(address)
235 - asyncio.create_task(self.watch_events())
236 - await self.qmp.runstate_changed.wait()
237 - await self.disconnect()
238 -
239 - See `qmp.events` for more detail on event handling patterns.
240 -
241 - """
242 - #: Logger object used for debugging messages.
243 - logger = logging.getLogger(__name__)
244 -
245 - # Read buffer default limit; 10MB like libvirt default
246 - _readbuflen = 10 * 1024 * 1024
247 -
248 - # Type alias for pending execute() result items
249 - _PendingT = Union[Message, ExecInterruptedError]
250 -
251 - def __init__(
252 - self,
253 - name: Optional[str] = None,
254 - readbuflen: int = _readbuflen
255 - ) -> None:
256 - super().__init__(name, readbuflen)
257 - Events.__init__(self)
258 -
259 - #: Whether or not to await a greeting after establishing a connection.
260 - #: Defaults to True; QGA servers expect this to be False.
261 - self.await_greeting: bool = True
262 -
263 - #: Whether or not to perform capabilities negotiation upon
264 - #: connection. Implies `await_greeting`. Defaults to True; QGA
265 - #: servers expect this to be False.
266 - self.negotiate: bool = True
267 -
268 - # Cached Greeting, if one was awaited.
269 - self._greeting: Optional[Greeting] = None
270 -
271 - # Command ID counter
272 - self._execute_id = 0
273 -
274 - # Incoming RPC reply messages.
275 - self._pending: Dict[
276 - Union[str, None],
277 - 'asyncio.Queue[QMPClient._PendingT]'
278 - ] = {}
279 -
280 - @property
281 - def greeting(self) -> Optional[Greeting]:
282 - """
283 - The `Greeting` from the QMP server, if any.
284 -
285 - Defaults to ``None``, and will be set after a greeting is
286 - received during the connection process. It is reset at the start
287 - of each connection attempt.
288 - """
289 - return self._greeting
290 -
291 - @upper_half
292 - async def _establish_session(self) -> None:
293 - """
294 - Initiate the QMP session.
295 -
296 - Wait for the QMP greeting and perform capabilities negotiation.
297 -
298 - :raise GreetingError: When the greeting is not understood.
299 - :raise NegotiationError: If the negotiation fails.
300 - :raise EOFError: When the server unexpectedly hangs up.
301 - :raise OSError: For underlying stream errors.
302 - """
303 - self._greeting = None
304 - self._pending = {}
305 -
306 - if self.await_greeting or self.negotiate:
307 - self._greeting = await self._get_greeting()
308 -
309 - if self.negotiate:
310 - await self._negotiate()
311 -
312 - # This will start the reader/writers:
313 - await super()._establish_session()
314 -
315 - @upper_half
316 - async def _get_greeting(self) -> Greeting:
317 - """
318 - :raise GreetingError: When the greeting is not understood.
319 - :raise EOFError: When the server unexpectedly hangs up.
320 - :raise OSError: For underlying stream errors.
321 -
322 - :return: the Greeting object given by the server.
323 - """
324 - self.logger.debug("Awaiting greeting ...")
325 -
326 - try:
327 - msg = await self._recv()
328 - return Greeting(msg)
329 - except (ProtocolError, KeyError, TypeError) as err:
330 - emsg = "Did not understand Greeting"
331 - self.logger.error("%s: %s", emsg, exception_summary(err))
332 - self.logger.debug("%s:\n%s\n", emsg, pretty_traceback())
333 - raise GreetingError(emsg, err) from err
334 - except BaseException as err:
335 - # EOFError, OSError, or something unexpected.
336 - emsg = "Failed to receive Greeting"
337 - self.logger.error("%s: %s", emsg, exception_summary(err))
338 - self.logger.debug("%s:\n%s\n", emsg, pretty_traceback())
339 - raise
340 -
341 - @upper_half
342 - async def _negotiate(self) -> None:
343 - """
344 - Perform QMP capabilities negotiation.
345 -
346 - :raise NegotiationError: When negotiation fails.
347 - :raise EOFError: When the server unexpectedly hangs up.
348 - :raise OSError: For underlying stream errors.
349 - """
350 - self.logger.debug("Negotiating capabilities ...")
351 -
352 - arguments: Dict[str, List[str]] = {}
353 - if self._greeting and 'oob' in self._greeting.QMP.capabilities:
354 - arguments.setdefault('enable', []).append('oob')
355 - msg = self.make_execute_msg('qmp_capabilities', arguments=arguments)
356 -
357 - # It's not safe to use execute() here, because the reader/writers
358 - # aren't running. AsyncProtocol *requires* that a new session
359 - # does not fail after the reader/writers are running!
360 - try:
361 - await self._send(msg)
362 - reply = await self._recv()
363 - assert 'return' in reply
364 - assert 'error' not in reply
365 - except (ProtocolError, AssertionError) as err:
366 - emsg = "Negotiation failed"
367 - self.logger.error("%s: %s", emsg, exception_summary(err))
368 - self.logger.debug("%s:\n%s\n", emsg, pretty_traceback())
369 - raise NegotiationError(emsg, err) from err
370 - except BaseException as err:
371 - # EOFError, OSError, or something unexpected.
372 - emsg = "Negotiation failed"
373 - self.logger.error("%s: %s", emsg, exception_summary(err))
374 - self.logger.debug("%s:\n%s\n", emsg, pretty_traceback())
375 - raise
376 -
377 - @bottom_half
378 - async def _bh_disconnect(self) -> None:
379 - try:
380 - await super()._bh_disconnect()
381 - finally:
382 - if self._pending:
383 - self.logger.debug("Cancelling pending executions")
384 - keys = self._pending.keys()
385 - for key in keys:
386 - self.logger.debug("Cancelling execution '%s'", key)
387 - self._pending[key].put_nowait(
388 - ExecInterruptedError("Disconnected")
389 - )
390 -
391 - self.logger.debug("QMP Disconnected.")
392 -
393 - @upper_half
394 - def _cleanup(self) -> None:
395 - super()._cleanup()
396 - assert not self._pending
397 -
398 - @bottom_half
399 - async def _on_message(self, msg: Message) -> None:
400 - """
401 - Add an incoming message to the appropriate queue/handler.
402 -
403 - :raise ServerParseError: When Message indicates server parse failure.
404 - """
405 - # Incoming messages are not fully parsed/validated here;
406 - # do only light peeking to know how to route the messages.
407 -
408 - if 'event' in msg:
409 - await self._event_dispatch(msg)
410 - return
411 -
412 - # Below, we assume everything left is an execute/exec-oob response.
413 -
414 - exec_id = cast(Optional[str], msg.get('id'))
415 -
416 - if exec_id in self._pending:
417 - await self._pending[exec_id].put(msg)
418 - return
419 -
420 - # We have a message we can't route back to a caller.
421 -
422 - is_error = 'error' in msg
423 - has_id = 'id' in msg
424 -
425 - if is_error and not has_id:
426 - # This is very likely a server parsing error.
427 - # It doesn't inherently belong to any pending execution.
428 - # Instead of performing clever recovery, just terminate.
429 - # See "NOTE" in interop/qmp-spec, "Error" section.
430 - raise ServerParseError(
431 - ("Server sent an error response without an ID, "
432 - "but there are no ID-less executions pending. "
433 - "Assuming this is a server parser failure."),
434 - msg
435 - )
436 -
437 - # qmp-spec.rst, "Commands Responses" section:
438 - # 'Clients should drop all the responses
439 - # that have an unknown "id" field.'
440 - self.logger.log(
441 - logging.ERROR if is_error else logging.WARNING,
442 - "Unknown ID '%s', message dropped.",
443 - exec_id,
444 - )
445 - self.logger.debug("Unroutable message: %s", str(msg))
446 -
447 - @upper_half
448 - @bottom_half
449 - async def _do_recv(self) -> Message:
450 - """
451 - :raise OSError: When a stream error is encountered.
452 - :raise EOFError: When the stream is at EOF.
453 - :raise ProtocolError:
454 - When the Message is not understood.
455 - See also `Message._deserialize`.
456 -
457 - :return: A single QMP `Message`.
458 - """
459 - msg_bytes = await self._readline()
460 - msg = Message(msg_bytes, eager=True)
461 - return msg
462 -
463 - @upper_half
464 - @bottom_half
465 - def _do_send(self, msg: Message) -> None:
466 - """
467 - :raise ValueError: JSON serialization failure
468 - :raise TypeError: JSON serialization failure
469 - :raise OSError: When a stream error is encountered.
470 - """
471 - assert self._writer is not None
472 - self._writer.write(bytes(msg))
473 -
474 - @upper_half
475 - def _get_exec_id(self) -> str:
476 - exec_id = f"__qmp#{self._execute_id:05d}"
477 - self._execute_id += 1
478 - return exec_id
479 -
480 - @upper_half
481 - async def _issue(self, msg: Message) -> Union[None, str]:
482 - """
483 - Issue a QMP `Message` and do not wait for a reply.
484 -
485 - :param msg: The QMP `Message` to send to the server.
486 -
487 - :return: The ID of the `Message` sent.
488 - """
489 - msg_id: Optional[str] = None
490 - if 'id' in msg:
491 - assert isinstance(msg['id'], str)
492 - msg_id = msg['id']
493 -
494 - self._pending[msg_id] = asyncio.Queue(maxsize=1)
495 - try:
496 - await self._outgoing.put(msg)
497 - except:
498 - del self._pending[msg_id]
499 - raise
500 -
501 - return msg_id
502 -
503 - @upper_half
504 - async def _reply(self, msg_id: Union[str, None]) -> Message:
505 - """
506 - Await a reply to a previously issued QMP message.
507 -
508 - :param msg_id: The ID of the previously issued message.
509 -
510 - :return: The reply from the server.
511 - :raise ExecInterruptedError:
512 - When the reply could not be retrieved because the connection
513 - was lost, or some other problem.
514 - """
515 - queue = self._pending[msg_id]
516 -
517 - try:
518 - result = await queue.get()
519 - if isinstance(result, ExecInterruptedError):
520 - raise result
521 - return result
522 - finally:
523 - del self._pending[msg_id]
524 -
525 - @upper_half
526 - async def _execute(self, msg: Message, assign_id: bool = True) -> Message:
527 - """
528 - Send a QMP `Message` to the server and await a reply.
529 -
530 - This method *assumes* you are sending some kind of an execute
531 - statement that *will* receive a reply.
532 -
533 - An execution ID will be assigned if assign_id is `True`. It can be
534 - disabled, but this requires that an ID is manually assigned
535 - instead. For manually assigned IDs, you must not use the string
536 - '__qmp#' anywhere in the ID.
537 -
538 - :param msg: The QMP `Message` to execute.
539 - :param assign_id: If True, assign a new execution ID.
540 -
541 - :return: Execution reply from the server.
542 - :raise ExecInterruptedError:
543 - When the reply could not be retrieved because the connection
544 - was lost, or some other problem.
545 - """
546 - if assign_id:
547 - msg['id'] = self._get_exec_id()
548 - elif 'id' in msg:
549 - assert isinstance(msg['id'], str)
550 - assert '__qmp#' not in msg['id']
551 -
552 - exec_id = await self._issue(msg)
553 - return await self._reply(exec_id)
554 -
555 - @upper_half
556 - @require(Runstate.RUNNING)
557 - async def _raw(
558 - self,
559 - msg: Union[Message, Mapping[str, object], bytes],
560 - assign_id: bool = True,
561 - ) -> Message:
562 - """
563 - Issue a raw `Message` to the QMP server and await a reply.
564 -
565 - :param msg:
566 - A Message to send to the server. It may be a `Message`, any
567 - Mapping (including Dict), or raw bytes.
568 - :param assign_id:
569 - Assign an arbitrary execution ID to this message. If
570 - `False`, the existing id must either be absent (and no other
571 - such pending execution may omit an ID) or a string. If it is
572 - a string, it must not start with '__qmp#' and no other such
573 - pending execution may currently be using that ID.
574 -
575 - :return: Execution reply from the server.
576 -
577 - :raise ExecInterruptedError:
578 - When the reply could not be retrieved because the connection
579 - was lost, or some other problem.
580 - :raise TypeError:
581 - When assign_id is `False`, an ID is given, and it is not a string.
582 - :raise ValueError:
583 - When assign_id is `False`, but the ID is not usable;
584 - Either because it starts with '__qmp#' or it is already in-use.
585 - """
586 - # 1. convert generic Mapping or bytes to a QMP Message
587 - # 2. copy Message objects so that we assign an ID only to the copy.
588 - msg = Message(msg)
589 -
590 - exec_id = msg.get('id')
591 - if not assign_id and 'id' in msg:
592 - if not isinstance(exec_id, str):
593 - raise TypeError(f"ID ('{exec_id}') must be a string.")
594 - if exec_id.startswith('__qmp#'):
595 - raise ValueError(
596 - f"ID ('{exec_id}') must not start with '__qmp#'."
597 - )
598 -
599 - if not assign_id and exec_id in self._pending:
600 - raise ValueError(
601 - f"ID '{exec_id}' is in-use and cannot be used."
602 - )
603 -
604 - return await self._execute(msg, assign_id=assign_id)
605 -
606 - @upper_half
607 - @require(Runstate.RUNNING)
608 - async def execute_msg(self, msg: Message) -> object:
609 - """
610 - Execute a QMP command on the server and return its value.
611 -
612 - :param msg: The QMP `Message` to execute.
613 -
614 - :return:
615 - The command execution return value from the server. The type of
616 - object returned depends on the command that was issued,
617 - though most in QEMU return a `dict`.
618 - :raise ValueError:
619 - If the QMP `Message` does not have either the 'execute' or
620 - 'exec-oob' fields set.
621 - :raise ExecuteError: When the server returns an error response.
622 - :raise ExecInterruptedError:
623 - If the connection was disrupted before
624 - receiving a reply from the server.
625 - """
626 - if not ('execute' in msg or 'exec-oob' in msg):
627 - raise ValueError("Requires 'execute' or 'exec-oob' message")
628 -
629 - # Copy the Message so that the ID assigned by _execute() is
630 - # local to this method; allowing the ID to be seen in raised
631 - # Exceptions but without modifying the caller's held copy.
632 - msg = Message(msg)
633 - reply = await self._execute(msg)
634 -
635 - if 'error' in reply:
636 - try:
637 - error_response = ErrorResponse(reply)
638 - except (KeyError, TypeError) as err:
639 - # Error response was malformed.
640 - raise BadReplyError(
641 - "QMP error reply is malformed", reply, msg,
642 - ) from err
643 -
644 - raise ExecuteError(error_response, msg, reply)
645 -
646 - if 'return' not in reply:
647 - raise BadReplyError(
648 - "QMP reply is missing a 'error' or 'return' member",
649 - reply, msg,
650 - )
651 -
652 - return reply['return']
653 -
654 - @classmethod
655 - def make_execute_msg(cls, cmd: str,
656 - arguments: Optional[Mapping[str, object]] = None,
657 - oob: bool = False) -> Message:
658 - """
659 - Create an executable message to be sent by `execute_msg` later.
660 -
661 - :param cmd: QMP command name.
662 - :param arguments: Arguments (if any). Must be JSON-serializable.
663 - :param oob:
664 - If `True`, execute "out of band". See `interop/qmp-spec`
665 - section "Out-of-band execution".
666 -
667 - :return: A QMP `Message` that can be executed with `execute_msg()`.
668 - """
669 - msg = Message({'exec-oob' if oob else 'execute': cmd})
670 - if arguments is not None:
671 - msg['arguments'] = arguments
672 - return msg
673 -
674 - @upper_half
675 - async def execute(self, cmd: str,
676 - arguments: Optional[Mapping[str, object]] = None,
677 - oob: bool = False) -> object:
678 - """
679 - Execute a QMP command on the server and return its value.
680 -
681 - :param cmd: QMP command name.
682 - :param arguments: Arguments (if any). Must be JSON-serializable.
683 - :param oob:
684 - If `True`, execute "out of band". See `interop/qmp-spec`
685 - section "Out-of-band execution".
686 -
687 - :return:
688 - The command execution return value from the server. The type of
689 - object returned depends on the command that was issued,
690 - though most in QEMU return a `dict`.
691 - :raise ExecuteError: When the server returns an error response.
692 - :raise ExecInterruptedError:
693 - If the connection was disrupted before
694 - receiving a reply from the server.
695 - """
696 - msg = self.make_execute_msg(cmd, arguments, oob=oob)
697 - return await self.execute_msg(msg)
698 -
699 - @upper_half
700 - @require(Runstate.RUNNING)
701 - def send_fd_scm(self, fd: int) -> None:
702 - """Send a file descriptor to the remote via SCM_RIGHTS.
703 -
704 - This method does not close the file descriptor.
705 -
706 - :param fd: The file descriptor to send to QEMU.
707 -
708 - This is an advanced feature of QEMU where file descriptors can
709 - be passed from client to server. This is usually used as a
710 - security measure to isolate the QEMU process from being able to
711 - open its own files. See the QMP commands ``getfd`` and
712 - ``add-fd`` for more information.
713 -
714 - See `socket.socket.sendmsg` for more information on the Python
715 - implementation for sending file descriptors over a UNIX socket.
716 - """
717 - assert self._writer is not None
718 - sock = self._writer.transport.get_extra_info('socket')
719 -
720 - if sock.family != socket.AF_UNIX:
721 - raise QMPError("Sending file descriptors requires a UNIX socket.")
722 -
723 - if not hasattr(sock, 'sendmsg'):
724 - # We need to void the warranty sticker.
725 - # Access to sendmsg is scheduled for removal in Python 3.11.
726 - # Find the real backing socket to use it anyway.
727 - sock = sock._sock # pylint: disable=protected-access
728 -
729 - sock.sendmsg(
730 - [b' '],
731 - [(socket.SOL_SOCKET, socket.SCM_RIGHTS, struct.pack('@i', fd))]
732 - )
python/qemu/qmp/qmp_shell.py deleted
-689
@@ -1,689 +0,0 @@
1 -#
2 -# Copyright (C) 2009-2022 Red Hat Inc.
3 -#
4 -# Authors:
5 -# Luiz Capitulino <lcapitulino@redhat.com>
6 -# John Snow <jsnow@redhat.com>
7 -#
8 -# This work is licensed under the terms of the GNU LGPL, version 2 or
9 -# later. See the COPYING file in the top-level directory.
10 -#
11 -
12 -"""
13 -qmp-shell - An interactive QEMU shell powered by QMP
14 -
15 -qmp-shell offers a simple shell with a convenient shorthand syntax as an
16 -alternative to typing JSON by hand. This syntax is not standardized and
17 -is not meant to be used as a scriptable interface. This shorthand *may*
18 -change incompatibly in the future, and it is strongly encouraged to use
19 -the QMP library to provide API-stable scripting when needed.
20 -
21 -usage: qmp-shell [-h] [-H] [-v] [-p] [-l LOGFILE] [-N] qmp_server
22 -
23 -positional arguments:
24 - qmp_server < UNIX socket path | TCP address:port >
25 -
26 -optional arguments:
27 - -h, --help show this help message and exit
28 - -H, --hmp Use HMP interface
29 - -v, --verbose Verbose (echo commands sent and received)
30 - -p, --pretty Pretty-print JSON
31 - -l LOGFILE, --logfile LOGFILE
32 - Save log of all QMP messages to PATH
33 - -N, --skip-negotiation
34 - Skip negotiate (for qemu-ga)
35 -
36 -Usage
37 ------
38 -
39 -First, start QEMU with::
40 -
41 - > qemu [...] -qmp unix:./qmp-sock,server=on[,wait=off]
42 -
43 -Then run the shell, passing the address of the socket::
44 -
45 - > qmp-shell ./qmp-sock
46 -
47 -Syntax
48 -------
49 -
50 -Commands have the following format::
51 -
52 - < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ]
53 -
54 -For example, to add a network device::
55 -
56 - (QEMU) device_add driver=e1000 id=net1
57 - {'return': {}}
58 - (QEMU)
59 -
60 -key=value pairs support either Python or JSON object literal notations,
61 -**without spaces**. Dictionaries/objects ``{}`` are supported, as are
62 -arrays ``[]``::
63 -
64 - example-command arg-name1={'key':'value','obj'={'prop':"value"}}
65 -
66 -Either JSON or Python formatting for compound values works, including
67 -both styles of string literal quotes (either single or double
68 -quotes). Both paradigms of literal values are accepted, including
69 -``null/true/false`` for JSON and ``None/True/False`` for Python.
70 -
71 -Transactions
72 -------------
73 -
74 -Transactions have the following multi-line format::
75 -
76 - transaction(
77 - action-name1 [ arg-name1=arg1 ] ... [arg-nameN=argN ]
78 - ...
79 - action-nameN [ arg-name1=arg1 ] ... [arg-nameN=argN ]
80 - )
81 -
82 -One line transactions are also supported::
83 -
84 - transaction( action-name1 ... )
85 -
86 -For example::
87 -
88 - (QEMU) transaction(
89 - TRANS> block-dirty-bitmap-add node=drive0 name=bitmap1
90 - TRANS> block-dirty-bitmap-clear node=drive0 name=bitmap0
91 - TRANS> )
92 - {"return": {}}
93 - (QEMU)
94 -
95 -Commands
96 ---------
97 -
98 -Autocomplete of command names using <tab> is supported. Pressing <tab>
99 -at a blank CLI prompt will show you a list of all available commands
100 -that the connected QEMU instance supports.
101 -
102 -For documentation on QMP commands and their arguments, please see
103 -`qmp ref`.
104 -
105 -Events
106 -------
107 -
108 -qmp-shell will display events received from the server, but this version
109 -does not do so asynchronously. To check for new events from the server,
110 -press <enter> on a blank line::
111 -
112 - (QEMU) ⏎
113 - {'timestamp': {'seconds': 1660071944, 'microseconds': 184667},
114 - 'event': 'STOP'}
115 -
116 -Display options
117 ----------------
118 -
119 -Use the -v and -p options to activate the verbose and pretty-print
120 -options, which will echo back the properly formatted JSON-compliant QMP
121 -that is being sent to QEMU. This is useful for debugging to see the
122 -wire-level QMP data being exchanged, and generating output for use in
123 -writing documentation for QEMU.
124 -"""
125 -
126 -import argparse
127 -import ast
128 -import json
129 -import logging
130 -import os
131 -import re
132 -import readline
133 -from subprocess import Popen
134 -import sys
135 -from typing import (
136 - IO,
137 - Dict,
138 - Iterator,
139 - List,
140 - NoReturn,
141 - Optional,
142 - Sequence,
143 - cast,
144 -)
145 -
146 -from qemu.qmp import (
147 - ConnectError,
148 - ExecuteError,
149 - QMPError,
150 - SocketAddrT,
151 -)
152 -from qemu.qmp.legacy import (
153 - QEMUMonitorProtocol,
154 - QMPBadPortError,
155 - QMPMessage,
156 - QMPObject,
157 -)
158 -
159 -
160 -LOG = logging.getLogger(__name__)
161 -
162 -
163 -class QMPCompleter:
164 - """
165 - QMPCompleter provides a readline library tab-complete behavior.
166 - """
167 - # NB: Python 3.9+ will probably allow us to subclass list[str] directly,
168 - # but pylint as of today does not know that List[str] is simply 'list'.
169 - def __init__(self) -> None:
170 - self._matches: List[str] = []
171 -
172 - def append(self, value: str) -> None:
173 - """Append a new valid completion to the list of possibilities."""
174 - return self._matches.append(value)
175 -
176 - def complete(self, text: str, state: int) -> Optional[str]:
177 - """readline.set_completer() callback implementation."""
178 - for cmd in self._matches:
179 - if cmd.startswith(text):
180 - if state == 0:
181 - return cmd
182 - state -= 1
183 - return None
184 -
185 -
186 -class QMPShellError(QMPError):
187 - """
188 - QMP Shell Base error class.
189 - """
190 -
191 -
192 -class FuzzyJSON(ast.NodeTransformer):
193 - """
194 - This extension of ast.NodeTransformer filters literal "true/false/null"
195 - values in a Python AST and replaces them by proper "True/False/None" values
196 - that Python can properly evaluate.
197 - """
198 -
199 - @classmethod
200 - def visit_Name(cls, # pylint: disable=invalid-name
201 - node: ast.Name) -> ast.AST:
202 - """
203 - Transform Name nodes with certain values into Constant (keyword) nodes.
204 - """
205 - if node.id == 'true':
206 - return ast.Constant(value=True)
207 - if node.id == 'false':
208 - return ast.Constant(value=False)
209 - if node.id == 'null':
210 - return ast.Constant(value=None)
211 - return node
212 -
213 -
214 -class QMPShell(QEMUMonitorProtocol):
215 - """
216 - QMPShell provides a basic readline-based QMP shell.
217 -
218 - :param address: Address of the QMP server.
219 - :param pretty: Pretty-print QMP messages.
220 - :param verbose: Echo outgoing QMP messages to console.
221 - """
222 - def __init__(self, address: SocketAddrT,
223 - pretty: bool = False,
224 - verbose: bool = False,
225 - server: bool = False,
226 - logfile: Optional[str] = None):
227 - super().__init__(address, server=server)
228 - self._greeting: Optional[QMPMessage] = None
229 - self._completer = QMPCompleter()
230 - self._transmode = False
231 - self._actions: List[QMPMessage] = []
232 - self._histfile = os.path.join(os.path.expanduser('~'),
233 - '.qmp-shell_history')
234 - self.pretty = pretty
235 - self.verbose = verbose
236 - self.logfile = None
237 -
238 - if logfile is not None:
239 - self.logfile = open(logfile, "w", encoding='utf-8')
240 -
241 - def close(self) -> None:
242 - # Hook into context manager of parent to save shell history.
243 - self._save_history()
244 - super().close()
245 -
246 - def _fill_completion(self) -> None:
247 - try:
248 - cmds = cast(List[Dict[str, str]], self.cmd('query-commands'))
249 - for cmd in cmds:
250 - self._completer.append(cmd['name'])
251 - except ExecuteError:
252 - pass
253 -
254 - def _completer_setup(self) -> None:
255 - self._completer = QMPCompleter()
256 - self._fill_completion()
257 - readline.set_history_length(1024)
258 - readline.set_completer(self._completer.complete)
259 - readline.parse_and_bind("tab: complete")
260 - # NB: default delimiters conflict with some command names
261 - # (eg. query-), clearing everything as it doesn't seem to matter
262 - readline.set_completer_delims('')
263 - try:
264 - readline.read_history_file(self._histfile)
265 - except FileNotFoundError:
266 - pass
267 - except IOError as err:
268 - msg = f"Failed to read history '{self._histfile}': {err!s}"
269 - LOG.warning(msg)
270 -
271 - def _save_history(self) -> None:
272 - try:
273 - readline.write_history_file(self._histfile)
274 - except IOError as err:
275 - msg = f"Failed to save history file '{self._histfile}': {err!s}"
276 - LOG.warning(msg)
277 -
278 - @classmethod
279 - def _parse_value(cls, val: str) -> object:
280 - try:
281 - return int(val)
282 - except ValueError:
283 - pass
284 -
285 - if val.lower() == 'true':
286 - return True
287 - if val.lower() == 'false':
288 - return False
289 - if val.startswith(('{', '[')):
290 - # Try first as pure JSON:
291 - try:
292 - return json.loads(val)
293 - except ValueError:
294 - pass
295 - # Try once again as FuzzyJSON:
296 - try:
297 - tree = ast.parse(val, mode='eval')
298 - transformed = FuzzyJSON().visit(tree)
299 - return ast.literal_eval(transformed)
300 - except (SyntaxError, ValueError):
301 - pass
302 - return val
303 -
304 - def _cli_expr(self,
305 - tokens: Sequence[str],
306 - parent: QMPObject) -> None:
307 - for arg in tokens:
308 - (key, sep, val) = arg.partition('=')
309 - if sep != '=':
310 - raise QMPShellError(
311 - f"Expected a key=value pair, got '{arg!s}'"
312 - )
313 -
314 - value = self._parse_value(val)
315 - optpath = key.split('.')
316 - curpath = []
317 - for path in optpath[:-1]:
318 - curpath.append(path)
319 - obj = parent.get(path, {})
320 - if not isinstance(obj, dict):
321 - msg = 'Cannot use "{:s}" as both leaf and non-leaf key'
322 - raise QMPShellError(msg.format('.'.join(curpath)))
323 - parent[path] = obj
324 - parent = obj
325 - if optpath[-1] in parent:
326 - if isinstance(parent[optpath[-1]], dict):
327 - msg = 'Cannot use "{:s}" as both leaf and non-leaf key'
328 - raise QMPShellError(msg.format('.'.join(curpath)))
329 - raise QMPShellError(f'Cannot set "{key}" multiple times')
330 - parent[optpath[-1]] = value
331 -
332 - def _build_cmd(self, cmdline: str) -> Optional[QMPMessage]:
333 - """
334 - Build a QMP input object from a user provided command-line in the
335 - following format:
336 -
337 - < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ]
338 - """
339 - argument_regex = r'''(?:[^\s"']|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+'''
340 - cmdargs = re.findall(argument_regex, cmdline)
341 - qmpcmd: QMPMessage
342 -
343 - # Transactional CLI entry:
344 - if cmdargs and cmdargs[0] == 'transaction(':
345 - self._transmode = True
346 - self._actions = []
347 - cmdargs.pop(0)
348 -
349 - # Transactional CLI exit:
350 - if cmdargs and cmdargs[0] == ')' and self._transmode:
351 - self._transmode = False
352 - if len(cmdargs) > 1:
353 - msg = 'Unexpected input after close of Transaction sub-shell'
354 - raise QMPShellError(msg)
355 - qmpcmd = {
356 - 'execute': 'transaction',
357 - 'arguments': {'actions': self._actions}
358 - }
359 - return qmpcmd
360 -
361 - # No args, or no args remaining
362 - if not cmdargs:
363 - return None
364 -
365 - if self._transmode:
366 - # Parse and cache this Transactional Action
367 - finalize = False
368 - action = {'type': cmdargs[0], 'data': {}}
369 - if cmdargs[-1] == ')':
370 - cmdargs.pop(-1)
371 - finalize = True
372 - self._cli_expr(cmdargs[1:], action['data'])
373 - self._actions.append(action)
374 - return self._build_cmd(')') if finalize else None
375 -
376 - # Standard command: parse and return it to be executed.
377 - qmpcmd = {'execute': cmdargs[0], 'arguments': {}}
378 - self._cli_expr(cmdargs[1:], qmpcmd['arguments'])
379 - return qmpcmd
380 -
381 - def _print(self, qmp_message: object, fh: IO[str] = sys.stdout) -> None:
382 - jsobj = json.dumps(qmp_message,
383 - indent=4 if self.pretty else None,
384 - sort_keys=self.pretty)
385 - print(str(jsobj), file=fh)
386 -
387 - def _execute_cmd(self, cmdline: str) -> bool:
388 - try:
389 - qmpcmd = self._build_cmd(cmdline)
390 - except QMPShellError as err:
391 - print(
392 - f"Error while parsing command line: {err!s}\n"
393 - "command format: <command-name> "
394 - "[arg-name1=arg1] ... [arg-nameN=argN",
395 - file=sys.stderr
396 - )
397 - return True
398 - # For transaction mode, we may have just cached the action:
399 - if qmpcmd is None:
400 - return True
401 - if self.verbose:
402 - self._print(qmpcmd)
403 - resp = self.cmd_obj(qmpcmd)
404 - if resp is None:
405 - print('Disconnected')
406 - return False
407 - self._print(resp)
408 - if self.logfile is not None:
409 - cmd = {**qmpcmd, **resp}
410 - self._print(cmd, fh=self.logfile)
411 - return True
412 -
413 - def connect(self, negotiate: bool = True) -> None:
414 - self._greeting = super().connect(negotiate)
415 - self._completer_setup()
416 -
417 - def show_banner(self,
418 - msg: str = 'Welcome to the QMP low-level shell!') -> None:
419 - """
420 - Print to stdio a greeting, and the QEMU version if available.
421 - """
422 - print(msg)
423 - if not self._greeting:
424 - print('Connected')
425 - return
426 - version = self._greeting['QMP']['version']['qemu']
427 - print("Connected to QEMU {major}.{minor}.{micro}\n".format(**version))
428 -
429 - @property
430 - def prompt(self) -> str:
431 - """
432 - Return the current shell prompt, including a trailing space.
433 - """
434 - if self._transmode:
435 - return 'TRANS> '
436 - return '(QEMU) '
437 -
438 - def read_exec_command(self) -> bool:
439 - """
440 - Read and execute a command.
441 -
442 - @return True if execution was ok, return False if disconnected.
443 - """
444 - try:
445 - cmdline = input(self.prompt)
446 - except EOFError:
447 - print()
448 - return False
449 -
450 - if cmdline == '':
451 - for event in self.get_events():
452 - print(event)
453 - return True
454 -
455 - return self._execute_cmd(cmdline)
456 -
457 - def repl(self) -> Iterator[None]:
458 - """
459 - Return an iterator that implements the REPL.
460 - """
461 - self.show_banner()
462 - while self.read_exec_command():
463 - yield
464 - self.close()
465 -
466 -
467 -class HMPShell(QMPShell):
468 - """
469 - HMPShell provides a basic readline-based HMP shell, tunnelled via QMP.
470 -
471 - :param address: Address of the QMP server.
472 - :param pretty: Pretty-print QMP messages.
473 - :param verbose: Echo outgoing QMP messages to console.
474 - """
475 - def __init__(self, address: SocketAddrT,
476 - pretty: bool = False,
477 - verbose: bool = False,
478 - server: bool = False,
479 - logfile: Optional[str] = None):
480 - super().__init__(address, pretty, verbose, server, logfile)
481 - self._cpu_index = 0
482 -
483 - def _cmd_completion(self) -> None:
484 - for cmd in self._cmd_passthrough('help')['return'].split('\r\n'):
485 - if cmd and cmd[0] != '[' and cmd[0] != '\t':
486 - name = cmd.split()[0] # drop help text
487 - if name == 'info':
488 - continue
489 - if name.find('|') != -1:
490 - # Command in the form 'foobar|f' or 'f|foobar', take the
491 - # full name
492 - opt = name.split('|')
493 - if len(opt[0]) == 1:
494 - name = opt[1]
495 - else:
496 - name = opt[0]
497 - self._completer.append(name)
498 - self._completer.append('help ' + name) # help completion
499 -
500 - def _info_completion(self) -> None:
501 - for cmd in self._cmd_passthrough('info')['return'].split('\r\n'):
502 - if cmd:
503 - self._completer.append('info ' + cmd.split()[1])
504 -
505 - def _other_completion(self) -> None:
506 - # special cases
507 - self._completer.append('help info')
508 -
509 - def _fill_completion(self) -> None:
510 - self._cmd_completion()
511 - self._info_completion()
512 - self._other_completion()
513 -
514 - def _cmd_passthrough(self, cmdline: str,
515 - cpu_index: int = 0) -> QMPMessage:
516 - return self.cmd_obj({
517 - 'execute': 'human-monitor-command',
518 - 'arguments': {
519 - 'command-line': cmdline,
520 - 'cpu-index': cpu_index
521 - }
522 - })
523 -
524 - def _execute_cmd(self, cmdline: str) -> bool:
525 - if cmdline.split()[0] == "cpu":
526 - # trap the cpu command, it requires special setting
527 - try:
528 - idx = int(cmdline.split()[1])
529 - if 'return' not in self._cmd_passthrough('info version', idx):
530 - print('bad CPU index')
531 - return True
532 - self._cpu_index = idx
533 - except ValueError:
534 - print('cpu command takes an integer argument')
535 - return True
536 - resp = self._cmd_passthrough(cmdline, self._cpu_index)
537 - if resp is None:
538 - print('Disconnected')
539 - return False
540 - assert 'return' in resp or 'error' in resp
541 - if 'return' in resp:
542 - # Success
543 - if len(resp['return']) > 0:
544 - print(resp['return'], end=' ')
545 - else:
546 - # Error
547 - print('%s: %s' % (resp['error']['class'], resp['error']['desc']))
548 - return True
549 -
550 - def show_banner(self, msg: str = 'Welcome to the HMP shell!') -> None:
551 - QMPShell.show_banner(self, msg)
552 -
553 -
554 -def die(msg: str) -> NoReturn:
555 - """Write an error to stderr, then exit with a return code of 1."""
556 - sys.stderr.write('ERROR: %s\n' % msg)
557 - sys.exit(1)
558 -
559 -
560 -def common_parser() -> argparse.ArgumentParser:
561 - """Build common parsing options used by qmp-shell and qmp-shell-wrap."""
562 - parser = argparse.ArgumentParser()
563 - parser.add_argument('-H', '--hmp', action='store_true',
564 - help='Use HMP interface')
565 - parser.add_argument('-v', '--verbose', action='store_true',
566 - help='Verbose (echo commands sent and received)')
567 - parser.add_argument('-p', '--pretty', action='store_true',
568 - help='Pretty-print JSON')
569 - parser.add_argument('-l', '--logfile',
570 - help='Save log of all QMP messages to PATH')
571 - # NOTE: When changing arguments, update both this module docstring
572 - # and the manpage synopsis in docs/man/qmp_shell.rst.
573 - return parser
574 -
575 -
576 -def main() -> None:
577 - """
578 - qmp-shell entry point: parse command line arguments and start the REPL.
579 - """
580 - parser = common_parser()
581 - parser.add_argument('-N', '--skip-negotiation', action='store_true',
582 - help='Skip negotiate (for qemu-ga)')
583 -
584 - default_server = os.environ.get('QMP_SOCKET')
585 - parser.add_argument('qmp_server', action='store',
586 - default=default_server,
587 - help='< UNIX socket path | TCP address:port >')
588 -
589 - args = parser.parse_args()
590 - if args.qmp_server is None:
591 - parser.error("QMP socket or TCP address must be specified")
592 -
593 - shell_class = HMPShell if args.hmp else QMPShell
594 -
595 - try:
596 - address = shell_class.parse_address(args.qmp_server)
597 - except QMPBadPortError:
598 - parser.error(f"Bad port number: {args.qmp_server}")
599 - return # pycharm doesn't know error() is noreturn
600 -
601 - with shell_class(address, args.pretty, args.verbose, args.logfile) as qemu:
602 - try:
603 - qemu.connect(negotiate=not args.skip_negotiation)
604 - except ConnectError as err:
605 - if isinstance(err.exc, OSError):
606 - die(f"Couldn't connect to {args.qmp_server}: {err!s}")
607 - die(str(err))
608 -
609 - for _ in qemu.repl():
610 - pass
611 -
612 -
613 -def main_wrap() -> None:
614 - """
615 - qmp-shell-wrap - QEMU + qmp-shell launcher utility
616 -
617 - Launch QEMU and connect to it with `qmp-shell` in a single command.
618 - CLI arguments will be forwarded to qemu, with additional arguments
619 - added to allow `qmp-shell` to then connect to the recently launched
620 - QEMU instance.
621 -
622 - usage: qmp-shell-wrap [-h] [-H] [-v] [-p] [-l LOGFILE] ...
623 -
624 - positional arguments:
625 - command QEMU command line to invoke
626 -
627 - optional arguments:
628 - -h, --help show this help message and exit
629 - -H, --hmp Use HMP interface
630 - -v, --verbose Verbose (echo commands sent and received)
631 - -p, --pretty Pretty-print JSON
632 - -l LOGFILE, --logfile LOGFILE
633 - Save log of all QMP messages to PATH
634 -
635 - Usage
636 - -----
637 -
638 - Prepend "qmp-shell-wrap" to your usual QEMU command line::
639 -
640 - > qmp-shell-wrap qemu-system-x86_64 -M q35 -m 4096 -display none
641 - Welcome to the QMP low-level shell!
642 - Connected
643 - (QEMU)
644 - """
645 - parser = common_parser()
646 - parser.add_argument('command', nargs=argparse.REMAINDER,
647 - help='QEMU command line to invoke')
648 -
649 - args = parser.parse_args()
650 -
651 - cmd = args.command
652 - if len(cmd) != 0 and cmd[0] == '--':
653 - cmd = cmd[1:]
654 - if len(cmd) == 0:
655 - cmd = ["qemu-system-x86_64"]
656 -
657 - sockpath = "qmp-shell-wrap-%d" % os.getpid()
658 - cmd += ["-qmp", "unix:%s" % sockpath]
659 -
660 - shell_class = HMPShell if args.hmp else QMPShell
661 -
662 - try:
663 - address = shell_class.parse_address(sockpath)
664 - except QMPBadPortError:
665 - parser.error(f"Bad port number: {sockpath}")
666 - return # pycharm doesn't know error() is noreturn
667 -
668 - try:
669 - with shell_class(address, args.pretty, args.verbose,
670 - True, args.logfile) as qemu:
671 - with Popen(cmd):
672 -
673 - try:
674 - qemu.accept()
675 - except ConnectError as err:
676 - if isinstance(err.exc, OSError):
677 - die(f"Couldn't connect to {args.qmp_server}: {err!s}")
678 - die(str(err))
679 -
680 - for _ in qemu.repl():
681 - pass
682 - except FileNotFoundError:
683 - sys.stderr.write(f"ERROR: QEMU executable '{cmd[0]}' not found.\n")
684 - finally:
685 - os.unlink(sockpath)
686 -
687 -
688 -if __name__ == '__main__':
689 - main()
python/qemu/qmp/qmp_tui.py deleted
-665
@@ -1,665 +0,0 @@
1 -# Copyright (c) 2021
2 -#
3 -# Authors:
4 -# Niteesh Babu G S <niteesh.gs@gmail.com>
5 -#
6 -# This work is licensed under the terms of the GNU LGPL, version 2 or
7 -# later. See the COPYING file in the top-level directory.
8 -"""
9 -QMP TUI
10 -
11 -QMP TUI is an asynchronous interface built on top the of the QMP library.
12 -It is the successor of QMP-shell and is bought-in as a replacement for it.
13 -
14 -Example Usage: qmp-tui <SOCKET | TCP IP:PORT>
15 -Full Usage: qmp-tui --help
16 -"""
17 -
18 -import argparse
19 -import asyncio
20 -import json
21 -import logging
22 -from logging import Handler, LogRecord
23 -import signal
24 -import sys
25 -from typing import (
26 - List,
27 - Optional,
28 - Tuple,
29 - Type,
30 - Union,
31 - cast,
32 -)
33 -
34 -
35 -try:
36 - from pygments import lexers
37 - from pygments import token as Token
38 - import urwid
39 - import urwid_readline
40 -except ModuleNotFoundError as exc:
41 - print(
42 - f"Module '{exc.name}' not found.",
43 - "You need the optional 'tui' group: pip install qemu.qmp[tui]",
44 - sep='\n',
45 - file=sys.stderr,
46 - )
47 - sys.exit(1)
48 -
49 -from .error import ProtocolError
50 -from .legacy import QEMUMonitorProtocol, QMPBadPortError
51 -from .message import DeserializationError, Message, UnexpectedTypeError
52 -from .protocol import ConnectError, Runstate
53 -from .qmp_client import ExecInterruptedError, QMPClient
54 -from .util import get_or_create_event_loop, pretty_traceback
55 -
56 -
57 -# The name of the signal that is used to update the history list
58 -UPDATE_MSG: str = 'UPDATE_MSG'
59 -
60 -
61 -palette = [
62 - (Token.Punctuation, '', '', '', 'h15,bold', 'g7'),
63 - (Token.Text, '', '', '', '', 'g7'),
64 - (Token.Name.Tag, '', '', '', 'bold,#f88', 'g7'),
65 - (Token.Literal.Number.Integer, '', '', '', '#fa0', 'g7'),
66 - (Token.Literal.String.Double, '', '', '', '#6f6', 'g7'),
67 - (Token.Keyword.Constant, '', '', '', '#6af', 'g7'),
68 - ('DEBUG', '', '', '', '#ddf', 'g7'),
69 - ('INFO', '', '', '', 'g100', 'g7'),
70 - ('WARNING', '', '', '', '#ff6', 'g7'),
71 - ('ERROR', '', '', '', '#a00', 'g7'),
72 - ('CRITICAL', '', '', '', '#a00', 'g7'),
73 - ('background', '', 'black', '', '', 'g7'),
74 -]
75 -
76 -
77 -def format_json(msg: str) -> str:
78 - """
79 - Formats valid/invalid multi-line JSON message into a single-line message.
80 -
81 - Formatting is first tried using the standard json module. If that fails
82 - due to an decoding error then a simple string manipulation is done to
83 - achieve a single line JSON string.
84 -
85 - Converting into single line is more aesthetically pleasing when looking
86 - along with error messages.
87 -
88 - Eg:
89 - Input:
90 - [ 1,
91 - true,
92 - 3 ]
93 - The above input is not a valid QMP message and produces the following error
94 - "QMP message is not a JSON object."
95 - When displaying this in TUI in multiline mode we get
96 -
97 - [ 1,
98 - true,
99 - 3 ]: QMP message is not a JSON object.
100 -
101 - whereas in singleline mode we get the following
102 -
103 - [1, true, 3]: QMP message is not a JSON object.
104 -
105 - The single line mode is more aesthetically pleasing.
106 -
107 - :param msg:
108 - The message to formatted into single line.
109 -
110 - :return: Formatted singleline message.
111 - """
112 - try:
113 - msg = json.loads(msg)
114 - return str(json.dumps(msg))
115 - except json.decoder.JSONDecodeError:
116 - msg = msg.replace('\n', '')
117 - words = msg.split(' ')
118 - words = list(filter(None, words))
119 - return ' '.join(words)
120 -
121 -
122 -def has_handler_type(logger: logging.Logger,
123 - handler_type: Type[Handler]) -> bool:
124 - """
125 - The Logger class has no interface to check if a certain type of handler is
126 - installed or not. So we provide an interface to do so.
127 -
128 - :param logger:
129 - Logger object
130 - :param handler_type:
131 - The type of the handler to be checked.
132 -
133 - :return: returns True if handler of type `handler_type`.
134 - """
135 - for handler in logger.handlers:
136 - if isinstance(handler, handler_type):
137 - return True
138 - return False
139 -
140 -
141 -class App(QMPClient):
142 - """
143 - Implements the QMP TUI.
144 -
145 - Initializes the widgets and starts the urwid event loop.
146 -
147 - :param address:
148 - Address of the server to connect to.
149 - :param num_retries:
150 - The number of times to retry before stopping to reconnect.
151 - :param retry_delay:
152 - The delay(sec) before each retry
153 - """
154 - def __init__(self, address: Union[str, Tuple[str, int]], num_retries: int,
155 - retry_delay: Optional[int]) -> None:
156 - urwid.register_signal(type(self), UPDATE_MSG)
157 - self.window = Window(self)
158 - self.address = address
159 - self.aloop: Optional[asyncio.AbstractEventLoop] = None
160 - self.num_retries = num_retries
161 - self.retry_delay = retry_delay if retry_delay else 2
162 - self.retry: bool = False
163 - self.exiting: bool = False
164 - super().__init__()
165 -
166 - def add_to_history(self, msg: str, level: Optional[str] = None) -> None:
167 - """
168 - Appends the msg to the history list.
169 -
170 - :param msg:
171 - The raw message to be appended in string type.
172 - """
173 - urwid.emit_signal(self, UPDATE_MSG, msg, level)
174 -
175 - def _cb_outbound(self, msg: Message) -> Message:
176 - """
177 - Callback: outbound message hook.
178 -
179 - Appends the outgoing messages to the history box.
180 -
181 - :param msg: raw outbound message.
182 - :return: final outbound message.
183 - """
184 - str_msg = str(msg)
185 -
186 - if not has_handler_type(logging.getLogger(), TUILogHandler):
187 - logging.debug('Request: %s', str_msg)
188 - self.add_to_history('<-- ' + str_msg)
189 - return msg
190 -
191 - def _cb_inbound(self, msg: Message) -> Message:
192 - """
193 - Callback: outbound message hook.
194 -
195 - Appends the incoming messages to the history box.
196 -
197 - :param msg: raw inbound message.
198 - :return: final inbound message.
199 - """
200 - str_msg = str(msg)
201 -
202 - if not has_handler_type(logging.getLogger(), TUILogHandler):
203 - logging.debug('Request: %s', str_msg)
204 - self.add_to_history('--> ' + str_msg)
205 - return msg
206 -
207 - async def _send_to_server(self, msg: Message) -> None:
208 - """
209 - This coroutine sends the message to the server.
210 - The message has to be pre-validated.
211 -
212 - :param msg:
213 - Pre-validated message to be to sent to the server.
214 -
215 - :raise Exception: When an unhandled exception is caught.
216 - """
217 - try:
218 - await self._raw(msg, assign_id='id' not in msg)
219 - except ExecInterruptedError as err:
220 - logging.info('Error server disconnected before reply %s', str(err))
221 - self.add_to_history('Server disconnected before reply', 'ERROR')
222 - except Exception as err:
223 - logging.error('Exception from _send_to_server: %s', str(err))
224 - raise err
225 -
226 - def cb_send_to_server(self, raw_msg: str) -> None:
227 - """
228 - Validates and sends the message to the server.
229 - The raw string message is first converted into a Message object
230 - and is then sent to the server.
231 -
232 - :param raw_msg:
233 - The raw string message to be sent to the server.
234 -
235 - :raise Exception: When an unhandled exception is caught.
236 - """
237 - try:
238 - msg = Message(bytes(raw_msg, encoding='utf-8'))
239 - asyncio.create_task(self._send_to_server(msg))
240 - except (DeserializationError, UnexpectedTypeError) as err:
241 - raw_msg = format_json(raw_msg)
242 - logging.info('Invalid message: %s', err.error_message)
243 - self.add_to_history(f'{raw_msg}: {err.error_message}', 'ERROR')
244 -
245 - def unhandled_input(self, key: str) -> None:
246 - """
247 - Handle's keys which haven't been handled by the child widgets.
248 -
249 - :param key:
250 - Unhandled key
251 - """
252 - if key == 'esc':
253 - self.kill_app()
254 -
255 - def kill_app(self) -> None:
256 - """
257 - Initiates killing of app. A bridge between asynchronous and synchronous
258 - code.
259 - """
260 - asyncio.create_task(self._kill_app())
261 -
262 - async def _kill_app(self) -> None:
263 - """
264 - This coroutine initiates the actual disconnect process and calls
265 - urwid.ExitMainLoop() to kill the TUI.
266 -
267 - :raise Exception: When an unhandled exception is caught.
268 - """
269 - self.exiting = True
270 - await self.disconnect()
271 - logging.debug('Disconnect finished. Exiting app')
272 - raise urwid.ExitMainLoop()
273 -
274 - async def disconnect(self) -> None:
275 - """
276 - Overrides the disconnect method to handle the errors locally.
277 - """
278 - try:
279 - await super().disconnect()
280 - except (OSError, EOFError) as err:
281 - logging.info('disconnect: %s', str(err))
282 - self.retry = True
283 - except ProtocolError as err:
284 - logging.info('disconnect: %s', str(err))
285 - except Exception as err:
286 - logging.error('disconnect: Unhandled exception %s', str(err))
287 - raise err
288 -
289 - def _set_status(self, msg: str) -> None:
290 - """
291 - Sets the message as the status.
292 -
293 - :param msg:
294 - The message to be displayed in the status bar.
295 - """
296 - self.window.footer.set_text(msg)
297 -
298 - def _get_formatted_address(self) -> str:
299 - """
300 - Returns a formatted version of the server's address.
301 -
302 - :return: formatted address
303 - """
304 - if isinstance(self.address, tuple):
305 - host, port = self.address
306 - addr = f'{host}:{port}'
307 - else:
308 - addr = f'{self.address}'
309 - return addr
310 -
311 - async def _initiate_connection(self) -> Optional[ConnectError]:
312 - """
313 - Tries connecting to a server a number of times with a delay between
314 - each try. If all retries failed then return the error faced during
315 - the last retry.
316 -
317 - :return: Error faced during last retry.
318 - """
319 - current_retries = 0
320 - err = None
321 -
322 - # initial try
323 - await self.connect_server()
324 - while self.retry and current_retries < self.num_retries:
325 - logging.info('Connection Failed, retrying in %d', self.retry_delay)
326 - status = f'[Retry #{current_retries} ({self.retry_delay}s)]'
327 - self._set_status(status)
328 -
329 - await asyncio.sleep(self.retry_delay)
330 -
331 - err = await self.connect_server()
332 - current_retries += 1
333 - # If all retries failed report the last error
334 - if err:
335 - logging.info('All retries failed: %s', err)
336 - return err
337 - return None
338 -
339 - async def manage_connection(self) -> None:
340 - """
341 - Manage the connection based on the current run state.
342 -
343 - A reconnect is issued when the current state is IDLE and the number
344 - of retries is not exhausted.
345 - A disconnect is issued when the current state is DISCONNECTING.
346 - """
347 - while not self.exiting:
348 - if self.runstate == Runstate.IDLE:
349 - err = await self._initiate_connection()
350 - # If retry is still true then, we have exhausted all our tries.
351 - if err:
352 - self._set_status(f'[Error: {err.error_message}]')
353 - else:
354 - addr = self._get_formatted_address()
355 - self._set_status(f'[Connected {addr}]')
356 - elif self.runstate == Runstate.DISCONNECTING:
357 - self._set_status('[Disconnected]')
358 - await self.disconnect()
359 - # check if a retry is needed
360 - # mypy 1.4.0 doesn't believe runstate can change after
361 - # disconnect(), hence the cast.
362 - state = cast(Runstate, self.runstate)
363 - if state == Runstate.IDLE:
364 - continue
365 - await self.runstate_changed()
366 -
367 - async def connect_server(self) -> Optional[ConnectError]:
368 - """
369 - Initiates a connection to the server at address `self.address`
370 - and in case of a failure, sets the status to the respective error.
371 - """
372 - try:
373 - await self.connect(self.address)
374 - self.retry = False
375 - except ConnectError as err:
376 - logging.info('connect_server: ConnectError %s', str(err))
377 - self.retry = True
378 - return err
379 - return None
380 -
381 - def run(self, debug: bool = False) -> None:
382 - """
383 - Starts the long running co-routines and the urwid event loop.
384 -
385 - :param debug:
386 - Enables/Disables asyncio event loop debugging
387 - """
388 - screen = urwid.raw_display.Screen()
389 - screen.set_terminal_properties(256)
390 - self.aloop = get_or_create_event_loop()
391 - self.aloop.set_debug(debug)
392 -
393 - # Gracefully handle SIGTERM and SIGINT signals
394 - cancel_signals = [signal.SIGTERM, signal.SIGINT]
395 - for sig in cancel_signals:
396 - self.aloop.add_signal_handler(sig, self.kill_app)
397 -
398 - event_loop = urwid.AsyncioEventLoop(loop=self.aloop)
399 - main_loop = urwid.MainLoop(urwid.AttrMap(self.window, 'background'),
400 - unhandled_input=self.unhandled_input,
401 - screen=screen,
402 - palette=palette,
403 - handle_mouse=True,
404 - event_loop=event_loop)
405 -
406 - self.aloop.create_task(self.manage_connection())
407 - try:
408 - main_loop.run()
409 - except Exception as err:
410 - logging.error('%s\n%s\n', str(err), pretty_traceback())
411 - raise err
412 -
413 -
414 -class StatusBar(urwid.Text):
415 - """
416 - A simple statusbar modelled using the Text widget. The status can be
417 - set using the set_text function. All text set is aligned to right.
418 -
419 - :param text: Initial text to be displayed. Default is empty str.
420 - """
421 - def __init__(self, text: str = ''):
422 - super().__init__(text, align='right')
423 -
424 -
425 -class Editor(urwid_readline.ReadlineEdit):
426 - """
427 - A simple editor modelled using the urwid_readline.ReadlineEdit widget.
428 - Mimcs GNU readline shortcuts and provides history support.
429 -
430 - The readline shortcuts can be found below:
431 - https://github.com/rr-/urwid_readline#features
432 -
433 - Along with the readline features, this editor also has support for
434 - history. Pressing the 'up'/'down' switches between the prev/next messages
435 - available in the history.
436 -
437 - Currently there is no support to save the history to a file. The history of
438 - previous commands is lost on exit.
439 -
440 - :param parent: Reference to the TUI object.
441 - """
442 - def __init__(self, parent: App) -> None:
443 - super().__init__(caption='> ', multiline=True)
444 - self.parent = parent
445 - self.history: List[str] = []
446 - self.last_index: int = -1
447 - self.show_history: bool = False
448 -
449 - def keypress(self, size: Tuple[int, int], key: str) -> Optional[str]:
450 - """
451 - Handles the keypress on this widget.
452 -
453 - :param size:
454 - The current size of the widget.
455 - :param key:
456 - The key to be handled.
457 -
458 - :return: Unhandled key if any.
459 - """
460 - msg = self.get_edit_text()
461 - if key == 'up' and not msg:
462 - # Show the history when 'up arrow' is pressed with no input text.
463 - # NOTE: The show_history logic is necessary because in 'multiline'
464 - # mode (which we use) 'up arrow' is used to move between lines.
465 - if not self.history:
466 - return None
467 - self.show_history = True
468 - last_msg = self.history[self.last_index]
469 - self.set_edit_text(last_msg)
470 - self.edit_pos = len(last_msg)
471 - elif key == 'up' and self.show_history:
472 - self.last_index = max(self.last_index - 1, -len(self.history))
473 - self.set_edit_text(self.history[self.last_index])
474 - self.edit_pos = len(self.history[self.last_index])
475 - elif key == 'down' and self.show_history:
476 - if self.last_index == -1:
477 - self.set_edit_text('')
478 - self.show_history = False
479 - else:
480 - self.last_index += 1
481 - self.set_edit_text(self.history[self.last_index])
482 - self.edit_pos = len(self.history[self.last_index])
483 - elif key == 'meta enter':
484 - # When using multiline, enter inserts a new line into the editor
485 - # send the input to the server on alt + enter
486 - self.parent.cb_send_to_server(msg)
487 - self.history.append(msg)
488 - self.set_edit_text('')
489 - self.last_index = -1
490 - self.show_history = False
491 - else:
492 - self.show_history = False
493 - self.last_index = -1
494 - return cast(Optional[str], super().keypress(size, key))
495 - return None
496 -
497 -
498 -class EditorWidget(urwid.Filler):
499 - """
500 - Wrapper around the editor widget.
501 -
502 - The Editor is a flow widget and has to wrapped inside a box widget.
503 - This class wraps the Editor inside filler widget.
504 -
505 - :param parent: Reference to the TUI object.
506 - """
507 - def __init__(self, parent: App) -> None:
508 - super().__init__(Editor(parent), valign='top')
509 -
510 -
511 -class HistoryBox(urwid.ListBox):
512 - """
513 - This widget is modelled using the ListBox widget, contains the list of
514 - all messages both QMP messages and log messages to be shown in the TUI.
515 -
516 - The messages are urwid.Text widgets. On every append of a message, the
517 - focus is shifted to the last appended message.
518 -
519 - :param parent: Reference to the TUI object.
520 - """
521 - def __init__(self, parent: App) -> None:
522 - self.parent = parent
523 - self.history = urwid.SimpleFocusListWalker([])
524 - super().__init__(self.history)
525 -
526 - def add_to_history(self,
527 - history: Union[str, List[Tuple[str, str]]]) -> None:
528 - """
529 - Appends a message to the list and set the focus to the last appended
530 - message.
531 -
532 - :param history:
533 - The history item(message/event) to be appended to the list.
534 - """
535 - self.history.append(urwid.Text(history))
536 - self.history.set_focus(len(self.history) - 1)
537 -
538 - def mouse_event(self, size: Tuple[int, int], _event: str, button: float,
539 - _x: int, _y: int, focus: bool) -> None:
540 - # Unfortunately there are no urwid constants that represent the mouse
541 - # events.
542 - if button == 4: # Scroll up event
543 - super().keypress(size, 'up')
544 - elif button == 5: # Scroll down event
545 - super().keypress(size, 'down')
546 -
547 -
548 -class HistoryWindow(urwid.Frame):
549 - """
550 - This window composes the HistoryBox and EditorWidget in a horizontal split.
551 - By default the first focus is given to the history box.
552 -
553 - :param parent: Reference to the TUI object.
554 - """
555 - def __init__(self, parent: App) -> None:
556 - self.parent = parent
557 - self.editor_widget = EditorWidget(parent)
558 - self.editor = urwid.LineBox(self.editor_widget)
559 - self.history = HistoryBox(parent)
560 - self.body = urwid.Pile([('weight', 80, self.history),
561 - ('weight', 20, self.editor)])
562 - super().__init__(self.body)
563 - urwid.connect_signal(self.parent, UPDATE_MSG, self.cb_add_to_history)
564 -
565 - def cb_add_to_history(self, msg: str, level: Optional[str] = None) -> None:
566 - """
567 - Appends a message to the history box
568 -
569 - :param msg:
570 - The message to be appended to the history box.
571 - :param level:
572 - The log level of the message, if it is a log message.
573 - """
574 - formatted = []
575 - if level:
576 - msg = f'[{level}]: {msg}'
577 - formatted.append((level, msg))
578 - else:
579 - lexer = lexers.JsonLexer() # pylint: disable=no-member
580 - for token in lexer.get_tokens(msg):
581 - formatted.append(token)
582 - self.history.add_to_history(formatted)
583 -
584 -
585 -class Window(urwid.Frame):
586 - """
587 - This window is the top most widget of the TUI and will contain other
588 - windows. Each child of this widget is responsible for displaying a specific
589 - functionality.
590 -
591 - :param parent: Reference to the TUI object.
592 - """
593 - def __init__(self, parent: App) -> None:
594 - self.parent = parent
595 - footer = StatusBar()
596 - body = HistoryWindow(parent)
597 - super().__init__(body, footer=footer)
598 -
599 -
600 -class TUILogHandler(Handler):
601 - """
602 - This handler routes all the log messages to the TUI screen.
603 - It is installed to the root logger to so that the log message from all
604 - libraries begin used is routed to the screen.
605 -
606 - :param tui: Reference to the TUI object.
607 - """
608 - def __init__(self, tui: App) -> None:
609 - super().__init__()
610 - self.tui = tui
611 -
612 - def emit(self, record: LogRecord) -> None:
613 - """
614 - Emits a record to the TUI screen.
615 -
616 - Appends the log message to the TUI screen
617 - """
618 - level = record.levelname
619 - msg = record.getMessage()
620 - self.tui.add_to_history(msg, level)
621 -
622 -
623 -def main() -> None:
624 - """
625 - Driver of the whole script, parses arguments, initialize the TUI and
626 - the logger.
627 - """
628 - parser = argparse.ArgumentParser(description='QMP TUI')
629 - parser.add_argument('qmp_server', help='Address of the QMP server. '
630 - 'Format <UNIX socket path | TCP addr:port>')
631 - parser.add_argument('--num-retries', type=int, default=10,
632 - help='Number of times to reconnect before giving up.')
633 - parser.add_argument('--retry-delay', type=int,
634 - help='Time(s) to wait before next retry. '
635 - 'Default action is to wait 2s between each retry.')
636 - parser.add_argument('--log-file', help='The Log file name')
637 - parser.add_argument('--log-level', default='WARNING',
638 - help='Log level <CRITICAL|ERROR|WARNING|INFO|DEBUG|>')
639 - parser.add_argument('--asyncio-debug', action='store_true',
640 - help='Enable debug mode for asyncio loop. '
641 - 'Generates lot of output, makes TUI unusable when '
642 - 'logs are logged in the TUI. '
643 - 'Use only when logging to a file.')
644 - args = parser.parse_args()
645 -
646 - try:
647 - address = QEMUMonitorProtocol.parse_address(args.qmp_server)
648 - except QMPBadPortError as err:
649 - parser.error(str(err))
650 -
651 - app = App(address, args.num_retries, args.retry_delay)
652 -
653 - root_logger = logging.getLogger()
654 - root_logger.setLevel(logging.getLevelName(args.log_level))
655 -
656 - if args.log_file:
657 - root_logger.addHandler(logging.FileHandler(args.log_file))
658 - else:
659 - root_logger.addHandler(TUILogHandler(app))
660 -
661 - app.run(args.asyncio_debug)
662 -
663 -
664 -if __name__ == '__main__':
665 - main()
python/qemu/qmp/util.py deleted
-150
@@ -1,150 +0,0 @@
1 -"""
2 -Miscellaneous Utilities
3 -
4 -This module provides asyncio and various logging and debugging
5 -utilities, such as `exception_summary()` and `pretty_traceback()`, used
6 -primarily for adding information into the logging stream.
7 -"""
8 -
9 -import asyncio
10 -import sys
11 -import traceback
12 -from typing import TypeVar, cast
13 -import warnings
14 -
15 -
16 -T = TypeVar('T')
17 -
18 -
19 -# --------------------------
20 -# Section: Utility Functions
21 -# --------------------------
22 -
23 -
24 -def get_or_create_event_loop() -> asyncio.AbstractEventLoop:
25 - """
26 - Return this thread's current event loop, or create a new one.
27 -
28 - This function behaves similarly to asyncio.get_event_loop() in
29 - Python<=3.13, where if there is no event loop currently associated
30 - with the current context, it will create and register one. It should
31 - generally not be used in any asyncio-native applications.
32 - """
33 - try:
34 - with warnings.catch_warnings():
35 - # Python <= 3.13 will trigger deprecation warnings if no
36 - # event loop is set, but will create and set a new loop.
37 - warnings.simplefilter("ignore")
38 - loop = asyncio.get_event_loop()
39 - except RuntimeError:
40 - # Python 3.14+: No event loop set for this thread,
41 - # create and set one.
42 - loop = asyncio.new_event_loop()
43 - # Set this loop as the current thread's loop, to be returned
44 - # by calls to get_event_loop() in the future.
45 - asyncio.set_event_loop(loop)
46 -
47 - return loop
48 -
49 -
50 -async def flush(writer: asyncio.StreamWriter) -> None:
51 - """
52 - Utility function to ensure an `asyncio.StreamWriter` is *fully* drained.
53 -
54 - `asyncio.StreamWriter.drain` only promises we will return to below
55 - the "high-water mark". This function ensures we flush the entire
56 - buffer -- by setting the high water mark to 0 and then calling
57 - drain. The flow control limits are restored after the call is
58 - completed.
59 - """
60 - transport = cast( # type: ignore[redundant-cast]
61 - asyncio.WriteTransport, writer.transport
62 - )
63 -
64 - # https://github.com/python/typeshed/issues/5779
65 - low, high = transport.get_write_buffer_limits() # type: ignore
66 - transport.set_write_buffer_limits(0, 0)
67 - try:
68 - await writer.drain()
69 - finally:
70 - transport.set_write_buffer_limits(high, low)
71 -
72 -
73 -def upper_half(func: T) -> T:
74 - """
75 - Do-nothing decorator that annotates a method as an "upper-half" method.
76 -
77 - These methods must not call bottom-half functions directly, but can
78 - schedule them to run.
79 - """
80 - return func
81 -
82 -
83 -def bottom_half(func: T) -> T:
84 - """
85 - Do-nothing decorator that annotates a method as a "bottom-half" method.
86 -
87 - These methods must take great care to handle their own exceptions whenever
88 - possible. If they go unhandled, they will cause termination of the loop.
89 -
90 - These methods do not, in general, have the ability to directly
91 - report information to a caller’s context and will usually be
92 - collected as an `asyncio.Task` result instead.
93 -
94 - They must not call upper-half functions directly.
95 - """
96 - return func
97 -
98 -
99 -# ----------------------------
100 -# Section: Logging & Debugging
101 -# ----------------------------
102 -
103 -
104 -def exception_summary(exc: BaseException) -> str:
105 - """
106 - Return a summary string of an arbitrary exception.
107 -
108 - It will be of the form "ExceptionType: Error Message" if the error
109 - string is non-empty, and just "ExceptionType" otherwise.
110 -
111 - This code is based on CPython's implementation of
112 - `traceback.TracebackException.format_exception_only`.
113 - """
114 - name = type(exc).__qualname__
115 - smod = type(exc).__module__
116 - if smod not in ("__main__", "builtins"):
117 - name = smod + '.' + name
118 -
119 - error = str(exc)
120 - if error:
121 - return f"{name}: {error}"
122 - return name
123 -
124 -
125 -def pretty_traceback(prefix: str = " | ") -> str:
126 - """
127 - Formats the current traceback, indented to provide visual distinction.
128 -
129 - This is useful for printing a traceback within a traceback for
130 - debugging purposes when encapsulating errors to deliver them up the
131 - stack; when those errors are printed, this helps provide a nice
132 - visual grouping to quickly identify the parts of the error that
133 - belong to the inner exception.
134 -
135 - :param prefix: The prefix to append to each line of the traceback.
136 - :return: A string, formatted something like the following::
137 -
138 - | Traceback (most recent call last):
139 - | File "foobar.py", line 42, in arbitrary_example
140 - | foo.baz()
141 - | ArbitraryError: [Errno 42] Something bad happened!
142 - """
143 - output = "".join(traceback.format_exception(*sys.exc_info()))
144 -
145 - exc_lines = []
146 - for line in output.split('\n'):
147 - exc_lines.append(prefix + line)
148 -
149 - # The last line is always empty, omit it
150 - return "\n".join(exc_lines[:-1])
python/qemu/utils/qom_fuse.py
-1
@@ -47,7 +47,6 @@ from typing import (
47
48 import fuse
49 from fuse import FUSE, FuseOSError, Operations
50 -
50 from qemu.qmp import ExecuteError
51
52 from .qom_common import QOMCommand
python/setup.cfg
+3 -28
@@ -24,9 +24,10 @@ classifiers =
24 [options]
25 python_requires = >= 3.9
26 packages =
27 - qemu.qmp
27 qemu.machine
28 qemu.utils
29 +install_requires =
30 + qemu.qmp
31
32 [options.package_data]
33 * = py.typed
@@ -38,26 +39,17 @@ devel =
39 distlib >= 0.3.6
40 flake8 >= 5.0.4
41 fusepy >= 2.0.4
41 - isort >= 5.1.2
42 + isort >= 5.6.0
43 mypy >= 1.4.0
44 pylint >= 2.17.3
45 pylint != 3.2.4; python_version<"3.9"
46 tox >= 3.18.0
46 - urwid >= 2.1.2
47 - urwid-readline >= 0.13
48 - Pygments >= 2.9.0
47 sphinx >= 3.4.3
48
49 # Provides qom-fuse functionality
50 fuse =
51 fusepy >= 2.0.4
52
55 -# QMP TUI dependencies
56 -tui =
57 - urwid >= 2.1.2
58 - urwid-readline >= 0.13
59 - Pygments >= 2.9.0
60 -
53 [options.entry_points]
54 console_scripts =
55 qom = qemu.utils.qom:main
@@ -67,9 +59,6 @@ console_scripts =
59 qom-tree = qemu.utils.qom:QOMTree.entry_point
60 qom-fuse = qemu.utils.qom_fuse:QOMFuse.entry_point [fuse]
61 qemu-ga-client = qemu.utils.qemu_ga_client:main
70 - qmp-shell = qemu.qmp.qmp_shell:main
71 - qmp-shell-wrap = qemu.qmp.qmp_shell:main_wrap
72 - qmp-tui = qemu.qmp.qmp_tui:main [tui]
62
63 [flake8]
64 # Prefer pylint's bare-except checks to flake8's
@@ -86,10 +75,6 @@ warn_unused_ignores = False
75 # fusepy has no type stubs:
76 allow_subclassing_any = True
77
89 -[mypy-qemu.qmp.qmp_tui]
90 -# urwid and urwid_readline have no type stubs:
91 -allow_subclassing_any = True
92 -
78 # The following missing import directives are because these libraries do not
79 # provide type stubs. Allow them on an as-needed basis for mypy.
80 [mypy-fuse]
@@ -101,15 +86,6 @@ ignore_missing_imports = True
86 [mypy-tomllib]
87 ignore_missing_imports = True
88
104 -[mypy-urwid]
105 -ignore_missing_imports = True
106 -
107 -[mypy-urwid_readline]
108 -ignore_missing_imports = True
109 -
110 -[mypy-pygments]
111 -ignore_missing_imports = True
112 -
89 [mypy-distlib]
90 ignore_missing_imports = True
91
@@ -194,7 +170,6 @@ allowlist_externals = make
170 deps =
171 .[devel]
172 .[fuse] # Workaround to trigger tox venv rebuild
197 - .[tui] # Workaround to trigger tox venv rebuild
173 commands =
174 make check
175
python/tests/minreqs.txt
+3 -5
@@ -20,10 +20,8 @@ setuptools<=70
20 # Dependencies for qapidoc/qapi_domain et al
21 sphinx==3.4.3
22
23 -# Dependencies for the TUI addon (Required for successful linting)
24 -urwid==2.1.2
25 -urwid-readline==0.13
26 -Pygments==2.9.0
23 +# Dependencies for qemu.machine
24 +qemu.qmp==0.0.5
25
26 # Dependencies for mkvenv
27 distlib==0.3.6
@@ -36,7 +34,7 @@ avocado-framework==90.0
34
35 # Linters
36 flake8==5.0.4
39 -isort==5.1.2
37 +isort==5.6.0
38 mypy==1.4.0
39 pylint==2.17.3
40
python/tests/protocol.py deleted
-596
@@ -1,596 +0,0 @@
1 -import asyncio
2 -from contextlib import contextmanager
3 -import os
4 -import socket
5 -from tempfile import TemporaryDirectory
6 -
7 -import avocado
8 -
9 -from qemu.qmp import ConnectError, Runstate
10 -from qemu.qmp.protocol import AsyncProtocol, StateError
11 -
12 -
13 -class NullProtocol(AsyncProtocol[None]):
14 - """
15 - NullProtocol is a test mockup of an AsyncProtocol implementation.
16 -
17 - It adds a fake_session instance variable that enables a code path
18 - that bypasses the actual connection logic, but still allows the
19 - reader/writers to start.
20 -
21 - Because the message type is defined as None, an asyncio.Event named
22 - 'trigger_input' is created that prohibits the reader from
23 - incessantly being able to yield None; this event can be poked to
24 - simulate an incoming message.
25 -
26 - For testing symmetry with do_recv, an interface is added to "send" a
27 - Null message.
28 -
29 - For testing purposes, a "simulate_disconnection" method is also
30 - added which allows us to trigger a bottom half disconnect without
31 - injecting any real errors into the reader/writer loops; in essence
32 - it performs exactly half of what disconnect() normally does.
33 - """
34 - def __init__(self, name=None):
35 - self.fake_session = False
36 - self.trigger_input: asyncio.Event
37 - super().__init__(name)
38 -
39 - async def _establish_session(self):
40 - self.trigger_input = asyncio.Event()
41 - await super()._establish_session()
42 -
43 - async def _do_start_server(self, address, ssl=None):
44 - if self.fake_session:
45 - self._accepted = asyncio.Event()
46 - self._set_state(Runstate.CONNECTING)
47 - await asyncio.sleep(0)
48 - else:
49 - await super()._do_start_server(address, ssl)
50 -
51 - async def _do_accept(self):
52 - if self.fake_session:
53 - self._accepted = None
54 - else:
55 - await super()._do_accept()
56 -
57 - async def _do_connect(self, address, ssl=None):
58 - if self.fake_session:
59 - self._set_state(Runstate.CONNECTING)
60 - await asyncio.sleep(0)
61 - else:
62 - await super()._do_connect(address, ssl)
63 -
64 - async def _do_recv(self) -> None:
65 - await self.trigger_input.wait()
66 - self.trigger_input.clear()
67 -
68 - def _do_send(self, msg: None) -> None:
69 - pass
70 -
71 - async def send_msg(self) -> None:
72 - await self._outgoing.put(None)
73 -
74 - async def simulate_disconnect(self) -> None:
75 - """
76 - Simulates a bottom-half disconnect.
77 -
78 - This method schedules a disconnection but does not wait for it
79 - to complete. This is used to put the loop into the DISCONNECTING
80 - state without fully quiescing it back to IDLE. This is normally
81 - something you cannot coax AsyncProtocol to do on purpose, but it
82 - will be similar to what happens with an unhandled Exception in
83 - the reader/writer.
84 -
85 - Under normal circumstances, the library design requires you to
86 - await on disconnect(), which awaits the disconnect task and
87 - returns bottom half errors as a pre-condition to allowing the
88 - loop to return back to IDLE.
89 - """
90 - self._schedule_disconnect()
91 -
92 -
93 -class LineProtocol(AsyncProtocol[str]):
94 - def __init__(self, name=None):
95 - super().__init__(name)
96 - self.rx_history = []
97 -
98 - async def _do_recv(self) -> str:
99 - raw = await self._readline()
100 - msg = raw.decode()
101 - self.rx_history.append(msg)
102 - return msg
103 -
104 - def _do_send(self, msg: str) -> None:
105 - assert self._writer is not None
106 - self._writer.write(msg.encode() + b'\n')
107 -
108 - async def send_msg(self, msg: str) -> None:
109 - await self._outgoing.put(msg)
110 -
111 -
112 -def run_as_task(coro, allow_cancellation=False):
113 - """
114 - Run a given coroutine as a task.
115 -
116 - Optionally, wrap it in a try..except block that allows this
117 - coroutine to be canceled gracefully.
118 - """
119 - async def _runner():
120 - try:
121 - await coro
122 - except asyncio.CancelledError:
123 - if allow_cancellation:
124 - return
125 - raise
126 - return asyncio.create_task(_runner())
127 -
128 -
129 -@contextmanager
130 -def jammed_socket():
131 - """
132 - Opens up a random unused TCP port on localhost, then jams it.
133 - """
134 - socks = []
135 -
136 - try:
137 - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
138 - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
139 - sock.bind(('127.0.0.1', 0))
140 - sock.listen(1)
141 - address = sock.getsockname()
142 -
143 - socks.append(sock)
144 -
145 - # I don't *fully* understand why, but it takes *two* un-accepted
146 - # connections to start jamming the socket.
147 - for _ in range(2):
148 - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
149 - sock.connect(address)
150 - socks.append(sock)
151 -
152 - yield address
153 -
154 - finally:
155 - for sock in socks:
156 - sock.close()
157 -
158 -
159 -class Smoke(avocado.Test):
160 -
161 - def setUp(self):
162 - self.proto = NullProtocol()
163 -
164 - def test__repr__(self):
165 - self.assertEqual(
166 - repr(self.proto),
167 - "<NullProtocol runstate=IDLE>"
168 - )
169 -
170 - def testRunstate(self):
171 - self.assertEqual(
172 - self.proto.runstate,
173 - Runstate.IDLE
174 - )
175 -
176 - def testDefaultName(self):
177 - self.assertEqual(
178 - self.proto.name,
179 - None
180 - )
181 -
182 - def testLogger(self):
183 - self.assertEqual(
184 - self.proto.logger.name,
185 - 'qemu.qmp.protocol'
186 - )
187 -
188 - def testName(self):
189 - self.proto = NullProtocol('Steve')
190 -
191 - self.assertEqual(
192 - self.proto.name,
193 - 'Steve'
194 - )
195 -
196 - self.assertEqual(
197 - self.proto.logger.name,
198 - 'qemu.qmp.protocol.Steve'
199 - )
200 -
201 - self.assertEqual(
202 - repr(self.proto),
203 - "<NullProtocol name='Steve' runstate=IDLE>"
204 - )
205 -
206 -
207 -class TestBase(avocado.Test):
208 -
209 - def setUp(self):
210 - self.proto = NullProtocol(type(self).__name__)
211 - self.assertEqual(self.proto.runstate, Runstate.IDLE)
212 - self.runstate_watcher = None
213 -
214 - def tearDown(self):
215 - self.assertEqual(self.proto.runstate, Runstate.IDLE)
216 -
217 - async def _asyncSetUp(self):
218 - pass
219 -
220 - async def _asyncTearDown(self):
221 - if self.runstate_watcher:
222 - await self.runstate_watcher
223 -
224 - @staticmethod
225 - def async_test(async_test_method):
226 - """
227 - Decorator; adds SetUp and TearDown to async tests.
228 - """
229 - async def _wrapper(self, *args, **kwargs):
230 - loop = asyncio.get_running_loop()
231 - loop.set_debug(True)
232 -
233 - await self._asyncSetUp()
234 - await async_test_method(self, *args, **kwargs)
235 - await self._asyncTearDown()
236 -
237 - return _wrapper
238 -
239 - # Definitions
240 -
241 - # The states we expect a "bad" connect/accept attempt to transition through
242 - BAD_CONNECTION_STATES = (
243 - Runstate.CONNECTING,
244 - Runstate.DISCONNECTING,
245 - Runstate.IDLE,
246 - )
247 -
248 - # The states we expect a "good" session to transition through
249 - GOOD_CONNECTION_STATES = (
250 - Runstate.CONNECTING,
251 - Runstate.RUNNING,
252 - Runstate.DISCONNECTING,
253 - Runstate.IDLE,
254 - )
255 -
256 - # Helpers
257 -
258 - async def _watch_runstates(self, *states):
259 - """
260 - This launches a task alongside (most) tests below to confirm that
261 - the sequence of runstate changes that occur is exactly as
262 - anticipated.
263 - """
264 - async def _watcher():
265 - for state in states:
266 - new_state = await self.proto.runstate_changed()
267 - self.assertEqual(
268 - new_state,
269 - state,
270 - msg=f"Expected state '{state.name}'",
271 - )
272 -
273 - self.runstate_watcher = asyncio.create_task(_watcher())
274 - # Kick the loop and force the task to block on the event.
275 - await asyncio.sleep(0)
276 -
277 -
278 -class State(TestBase):
279 -
280 - @TestBase.async_test
281 - async def testSuperfluousDisconnect(self):
282 - """
283 - Test calling disconnect() while already disconnected.
284 - """
285 - await self._watch_runstates(
286 - Runstate.DISCONNECTING,
287 - Runstate.IDLE,
288 - )
289 - await self.proto.disconnect()
290 -
291 -
292 -class Connect(TestBase):
293 - """
294 - Tests primarily related to calling Connect().
295 - """
296 - async def _bad_connection(self, family: str):
297 - assert family in ('INET', 'UNIX')
298 -
299 - if family == 'INET':
300 - await self.proto.connect(('127.0.0.1', 0))
301 - elif family == 'UNIX':
302 - await self.proto.connect('/dev/null')
303 -
304 - async def _hanging_connection(self):
305 - with jammed_socket() as addr:
306 - await self.proto.connect(addr)
307 -
308 - async def _bad_connection_test(self, family: str):
309 - await self._watch_runstates(*self.BAD_CONNECTION_STATES)
310 -
311 - with self.assertRaises(ConnectError) as context:
312 - await self._bad_connection(family)
313 -
314 - self.assertIsInstance(context.exception.exc, OSError)
315 - self.assertEqual(
316 - context.exception.error_message,
317 - "Failed to establish connection"
318 - )
319 -
320 - @TestBase.async_test
321 - async def testBadINET(self):
322 - """
323 - Test an immediately rejected call to an IP target.
324 - """
325 - await self._bad_connection_test('INET')
326 -
327 - @TestBase.async_test
328 - async def testBadUNIX(self):
329 - """
330 - Test an immediately rejected call to a UNIX socket target.
331 - """
332 - await self._bad_connection_test('UNIX')
333 -
334 - @TestBase.async_test
335 - async def testCancellation(self):
336 - """
337 - Test what happens when a connection attempt is aborted.
338 - """
339 - # Note that accept() cannot be cancelled outright, as it isn't a task.
340 - # However, we can wrap it in a task and cancel *that*.
341 - await self._watch_runstates(*self.BAD_CONNECTION_STATES)
342 - task = run_as_task(self._hanging_connection(), allow_cancellation=True)
343 -
344 - state = await self.proto.runstate_changed()
345 - self.assertEqual(state, Runstate.CONNECTING)
346 -
347 - # This is insider baseball, but the connection attempt has
348 - # yielded *just* before the actual connection attempt, so kick
349 - # the loop to make sure it's truly wedged.
350 - await asyncio.sleep(0)
351 -
352 - task.cancel()
353 - await task
354 -
355 - @TestBase.async_test
356 - async def testTimeout(self):
357 - """
358 - Test what happens when a connection attempt times out.
359 - """
360 - await self._watch_runstates(*self.BAD_CONNECTION_STATES)
361 - task = run_as_task(self._hanging_connection())
362 -
363 - # More insider baseball: to improve the speed of this test while
364 - # guaranteeing that the connection even gets a chance to start,
365 - # verify that the connection hangs *first*, then await the
366 - # result of the task with a nearly-zero timeout.
367 -
368 - state = await self.proto.runstate_changed()
369 - self.assertEqual(state, Runstate.CONNECTING)
370 - await asyncio.sleep(0)
371 -
372 - with self.assertRaises(asyncio.TimeoutError):
373 - await asyncio.wait_for(task, timeout=0)
374 -
375 - @TestBase.async_test
376 - async def testRequire(self):
377 - """
378 - Test what happens when a connection attempt is made while CONNECTING.
379 - """
380 - await self._watch_runstates(*self.BAD_CONNECTION_STATES)
381 - task = run_as_task(self._hanging_connection(), allow_cancellation=True)
382 -
383 - state = await self.proto.runstate_changed()
384 - self.assertEqual(state, Runstate.CONNECTING)
385 -
386 - with self.assertRaises(StateError) as context:
387 - await self._bad_connection('UNIX')
388 -
389 - self.assertEqual(
390 - context.exception.error_message,
391 - "NullProtocol is currently connecting."
392 - )
393 - self.assertEqual(context.exception.state, Runstate.CONNECTING)
394 - self.assertEqual(context.exception.required, Runstate.IDLE)
395 -
396 - task.cancel()
397 - await task
398 -
399 - @TestBase.async_test
400 - async def testImplicitRunstateInit(self):
401 - """
402 - Test what happens if we do not wait on the runstate event until
403 - AFTER a connection is made, i.e., connect()/accept() themselves
404 - initialize the runstate event. All of the above tests force the
405 - initialization by waiting on the runstate *first*.
406 - """
407 - task = run_as_task(self._hanging_connection(), allow_cancellation=True)
408 -
409 - # Kick the loop to coerce the state change
410 - await asyncio.sleep(0)
411 - assert self.proto.runstate == Runstate.CONNECTING
412 -
413 - # We already missed the transition to CONNECTING
414 - await self._watch_runstates(Runstate.DISCONNECTING, Runstate.IDLE)
415 -
416 - task.cancel()
417 - await task
418 -
419 -
420 -class Accept(Connect):
421 - """
422 - All of the same tests as Connect, but using the accept() interface.
423 - """
424 - async def _bad_connection(self, family: str):
425 - assert family in ('INET', 'UNIX')
426 -
427 - if family == 'INET':
428 - await self.proto.start_server_and_accept(('example.com', 1))
429 - elif family == 'UNIX':
430 - await self.proto.start_server_and_accept('/dev/null')
431 -
432 - async def _hanging_connection(self):
433 - with TemporaryDirectory(suffix='.qmp') as tmpdir:
434 - sock = os.path.join(tmpdir, type(self.proto).__name__ + ".sock")
435 - await self.proto.start_server_and_accept(sock)
436 -
437 -
438 -class FakeSession(TestBase):
439 -
440 - def setUp(self):
441 - super().setUp()
442 - self.proto.fake_session = True
443 -
444 - async def _asyncSetUp(self):
445 - await super()._asyncSetUp()
446 - await self._watch_runstates(*self.GOOD_CONNECTION_STATES)
447 -
448 - async def _asyncTearDown(self):
449 - await self.proto.disconnect()
450 - await super()._asyncTearDown()
451 -
452 - ####
453 -
454 - @TestBase.async_test
455 - async def testFakeConnect(self):
456 -
457 - """Test the full state lifecycle (via connect) with a no-op session."""
458 - await self.proto.connect('/not/a/real/path')
459 - self.assertEqual(self.proto.runstate, Runstate.RUNNING)
460 -
461 - @TestBase.async_test
462 - async def testFakeAccept(self):
463 - """Test the full state lifecycle (via accept) with a no-op session."""
464 - await self.proto.start_server_and_accept('/not/a/real/path')
465 - self.assertEqual(self.proto.runstate, Runstate.RUNNING)
466 -
467 - @TestBase.async_test
468 - async def testFakeRecv(self):
469 - """Test receiving a fake/null message."""
470 - await self.proto.start_server_and_accept('/not/a/real/path')
471 -
472 - logname = self.proto.logger.name
473 - with self.assertLogs(logname, level='DEBUG') as context:
474 - self.proto.trigger_input.set()
475 - self.proto.trigger_input.clear()
476 - await asyncio.sleep(0) # Kick reader.
477 -
478 - self.assertEqual(
479 - context.output,
480 - [f"DEBUG:{logname}:<-- None"],
481 - )
482 -
483 - @TestBase.async_test
484 - async def testFakeSend(self):
485 - """Test sending a fake/null message."""
486 - await self.proto.start_server_and_accept('/not/a/real/path')
487 -
488 - logname = self.proto.logger.name
489 - with self.assertLogs(logname, level='DEBUG') as context:
490 - # Cheat: Send a Null message to nobody.
491 - await self.proto.send_msg()
492 - # Kick writer; awaiting on a queue.put isn't sufficient to yield.
493 - await asyncio.sleep(0)
494 -
495 - self.assertEqual(
496 - context.output,
497 - [f"DEBUG:{logname}:--> None"],
498 - )
499 -
500 - async def _prod_session_api(
501 - self,
502 - current_state: Runstate,
503 - error_message: str,
504 - accept: bool = True
505 - ):
506 - with self.assertRaises(StateError) as context:
507 - if accept:
508 - await self.proto.start_server_and_accept('/not/a/real/path')
509 - else:
510 - await self.proto.connect('/not/a/real/path')
511 -
512 - self.assertEqual(context.exception.error_message, error_message)
513 - self.assertEqual(context.exception.state, current_state)
514 - self.assertEqual(context.exception.required, Runstate.IDLE)
515 -
516 - @TestBase.async_test
517 - async def testAcceptRequireRunning(self):
518 - """Test that accept() cannot be called when Runstate=RUNNING"""
519 - await self.proto.start_server_and_accept('/not/a/real/path')
520 -
521 - await self._prod_session_api(
522 - Runstate.RUNNING,
523 - "NullProtocol is already connected and running.",
524 - accept=True,
525 - )
526 -
527 - @TestBase.async_test
528 - async def testConnectRequireRunning(self):
529 - """Test that connect() cannot be called when Runstate=RUNNING"""
530 - await self.proto.start_server_and_accept('/not/a/real/path')
531 -
532 - await self._prod_session_api(
533 - Runstate.RUNNING,
534 - "NullProtocol is already connected and running.",
535 - accept=False,
536 - )
537 -
538 - @TestBase.async_test
539 - async def testAcceptRequireDisconnecting(self):
540 - """Test that accept() cannot be called when Runstate=DISCONNECTING"""
541 - await self.proto.start_server_and_accept('/not/a/real/path')
542 -
543 - # Cheat: force a disconnect.
544 - await self.proto.simulate_disconnect()
545 -
546 - await self._prod_session_api(
547 - Runstate.DISCONNECTING,
548 - ("NullProtocol is disconnecting."
549 - " Call disconnect() to return to IDLE state."),
550 - accept=True,
551 - )
552 -
553 - @TestBase.async_test
554 - async def testConnectRequireDisconnecting(self):
555 - """Test that connect() cannot be called when Runstate=DISCONNECTING"""
556 - await self.proto.start_server_and_accept('/not/a/real/path')
557 -
558 - # Cheat: force a disconnect.
559 - await self.proto.simulate_disconnect()
560 -
561 - await self._prod_session_api(
562 - Runstate.DISCONNECTING,
563 - ("NullProtocol is disconnecting."
564 - " Call disconnect() to return to IDLE state."),
565 - accept=False,
566 - )
567 -
568 -
569 -class SimpleSession(TestBase):
570 -
571 - def setUp(self):
572 - super().setUp()
573 - self.server = LineProtocol(type(self).__name__ + '-server')
574 -
575 - async def _asyncSetUp(self):
576 - await super()._asyncSetUp()
577 - await self._watch_runstates(*self.GOOD_CONNECTION_STATES)
578 -
579 - async def _asyncTearDown(self):
580 - await self.proto.disconnect()
581 - try:
582 - await self.server.disconnect()
583 - except EOFError:
584 - pass
585 - await super()._asyncTearDown()
586 -
587 - @TestBase.async_test
588 - async def testSmoke(self):
589 - with TemporaryDirectory(suffix='.qmp') as tmpdir:
590 - sock = os.path.join(tmpdir, type(self.proto).__name__ + ".sock")
591 - server_task = asyncio.create_task(
592 - self.server.start_server_and_accept(sock))
593 -
594 - # give the server a chance to start listening [...]
595 - await asyncio.sleep(0)
596 - await self.proto.connect(sock)