]>
Commit | Line | Data |
---|---|---|
cedebdac | 1 | # |
0e08b947 | 2 | # Copyright (C) 2009-2022 Red Hat Inc. |
cedebdac LC |
3 | # |
4 | # Authors: | |
5 | # Luiz Capitulino <lcapitulino@redhat.com> | |
0e08b947 | 6 | # John Snow <jsnow@redhat.com> |
cedebdac | 7 | # |
0e08b947 JS |
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. | |
cedebdac | 10 | # |
7fc29896 JS |
11 | |
12 | """ | |
13 | Low-level QEMU shell on top of QMP. | |
14 | ||
15 | usage: qmp-shell [-h] [-H] [-N] [-v] [-p] qmp_server | |
16 | ||
17 | positional arguments: | |
18 | qmp_server < UNIX socket path | TCP address:port > | |
19 | ||
20 | optional arguments: | |
21 | -h, --help show this help message and exit | |
22 | -H, --hmp Use HMP interface | |
23 | -N, --skip-negotiation | |
24 | Skip negotiate (for qemu-ga) | |
25 | -v, --verbose Verbose (echo commands sent and received) | |
26 | -p, --pretty Pretty-print JSON | |
27 | ||
28 | ||
29 | Start QEMU with: | |
30 | ||
31 | # qemu [...] -qmp unix:./qmp-sock,server | |
32 | ||
33 | Run the shell: | |
34 | ||
35 | $ qmp-shell ./qmp-sock | |
36 | ||
37 | Commands have the following format: | |
38 | ||
39 | < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ] | |
40 | ||
41 | For example: | |
42 | ||
43 | (QEMU) device_add driver=e1000 id=net1 | |
44 | {'return': {}} | |
45 | (QEMU) | |
46 | ||
47 | key=value pairs also support Python or JSON object literal subset notations, | |
48 | without spaces. Dictionaries/objects {} are supported as are arrays []. | |
49 | ||
50 | example-command arg-name1={'key':'value','obj'={'prop':"value"}} | |
51 | ||
52 | Both JSON and Python formatting should work, including both styles of | |
53 | string literal quotes. Both paradigms of literal values should work, | |
54 | including null/true/false for JSON and None/True/False for Python. | |
55 | ||
56 | ||
57 | Transactions have the following multi-line format: | |
58 | ||
59 | transaction( | |
60 | action-name1 [ arg-name1=arg1 ] ... [arg-nameN=argN ] | |
61 | ... | |
62 | action-nameN [ arg-name1=arg1 ] ... [arg-nameN=argN ] | |
63 | ) | |
64 | ||
65 | One line transactions are also supported: | |
66 | ||
67 | transaction( action-name1 ... ) | |
68 | ||
69 | For example: | |
70 | ||
71 | (QEMU) transaction( | |
72 | TRANS> block-dirty-bitmap-add node=drive0 name=bitmap1 | |
73 | TRANS> block-dirty-bitmap-clear node=drive0 name=bitmap0 | |
74 | TRANS> ) | |
75 | {"return": {}} | |
76 | (QEMU) | |
77 | ||
78 | Use the -v and -p options to activate the verbose and pretty-print options, | |
79 | which will echo back the properly formatted JSON-compliant QMP that is being | |
80 | sent to QEMU, which is useful for debugging and documentation generation. | |
81 | """ | |
82 | ||
17329be2 | 83 | import argparse |
6092c3ec | 84 | import ast |
badf4629 | 85 | import json |
be19c6a7 | 86 | import logging |
badf4629 | 87 | import os |
b35203b2 | 88 | import re |
badf4629 | 89 | import readline |
43912529 | 90 | from subprocess import Popen |
badf4629 | 91 | import sys |
1eab8872 | 92 | from typing import ( |
5c66d7d8 | 93 | IO, |
2cee9ca9 | 94 | Dict, |
1eab8872 JS |
95 | Iterator, |
96 | List, | |
97 | NoReturn, | |
98 | Optional, | |
99 | Sequence, | |
2cee9ca9 | 100 | cast, |
1eab8872 | 101 | ) |
badf4629 | 102 | |
2cee9ca9 VSO |
103 | from qemu.qmp import ( |
104 | ConnectError, | |
105 | ExecuteError, | |
106 | QMPError, | |
107 | SocketAddrT, | |
108 | ) | |
37094b6d | 109 | from qemu.qmp.legacy import ( |
f3efd129 JS |
110 | QEMUMonitorProtocol, |
111 | QMPBadPortError, | |
112 | QMPMessage, | |
113 | QMPObject, | |
114 | ) | |
8f8fd9ed | 115 | |
badf4629 | 116 | |
be19c6a7 JS |
117 | LOG = logging.getLogger(__name__) |
118 | ||
119 | ||
db12abc2 | 120 | class QMPCompleter: |
e359c5a8 JS |
121 | """ |
122 | QMPCompleter provides a readline library tab-complete behavior. | |
123 | """ | |
db12abc2 JS |
124 | # NB: Python 3.9+ will probably allow us to subclass list[str] directly, |
125 | # but pylint as of today does not know that List[str] is simply 'list'. | |
126 | def __init__(self) -> None: | |
127 | self._matches: List[str] = [] | |
128 | ||
129 | def append(self, value: str) -> None: | |
e359c5a8 | 130 | """Append a new valid completion to the list of possibilities.""" |
db12abc2 JS |
131 | return self._matches.append(value) |
132 | ||
133 | def complete(self, text: str, state: int) -> Optional[str]: | |
e359c5a8 | 134 | """readline.set_completer() callback implementation.""" |
db12abc2 | 135 | for cmd in self._matches: |
9bed0d0d | 136 | if cmd.startswith(text): |
2813dee0 | 137 | if state == 0: |
9bed0d0d | 138 | return cmd |
2813dee0 JS |
139 | state -= 1 |
140 | return None | |
cedebdac | 141 | |
169b43b3 | 142 | |
f3efd129 | 143 | class QMPShellError(QMPError): |
e359c5a8 JS |
144 | """ |
145 | QMP Shell Base error class. | |
146 | """ | |
9bed0d0d | 147 | |
9bed0d0d | 148 | |
6092c3ec | 149 | class FuzzyJSON(ast.NodeTransformer): |
c6be2bf8 JS |
150 | """ |
151 | This extension of ast.NodeTransformer filters literal "true/false/null" | |
6faf2384 JS |
152 | values in a Python AST and replaces them by proper "True/False/None" values |
153 | that Python can properly evaluate. | |
c6be2bf8 JS |
154 | """ |
155 | ||
c4a1447f | 156 | @classmethod |
6faf2384 JS |
157 | def visit_Name(cls, # pylint: disable=invalid-name |
158 | node: ast.Name) -> ast.AST: | |
e359c5a8 JS |
159 | """ |
160 | Transform Name nodes with certain values into Constant (keyword) nodes. | |
161 | """ | |
6092c3ec | 162 | if node.id == 'true': |
6faf2384 | 163 | return ast.Constant(value=True) |
6092c3ec | 164 | if node.id == 'false': |
6faf2384 | 165 | return ast.Constant(value=False) |
6092c3ec | 166 | if node.id == 'null': |
6faf2384 | 167 | return ast.Constant(value=None) |
6092c3ec JS |
168 | return node |
169 | ||
169b43b3 | 170 | |
f3efd129 | 171 | class QMPShell(QEMUMonitorProtocol): |
e359c5a8 JS |
172 | """ |
173 | QMPShell provides a basic readline-based QMP shell. | |
174 | ||
175 | :param address: Address of the QMP server. | |
176 | :param pretty: Pretty-print QMP messages. | |
177 | :param verbose: Echo outgoing QMP messages to console. | |
178 | """ | |
f3efd129 | 179 | def __init__(self, address: SocketAddrT, |
43912529 DB |
180 | pretty: bool = False, |
181 | verbose: bool = False, | |
5c66d7d8 DB |
182 | server: bool = False, |
183 | logfile: Optional[str] = None): | |
43912529 | 184 | super().__init__(address, server=server) |
1eab8872 | 185 | self._greeting: Optional[QMPMessage] = None |
41574295 | 186 | self._completer = QMPCompleter() |
30bd6815 | 187 | self._transmode = False |
1eab8872 | 188 | self._actions: List[QMPMessage] = [] |
aa3b167f JS |
189 | self._histfile = os.path.join(os.path.expanduser('~'), |
190 | '.qmp-shell_history') | |
6e24a7ed | 191 | self.pretty = pretty |
2ac3f378 | 192 | self.verbose = verbose |
5c66d7d8 DB |
193 | self.logfile = None |
194 | ||
195 | if logfile is not None: | |
196 | self.logfile = open(logfile, "w", encoding='utf-8') | |
9bed0d0d | 197 | |
d1d14e59 JS |
198 | def close(self) -> None: |
199 | # Hook into context manager of parent to save shell history. | |
200 | self._save_history() | |
201 | super().close() | |
202 | ||
1eab8872 | 203 | def _fill_completion(self) -> None: |
2cee9ca9 | 204 | try: |
684750ab | 205 | cmds = cast(List[Dict[str, str]], self.cmd('query-commands')) |
2cee9ca9 VSO |
206 | for cmd in cmds: |
207 | self._completer.append(cmd['name']) | |
208 | except ExecuteError: | |
209 | pass | |
9bed0d0d | 210 | |
a64fe44d | 211 | def _completer_setup(self) -> None: |
9bed0d0d LC |
212 | self._completer = QMPCompleter() |
213 | self._fill_completion() | |
aa3b167f | 214 | readline.set_history_length(1024) |
9bed0d0d LC |
215 | readline.set_completer(self._completer.complete) |
216 | readline.parse_and_bind("tab: complete") | |
169b43b3 JS |
217 | # NB: default delimiters conflict with some command names |
218 | # (eg. query-), clearing everything as it doesn't seem to matter | |
9bed0d0d | 219 | readline.set_completer_delims('') |
aa3b167f JS |
220 | try: |
221 | readline.read_history_file(self._histfile) | |
d962ec85 JS |
222 | except FileNotFoundError: |
223 | pass | |
224 | except IOError as err: | |
be19c6a7 JS |
225 | msg = f"Failed to read history '{self._histfile}': {err!s}" |
226 | LOG.warning(msg) | |
aa3b167f | 227 | |
d1d14e59 | 228 | def _save_history(self) -> None: |
aa3b167f JS |
229 | try: |
230 | readline.write_history_file(self._histfile) | |
d962ec85 | 231 | except IOError as err: |
be19c6a7 JS |
232 | msg = f"Failed to save history file '{self._histfile}': {err!s}" |
233 | LOG.warning(msg) | |
9bed0d0d | 234 | |
c4a1447f | 235 | @classmethod |
a64fe44d | 236 | def _parse_value(cls, val: str) -> object: |
6092c3ec JS |
237 | try: |
238 | return int(val) | |
239 | except ValueError: | |
240 | pass | |
241 | ||
242 | if val.lower() == 'true': | |
243 | return True | |
244 | if val.lower() == 'false': | |
245 | return False | |
246 | if val.startswith(('{', '[')): | |
247 | # Try first as pure JSON: | |
9bed0d0d | 248 | try: |
6092c3ec | 249 | return json.loads(val) |
9bed0d0d | 250 | except ValueError: |
6092c3ec JS |
251 | pass |
252 | # Try once again as FuzzyJSON: | |
253 | try: | |
628b92dd | 254 | tree = ast.parse(val, mode='eval') |
6faf2384 JS |
255 | transformed = FuzzyJSON().visit(tree) |
256 | return ast.literal_eval(transformed) | |
257 | except (SyntaxError, ValueError): | |
6092c3ec JS |
258 | pass |
259 | return val | |
260 | ||
a64fe44d JS |
261 | def _cli_expr(self, |
262 | tokens: Sequence[str], | |
f3efd129 | 263 | parent: QMPObject) -> None: |
6092c3ec | 264 | for arg in tokens: |
f880cd6b DB |
265 | (key, sep, val) = arg.partition('=') |
266 | if sep != '=': | |
169b43b3 JS |
267 | raise QMPShellError( |
268 | f"Expected a key=value pair, got '{arg!s}'" | |
269 | ) | |
6092c3ec | 270 | |
a64fe44d | 271 | value = self._parse_value(val) |
6092c3ec | 272 | optpath = key.split('.') |
cd159d09 | 273 | curpath = [] |
628b92dd JS |
274 | for path in optpath[:-1]: |
275 | curpath.append(path) | |
276 | obj = parent.get(path, {}) | |
90bd8eb8 | 277 | if not isinstance(obj, dict): |
169b43b3 JS |
278 | msg = 'Cannot use "{:s}" as both leaf and non-leaf key' |
279 | raise QMPShellError(msg.format('.'.join(curpath))) | |
628b92dd JS |
280 | parent[path] = obj |
281 | parent = obj | |
cd159d09 | 282 | if optpath[-1] in parent: |
90bd8eb8 | 283 | if isinstance(parent[optpath[-1]], dict): |
169b43b3 JS |
284 | msg = 'Cannot use "{:s}" as both leaf and non-leaf key' |
285 | raise QMPShellError(msg.format('.'.join(curpath))) | |
73f699c9 | 286 | raise QMPShellError(f'Cannot set "{key}" multiple times') |
cd159d09 | 287 | parent[optpath[-1]] = value |
a7430a0b | 288 | |
a64fe44d | 289 | def _build_cmd(self, cmdline: str) -> Optional[QMPMessage]: |
a7430a0b JS |
290 | """ |
291 | Build a QMP input object from a user provided command-line in the | |
292 | following format: | |
293 | ||
294 | < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ] | |
295 | """ | |
169b43b3 JS |
296 | argument_regex = r'''(?:[^\s"']|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+''' |
297 | cmdargs = re.findall(argument_regex, cmdline) | |
1eab8872 | 298 | qmpcmd: QMPMessage |
30bd6815 | 299 | |
c83055ef JS |
300 | # Transactional CLI entry: |
301 | if cmdargs and cmdargs[0] == 'transaction(': | |
30bd6815 | 302 | self._transmode = True |
c83055ef | 303 | self._actions = [] |
30bd6815 | 304 | cmdargs.pop(0) |
c83055ef JS |
305 | |
306 | # Transactional CLI exit: | |
307 | if cmdargs and cmdargs[0] == ')' and self._transmode: | |
30bd6815 JS |
308 | self._transmode = False |
309 | if len(cmdargs) > 1: | |
169b43b3 JS |
310 | msg = 'Unexpected input after close of Transaction sub-shell' |
311 | raise QMPShellError(msg) | |
312 | qmpcmd = { | |
313 | 'execute': 'transaction', | |
314 | 'arguments': {'actions': self._actions} | |
315 | } | |
30bd6815 JS |
316 | return qmpcmd |
317 | ||
c83055ef | 318 | # No args, or no args remaining |
30bd6815 JS |
319 | if not cmdargs: |
320 | return None | |
321 | ||
30bd6815 | 322 | if self._transmode: |
c83055ef | 323 | # Parse and cache this Transactional Action |
30bd6815 | 324 | finalize = False |
169b43b3 | 325 | action = {'type': cmdargs[0], 'data': {}} |
30bd6815 JS |
326 | if cmdargs[-1] == ')': |
327 | cmdargs.pop(-1) | |
328 | finalize = True | |
a64fe44d | 329 | self._cli_expr(cmdargs[1:], action['data']) |
30bd6815 | 330 | self._actions.append(action) |
a64fe44d | 331 | return self._build_cmd(')') if finalize else None |
30bd6815 JS |
332 | |
333 | # Standard command: parse and return it to be executed. | |
169b43b3 | 334 | qmpcmd = {'execute': cmdargs[0], 'arguments': {}} |
a64fe44d | 335 | self._cli_expr(cmdargs[1:], qmpcmd['arguments']) |
9bed0d0d LC |
336 | return qmpcmd |
337 | ||
5c66d7d8 | 338 | def _print(self, qmp_message: object, fh: IO[str] = sys.stdout) -> None: |
6e24a7ed JS |
339 | jsobj = json.dumps(qmp_message, |
340 | indent=4 if self.pretty else None, | |
341 | sort_keys=self.pretty) | |
5c66d7d8 | 342 | print(str(jsobj), file=fh) |
1ceca07e | 343 | |
1eab8872 | 344 | def _execute_cmd(self, cmdline: str) -> bool: |
9bed0d0d | 345 | try: |
a64fe44d | 346 | qmpcmd = self._build_cmd(cmdline) |
26d3ce9e JS |
347 | except QMPShellError as err: |
348 | print( | |
349 | f"Error while parsing command line: {err!s}\n" | |
350 | "command format: <command-name> " | |
351 | "[arg-name1=arg1] ... [arg-nameN=argN", | |
352 | file=sys.stderr | |
353 | ) | |
9bed0d0d | 354 | return True |
30bd6815 JS |
355 | # For transaction mode, we may have just cached the action: |
356 | if qmpcmd is None: | |
357 | return True | |
2ac3f378 | 358 | if self.verbose: |
1ceca07e | 359 | self._print(qmpcmd) |
9bed0d0d LC |
360 | resp = self.cmd_obj(qmpcmd) |
361 | if resp is None: | |
f03868bd | 362 | print('Disconnected') |
9bed0d0d | 363 | return False |
1ceca07e | 364 | self._print(resp) |
5c66d7d8 DB |
365 | if self.logfile is not None: |
366 | cmd = {**qmpcmd, **resp} | |
367 | self._print(cmd, fh=self.logfile) | |
9bed0d0d LC |
368 | return True |
369 | ||
1eab8872 | 370 | def connect(self, negotiate: bool = True) -> None: |
5cb02338 | 371 | self._greeting = super().connect(negotiate) |
a64fe44d | 372 | self._completer_setup() |
cedebdac | 373 | |
1eab8872 JS |
374 | def show_banner(self, |
375 | msg: str = 'Welcome to the QMP low-level shell!') -> None: | |
e359c5a8 JS |
376 | """ |
377 | Print to stdio a greeting, and the QEMU version if available. | |
378 | """ | |
f03868bd | 379 | print(msg) |
b13d2ff3 | 380 | if not self._greeting: |
f03868bd | 381 | print('Connected') |
b13d2ff3 | 382 | return |
9bed0d0d | 383 | version = self._greeting['QMP']['version']['qemu'] |
169b43b3 | 384 | print("Connected to QEMU {major}.{minor}.{micro}\n".format(**version)) |
cedebdac | 385 | |
1caa5057 | 386 | @property |
1eab8872 | 387 | def prompt(self) -> str: |
e359c5a8 JS |
388 | """ |
389 | Return the current shell prompt, including a trailing space. | |
390 | """ | |
30bd6815 | 391 | if self._transmode: |
1caa5057 JS |
392 | return 'TRANS> ' |
393 | return '(QEMU) ' | |
30bd6815 | 394 | |
1eab8872 | 395 | def read_exec_command(self) -> bool: |
9bed0d0d LC |
396 | """ |
397 | Read and execute a command. | |
cedebdac | 398 | |
9bed0d0d LC |
399 | @return True if execution was ok, return False if disconnected. |
400 | """ | |
cedebdac | 401 | try: |
1215a1fb | 402 | cmdline = input(self.prompt) |
cedebdac | 403 | except EOFError: |
f03868bd | 404 | print() |
9bed0d0d | 405 | return False |
73f699c9 | 406 | |
9bed0d0d | 407 | if cmdline == '': |
628b92dd JS |
408 | for event in self.get_events(): |
409 | print(event) | |
9bed0d0d | 410 | return True |
73f699c9 JS |
411 | |
412 | return self._execute_cmd(cmdline) | |
9bed0d0d | 413 | |
1eab8872 | 414 | def repl(self) -> Iterator[None]: |
e359c5a8 JS |
415 | """ |
416 | Return an iterator that implements the REPL. | |
417 | """ | |
ad4eebee JS |
418 | self.show_banner() |
419 | while self.read_exec_command(): | |
420 | yield | |
421 | self.close() | |
422 | ||
169b43b3 | 423 | |
11217a75 | 424 | class HMPShell(QMPShell): |
e359c5a8 JS |
425 | """ |
426 | HMPShell provides a basic readline-based HMP shell, tunnelled via QMP. | |
427 | ||
428 | :param address: Address of the QMP server. | |
429 | :param pretty: Pretty-print QMP messages. | |
430 | :param verbose: Echo outgoing QMP messages to console. | |
431 | """ | |
f3efd129 | 432 | def __init__(self, address: SocketAddrT, |
43912529 DB |
433 | pretty: bool = False, |
434 | verbose: bool = False, | |
5c66d7d8 DB |
435 | server: bool = False, |
436 | logfile: Optional[str] = None): | |
437 | super().__init__(address, pretty, verbose, server, logfile) | |
a64fe44d | 438 | self._cpu_index = 0 |
11217a75 | 439 | |
a64fe44d JS |
440 | def _cmd_completion(self) -> None: |
441 | for cmd in self._cmd_passthrough('help')['return'].split('\r\n'): | |
11217a75 | 442 | if cmd and cmd[0] != '[' and cmd[0] != '\t': |
169b43b3 | 443 | name = cmd.split()[0] # drop help text |
11217a75 LC |
444 | if name == 'info': |
445 | continue | |
446 | if name.find('|') != -1: | |
447 | # Command in the form 'foobar|f' or 'f|foobar', take the | |
448 | # full name | |
449 | opt = name.split('|') | |
450 | if len(opt[0]) == 1: | |
451 | name = opt[1] | |
452 | else: | |
453 | name = opt[0] | |
454 | self._completer.append(name) | |
169b43b3 | 455 | self._completer.append('help ' + name) # help completion |
11217a75 | 456 | |
a64fe44d JS |
457 | def _info_completion(self) -> None: |
458 | for cmd in self._cmd_passthrough('info')['return'].split('\r\n'): | |
11217a75 LC |
459 | if cmd: |
460 | self._completer.append('info ' + cmd.split()[1]) | |
461 | ||
a64fe44d | 462 | def _other_completion(self) -> None: |
11217a75 LC |
463 | # special cases |
464 | self._completer.append('help info') | |
465 | ||
1eab8872 | 466 | def _fill_completion(self) -> None: |
a64fe44d JS |
467 | self._cmd_completion() |
468 | self._info_completion() | |
469 | self._other_completion() | |
11217a75 | 470 | |
a64fe44d JS |
471 | def _cmd_passthrough(self, cmdline: str, |
472 | cpu_index: int = 0) -> QMPMessage: | |
169b43b3 JS |
473 | return self.cmd_obj({ |
474 | 'execute': 'human-monitor-command', | |
475 | 'arguments': { | |
476 | 'command-line': cmdline, | |
477 | 'cpu-index': cpu_index | |
478 | } | |
479 | }) | |
11217a75 | 480 | |
1eab8872 | 481 | def _execute_cmd(self, cmdline: str) -> bool: |
11217a75 LC |
482 | if cmdline.split()[0] == "cpu": |
483 | # trap the cpu command, it requires special setting | |
484 | try: | |
485 | idx = int(cmdline.split()[1]) | |
a64fe44d | 486 | if 'return' not in self._cmd_passthrough('info version', idx): |
f03868bd | 487 | print('bad CPU index') |
11217a75 | 488 | return True |
a64fe44d | 489 | self._cpu_index = idx |
11217a75 | 490 | except ValueError: |
f03868bd | 491 | print('cpu command takes an integer argument') |
11217a75 | 492 | return True |
a64fe44d | 493 | resp = self._cmd_passthrough(cmdline, self._cpu_index) |
11217a75 | 494 | if resp is None: |
f03868bd | 495 | print('Disconnected') |
11217a75 LC |
496 | return False |
497 | assert 'return' in resp or 'error' in resp | |
498 | if 'return' in resp: | |
499 | # Success | |
500 | if len(resp['return']) > 0: | |
f03868bd | 501 | print(resp['return'], end=' ') |
11217a75 LC |
502 | else: |
503 | # Error | |
f03868bd | 504 | print('%s: %s' % (resp['error']['class'], resp['error']['desc'])) |
11217a75 LC |
505 | return True |
506 | ||
1eab8872 | 507 | def show_banner(self, msg: str = 'Welcome to the HMP shell!') -> None: |
70e56740 | 508 | QMPShell.show_banner(self, msg) |
11217a75 | 509 | |
169b43b3 | 510 | |
1eab8872 | 511 | def die(msg: str) -> NoReturn: |
e359c5a8 | 512 | """Write an error to stderr, then exit with a return code of 1.""" |
9bed0d0d LC |
513 | sys.stderr.write('ERROR: %s\n' % msg) |
514 | sys.exit(1) | |
515 | ||
169b43b3 | 516 | |
1eab8872 | 517 | def main() -> None: |
e359c5a8 JS |
518 | """ |
519 | qmp-shell entry point: parse command line arguments and start the REPL. | |
520 | """ | |
17329be2 JS |
521 | parser = argparse.ArgumentParser() |
522 | parser.add_argument('-H', '--hmp', action='store_true', | |
523 | help='Use HMP interface') | |
524 | parser.add_argument('-N', '--skip-negotiation', action='store_true', | |
525 | help='Skip negotiate (for qemu-ga)') | |
526 | parser.add_argument('-v', '--verbose', action='store_true', | |
527 | help='Verbose (echo commands sent and received)') | |
528 | parser.add_argument('-p', '--pretty', action='store_true', | |
529 | help='Pretty-print JSON') | |
5c66d7d8 DB |
530 | parser.add_argument('-l', '--logfile', |
531 | help='Save log of all QMP messages to PATH') | |
17329be2 JS |
532 | |
533 | default_server = os.environ.get('QMP_SOCKET') | |
534 | parser.add_argument('qmp_server', action='store', | |
535 | default=default_server, | |
536 | help='< UNIX socket path | TCP address:port >') | |
537 | ||
538 | args = parser.parse_args() | |
539 | if args.qmp_server is None: | |
540 | parser.error("QMP socket or TCP address must be specified") | |
541 | ||
ad459132 | 542 | shell_class = HMPShell if args.hmp else QMPShell |
d1d14e59 | 543 | |
9bed0d0d | 544 | try: |
b0b8ca17 | 545 | address = shell_class.parse_address(args.qmp_server) |
f3efd129 | 546 | except QMPBadPortError: |
17329be2 JS |
547 | parser.error(f"Bad port number: {args.qmp_server}") |
548 | return # pycharm doesn't know error() is noreturn | |
9bed0d0d | 549 | |
5c66d7d8 | 550 | with shell_class(address, args.pretty, args.verbose, args.logfile) as qemu: |
d1d14e59 JS |
551 | try: |
552 | qemu.connect(negotiate=not args.skip_negotiation) | |
f3efd129 JS |
553 | except ConnectError as err: |
554 | if isinstance(err.exc, OSError): | |
555 | die(f"Couldn't connect to {args.qmp_server}: {err!s}") | |
556 | die(str(err)) | |
d1d14e59 JS |
557 | |
558 | for _ in qemu.repl(): | |
559 | pass | |
cedebdac | 560 | |
169b43b3 | 561 | |
43912529 DB |
562 | def main_wrap() -> None: |
563 | """ | |
564 | qmp-shell-wrap entry point: parse command line arguments and | |
565 | start the REPL. | |
566 | """ | |
567 | parser = argparse.ArgumentParser() | |
568 | parser.add_argument('-H', '--hmp', action='store_true', | |
569 | help='Use HMP interface') | |
570 | parser.add_argument('-v', '--verbose', action='store_true', | |
571 | help='Verbose (echo commands sent and received)') | |
572 | parser.add_argument('-p', '--pretty', action='store_true', | |
573 | help='Pretty-print JSON') | |
5c66d7d8 DB |
574 | parser.add_argument('-l', '--logfile', |
575 | help='Save log of all QMP messages to PATH') | |
43912529 DB |
576 | |
577 | parser.add_argument('command', nargs=argparse.REMAINDER, | |
578 | help='QEMU command line to invoke') | |
579 | ||
580 | args = parser.parse_args() | |
581 | ||
582 | cmd = args.command | |
583 | if len(cmd) != 0 and cmd[0] == '--': | |
584 | cmd = cmd[1:] | |
585 | if len(cmd) == 0: | |
586 | cmd = ["qemu-system-x86_64"] | |
587 | ||
588 | sockpath = "qmp-shell-wrap-%d" % os.getpid() | |
589 | cmd += ["-qmp", "unix:%s" % sockpath] | |
590 | ||
591 | shell_class = HMPShell if args.hmp else QMPShell | |
592 | ||
593 | try: | |
594 | address = shell_class.parse_address(sockpath) | |
595 | except QMPBadPortError: | |
596 | parser.error(f"Bad port number: {sockpath}") | |
597 | return # pycharm doesn't know error() is noreturn | |
598 | ||
599 | try: | |
5c66d7d8 DB |
600 | with shell_class(address, args.pretty, args.verbose, |
601 | True, args.logfile) as qemu: | |
43912529 DB |
602 | with Popen(cmd): |
603 | ||
604 | try: | |
605 | qemu.accept() | |
606 | except ConnectError as err: | |
607 | if isinstance(err.exc, OSError): | |
608 | die(f"Couldn't connect to {args.qmp_server}: {err!s}") | |
609 | die(str(err)) | |
610 | ||
611 | for _ in qemu.repl(): | |
612 | pass | |
613 | finally: | |
614 | os.unlink(sockpath) | |
615 | ||
616 | ||
cedebdac LC |
617 | if __name__ == '__main__': |
618 | main() |