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 1558c965d8SDaniel Latypovfrom typing import Iterator, Optional, Tuple 16021ed9f5SHeidi Fahim 176ebf5866SFelix Guoimport kunit_config 18021ed9f5SHeidi Fahimimport kunit_parser 1987c9c163SBrendan Higginsimport qemu_config 206ebf5866SFelix Guo 216ebf5866SFelix GuoKCONFIG_PATH = '.config' 22fcdb0bc0SAndy ShevchenkoKUNITCONFIG_PATH = '.kunitconfig' 23d9d6b822SDavid GowDEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config' 24021ed9f5SHeidi FahimBROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config' 25128dc4bcSAndy ShevchenkoOUTFILE_PATH = 'test.log' 2687c9c163SBrendan HigginsABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__)) 2787c9c163SBrendan HigginsQEMU_CONFIGS_DIR = os.path.join(ABS_TOOL_PATH, 'qemu_configs') 286ebf5866SFelix Guo 29f3ed003eSAndy Shevchenkodef get_file_path(build_dir, default): 30f3ed003eSAndy Shevchenko if build_dir: 31f3ed003eSAndy Shevchenko default = os.path.join(build_dir, default) 32f3ed003eSAndy Shevchenko return default 33f3ed003eSAndy Shevchenko 346ebf5866SFelix Guoclass ConfigError(Exception): 356ebf5866SFelix Guo """Represents an error trying to configure the Linux kernel.""" 366ebf5866SFelix Guo 376ebf5866SFelix Guo 386ebf5866SFelix Guoclass BuildError(Exception): 396ebf5866SFelix Guo """Represents an error trying to build the Linux kernel.""" 406ebf5866SFelix Guo 416ebf5866SFelix Guo 426ebf5866SFelix Guoclass LinuxSourceTreeOperations(object): 436ebf5866SFelix Guo """An abstraction over command line operations performed on a source tree.""" 446ebf5866SFelix Guo 4587c9c163SBrendan Higgins def __init__(self, linux_arch: str, cross_compile: Optional[str]): 4687c9c163SBrendan Higgins self._linux_arch = linux_arch 4787c9c163SBrendan Higgins self._cross_compile = cross_compile 4887c9c163SBrendan Higgins 4909641f7cSDaniel Latypov def make_mrproper(self) -> None: 506ebf5866SFelix Guo try: 515a9fcad7SWill Chen subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT) 526ebf5866SFelix Guo except OSError as e: 531abdd39fSDaniel Latypov raise ConfigError('Could not call make command: ' + str(e)) 546ebf5866SFelix Guo except subprocess.CalledProcessError as e: 551abdd39fSDaniel Latypov raise ConfigError(e.output.decode()) 566ebf5866SFelix Guo 5787c9c163SBrendan Higgins def make_arch_qemuconfig(self, kconfig: kunit_config.Kconfig) -> None: 5887c9c163SBrendan Higgins pass 5987c9c163SBrendan Higgins 6087c9c163SBrendan Higgins def make_allyesconfig(self, build_dir, make_options) -> None: 6187c9c163SBrendan Higgins raise ConfigError('Only the "um" arch is supported for alltests') 6287c9c163SBrendan Higgins 6309641f7cSDaniel Latypov def make_olddefconfig(self, build_dir, make_options) -> None: 6487c9c163SBrendan Higgins command = ['make', 'ARCH=' + self._linux_arch, 'olddefconfig'] 6587c9c163SBrendan Higgins if self._cross_compile: 6687c9c163SBrendan Higgins command += ['CROSS_COMPILE=' + self._cross_compile] 670476e69fSGreg Thelen if make_options: 680476e69fSGreg Thelen command.extend(make_options) 696ebf5866SFelix Guo if build_dir: 706ebf5866SFelix Guo command += ['O=' + build_dir] 7187c9c163SBrendan Higgins print('Populating config with:\n$', ' '.join(command)) 726ebf5866SFelix Guo try: 735a9fcad7SWill Chen subprocess.check_output(command, stderr=subprocess.STDOUT) 746ebf5866SFelix Guo except OSError as e: 751abdd39fSDaniel Latypov raise ConfigError('Could not call make command: ' + str(e)) 766ebf5866SFelix Guo except subprocess.CalledProcessError as e: 771abdd39fSDaniel Latypov raise ConfigError(e.output.decode()) 786ebf5866SFelix Guo 7987c9c163SBrendan Higgins def make(self, jobs, build_dir, make_options) -> None: 8087c9c163SBrendan Higgins command = ['make', 'ARCH=' + self._linux_arch, '--jobs=' + str(jobs)] 8187c9c163SBrendan Higgins if make_options: 8287c9c163SBrendan Higgins command.extend(make_options) 8387c9c163SBrendan Higgins if self._cross_compile: 8487c9c163SBrendan Higgins command += ['CROSS_COMPILE=' + self._cross_compile] 8587c9c163SBrendan Higgins if build_dir: 8687c9c163SBrendan Higgins command += ['O=' + build_dir] 8787c9c163SBrendan Higgins print('Building with:\n$', ' '.join(command)) 8887c9c163SBrendan Higgins try: 8987c9c163SBrendan Higgins proc = subprocess.Popen(command, 9087c9c163SBrendan Higgins stderr=subprocess.PIPE, 9187c9c163SBrendan Higgins stdout=subprocess.DEVNULL) 9287c9c163SBrendan Higgins except OSError as e: 9387c9c163SBrendan Higgins raise BuildError('Could not call execute make: ' + str(e)) 9487c9c163SBrendan Higgins except subprocess.CalledProcessError as e: 9587c9c163SBrendan Higgins raise BuildError(e.output) 9687c9c163SBrendan Higgins _, stderr = proc.communicate() 9787c9c163SBrendan Higgins if proc.returncode != 0: 9887c9c163SBrendan Higgins raise BuildError(stderr.decode()) 9987c9c163SBrendan Higgins if stderr: # likely only due to build warnings 10087c9c163SBrendan Higgins print(stderr.decode()) 10187c9c163SBrendan Higgins 10287c9c163SBrendan Higgins def run(self, params, timeout, build_dir, outfile) -> None: 10387c9c163SBrendan Higgins pass 10487c9c163SBrendan Higgins 10587c9c163SBrendan Higgins 10687c9c163SBrendan Higginsclass LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations): 10787c9c163SBrendan Higgins 10887c9c163SBrendan Higgins def __init__(self, qemu_arch_params: qemu_config.QemuArchParams, cross_compile: Optional[str]): 10987c9c163SBrendan Higgins super().__init__(linux_arch=qemu_arch_params.linux_arch, 11087c9c163SBrendan Higgins cross_compile=cross_compile) 11187c9c163SBrendan Higgins self._kconfig = qemu_arch_params.kconfig 11287c9c163SBrendan Higgins self._qemu_arch = qemu_arch_params.qemu_arch 11387c9c163SBrendan Higgins self._kernel_path = qemu_arch_params.kernel_path 11487c9c163SBrendan Higgins self._kernel_command_line = qemu_arch_params.kernel_command_line + ' kunit_shutdown=reboot' 11587c9c163SBrendan Higgins self._extra_qemu_params = qemu_arch_params.extra_qemu_params 11687c9c163SBrendan Higgins 11787c9c163SBrendan Higgins def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None: 11887c9c163SBrendan Higgins kconfig = kunit_config.Kconfig() 11987c9c163SBrendan Higgins kconfig.parse_from_string(self._kconfig) 12087c9c163SBrendan Higgins base_kunitconfig.merge_in_entries(kconfig) 12187c9c163SBrendan Higgins 12287c9c163SBrendan Higgins def run(self, params, timeout, build_dir, outfile): 12387c9c163SBrendan Higgins kernel_path = os.path.join(build_dir, self._kernel_path) 12487c9c163SBrendan Higgins qemu_command = ['qemu-system-' + self._qemu_arch, 12587c9c163SBrendan Higgins '-nodefaults', 12687c9c163SBrendan Higgins '-m', '1024', 12787c9c163SBrendan Higgins '-kernel', kernel_path, 12887c9c163SBrendan Higgins '-append', '\'' + ' '.join(params + [self._kernel_command_line]) + '\'', 12987c9c163SBrendan Higgins '-no-reboot', 13087c9c163SBrendan Higgins '-nographic', 13187c9c163SBrendan Higgins '-serial stdio'] + self._extra_qemu_params 13287c9c163SBrendan Higgins print('Running tests with:\n$', ' '.join(qemu_command)) 13387c9c163SBrendan Higgins with open(outfile, 'w') as output: 13487c9c163SBrendan Higgins process = subprocess.Popen(' '.join(qemu_command), 13587c9c163SBrendan Higgins stdin=subprocess.PIPE, 13687c9c163SBrendan Higgins stdout=output, 13787c9c163SBrendan Higgins stderr=subprocess.STDOUT, 13887c9c163SBrendan Higgins text=True, shell=True) 13987c9c163SBrendan Higgins try: 14087c9c163SBrendan Higgins process.wait(timeout=timeout) 14187c9c163SBrendan Higgins except Exception as e: 14287c9c163SBrendan Higgins print(e) 14387c9c163SBrendan Higgins process.terminate() 14487c9c163SBrendan Higgins return process 14587c9c163SBrendan Higgins 14687c9c163SBrendan Higginsclass LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations): 14787c9c163SBrendan Higgins """An abstraction over command line operations performed on a source tree.""" 14887c9c163SBrendan Higgins 14987c9c163SBrendan Higgins def __init__(self, cross_compile=None): 15087c9c163SBrendan Higgins super().__init__(linux_arch='um', cross_compile=cross_compile) 15187c9c163SBrendan Higgins 15209641f7cSDaniel Latypov def make_allyesconfig(self, build_dir, make_options) -> None: 153021ed9f5SHeidi Fahim kunit_parser.print_with_timestamp( 154021ed9f5SHeidi Fahim 'Enabling all CONFIGs for UML...') 15567e2fae3SBrendan Higgins command = ['make', 'ARCH=um', 'allyesconfig'] 15667e2fae3SBrendan Higgins if make_options: 15767e2fae3SBrendan Higgins command.extend(make_options) 15867e2fae3SBrendan Higgins if build_dir: 15967e2fae3SBrendan Higgins command += ['O=' + build_dir] 160021ed9f5SHeidi Fahim process = subprocess.Popen( 16167e2fae3SBrendan Higgins command, 162021ed9f5SHeidi Fahim stdout=subprocess.DEVNULL, 163021ed9f5SHeidi Fahim stderr=subprocess.STDOUT) 164021ed9f5SHeidi Fahim process.wait() 165021ed9f5SHeidi Fahim kunit_parser.print_with_timestamp( 166021ed9f5SHeidi Fahim 'Disabling broken configs to run KUnit tests...') 167a54ea2e0SDaniel Latypov 168a54ea2e0SDaniel Latypov with open(get_kconfig_path(build_dir), 'a') as config: 169a54ea2e0SDaniel Latypov with open(BROKEN_ALLCONFIG_PATH, 'r') as disable: 170a54ea2e0SDaniel Latypov config.write(disable.read()) 171021ed9f5SHeidi Fahim kunit_parser.print_with_timestamp( 172021ed9f5SHeidi Fahim 'Starting Kernel with all configs takes a few minutes...') 173021ed9f5SHeidi Fahim 17487c9c163SBrendan Higgins def run(self, params, timeout, build_dir, outfile): 1756ebf5866SFelix Guo """Runs the Linux UML binary. Must be named 'linux'.""" 176f3ed003eSAndy Shevchenko linux_bin = get_file_path(build_dir, 'linux') 177128dc4bcSAndy Shevchenko outfile = get_outfile_path(build_dir) 178021ed9f5SHeidi Fahim with open(outfile, 'w') as output: 179021ed9f5SHeidi Fahim process = subprocess.Popen([linux_bin] + params, 18087c9c163SBrendan Higgins stdin=subprocess.PIPE, 181021ed9f5SHeidi Fahim stdout=output, 18287c9c163SBrendan Higgins stderr=subprocess.STDOUT, 18387c9c163SBrendan Higgins text=True) 184021ed9f5SHeidi Fahim process.wait(timeout) 1856ebf5866SFelix Guo 18609641f7cSDaniel Latypovdef get_kconfig_path(build_dir) -> str: 187f3ed003eSAndy Shevchenko return get_file_path(build_dir, KCONFIG_PATH) 1886ebf5866SFelix Guo 18909641f7cSDaniel Latypovdef get_kunitconfig_path(build_dir) -> str: 190f3ed003eSAndy Shevchenko return get_file_path(build_dir, KUNITCONFIG_PATH) 191fcdb0bc0SAndy Shevchenko 19209641f7cSDaniel Latypovdef get_outfile_path(build_dir) -> str: 193f3ed003eSAndy Shevchenko return get_file_path(build_dir, OUTFILE_PATH) 194128dc4bcSAndy Shevchenko 19587c9c163SBrendan Higginsdef get_source_tree_ops(arch: str, cross_compile: Optional[str]) -> LinuxSourceTreeOperations: 19687c9c163SBrendan Higgins config_path = os.path.join(QEMU_CONFIGS_DIR, arch + '.py') 19787c9c163SBrendan Higgins if arch == 'um': 19887c9c163SBrendan Higgins return LinuxSourceTreeOperationsUml(cross_compile=cross_compile) 19987c9c163SBrendan Higgins elif os.path.isfile(config_path): 20087c9c163SBrendan Higgins return get_source_tree_ops_from_qemu_config(config_path, cross_compile)[1] 201*fe678fedSDaniel Latypov 202*fe678fedSDaniel Latypov options = [f[:-3] for f in os.listdir(QEMU_CONFIGS_DIR) if f.endswith('.py')] 203*fe678fedSDaniel Latypov raise ConfigError(arch + ' is not a valid arch, options are ' + str(sorted(options))) 20487c9c163SBrendan Higgins 20587c9c163SBrendan Higginsdef get_source_tree_ops_from_qemu_config(config_path: str, 20658c965d8SDaniel Latypov cross_compile: Optional[str]) -> Tuple[ 20787c9c163SBrendan Higgins str, LinuxSourceTreeOperations]: 20887c9c163SBrendan Higgins # The module name/path has very little to do with where the actual file 20987c9c163SBrendan Higgins # exists (I learned this through experimentation and could not find it 21087c9c163SBrendan Higgins # anywhere in the Python documentation). 21187c9c163SBrendan Higgins # 21287c9c163SBrendan Higgins # Bascially, we completely ignore the actual file location of the config 21387c9c163SBrendan Higgins # we are loading and just tell Python that the module lives in the 21487c9c163SBrendan Higgins # QEMU_CONFIGS_DIR for import purposes regardless of where it actually 21587c9c163SBrendan Higgins # exists as a file. 21687c9c163SBrendan Higgins module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path)) 21787c9c163SBrendan Higgins spec = importlib.util.spec_from_file_location(module_path, config_path) 21887c9c163SBrendan Higgins config = importlib.util.module_from_spec(spec) 21987c9c163SBrendan Higgins # TODO(brendanhiggins@google.com): I looked this up and apparently other 22087c9c163SBrendan Higgins # Python projects have noted that pytype complains that "No attribute 22187c9c163SBrendan Higgins # 'exec_module' on _importlib_modulespec._Loader". Disabling for now. 22287c9c163SBrendan Higgins spec.loader.exec_module(config) # pytype: disable=attribute-error 22387c9c163SBrendan Higgins return config.QEMU_ARCH.linux_arch, LinuxSourceTreeOperationsQemu( 22487c9c163SBrendan Higgins config.QEMU_ARCH, cross_compile=cross_compile) 22587c9c163SBrendan Higgins 2266ebf5866SFelix Guoclass LinuxSourceTree(object): 2276ebf5866SFelix Guo """Represents a Linux kernel source tree with KUnit tests.""" 2286ebf5866SFelix Guo 22987c9c163SBrendan Higgins def __init__( 23087c9c163SBrendan Higgins self, 23187c9c163SBrendan Higgins build_dir: str, 23287c9c163SBrendan Higgins load_config=True, 23387c9c163SBrendan Higgins kunitconfig_path='', 23487c9c163SBrendan Higgins arch=None, 23587c9c163SBrendan Higgins cross_compile=None, 23687c9c163SBrendan Higgins qemu_config_path=None) -> None: 237021ed9f5SHeidi Fahim signal.signal(signal.SIGINT, self.signal_handler) 23887c9c163SBrendan Higgins if qemu_config_path: 23987c9c163SBrendan Higgins self._arch, self._ops = get_source_tree_ops_from_qemu_config( 24087c9c163SBrendan Higgins qemu_config_path, cross_compile) 24187c9c163SBrendan Higgins else: 24287c9c163SBrendan Higgins self._arch = 'um' if arch is None else arch 24387c9c163SBrendan Higgins self._ops = get_source_tree_ops(self._arch, cross_compile) 2442b8fdbbfSDaniel Latypov 2452b8fdbbfSDaniel Latypov if not load_config: 2462b8fdbbfSDaniel Latypov return 2472b8fdbbfSDaniel Latypov 248243180f5SDaniel Latypov if kunitconfig_path: 2499854781dSDaniel Latypov if os.path.isdir(kunitconfig_path): 2509854781dSDaniel Latypov kunitconfig_path = os.path.join(kunitconfig_path, KUNITCONFIG_PATH) 251243180f5SDaniel Latypov if not os.path.exists(kunitconfig_path): 252243180f5SDaniel Latypov raise ConfigError(f'Specified kunitconfig ({kunitconfig_path}) does not exist') 253243180f5SDaniel Latypov else: 2542b8fdbbfSDaniel Latypov kunitconfig_path = get_kunitconfig_path(build_dir) 2552b8fdbbfSDaniel Latypov if not os.path.exists(kunitconfig_path): 256243180f5SDaniel Latypov shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path) 2572b8fdbbfSDaniel Latypov 2582b8fdbbfSDaniel Latypov self._kconfig = kunit_config.Kconfig() 2592b8fdbbfSDaniel Latypov self._kconfig.read_from_file(kunitconfig_path) 2602b8fdbbfSDaniel Latypov 26109641f7cSDaniel Latypov def clean(self) -> bool: 2626ebf5866SFelix Guo try: 2636ebf5866SFelix Guo self._ops.make_mrproper() 2646ebf5866SFelix Guo except ConfigError as e: 2656ebf5866SFelix Guo logging.error(e) 2666ebf5866SFelix Guo return False 2676ebf5866SFelix Guo return True 2686ebf5866SFelix Guo 26909641f7cSDaniel Latypov def validate_config(self, build_dir) -> bool: 270dde54b94SHeidi Fahim kconfig_path = get_kconfig_path(build_dir) 271dde54b94SHeidi Fahim validated_kconfig = kunit_config.Kconfig() 272dde54b94SHeidi Fahim validated_kconfig.read_from_file(kconfig_path) 273dde54b94SHeidi Fahim if not self._kconfig.is_subset_of(validated_kconfig): 274dde54b94SHeidi Fahim invalid = self._kconfig.entries() - validated_kconfig.entries() 275dde54b94SHeidi Fahim message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \ 276dde54b94SHeidi Fahim 'but not in .config: %s' % ( 277dde54b94SHeidi Fahim ', '.join([str(e) for e in invalid]) 278dde54b94SHeidi Fahim ) 279dde54b94SHeidi Fahim logging.error(message) 280dde54b94SHeidi Fahim return False 281dde54b94SHeidi Fahim return True 282dde54b94SHeidi Fahim 28309641f7cSDaniel Latypov def build_config(self, build_dir, make_options) -> bool: 2846ebf5866SFelix Guo kconfig_path = get_kconfig_path(build_dir) 2856ebf5866SFelix Guo if build_dir and not os.path.exists(build_dir): 2866ebf5866SFelix Guo os.mkdir(build_dir) 2876ebf5866SFelix Guo try: 28887c9c163SBrendan Higgins self._ops.make_arch_qemuconfig(self._kconfig) 28987c9c163SBrendan Higgins self._kconfig.write_to_file(kconfig_path) 2900476e69fSGreg Thelen self._ops.make_olddefconfig(build_dir, make_options) 2916ebf5866SFelix Guo except ConfigError as e: 2926ebf5866SFelix Guo logging.error(e) 2936ebf5866SFelix Guo return False 294dde54b94SHeidi Fahim return self.validate_config(build_dir) 2956ebf5866SFelix Guo 29609641f7cSDaniel Latypov def build_reconfig(self, build_dir, make_options) -> bool: 29714ee5cfdSSeongJae Park """Creates a new .config if it is not a subset of the .kunitconfig.""" 2986ebf5866SFelix Guo kconfig_path = get_kconfig_path(build_dir) 2996ebf5866SFelix Guo if os.path.exists(kconfig_path): 3006ebf5866SFelix Guo existing_kconfig = kunit_config.Kconfig() 3016ebf5866SFelix Guo existing_kconfig.read_from_file(kconfig_path) 30287c9c163SBrendan Higgins self._ops.make_arch_qemuconfig(self._kconfig) 3036ebf5866SFelix Guo if not self._kconfig.is_subset_of(existing_kconfig): 3046ebf5866SFelix Guo print('Regenerating .config ...') 3056ebf5866SFelix Guo os.remove(kconfig_path) 3060476e69fSGreg Thelen return self.build_config(build_dir, make_options) 3076ebf5866SFelix Guo else: 3086ebf5866SFelix Guo return True 3096ebf5866SFelix Guo else: 3106ebf5866SFelix Guo print('Generating .config ...') 3110476e69fSGreg Thelen return self.build_config(build_dir, make_options) 3126ebf5866SFelix Guo 31387c9c163SBrendan Higgins def build_kernel(self, alltests, jobs, build_dir, make_options) -> bool: 3146ebf5866SFelix Guo try: 31567e2fae3SBrendan Higgins if alltests: 31667e2fae3SBrendan Higgins self._ops.make_allyesconfig(build_dir, make_options) 3170476e69fSGreg Thelen self._ops.make_olddefconfig(build_dir, make_options) 3180476e69fSGreg Thelen self._ops.make(jobs, build_dir, make_options) 3196ebf5866SFelix Guo except (ConfigError, BuildError) as e: 3206ebf5866SFelix Guo logging.error(e) 3216ebf5866SFelix Guo return False 322dde54b94SHeidi Fahim return self.validate_config(build_dir) 3236ebf5866SFelix Guo 3247af29141SDaniel Latypov def run_kernel(self, args=None, build_dir='', filter_glob='', timeout=None) -> Iterator[str]: 3257af29141SDaniel Latypov if not args: 3267af29141SDaniel Latypov args = [] 327b6d5799bSDavid Gow args.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt']) 328d992880bSDaniel Latypov if filter_glob: 329d992880bSDaniel Latypov args.append('kunit.filter_glob='+filter_glob) 330128dc4bcSAndy Shevchenko outfile = get_outfile_path(build_dir) 33187c9c163SBrendan Higgins self._ops.run(args, timeout, build_dir, outfile) 332021ed9f5SHeidi Fahim subprocess.call(['stty', 'sane']) 333021ed9f5SHeidi Fahim with open(outfile, 'r') as file: 334021ed9f5SHeidi Fahim for line in file: 335021ed9f5SHeidi Fahim yield line 336021ed9f5SHeidi Fahim 33709641f7cSDaniel Latypov def signal_handler(self, sig, frame) -> None: 338021ed9f5SHeidi Fahim logging.error('Build interruption occurred. Cleaning console.') 339021ed9f5SHeidi Fahim subprocess.call(['stty', 'sane']) 340