add a --quiet flag

test-gdb and test-userland produce beautiful output by default

create def get_common_args to help forward common args to child calls...
it is ugly, but I'm lazy for a perfect solution now
This commit is contained in:
Ciro Santilli 六四事件 法轮功
2019-01-22 00:00:00 +00:00
parent 4d5ae213e0
commit 928b01f458
8 changed files with 200 additions and 137 deletions

View File

@@ -2914,7 +2914,7 @@ The target Linux kernel of the executable is a GCC toolchain build-time configur
First let's run a dynamically linked executable built with the Buildroot toolchain:
....
./build-qemu --arch aarch64 --userland
./build-qemu --arch aarch64 --user-mode
./build-userland --arch aarch64
./build-buildroot --arch aarch64
./run \
@@ -2931,7 +2931,7 @@ asdf
qw er
....
This runs link:userland/print_argv.c[]. `--userland` path resolution is analogous to <<baremetal-setup-getting-started,that of `./run --baremetal`>>.
This runs link:userland/print_argv.c[]. `--user-mode` path resolution is analogous to <<baremetal-setup-getting-started,that of `./run --baremetal`>>.
`./build-userland` is further documented at: <<userland-directory>>.

41
build
View File

@@ -48,12 +48,11 @@ class _Component:
(self.build_callback is not None) and
(self.supported_archs is None or arch in self.supported_archs)
):
self.build_callback(arch)
self.build_callback()
class Main(cli_function.CliFunction):
class Main(common.LkmcCliFunction):
def __init__(self):
super().__init__(
config_file=common.consts['config_file'],
description='''\
Build a component and all its dependencies.
@@ -110,24 +109,24 @@ This is equivalent to:
self.name_to_component_map = {
# Leaves without dependencies.
'baremetal-qemu': _Component(
lambda arch: self._run_cmd(['build-baremetal', '--emulator', 'qemu'], arch),
lambda: self.import_path_main('build-baremetal')(archs=self.env['archs'], emulators=['qemu']),
supported_archs=common.consts['crosstool_ng_supported_archs'],
),
'baremetal-gem5': _Component(
lambda arch: self._run_cmd(['build-baremetal', '--gem5'], arch),
lambda: self.import_path_main('build-baremetal')(archs=self.env['archs'], emulators=['gem5']),
supported_archs=common.consts['crosstool_ng_supported_archs'],
),
'baremetal-gem5-pbx': _Component(
lambda arch: self._run_cmd(['build-baremetal', '--gem5', '--machine', 'RealViewPBX'], arch),
lambda: self.import_path_main('build-baremetal')(archs=self.env['archs'], emulators=['gem5'], machine='RealViewPBX'),
supported_archs=common.consts['crosstool_ng_supported_archs'],
),
'buildroot': buildroot_component,
'buildroot-gcc': buildroot_component,
'copy-overlay': _Component(
lambda arch: self._run_cmd(['copy-overlay'], arch),
lambda: self.import_path_main('copy-overlay')(archs=self.env['archs']),
),
'crosstool-ng': _Component(
lambda arch: self._run_cmd(['build-crosstool-ng'], arch),
lambda: self.import_path_main('build-crosstool-ng')(archs=self.env['archs']),
supported_archs=common.consts['crosstool_ng_supported_archs'],
# http://crosstool-ng.github.io/docs/os-setup/
apt_get_pkgs={
@@ -147,7 +146,7 @@ This is equivalent to:
submodules={'crosstool-ng'},
),
'gem5': _Component(
lambda arch: self._run_cmd(['build-gem5'], arch),
lambda: self.import_path_main('build-gem5')(archs=self.env['archs']),
# TODO test it out on Docker and answer that question properly:
# https://askubuntu.com/questions/350475/how-can-i-install-gem5
apt_get_pkgs={
@@ -170,7 +169,7 @@ This is equivalent to:
submodules={'gem5'},
),
'gem5-debug': _Component(
lambda arch: self._run_cmd(['build-gem5', '--gem5-build-type', 'debug'], arch),
lambda: self.import_path_main('build-gem5')(archs=self.env['archs'], gem5_build_type='debug'),
),
'gem5-fast': _Component(
lambda arch: self._run_cmd(['build-gem5', '--gem5-build-type', 'fast'], arch),
@@ -309,18 +308,12 @@ Which components to build. Default: qemu-buildroot
'''
)
def _run_cmd(self, python_file, **kwargs):
python_file = os.path.join(common.consts['root_dir'], python_file)
run = common.import_path(python_file).Main()
run(**kwargs)
self.sh.run_cmd(cmd_abs)
def main(self, **kwargs):
self.sh = shell_helpers.ShellHelpers(dry_run=kwargs['dry_run'])
def timed_main(self):
self.sh = shell_helpers.ShellHelpers(dry_run=self.env['dry_run'])
# Decide components.
components = kwargs['components']
if kwargs['all']:
components = self.env['components']
if self.env['all']:
components = ['all']
elif components == []:
components = ['qemu-buildroot']
@@ -336,7 +329,7 @@ Which components to build. Default: qemu-buildroot
selected_components.append(component)
todo.extend(component.dependencies)
if kwargs['download_dependencies']:
if self.env['download_dependencies']:
apt_get_pkgs = {
# Core requirements for this repo.
'git',
@@ -374,7 +367,7 @@ Which components to build. Default: qemu-buildroot
python2_pkgs.update(component.python2_pkgs)
python3_pkgs.update(component.python3_pkgs)
if apt_get_pkgs or apt_build_deps:
if kwargs['travis']:
if self.env['travis']:
interacive_pkgs = {
'libsdl2-dev',
}
@@ -392,7 +385,7 @@ Which components to build. Default: qemu-buildroot
f.write(sources_txt)
else:
sudo = ['sudo']
if common.consts['in_docker'] or kwargs['travis']:
if common.consts['in_docker'] or self.env['travis']:
y = ['-y']
else:
y = []
@@ -465,7 +458,7 @@ Which components to build. Default: qemu-buildroot
# Do the build.
for component in selected_components:
component.build()
component.build(self.env['arch'])
if __name__ == '__main__':
Main().cli()

View File

@@ -9,7 +9,7 @@ class Main(common.BuildCliFunction):
def __init__(self):
super().__init__()
self.add_argument(
'--userland',
'--user-mode',
default=False,
help='Build QEMU user mode instead of system.',
)
@@ -27,7 +27,7 @@ class Main(common.BuildCliFunction):
verbose = ['V=1']
else:
verbose = []
if self.env['userland']:
if self.env['user_mode']:
target_list = '{}-linux-user'.format(self.env['arch'])
else:
target_list = '{}-softmmu'.format(self.env['arch'])

107
common.py
View File

@@ -110,7 +110,7 @@ class LkmcCliFunction(cli_function.CliFunction):
Common functionality shared across our CLI functions:
* command timing
* some common flags, e.g.: --arch, --dry-run, --verbose
* some common flags, e.g.: --arch, --dry-run, --quiet, --verbose
'''
def __init__(self, *args, defaults=None, supported_archs=None, **kwargs):
'''
@@ -122,6 +122,8 @@ class LkmcCliFunction(cli_function.CliFunction):
if defaults is None:
defaults = {}
self._defaults = defaults
self._is_common = True
self._common_args = set()
super().__init__(*args, **kwargs)
self.supported_archs = supported_archs
@@ -165,7 +167,17 @@ mkdir are generally omitted since those are obvious
)
self.add_argument(
'--print-time', default=True,
help='Print how long it took to run the command at the end.'
help='''\
Print how long it took to run the command at the end.
Implied by --quiet.
'''
)
self.add_argument(
'-q', '--quiet', default=False,
help='''\
Don't print anything to stdout, except if it is part of an interactive terminal.
TODO: implement fully, some stuff is escaping currently.
'''
)
self.add_argument(
'-v', '--verbose', default=False,
@@ -284,7 +296,22 @@ See the documentation for other values known to work.
# Userland.
self.add_argument(
'--userland-build-id', default=None
'-u', '--userland',
help='''\
Run the given userland executable in user mode instead of booting the Linux kernel
in full system mode. In gem5, user mode is called Syscall Emulation (SE) mode and
uses se.py.
Path resolution is similar to --baremetal.
'''
)
self.add_argument(
'--userland-args',
help='''\
CLI arguments to pass to the userland executable.
'''
)
self.add_argument(
'--userland-build-id'
)
# Run.
@@ -336,16 +363,21 @@ Emulator to use. If given multiple times, semantics are similar to --arch.
Valid emulators: {}
'''.format(emulators_string)
)
self._is_common = False
def __call__(self, **kwargs):
'''
For Python code calls, print the CLI equivalent of the call.
For Python code calls, in addition to base:
- print the CLI equivalent of the call
- automatically forward common arguments
'''
print_cmd = ['./' + self.extra_config_params, LF]
for line in self.get_cli(**kwargs):
print_cmd.extend(line)
print_cmd.append(LF)
shell_helpers.ShellHelpers.print_cmd(print_cmd)
if not ('quiet' in kwargs and kwargs['quiet']):
shell_helpers.ShellHelpers().print_cmd(print_cmd)
return super().__call__(**kwargs)
def _init_env(self, env):
@@ -606,24 +638,23 @@ Valid emulators: {}
env['image'] = path
def add_argument(self, *args, **kwargs):
'''
Also handle:
- modified defaults from child classes.
- common arguments to forward on Python calls
'''
shortname, longname, key, is_option = self.get_key(*args, **kwargs)
if key in self._defaults:
kwargs['default'] = self._defaults[key]
if self._is_common:
self._common_args.add(key)
super().add_argument(*args, **kwargs)
@staticmethod
def base64_encode(string):
return base64.b64encode(string.encode()).decode()
def gem5_list_checkpoint_dirs(self):
'''
List checkpoint directory, oldest first.
'''
prefix_re = re.compile(self.env['gem5_cpt_prefix'])
files = list(filter(lambda x: os.path.isdir(os.path.join(self.env['m5out_dir'], x)) and prefix_re.search(x), os.listdir(self.env['m5out_dir'])))
files.sort(key=lambda x: os.path.getmtime(os.path.join(self.env['m5out_dir'], x)))
return files
def get_elf_entry(self, elf_file_path):
readelf_header = subprocess.check_output([
self.get_toolchain_tool('readelf'),
@@ -637,6 +668,18 @@ Valid emulators: {}
break
return int(addr, 0)
def gem5_list_checkpoint_dirs(self):
'''
List checkpoint directory, oldest first.
'''
prefix_re = re.compile(self.env['gem5_cpt_prefix'])
files = list(filter(lambda x: os.path.isdir(os.path.join(self.env['m5out_dir'], x)) and prefix_re.search(x), os.listdir(self.env['m5out_dir'])))
files.sort(key=lambda x: os.path.getmtime(os.path.join(self.env['m5out_dir'], x)))
return files
def get_common_args(self):
return {key:self.env[key] for key in self._common_args}
def get_stats(self, stat_re=None, stats_file=None):
if stat_re is None:
stat_re = '^system.cpu[0-9]*.numCycles$'
@@ -737,29 +780,41 @@ Valid emulators: {}
'''
env = kwargs.copy()
env.update(consts)
if env['all_archs']:
env['archs'] = consts['all_long_archs']
real_all_archs= env['all_archs']
if real_all_archs:
real_archs = consts['all_long_archs']
else:
real_archs = env['archs']
if env['all_emulators']:
env['emulators'] = consts['all_long_emulators']
for emulator in env['emulators']:
for arch in env['archs']:
real_emulators = consts['all_long_emulators']
else:
real_emulators = env['emulators']
for emulator in real_emulators:
for arch in real_archs:
if arch in env['arch_short_to_long_dict']:
arch = env['arch_short_to_long_dict'][arch]
if self.supported_archs is None or arch in self.supported_archs:
if not env['dry_run']:
start_time = time.time()
env['arch'] = arch
env['archs'] = [arch]
env['all_archs'] = False
env['emulator'] = emulator
env['emulators'] = [emulator]
env['all_emulators'] = False
self.env = env.copy()
self._init_env(self.env)
self.sh = shell_helpers.ShellHelpers(dry_run=self.env['dry_run'])
self.sh = shell_helpers.ShellHelpers(
dry_run=self.env['dry_run'],
quiet=self.env['quiet'],
)
ret = self.timed_main()
if not env['dry_run']:
end_time = time.time()
self._print_time(end_time - start_time)
if ret is not None and ret != 0:
return ret
elif not env['all_archs']:
elif not real_all_archs:
raise Exception('Unsupported arch for this action: ' + arch)
return 0
@@ -787,7 +842,7 @@ Valid emulators: {}
return False
def _print_time(self, ellapsed_seconds):
if self.env['print_time']:
if self.env['print_time'] and not self.env['quiet']:
hours, rem = divmod(ellapsed_seconds, 3600)
minutes, seconds = divmod(rem, 60)
print('time {:02}:{:02}:{:02}'.format(int(hours), int(minutes), int(seconds)))
@@ -859,6 +914,14 @@ Valid emulators: {}
self.env['userland_build_ext'],
)
def test_setup(self, test_env, source):
if not self.env['verbose']:
test_env['quiet'] = True
test_id_string = '{} {} {}'.format(self.env['emulator'], self.env['arch'], source)
if not self.env['quiet']:
print(test_id_string)
return test_id_string
def timed_main(self):
'''
Main action of the derived class.

91
run
View File

@@ -178,21 +178,6 @@ to use this option:
'--tmux-args',
help='''\
Parameters to pass to the program running on the tmux split. Implies --tmux.
'''
)
self.add_argument(
'-u', '--userland',
help='''\
Run the given userland executable in user mode instead of booting the Linux kernel
in full system mode. In gem5, user mode is called Syscall Emulation (SE) mode and
uses se.py.
Path resolution is similar to --baremetal.
'''
)
self.add_argument(
'--userland-args',
help='''\
CLI arguments to pass to the userland executable.
'''
)
self.add_argument(
@@ -221,6 +206,8 @@ Run QEMU with VNC instead of the default SDL. Connect to it with:
# * https://unix.stackexchange.com/questions/397939/turning-off-kaslr-to-debug-linux-kernel-using-qemu-and-gdb
# * https://stackoverflow.com/questions/44612822/unable-to-debug-kernel-with-qemu-gdb/49840927#49840927
# Turned on by default since v4.12
raise_on_failure = True
show_stdout = True
kernel_cli = 'console_msg_format=syslog nokaslr norandmaps panic=-1 printk.devkmsg=on printk.time=y rw'
if self.env['kernel_cli'] is not None:
kernel_cli += ' {}'.format(self.env['kernel_cli'])
@@ -303,6 +290,8 @@ Run QEMU with VNC instead of the default SDL. Connect to it with:
raise Exception('Baremetal ELF file not found. Tried:\n' + '\n'.join(paths))
cmd = debug_vm.copy()
if self.env['emulator'] == 'gem5':
if self.env['quiet']:
show_stdout = False
if self.env['baremetal'] is None:
if not os.path.exists(self.env['rootfs_raw_file']):
if not os.path.exists(self.env['qcow2_file']):
@@ -415,7 +404,7 @@ Run QEMU with VNC instead of the default SDL. Connect to it with:
if self.env['wait_gdb']:
# https://stackoverflow.com/questions/49296092/how-to-make-gem5-wait-for-gdb-to-connect-to-reliably-break-at-start-kernel-of-th
cmd.extend(['--param', 'system.cpu[0].wait_for_remote_gdb = True', LF])
else:
elif self.env['emulator'] == 'qemu':
qemu_user_and_system_options = [
'-trace', 'enable={},file={}'.format(trace_type, self.env['qemu_trace_file']), LF,
]
@@ -432,6 +421,8 @@ Run QEMU with VNC instead of the default SDL. Connect to it with:
qemu_user_and_system_options +
debug_args
)
show_stdout = False
raise_on_failure = False
else:
if not os.path.exists(self.env['image']):
raise_image_not_found()
@@ -452,6 +443,8 @@ Run QEMU with VNC instead of the default SDL. Connect to it with:
else:
if self.env['background']:
serial_monitor = ['-serial', 'file:{}'.format(self.env['qemu_background_serial_file']), LF]
if self.env['quiet']:
show_stdout = False
else:
serial_monitor = ['-serial', 'mon:stdio', LF]
if self.env['kvm']:
@@ -592,36 +585,44 @@ Run QEMU with VNC instead of the default SDL. Connect to it with:
out_file = None
else:
out_file = self.env['termout_file']
self.sh.run_cmd(cmd, cmd_file=self.env['run_cmd_file'], out_file=out_file, extra_env=extra_env)
# Check if guest panicked.
if self.env['emulator'] == 'gem5':
# We have to do some parsing here because gem5 exits with status 0 even when panic happens.
# Grepping for '^panic: ' does not work because some errors don't show that message.
panic_msg = b'--- BEGIN LIBC BACKTRACE ---$'
else:
panic_msg = b'Kernel panic - not syncing'
panic_re = re.compile(panic_msg)
error_string_found = False
exit_status = 0
if out_file is not None and not self.env['dry_run']:
with open(self.env['termout_file'], 'br') as logfile:
line = None
for line in logfile:
if panic_re.search(line):
exit_status = 1
if line is not None:
last_line = line.rstrip()
match = re.search(b'Simulated exit code not 0! Exit code is (\d+)', last_line)
if match:
exit_status = int(match.group(1))
if not self.env['userland']:
if os.path.exists(self.env['guest_terminal_file']):
with open(self.env['guest_terminal_file'], 'br') as logfile:
lines = logfile.readlines()
if lines and lines[-1].rstrip() == self.env['magic_fail_string']:
exit_status = self.sh.run_cmd(
cmd,
cmd_file=self.env['run_cmd_file'],
extra_env=extra_env,
out_file=out_file,
raise_on_failure=raise_on_failure,
show_stdout=show_stdout,
)
if exit_status == 0:
# Check if guest panicked.
if self.env['emulator'] == 'gem5':
# We have to do some parsing here because gem5 exits with status 0 even when panic happens.
# Grepping for '^panic: ' does not work because some errors don't show that message.
panic_msg = b'--- BEGIN LIBC BACKTRACE ---$'
else:
panic_msg = b'Kernel panic - not syncing'
panic_re = re.compile(panic_msg)
error_string_found = False
exit_status = 0
if out_file is not None and not self.env['dry_run']:
with open(self.env['termout_file'], 'br') as logfile:
line = None
for line in logfile:
if panic_re.search(line):
exit_status = 1
if exit_status != 0:
self.log_error('simulation error detected by parsing logs')
if line is not None:
last_line = line.rstrip()
match = re.search(b'Simulated exit code not 0! Exit code is (\d+)', last_line)
if match:
exit_status = int(match.group(1))
if not self.env['userland']:
if os.path.exists(self.env['guest_terminal_file']):
with open(self.env['guest_terminal_file'], 'br') as logfile:
lines = logfile.readlines()
if lines and lines[-1].rstrip() == self.env['magic_fail_string']:
exit_status = 1
if exit_status != 0:
self.log_error('simulation error detected by parsing logs')
return exit_status
if __name__ == '__main__':

View File

@@ -31,18 +31,25 @@ class ShellHelpers:
_print_lock = threading.Lock()
def __init__(self, dry_run=False):
def __init__(self, dry_run=False, quiet=False):
'''
:param dry_run: don't run the commands, just potentially print them. Debug aid.
:type dry_run: Bool
:param quiet: don't print the commands.
:type dry_run: Bool
'''
self.dry_run = dry_run
self.quiet = quiet
@classmethod
def _print_thread_safe(cls, string):
# Python sucks: a naive print adds a bunch of random spaces to stdout,
# and then copy pasting the command fails.
# https://stackoverflow.com/questions/3029816/how-do-i-get-a-thread-safe-print-in-python-2-6
'''
Python sucks: a naive print adds a bunch of random spaces to stdout,
and then copy pasting the command fails.
https://stackoverflow.com/questions/3029816/how-do-i-get-a-thread-safe-print-in-python-2-6
The initial use case was test-gdb which must create a thread for GDB to run the program in parallel.
'''
cls._print_lock.acquire()
sys.stdout.write(string + '\n')
sys.stdout.flush()
@@ -107,8 +114,7 @@ class ShellHelpers:
update=1,
)
@classmethod
def print_cmd(cls, cmd, cwd=None, cmd_file=None, extra_env=None, extra_paths=None):
def print_cmd(self, cmd, cwd=None, cmd_file=None, extra_env=None, extra_paths=None):
'''
Print cmd_to_string to stdout.
@@ -121,8 +127,9 @@ class ShellHelpers:
if type(cmd) is str:
cmd_string = cmd
else:
cmd_string = cls.cmd_to_string(cmd, cwd=cwd, extra_env=extra_env, extra_paths=extra_paths)
cls._print_thread_safe('+ ' + cmd_string)
cmd_string = self.cmd_to_string(cmd, cwd=cwd, extra_env=extra_env, extra_paths=extra_paths)
if not self.quiet:
self._print_thread_safe('+ ' + cmd_string)
if cmd_file is not None:
with open(cmd_file, 'w') as f:
f.write('#!/usr/bin/env bash\n')

