1""" 2QEMU development and testing utilities 3 4This package provides a small handful of utilities for performing 5various tasks not directly related to the launching of a VM. 6""" 7 8# Copyright (C) 2021 Red Hat Inc. 9# 10# Authors: 11# John Snow <jsnow@redhat.com> 12# Cleber Rosa <crosa@redhat.com> 13# 14# This work is licensed under the terms of the GNU GPL, version 2. See 15# the COPYING file in the top-level directory. 16# 17 18import os 19import re 20import shutil 21from subprocess import CalledProcessError 22import textwrap 23from typing import Optional 24 25# pylint: disable=import-error 26from .accel import ( 27 hvf_available, 28 kvm_available, 29 list_accel, 30 tcg_available, 31) 32 33 34__all__ = ( 35 'VerboseProcessError', 36 'add_visual_margin', 37 'get_info_usernet_hostfwd_port', 38 'hvf_available', 39 'kvm_available', 40 'list_accel', 41 'tcg_available', 42) 43 44 45def get_info_usernet_hostfwd_port(info_usernet_output: str) -> Optional[int]: 46 """ 47 Returns the port given to the hostfwd parameter via info usernet 48 49 :param info_usernet_output: output generated by hmp command "info usernet" 50 :return: the port number allocated by the hostfwd option 51 """ 52 for line in info_usernet_output.split('\r\n'): 53 regex = r'TCP.HOST_FORWARD.*127\.0\.0\.1\s+(\d+)\s+10\.' 54 match = re.search(regex, line) 55 if match is not None: 56 return int(match[1]) 57 return None 58 59 60# pylint: disable=too-many-arguments 61def add_visual_margin( 62 content: str = '', 63 width: Optional[int] = None, 64 name: Optional[str] = None, 65 padding: int = 1, 66 upper_left: str = '┏', 67 lower_left: str = '┗', 68 horizontal: str = '━', 69 vertical: str = '┃', 70) -> str: 71 """ 72 Decorate and wrap some text with a visual decoration around it. 73 74 This function assumes that the text decoration characters are single 75 characters that display using a single monospace column. 76 77 ┏━ Example ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 78 ┃ This is what this function looks like with text content that's 79 ┃ wrapped to 66 characters. The right-hand margin is left open to 80 ┃ accommodate the occasional unicode character that might make 81 ┃ predicting the total "visual" width of a line difficult. This 82 ┃ provides a visual distinction that's good-enough, though. 83 ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 84 85 :param content: The text to wrap and decorate. 86 :param width: 87 The number of columns to use, including for the decoration 88 itself. The default (None) uses the available width of the 89 current terminal, or a fallback of 72 lines. A negative number 90 subtracts a fixed-width from the default size. The default obeys 91 the COLUMNS environment variable, if set. 92 :param name: A label to apply to the upper-left of the box. 93 :param padding: How many columns of padding to apply inside. 94 :param upper_left: Upper-left single-width text decoration character. 95 :param lower_left: Lower-left single-width text decoration character. 96 :param horizontal: Horizontal single-width text decoration character. 97 :param vertical: Vertical single-width text decoration character. 98 """ 99 if width is None or width < 0: 100 avail = shutil.get_terminal_size(fallback=(72, 24))[0] 101 if width is None: 102 _width = avail 103 else: 104 _width = avail + width 105 else: 106 _width = width 107 108 prefix = vertical + (' ' * padding) 109 110 def _bar(name: Optional[str], top: bool = True) -> str: 111 ret = upper_left if top else lower_left 112 if name is not None: 113 ret += f"{horizontal} {name} " 114 115 filler_len = _width - len(ret) 116 ret += f"{horizontal * filler_len}" 117 return ret 118 119 def _wrap(line: str) -> str: 120 return os.linesep.join( 121 textwrap.wrap( 122 line, width=_width - padding, initial_indent=prefix, 123 subsequent_indent=prefix, replace_whitespace=False, 124 drop_whitespace=True, break_on_hyphens=False) 125 ) 126 127 return os.linesep.join(( 128 _bar(name, top=True), 129 os.linesep.join(_wrap(line) for line in content.splitlines()), 130 _bar(None, top=False), 131 )) 132 133 134class VerboseProcessError(CalledProcessError): 135 """ 136 The same as CalledProcessError, but more verbose. 137 138 This is useful for debugging failed calls during test executions. 139 The return code, signal (if any), and terminal output will be displayed 140 on unhandled exceptions. 141 """ 142 def summary(self) -> str: 143 """Return the normal CalledProcessError str() output.""" 144 return super().__str__() 145 146 def __str__(self) -> str: 147 lmargin = ' ' 148 width = -len(lmargin) 149 sections = [] 150 151 # Does self.stdout contain both stdout and stderr? 152 has_combined_output = self.stderr is None 153 154 name = 'output' if has_combined_output else 'stdout' 155 if self.stdout: 156 sections.append(add_visual_margin(self.stdout, width, name)) 157 else: 158 sections.append(f"{name}: N/A") 159 160 if self.stderr: 161 sections.append(add_visual_margin(self.stderr, width, 'stderr')) 162 elif not has_combined_output: 163 sections.append("stderr: N/A") 164 165 return os.linesep.join(( 166 self.summary(), 167 textwrap.indent(os.linesep.join(sections), prefix=lmargin), 168 )) 169