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