16ebf5866SFelix Guo# SPDX-License-Identifier: GPL-2.0 26ebf5866SFelix Guo# 36ebf5866SFelix Guo# Runs UML kernel, collects output, and handles errors. 46ebf5866SFelix Guo# 56ebf5866SFelix Guo# Copyright (C) 2019, Google LLC. 66ebf5866SFelix Guo# Author: Felix Guo <felixguoxiuping@gmail.com> 76ebf5866SFelix Guo# Author: Brendan Higgins <brendanhiggins@google.com> 86ebf5866SFelix Guo 987c9c163SBrendan Higginsimport importlib.util 106ebf5866SFelix Guoimport logging 116ebf5866SFelix Guoimport subprocess 126ebf5866SFelix Guoimport os 13fcdb0bc0SAndy Shevchenkoimport shutil 14021ed9f5SHeidi Fahimimport signal 157d7c48dfSDaniel Latypovimport threading 167d7c48dfSDaniel Latypovfrom typing import Iterator, List, Optional, Tuple 17021ed9f5SHeidi Fahim 186ebf5866SFelix Guoimport kunit_config 19021ed9f5SHeidi Fahimimport kunit_parser 2087c9c163SBrendan Higginsimport qemu_config 216ebf5866SFelix Guo 226ebf5866SFelix GuoKCONFIG_PATH = '.config' 23fcdb0bc0SAndy ShevchenkoKUNITCONFIG_PATH = '.kunitconfig' 24d9d6b822SDavid GowDEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config' 25021ed9f5SHeidi FahimBROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config' 26128dc4bcSAndy ShevchenkoOUTFILE_PATH = 'test.log' 2787c9c163SBrendan HigginsABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__)) 2887c9c163SBrendan HigginsQEMU_CONFIGS_DIR = os.path.join(ABS_TOOL_PATH, 'qemu_configs') 296ebf5866SFelix Guo 30f3ed003eSAndy Shevchenkodef get_file_path(build_dir, default): 31f3ed003eSAndy Shevchenko if build_dir: 32f3ed003eSAndy Shevchenko default = os.path.join(build_dir, default) 33f3ed003eSAndy Shevchenko return default 34f3ed003eSAndy Shevchenko 356ebf5866SFelix Guoclass ConfigError(Exception): 366ebf5866SFelix Guo """Represents an error trying to configure the Linux kernel.""" 376ebf5866SFelix Guo 386ebf5866SFelix Guo 396ebf5866SFelix Guoclass BuildError(Exception): 406ebf5866SFelix Guo """Represents an error trying to build the Linux kernel.""" 416ebf5866SFelix Guo 426ebf5866SFelix Guo 436ebf5866SFelix Guoclass LinuxSourceTreeOperations(object): 446ebf5866SFelix Guo """An abstraction over command line operations performed on a source tree.""" 456ebf5866SFelix Guo 4687c9c163SBrendan Higgins def __init__(self, linux_arch: str, cross_compile: Optional[str]): 4787c9c163SBrendan Higgins self._linux_arch = linux_arch 4887c9c163SBrendan Higgins self._cross_compile = cross_compile 4987c9c163SBrendan Higgins 5009641f7cSDaniel Latypov def make_mrproper(self) -> None: 516ebf5866SFelix Guo try: 525a9fcad7SWill Chen subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT) 536ebf5866SFelix Guo except OSError as e: 541abdd39fSDaniel Latypov raise ConfigError('Could not call make command: ' + str(e)) 556ebf5866SFelix Guo except subprocess.CalledProcessError as e: 561abdd39fSDaniel Latypov raise ConfigError(e.output.decode()) 576ebf5866SFelix Guo 5887c9c163SBrendan Higgins def make_arch_qemuconfig(self, kconfig: kunit_config.Kconfig) -> None: 5987c9c163SBrendan Higgins pass 6087c9c163SBrendan Higgins 6187c9c163SBrendan Higgins def make_allyesconfig(self, build_dir, make_options) -> None: 6287c9c163SBrendan Higgins raise ConfigError('Only the "um" arch is supported for alltests') 6387c9c163SBrendan Higgins 6409641f7cSDaniel Latypov def make_olddefconfig(self, build_dir, make_options) -> None: 6587c9c163SBrendan Higgins command = ['make', 'ARCH=' + self._linux_arch, 'olddefconfig'] 6687c9c163SBrendan Higgins if self._cross_compile: 6787c9c163SBrendan Higgins command += ['CROSS_COMPILE=' + self._cross_compile] 680476e69fSGreg Thelen if make_options: 690476e69fSGreg Thelen command.extend(make_options) 706ebf5866SFelix Guo if build_dir: 716ebf5866SFelix Guo command += ['O=' + build_dir] 7287c9c163SBrendan Higgins print('Populating config with:\n$', ' '.join(command)) 736ebf5866SFelix Guo try: 745a9fcad7SWill Chen subprocess.check_output(command, stderr=subprocess.STDOUT) 756ebf5866SFelix Guo except OSError as e: 761abdd39fSDaniel Latypov raise ConfigError('Could not call make command: ' + str(e)) 776ebf5866SFelix Guo except subprocess.CalledProcessError as e: 781abdd39fSDaniel Latypov raise ConfigError(e.output.decode()) 796ebf5866SFelix Guo 8087c9c163SBrendan Higgins def make(self, jobs, build_dir, make_options) -> None: 8187c9c163SBrendan Higgins command = ['make', 'ARCH=' + self._linux_arch, '--jobs=' + str(jobs)] 8287c9c163SBrendan Higgins if make_options: 8387c9c163SBrendan Higgins command.extend(make_options) 8487c9c163SBrendan Higgins if self._cross_compile: 8587c9c163SBrendan Higgins command += ['CROSS_COMPILE=' + self._cross_compile] 8687c9c163SBrendan Higgins if build_dir: 8787c9c163SBrendan Higgins command += ['O=' + build_dir] 8887c9c163SBrendan Higgins print('Building with:\n$', ' '.join(command)) 8987c9c163SBrendan Higgins try: 9087c9c163SBrendan Higgins proc = subprocess.Popen(command, 9187c9c163SBrendan Higgins stderr=subprocess.PIPE, 9287c9c163SBrendan Higgins stdout=subprocess.DEVNULL) 9387c9c163SBrendan Higgins except OSError as e: 9487c9c163SBrendan Higgins raise BuildError('Could not call execute make: ' + str(e)) 9587c9c163SBrendan Higgins except subprocess.CalledProcessError as e: 9687c9c163SBrendan Higgins raise BuildError(e.output) 9787c9c163SBrendan Higgins _, stderr = proc.communicate() 9887c9c163SBrendan Higgins if proc.returncode != 0: 9987c9c163SBrendan Higgins raise BuildError(stderr.decode()) 10087c9c163SBrendan Higgins if stderr: # likely only due to build warnings 10187c9c163SBrendan Higgins print(stderr.decode()) 10287c9c163SBrendan Higgins 1037d7c48dfSDaniel Latypov def start(self, params: List[str], build_dir: str) -> subprocess.Popen: 1047d7c48dfSDaniel Latypov raise RuntimeError('not implemented!') 10587c9c163SBrendan Higgins 10687c9c163SBrendan Higgins 10787c9c163SBrendan Higginsclass LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations): 10887c9c163SBrendan Higgins 10987c9c163SBrendan Higgins def __init__(self, qemu_arch_params: qemu_config.QemuArchParams, cross_compile: Optional[str]): 11087c9c163SBrendan Higgins super().__init__(linux_arch=qemu_arch_params.linux_arch, 11187c9c163SBrendan Higgins cross_compile=cross_compile) 11287c9c163SBrendan Higgins self._kconfig = qemu_arch_params.kconfig 11387c9c163SBrendan Higgins self._qemu_arch = qemu_arch_params.qemu_arch 11487c9c163SBrendan Higgins self._kernel_path = qemu_arch_params.kernel_path 11587c9c163SBrendan Higgins self._kernel_command_line = qemu_arch_params.kernel_command_line + ' kunit_shutdown=reboot' 11687c9c163SBrendan Higgins self._extra_qemu_params = qemu_arch_params.extra_qemu_params 11787c9c163SBrendan Higgins 11887c9c163SBrendan Higgins def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None: 11987c9c163SBrendan Higgins kconfig = kunit_config.Kconfig() 12087c9c163SBrendan Higgins kconfig.parse_from_string(self._kconfig) 12187c9c163SBrendan Higgins base_kunitconfig.merge_in_entries(kconfig) 12287c9c163SBrendan Higgins 1237d7c48dfSDaniel Latypov def start(self, params: List[str], build_dir: str) -> subprocess.Popen: 12487c9c163SBrendan Higgins kernel_path = os.path.join(build_dir, self._kernel_path) 12587c9c163SBrendan Higgins qemu_command = ['qemu-system-' + self._qemu_arch, 12687c9c163SBrendan Higgins '-nodefaults', 12787c9c163SBrendan Higgins '-m', '1024', 12887c9c163SBrendan Higgins '-kernel', kernel_path, 12987c9c163SBrendan Higgins '-append', '\'' + ' '.join(params + [self._kernel_command_line]) + '\'', 13087c9c163SBrendan Higgins '-no-reboot', 13187c9c163SBrendan Higgins '-nographic', 13287c9c163SBrendan Higgins '-serial stdio'] + self._extra_qemu_params 13387c9c163SBrendan Higgins print('Running tests with:\n$', ' '.join(qemu_command)) 1347d7c48dfSDaniel Latypov return subprocess.Popen(' '.join(qemu_command), 13587c9c163SBrendan Higgins stdin=subprocess.PIPE, 1367d7c48dfSDaniel Latypov stdout=subprocess.PIPE, 13787c9c163SBrendan Higgins stderr=subprocess.STDOUT, 138*2ab5d5e6SDaniel Latypov text=True, shell=True, errors='backslashreplace') 13987c9c163SBrendan Higgins 14087c9c163SBrendan Higginsclass LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations): 14187c9c163SBrendan Higgins """An abstraction over command line operations performed on a source tree.""" 14287c9c163SBrendan Higgins 14387c9c163SBrendan Higgins def __init__(self, cross_compile=None): 14487c9c163SBrendan Higgins super().__init__(linux_arch='um', cross_compile=cross_compile) 14587c9c163SBrendan Higgins 14609641f7cSDaniel Latypov def make_allyesconfig(self, build_dir, make_options) -> None: 147021ed9f5SHeidi Fahim kunit_parser.print_with_timestamp( 148021ed9f5SHeidi Fahim 'Enabling all CONFIGs for UML...') 14967e2fae3SBrendan Higgins command = ['make', 'ARCH=um', 'allyesconfig'] 15067e2fae3SBrendan Higgins if make_options: 15167e2fae3SBrendan Higgins command.extend(make_options) 15267e2fae3SBrendan Higgins if build_dir: 15367e2fae3SBrendan Higgins command += ['O=' + build_dir] 154021ed9f5SHeidi Fahim process = subprocess.Popen( 15567e2fae3SBrendan Higgins command, 156021ed9f5SHeidi Fahim stdout=subprocess.DEVNULL, 157021ed9f5SHeidi Fahim stderr=subprocess.STDOUT) 158021ed9f5SHeidi Fahim process.wait() 159021ed9f5SHeidi Fahim kunit_parser.print_with_timestamp( 160021ed9f5SHeidi Fahim 'Disabling broken configs to run KUnit tests...') 161a54ea2e0SDaniel Latypov 162a54ea2e0SDaniel Latypov with open(get_kconfig_path(build_dir), 'a') as config: 163a54ea2e0SDaniel Latypov with open(BROKEN_ALLCONFIG_PATH, 'r') as disable: 164a54ea2e0SDaniel Latypov config.write(disable.read()) 165021ed9f5SHeidi Fahim kunit_parser.print_with_timestamp( 166021ed9f5SHeidi Fahim 'Starting Kernel with all configs takes a few minutes...') 167021ed9f5SHeidi Fahim 1687d7c48dfSDaniel Latypov def start(self, params: List[str], build_dir: str) -> subprocess.Popen: 1696ebf5866SFelix Guo """Runs the Linux UML binary. Must be named 'linux'.""" 170f3ed003eSAndy Shevchenko linux_bin = get_file_path(build_dir, 'linux') 1717d7c48dfSDaniel Latypov return subprocess.Popen([linux_bin] + params, 17287c9c163SBrendan Higgins stdin=subprocess.PIPE, 1737d7c48dfSDaniel Latypov stdout=subprocess.PIPE, 17487c9c163SBrendan Higgins stderr=subprocess.STDOUT, 175*2ab5d5e6SDaniel Latypov text=True, errors='backslashreplace') 1766ebf5866SFelix Guo 17709641f7cSDaniel Latypovdef get_kconfig_path(build_dir) -> str: 178f3ed003eSAndy Shevchenko return get_file_path(build_dir, KCONFIG_PATH) 1796ebf5866SFelix Guo 18009641f7cSDaniel Latypovdef get_kunitconfig_path(build_dir) -> str: 181f3ed003eSAndy Shevchenko return get_file_path(build_dir, KUNITCONFIG_PATH) 182fcdb0bc0SAndy Shevchenko 18309641f7cSDaniel Latypovdef get_outfile_path(build_dir) -> str: 184f3ed003eSAndy Shevchenko return get_file_path(build_dir, OUTFILE_PATH) 185128dc4bcSAndy Shevchenko 18687c9c163SBrendan Higginsdef get_source_tree_ops(arch: str, cross_compile: Optional[str]) -> LinuxSourceTreeOperations: 18787c9c163SBrendan Higgins config_path = os.path.join(QEMU_CONFIGS_DIR, arch + '.py') 18887c9c163SBrendan Higgins if arch == 'um': 18987c9c163SBrendan Higgins return LinuxSourceTreeOperationsUml(cross_compile=cross_compile) 19087c9c163SBrendan Higgins elif os.path.isfile(config_path): 19187c9c163SBrendan Higgins return get_source_tree_ops_from_qemu_config(config_path, cross_compile)[1] 192fe678fedSDaniel Latypov 193fe678fedSDaniel Latypov options = [f[:-3] for f in os.listdir(QEMU_CONFIGS_DIR) if f.endswith('.py')] 194fe678fedSDaniel Latypov raise ConfigError(arch + ' is not a valid arch, options are ' + str(sorted(options))) 19587c9c163SBrendan Higgins 19687c9c163SBrendan Higginsdef get_source_tree_ops_from_qemu_config(config_path: str, 19758c965d8SDaniel Latypov cross_compile: Optional[str]) -> Tuple[ 19887c9c163SBrendan Higgins str, LinuxSourceTreeOperations]: 19987c9c163SBrendan Higgins # The module name/path has very little to do with where the actual file 20087c9c163SBrendan Higgins # exists (I learned this through experimentation and could not find it 20187c9c163SBrendan Higgins # anywhere in the Python documentation). 20287c9c163SBrendan Higgins # 20387c9c163SBrendan Higgins # Bascially, we completely ignore the actual file location of the config 20487c9c163SBrendan Higgins # we are loading and just tell Python that the module lives in the 20587c9c163SBrendan Higgins # QEMU_CONFIGS_DIR for import purposes regardless of where it actually 20687c9c163SBrendan Higgins # exists as a file. 20787c9c163SBrendan Higgins module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path)) 20887c9c163SBrendan Higgins spec = importlib.util.spec_from_file_location(module_path, config_path) 20987c9c163SBrendan Higgins config = importlib.util.module_from_spec(spec) 21087c9c163SBrendan Higgins # TODO(brendanhiggins@google.com): I looked this up and apparently other 21187c9c163SBrendan Higgins # Python projects have noted that pytype complains that "No attribute 21287c9c163SBrendan Higgins # 'exec_module' on _importlib_modulespec._Loader". Disabling for now. 21387c9c163SBrendan Higgins spec.loader.exec_module(config) # pytype: disable=attribute-error 21487c9c163SBrendan Higgins return config.QEMU_ARCH.linux_arch, LinuxSourceTreeOperationsQemu( 21587c9c163SBrendan Higgins config.QEMU_ARCH, cross_compile=cross_compile) 21687c9c163SBrendan Higgins 2176ebf5866SFelix Guoclass LinuxSourceTree(object): 2186ebf5866SFelix Guo """Represents a Linux kernel source tree with KUnit tests.""" 2196ebf5866SFelix Guo 22087c9c163SBrendan Higgins def __init__( 22187c9c163SBrendan Higgins self, 22287c9c163SBrendan Higgins build_dir: str, 22387c9c163SBrendan Higgins load_config=True, 22487c9c163SBrendan Higgins kunitconfig_path='', 22587c9c163SBrendan Higgins arch=None, 22687c9c163SBrendan Higgins cross_compile=None, 22787c9c163SBrendan Higgins qemu_config_path=None) -> None: 228021ed9f5SHeidi Fahim signal.signal(signal.SIGINT, self.signal_handler) 22987c9c163SBrendan Higgins if qemu_config_path: 23087c9c163SBrendan Higgins self._arch, self._ops = get_source_tree_ops_from_qemu_config( 23187c9c163SBrendan Higgins qemu_config_path, cross_compile) 23287c9c163SBrendan Higgins else: 23387c9c163SBrendan Higgins self._arch = 'um' if arch is None else arch 23487c9c163SBrendan Higgins self._ops = get_source_tree_ops(self._arch, cross_compile) 2352b8fdbbfSDaniel Latypov 2362b8fdbbfSDaniel Latypov if not load_config: 2372b8fdbbfSDaniel Latypov return 2382b8fdbbfSDaniel Latypov 239243180f5SDaniel Latypov if kunitconfig_path: 2409854781dSDaniel Latypov if os.path.isdir(kunitconfig_path): 2419854781dSDaniel Latypov kunitconfig_path = os.path.join(kunitconfig_path, KUNITCONFIG_PATH) 242243180f5SDaniel Latypov if not os.path.exists(kunitconfig_path): 243243180f5SDaniel Latypov raise ConfigError(f'Specified kunitconfig ({kunitconfig_path}) does not exist') 244243180f5SDaniel Latypov else: 2452b8fdbbfSDaniel Latypov kunitconfig_path = get_kunitconfig_path(build_dir) 2462b8fdbbfSDaniel Latypov if not os.path.exists(kunitconfig_path): 247243180f5SDaniel Latypov shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path) 2482b8fdbbfSDaniel Latypov 2492b8fdbbfSDaniel Latypov self._kconfig = kunit_config.Kconfig() 2502b8fdbbfSDaniel Latypov self._kconfig.read_from_file(kunitconfig_path) 2512b8fdbbfSDaniel Latypov 25209641f7cSDaniel Latypov def clean(self) -> bool: 2536ebf5866SFelix Guo try: 2546ebf5866SFelix Guo self._ops.make_mrproper() 2556ebf5866SFelix Guo except ConfigError as e: 2566ebf5866SFelix Guo logging.error(e) 2576ebf5866SFelix Guo return False 2586ebf5866SFelix Guo return True 2596ebf5866SFelix Guo 26009641f7cSDaniel Latypov def validate_config(self, build_dir) -> bool: 261dde54b94SHeidi Fahim kconfig_path = get_kconfig_path(build_dir) 262dde54b94SHeidi Fahim validated_kconfig = kunit_config.Kconfig() 263dde54b94SHeidi Fahim validated_kconfig.read_from_file(kconfig_path) 264dde54b94SHeidi Fahim if not self._kconfig.is_subset_of(validated_kconfig): 265dde54b94SHeidi Fahim invalid = self._kconfig.entries() - validated_kconfig.entries() 266dde54b94SHeidi Fahim message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \ 267dde54b94SHeidi Fahim 'but not in .config: %s' % ( 268dde54b94SHeidi Fahim ', '.join([str(e) for e in invalid]) 269dde54b94SHeidi Fahim ) 270dde54b94SHeidi Fahim logging.error(message) 271dde54b94SHeidi Fahim return False 272dde54b94SHeidi Fahim return True 273dde54b94SHeidi Fahim 27409641f7cSDaniel Latypov def build_config(self, build_dir, make_options) -> bool: 2756ebf5866SFelix Guo kconfig_path = get_kconfig_path(build_dir) 2766ebf5866SFelix Guo if build_dir and not os.path.exists(build_dir): 2776ebf5866SFelix Guo os.mkdir(build_dir) 2786ebf5866SFelix Guo try: 27987c9c163SBrendan Higgins self._ops.make_arch_qemuconfig(self._kconfig) 28087c9c163SBrendan Higgins self._kconfig.write_to_file(kconfig_path) 2810476e69fSGreg Thelen self._ops.make_olddefconfig(build_dir, make_options) 2826ebf5866SFelix Guo except ConfigError as e: 2836ebf5866SFelix Guo logging.error(e) 2846ebf5866SFelix Guo return False 285dde54b94SHeidi Fahim return self.validate_config(build_dir) 2866ebf5866SFelix Guo 28709641f7cSDaniel Latypov def build_reconfig(self, build_dir, make_options) -> bool: 28814ee5cfdSSeongJae Park """Creates a new .config if it is not a subset of the .kunitconfig.""" 2896ebf5866SFelix Guo kconfig_path = get_kconfig_path(build_dir) 2906ebf5866SFelix Guo if os.path.exists(kconfig_path): 2916ebf5866SFelix Guo existing_kconfig = kunit_config.Kconfig() 2926ebf5866SFelix Guo existing_kconfig.read_from_file(kconfig_path) 29387c9c163SBrendan Higgins self._ops.make_arch_qemuconfig(self._kconfig) 2946ebf5866SFelix Guo if not self._kconfig.is_subset_of(existing_kconfig): 2956ebf5866SFelix Guo print('Regenerating .config ...') 2966ebf5866SFelix Guo os.remove(kconfig_path) 2970476e69fSGreg Thelen return self.build_config(build_dir, make_options) 2986ebf5866SFelix Guo else: 2996ebf5866SFelix Guo return True 3006ebf5866SFelix Guo else: 3016ebf5866SFelix Guo print('Generating .config ...') 3020476e69fSGreg Thelen return self.build_config(build_dir, make_options) 3036ebf5866SFelix Guo 30487c9c163SBrendan Higgins def build_kernel(self, alltests, jobs, build_dir, make_options) -> bool: 3056ebf5866SFelix Guo try: 30667e2fae3SBrendan Higgins if alltests: 30767e2fae3SBrendan Higgins self._ops.make_allyesconfig(build_dir, make_options) 3080476e69fSGreg Thelen self._ops.make_olddefconfig(build_dir, make_options) 3090476e69fSGreg Thelen self._ops.make(jobs, build_dir, make_options) 3106ebf5866SFelix Guo except (ConfigError, BuildError) as e: 3116ebf5866SFelix Guo logging.error(e) 3126ebf5866SFelix Guo return False 313dde54b94SHeidi Fahim return self.validate_config(build_dir) 3146ebf5866SFelix Guo 3157af29141SDaniel Latypov def run_kernel(self, args=None, build_dir='', filter_glob='', timeout=None) -> Iterator[str]: 3167af29141SDaniel Latypov if not args: 3177af29141SDaniel Latypov args = [] 318b6d5799bSDavid Gow args.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt']) 319d992880bSDaniel Latypov if filter_glob: 320d992880bSDaniel Latypov args.append('kunit.filter_glob='+filter_glob) 3217d7c48dfSDaniel Latypov 3227d7c48dfSDaniel Latypov process = self._ops.start(args, build_dir) 3237d7c48dfSDaniel Latypov assert process.stdout is not None # tell mypy it's set 3247d7c48dfSDaniel Latypov 3257d7c48dfSDaniel Latypov # Enforce the timeout in a background thread. 3267d7c48dfSDaniel Latypov def _wait_proc(): 3277d7c48dfSDaniel Latypov try: 3287d7c48dfSDaniel Latypov process.wait(timeout=timeout) 3297d7c48dfSDaniel Latypov except Exception as e: 3307d7c48dfSDaniel Latypov print(e) 3317d7c48dfSDaniel Latypov process.terminate() 3327d7c48dfSDaniel Latypov process.wait() 3337d7c48dfSDaniel Latypov waiter = threading.Thread(target=_wait_proc) 3347d7c48dfSDaniel Latypov waiter.start() 3357d7c48dfSDaniel Latypov 3367d7c48dfSDaniel Latypov output = open(get_outfile_path(build_dir), 'w') 3377d7c48dfSDaniel Latypov try: 3387d7c48dfSDaniel Latypov # Tee the output to the file and to our caller in real time. 3397d7c48dfSDaniel Latypov for line in process.stdout: 3407d7c48dfSDaniel Latypov output.write(line) 341021ed9f5SHeidi Fahim yield line 3427d7c48dfSDaniel Latypov # This runs even if our caller doesn't consume every line. 3437d7c48dfSDaniel Latypov finally: 3447d7c48dfSDaniel Latypov # Flush any leftover output to the file 3457d7c48dfSDaniel Latypov output.write(process.stdout.read()) 3467d7c48dfSDaniel Latypov output.close() 3477d7c48dfSDaniel Latypov process.stdout.close() 3487d7c48dfSDaniel Latypov 3497d7c48dfSDaniel Latypov waiter.join() 3507d7c48dfSDaniel Latypov subprocess.call(['stty', 'sane']) 351021ed9f5SHeidi Fahim 35209641f7cSDaniel Latypov def signal_handler(self, sig, frame) -> None: 353021ed9f5SHeidi Fahim logging.error('Build interruption occurred. Cleaning console.') 354021ed9f5SHeidi Fahim subprocess.call(['stty', 'sane']) 355