View File

@@ -7,7 +7,11 @@ import common
class Main(common.LkmcCliFunction):
def __init__(self):
super().__init__()
super().__init__(
defaults={
'print_time': False,
},
)
self.add_argument(
'tests',
metavar='tests',
@@ -19,8 +23,8 @@ found by searching for the Python test files.
)
def timed_main(self):
run = self.import_path('run').Main()
run_gdb = self.import_path('run-gdb').Main()
run = self.import_path_main('run')
run_gdb = self.import_path_main('run-gdb')
if self.env['arch'] in self.env['crosstool_ng_supported_archs']:
if self.env['tests'] == []:
test_scripts_noext = []
@@ -38,22 +42,17 @@ found by searching for the Python test files.
else:
test_scripts_noext = self.env['tests']
for test_script_noext in test_scripts_noext:
run_thread = threading.Thread(target=lambda: run(
archs=[self.env['arch']],
background=True,
baremetal=test_script_noext,
dry_run=self.env['dry_run'],
emulators=self.env['emulator'],
wait_gdb=True
))
common_args = self.get_common_args()
common_args['baremetal'] = test_script_noext
test_id_string = self.test_setup(common_args, test_script_noext)
run_args = common_args.copy()
run_args['wait_gdb'] = True
run_args['background'] = True
run_thread = threading.Thread(target=lambda: run(**run_args))
run_thread.start()
run_gdb(
archs=[self.env['arch']],
baremetal=test_script_noext,
dry_run=self.env['dry_run'],
emulators=self.env['emulator'],
test=True,
)
gdb_args = common_args.copy()
gdb_args['test'] = True
run_gdb(**gdb_args)
run_thread.join()
if __name__ == '__main__':

