summaryrefslogtreecommitdiff
path: root/python/qemu/utils/__init__.py
blob: 017cfdcda75b5fcbbcae622eccfd2520f4ada81d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
"""
QEMU development and testing utilities

This package provides a small handful of utilities for performing
various tasks not directly related to the launching of a VM.
"""

# Copyright (C) 2021 Red Hat Inc.
#
# Authors:
#  John Snow <jsnow@redhat.com>
#  Cleber Rosa <crosa@redhat.com>
#
# This work is licensed under the terms of the GNU GPL, version 2.  See
# the COPYING file in the top-level directory.
#

import os
import re
import shutil
from subprocess import CalledProcessError
import textwrap
from typing import Optional

# pylint: disable=import-error
from .accel import kvm_available, list_accel, tcg_available


__all__ = (
    'VerboseProcessError',
    'add_visual_margin',
    'get_info_usernet_hostfwd_port',
    'kvm_available',
    'list_accel',
    'tcg_available',
)


def get_info_usernet_hostfwd_port(info_usernet_output: str) -> Optional[int]:
    """
    Returns the port given to the hostfwd parameter via info usernet

    :param info_usernet_output: output generated by hmp command "info usernet"
    :return: the port number allocated by the hostfwd option
    """
    for line in info_usernet_output.split('\r\n'):
        regex = r'TCP.HOST_FORWARD.*127\.0\.0\.1\s+(\d+)\s+10\.'
        match = re.search(regex, line)
        if match is not None:
            return int(match[1])
    return None


# pylint: disable=too-many-arguments
def add_visual_margin(
        content: str = '',
        width: Optional[int] = None,
        name: Optional[str] = None,
        padding: int = 1,
        upper_left: str = '┏',
        lower_left: str = '┗',
        horizontal: str = '━',
        vertical: str = '┃',
) -> str:
    """
    Decorate and wrap some text with a visual decoration around it.

    This function assumes that the text decoration characters are single
    characters that display using a single monospace column.

    ┏━ Example ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    ┃ This is what this function looks like with text content that's
    ┃ wrapped to 66 characters. The right-hand margin is left open to
    ┃ accommodate the occasional unicode character that might make
    ┃ predicting the total "visual" width of a line difficult. This
    ┃ provides a visual distinction that's good-enough, though.
    ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

    :param content: The text to wrap and decorate.
    :param width:
        The number of columns to use, including for the decoration
        itself. The default (None) uses the available width of the
        current terminal, or a fallback of 72 lines. A negative number
        subtracts a fixed-width from the default size. The default obeys
        the COLUMNS environment variable, if set.
    :param name: A label to apply to the upper-left of the box.
    :param padding: How many columns of padding to apply inside.
    :param upper_left: Upper-left single-width text decoration character.
    :param lower_left: Lower-left single-width text decoration character.
    :param horizontal: Horizontal single-width text decoration character.
    :param vertical: Vertical single-width text decoration character.
    """
    if width is None or width < 0:
        avail = shutil.get_terminal_size(fallback=(72, 24))[0]
        if width is None:
            _width = avail
        else:
            _width = avail + width
    else:
        _width = width

    prefix = vertical + (' ' * padding)

    def _bar(name: Optional[str], top: bool = True) -> str:
        ret = upper_left if top else lower_left
        if name is not None:
            ret += f"{horizontal} {name} "

        filler_len = _width - len(ret)
        ret += f"{horizontal * filler_len}"
        return ret

    def _wrap(line: str) -> str:
        return os.linesep.join(
            textwrap.wrap(
                line, width=_width - padding, initial_indent=prefix,
                subsequent_indent=prefix, replace_whitespace=False,
                drop_whitespace=True, break_on_hyphens=False)
        )

    return os.linesep.join((
        _bar(name, top=True),
        os.linesep.join(_wrap(line) for line in content.splitlines()),
        _bar(None, top=False),
    ))


class VerboseProcessError(CalledProcessError):
    """
    The same as CalledProcessError, but more verbose.

    This is useful for debugging failed calls during test executions.
    The return code, signal (if any), and terminal output will be displayed
    on unhandled exceptions.
    """
    def summary(self) -> str:
        """Return the normal CalledProcessError str() output."""
        return super().__str__()

    def __str__(self) -> str:
        lmargin = '  '
        width = -len(lmargin)
        sections = []

        # Does self.stdout contain both stdout and stderr?
        has_combined_output = self.stderr is None

        name = 'output' if has_combined_output else 'stdout'
        if self.stdout:
            sections.append(add_visual_margin(self.stdout, width, name))
        else:
            sections.append(f"{name}: N/A")

        if self.stderr:
            sections.append(add_visual_margin(self.stderr, width, 'stderr'))
        elif not has_combined_output:
            sections.append("stderr: N/A")

        return os.linesep.join((
            self.summary(),
            textwrap.indent(os.linesep.join(sections), prefix=lmargin),
        ))