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)