View File

@@ -1,12 +1,17 @@
#!/usr/bin/env python3
import os
import sys
import common
class Main(common.LkmcCliFunction):
def __init__(self):
super().__init__()
super().__init__(
defaults={
'print_time': False,
},
)
self.add_argument(
'tests',
metavar='tests',
@@ -17,13 +22,10 @@ If given, run only the given tests. Otherwise, run all tests.
)
def timed_main(self):
run = self.import_path('run').Main()
run = self.import_path_main('run')
run_args = self.get_common_args()
if self.env['emulator'] == 'gem5':
extra_args = {
'userland_build_id': 'static',
}
else:
extra_args = {}
run_args['userland_build_id'] = 'static'
if self.env['tests'] == []:
sources = [
'add.c',
@@ -46,21 +48,19 @@ If given, run only the given tests. Otherwise, run all tests.
else:
sources = self.env['tests']
for source in sources:
exit_status = run(
archs=[self.env['arch']],
dry_run=self.env['dry_run'],
userland=source,
emulators=[self.env['emulator']],
**extra_args,
)
run_args['userland'] = source
test_id_string = self.test_setup(run_args, source)
run_args['background'] = True
exit_status = run(**run_args)
# TODO forward all args attempt. In particular, --dry-run.
#new_env = self.env.copy()
#new_env['userland'] = source
#new_env['emulator'] = emulator
#new_env.update(extra_args)
#new_env.update(run_args)
#exit_status = run(**new_env)
if exit_status != 0:
raise Exception('Test failed: {} {} {} {}'.format(emulator, arch, source, exit_status))
self.log_error('test failed, program exit status: {} test id: {}'.format(exit_status, test_id_string))
sys.exit(1)
if __name__ == '__main__':
Main().cli()