|  | #!/usr/bin/env python3 | 
|  | # -*- coding: utf-8 -*- | 
|  | # | 
|  | # Copyright (C) 2017-2020  The Project X-Ray Authors. | 
|  | # | 
|  | # Use of this source code is governed by a ISC-style | 
|  | # license that can be found in the LICENSE file or at | 
|  | # https://opensource.org/licenses/ISC | 
|  | # | 
|  | # SPDX-License-Identifier: ISC | 
|  |  | 
|  | from datetime import datetime, timedelta | 
|  |  | 
|  | import argparse | 
|  | import collections | 
|  | import io | 
|  | import os | 
|  | import os.path | 
|  | import re | 
|  | import resource | 
|  | import signal | 
|  | import subprocess | 
|  | import sys | 
|  | import traceback | 
|  | from utils.create_environment import get_environment_variables | 
|  |  | 
|  | import junit_xml | 
|  |  | 
|  | secs_in_min = 60 | 
|  | secs_in_hour = 60 * secs_in_min | 
|  |  | 
|  |  | 
|  | def pretty_timedelta_str(d): | 
|  | """Pretty print a time delta object. | 
|  |  | 
|  | >>> pretty_timedelta_str(timedelta(seconds=2)) | 
|  | '2s' | 
|  |  | 
|  | >>> pretty_timedelta_str(timedelta(seconds=61)) | 
|  | '1m01s' | 
|  |  | 
|  | >>> pretty_timedelta_str(timedelta(seconds=125)) | 
|  | '2m05s' | 
|  |  | 
|  | >>> pretty_timedelta_str(timedelta(seconds=secs_in_hour * 5)) | 
|  | '5h00m' | 
|  |  | 
|  | Once we get to displaying hours, we don't bother displaying seconds. | 
|  | >>> pretty_timedelta_str(timedelta(seconds=secs_in_hour * 5 + 1)) | 
|  | '5h00m' | 
|  |  | 
|  | >>> pretty_timedelta_str(timedelta( | 
|  | ...   seconds=secs_in_hour * 5 + secs_in_min * 2 + 1)) | 
|  | '5h02m' | 
|  |  | 
|  | >>> pretty_timedelta_str(timedelta(seconds=secs_in_hour * 5.5)) | 
|  | '5h30m' | 
|  | """ | 
|  | assert isinstance(d, timedelta) | 
|  | ts = d.total_seconds() | 
|  | assert ts > 0, (d, ts) | 
|  |  | 
|  | bits = [] | 
|  | hours = int(ts / secs_in_hour) | 
|  | if hours > 0: | 
|  | bits.append("{}h".format(hours)) | 
|  | ts -= hours * secs_in_hour | 
|  |  | 
|  | mins = int(ts / secs_in_min) | 
|  | if mins > 0 or hours > 0: | 
|  | if hours > 0: | 
|  | bits.append("{:02d}m".format(mins)) | 
|  | else: | 
|  | bits.append("{:d}m".format(mins)) | 
|  | ts -= mins * secs_in_min | 
|  |  | 
|  | if hours == 0: | 
|  | if mins > 0: | 
|  | bits.append("{:02d}s".format(int(ts))) | 
|  | else: | 
|  | bits.append("{:d}s".format(int(ts))) | 
|  |  | 
|  | assert len(bits) > 0, d | 
|  |  | 
|  | return "".join(bits) | 
|  |  | 
|  |  | 
|  | class Logger: | 
|  | """Output a bunch of lines with a prefix. | 
|  |  | 
|  | >>> l = Logger("fuzz", datetime(2001, 11, 1), 6) | 
|  | >>> l._now = lambda: datetime(2001, 11, 1, second=10) | 
|  | >>> l.log("Test!") | 
|  | 2001-11-01T00:00:10 - fuzz   -   10s: Test! | 
|  | >>> l.log("Format {} {t}", [1,], {'t': 2}) | 
|  | 2001-11-01T00:00:10 - fuzz   -   10s: Format 1 2 | 
|  | >>> l.log('''\\ | 
|  | ... Line 1 | 
|  | ... Line 2 | 
|  | ... Line 3 | 
|  | ... ''') | 
|  | 2001-11-01T00:00:10 - fuzz   -   10s: Line 1 | 
|  | 2001-11-01T00:00:10 - fuzz   -   10s: Line 2 | 
|  | 2001-11-01T00:00:10 - fuzz   -   10s: Line 3 | 
|  | """ | 
|  |  | 
|  | def __init__(self, fuzzer, time_start, padding): | 
|  | self.fuzzer = fuzzer + ' ' * (padding - len(fuzzer)) | 
|  | self.time_start = time_start | 
|  | self.queue = collections.deque() | 
|  |  | 
|  | def _now(self): | 
|  | return datetime.utcnow() | 
|  |  | 
|  | def log(self, msg, args=None, kw=None, flush=True): | 
|  | """Queue a log message. | 
|  |  | 
|  | This is safe to do in a signal handler where you can't do io. | 
|  | """ | 
|  | if args is None: | 
|  | args = [] | 
|  | if kw is None: | 
|  | kw = {} | 
|  |  | 
|  | time_log = self._now() | 
|  | self.queue.append((time_log, msg, args, kw)) | 
|  | if flush: | 
|  | self.flush() | 
|  |  | 
|  | def flush(self): | 
|  | while len(self.queue) > 0: | 
|  | time_log, msg, args, kw = self.queue.popleft() | 
|  | self._output(time_log, msg, args, kw) | 
|  |  | 
|  | def _output(self, time_log, msg, args, kw): | 
|  | running_for = time_log - self.time_start | 
|  | msg = msg.format(*args, **kw) | 
|  |  | 
|  | time_log = time_log.replace(microsecond=0) | 
|  |  | 
|  | log_prefix = "{:s} - {}/{:s} - {:>5s}: ".format( | 
|  | time_log.isoformat(), | 
|  | os.environ['XRAY_PART'], | 
|  | self.fuzzer, | 
|  | pretty_timedelta_str(running_for), | 
|  | ) | 
|  |  | 
|  | msg = "\n".join(log_prefix + x for x in msg.splitlines()) | 
|  | print(msg, flush=True) | 
|  |  | 
|  |  | 
|  | def last_lines(f, num, blocksize=100): | 
|  | r"""Return n last lines in a file. | 
|  |  | 
|  | >>> f = io.StringIO("") | 
|  | >>> last_lines(f, 100) | 
|  | [''] | 
|  |  | 
|  | >>> f = io.StringIO("1\n2") | 
|  | >>> last_lines(f, 100) | 
|  | ['1', '2'] | 
|  |  | 
|  | >>> f = io.StringIO("11\n22\n33\n44\n55") | 
|  | >>> last_lines(f, 100) | 
|  | ['11', '22', '33', '44', '55'] | 
|  |  | 
|  | >>> f = io.StringIO("11\n22\n33\n44\n55") | 
|  | >>> last_lines(f, 100, 2) | 
|  | ['11', '22', '33', '44', '55'] | 
|  |  | 
|  | >>> f = io.StringIO("11\n22\n33\n44\n55") | 
|  | >>> last_lines(f, 100, 3) | 
|  | ['11', '22', '33', '44', '55'] | 
|  |  | 
|  | >>> f = io.StringIO("11\n22\n33\n44\n55") | 
|  | >>> last_lines(f, 2, 2) | 
|  | ['44', '55'] | 
|  |  | 
|  | >>> f = io.StringIO("11\n22\n33\n44\n55") | 
|  | >>> last_lines(f, 2, 3) | 
|  | ['44', '55'] | 
|  |  | 
|  | >>> f = io.StringIO("11\n22\n33\n44\n55") | 
|  | >>> last_lines(f, 2, 4) | 
|  | ['44', '55'] | 
|  |  | 
|  | >>> f = io.StringIO("1\n22\n3333333\n") | 
|  | >>> last_lines(f, 100, 3) | 
|  | ['1', '22', '3333333', ''] | 
|  |  | 
|  | """ | 
|  | lines = collections.deque(['']) | 
|  | # Seek to the end of the file | 
|  | f.seek(0, os.SEEK_END) | 
|  | fpos = f.tell() | 
|  |  | 
|  | while len(lines) < (num + 1) and fpos > 0: | 
|  | bs = min(blocksize, fpos) | 
|  | fpos = fpos - bs | 
|  | f.seek(fpos) | 
|  | data = f.read(bs) | 
|  |  | 
|  | while True: | 
|  | lpos = data.rfind('\n') | 
|  | lines[0] = data[lpos + 1:] + lines[0] | 
|  | if lpos == -1: | 
|  | break | 
|  | lines.insert(0, '') | 
|  | data = data[:lpos] | 
|  |  | 
|  | if fpos == 0: | 
|  | lines.insert(0, '') | 
|  |  | 
|  | return list(lines)[1:] | 
|  |  | 
|  |  | 
|  | def get_usage(): | 
|  | # This function only works if you have a signal handler for the | 
|  | # signal.SIGCHLD signal. | 
|  | raw_usage = resource.getrusage(resource.RUSAGE_CHILDREN) | 
|  | # 0   ru_utime    time in user mode (float) | 
|  | # 1   ru_stime    time in system mode (float) | 
|  | # 2   ru_maxrss   maximum resident set size | 
|  | # | 
|  | # These fields are always zero on Linux | 
|  | # 3   ru_ixrss    shared memory size | 
|  | # 4   ru_idrss    unshared memory size | 
|  | # 5   ru_isrss    unshared stack size | 
|  | return "User:{}s System:{}s".format( | 
|  | int(raw_usage.ru_utime), | 
|  | int(raw_usage.ru_stime), | 
|  | ) | 
|  |  | 
|  |  | 
|  | def get_load(): | 
|  | """Return current load average. | 
|  |  | 
|  | Values is average perecntage of CPU used over 1 minute, 5 minutes, 15 | 
|  | minutes. | 
|  | """ | 
|  | a1, a5, a15 = os.getloadavg() | 
|  | cpus = os.cpu_count() | 
|  | return a1 / cpus * 100.0, a5 / cpus * 100.0, a15 / cpus * 100.0 | 
|  |  | 
|  |  | 
|  | class PsTree: | 
|  | _pstree_features = None | 
|  |  | 
|  | @classmethod | 
|  | def get_features(cls): | 
|  | if cls._pstree_features is None: | 
|  | cls._pstree_features = [] | 
|  | for f in ['-T', '-l']: | 
|  | try: | 
|  | subprocess.check_output( | 
|  | "pstree {}".format(f), | 
|  | stderr=subprocess.STDOUT, | 
|  | shell=True, | 
|  | ) | 
|  | cls._pstree_features.append(f) | 
|  | except subprocess.CalledProcessError: | 
|  | continue | 
|  | return cls._pstree_features | 
|  |  | 
|  | @classmethod | 
|  | def get(cls, pid): | 
|  | """Get processes under current one. | 
|  |  | 
|  | Requires the pstree until be installed, otherwise returns empty string. | 
|  | """ | 
|  | p = subprocess.Popen( | 
|  | "pstree {} {}".format(" ".join(cls.get_features()), pid), | 
|  | shell=True, | 
|  | stdout=subprocess.PIPE, | 
|  | stderr=subprocess.PIPE, | 
|  | stdin=subprocess.DEVNULL, | 
|  | universal_newlines=True, | 
|  | ) | 
|  | stdout, stderr = p.communicate() | 
|  |  | 
|  | # Special case segfaults. | 
|  | if 'Segmentation fault' in stderr: | 
|  | stderr = '' | 
|  | stdout = '(result too big, pstree segfaulted.)' | 
|  |  | 
|  | # If no error, just return stdout | 
|  | if not stderr: | 
|  | return stdout | 
|  |  | 
|  | return "{}\n{}".format(stdout, stderr) | 
|  |  | 
|  |  | 
|  | def mem_convert(s): | 
|  | """ | 
|  | >>> mem_convert('62G') | 
|  | 62000000000.0 | 
|  | >>> mem_convert('62M') | 
|  | 62000000.0 | 
|  | >>> mem_convert('62B') | 
|  | 62.0 | 
|  | """ | 
|  | units = { | 
|  | 'Gi': pow(2, 30), | 
|  | 'Mi': pow(2, 20), | 
|  | 'Ki': pow(2, 10), | 
|  | 'G': 1e9, | 
|  | 'M': 1e6, | 
|  | 'K': 1e3, | 
|  | 'B': 1, | 
|  | } | 
|  | u = '', | 
|  | m = 1 | 
|  | for u, m in units.items(): | 
|  | if s.endswith(u): | 
|  | break | 
|  |  | 
|  | v = float(s[:-len(u)]) | 
|  | v = v * m | 
|  | return v | 
|  |  | 
|  |  | 
|  | def get_memory(memstr=None): | 
|  | r""" | 
|  | >>> import pprint | 
|  | >>> pprint.pprint(get_memory('''\ | 
|  | ...               total        used        free      shared  buff/cache   available | 
|  | ... Mem:           62G        19G       4.8G       661M        38G        42G | 
|  | ... Swap:           0B         0B         0B | 
|  | ... ''')) | 
|  | {'mem': {'available': 42000000000.0, | 
|  | 'buff/cache': 38000000000.0, | 
|  | 'free': 4800000000.0, | 
|  | 'shared': 661000000.0, | 
|  | 'total': 62000000000.0, | 
|  | 'used': 19000000000.0}, | 
|  | 'swap': {'free': 0.0, 'total': 0.0, 'used': 0.0}} | 
|  | """ | 
|  | if memstr is None: | 
|  | memstr = subprocess.check_output( | 
|  | 'free -mh', shell=True).decode("utf-8") | 
|  |  | 
|  | lines = [x.split() for x in memstr.strip().splitlines()] | 
|  | lines[0].insert(0, 'type:') | 
|  |  | 
|  | for l in lines: | 
|  | l[0] = l[0][:-1].lower() | 
|  |  | 
|  | headers = lines[0][1:] | 
|  | lines = lines[1:] | 
|  |  | 
|  | memory = {} | 
|  | for l in lines: | 
|  | t, l = l[0], l[1:] | 
|  |  | 
|  | d = {} | 
|  | for k, v in zip(headers, l): | 
|  | d[k] = mem_convert(v) | 
|  | memory[t] = d | 
|  |  | 
|  | return memory | 
|  |  | 
|  |  | 
|  | def should_run_submake(make_flags): | 
|  | """Check if make_flags indicate that we should execute things. | 
|  |  | 
|  | See https://www.gnu.org/software/make/manual/html_node/Instead-of-Execution.html#Instead-of-Execution  # noqa | 
|  |  | 
|  | If this is a dry run or question then we shouldn't execute or output | 
|  | anything. | 
|  |  | 
|  | The flags end up as single letter versions in the MAKEFLAGS environment | 
|  | variable. | 
|  |  | 
|  | >>> should_run_submake('') | 
|  | True | 
|  |  | 
|  | The following flags are important; | 
|  |  | 
|  | -n == --dry-run | 
|  |  | 
|  | >>> should_run_submake('n') | 
|  | False | 
|  | >>> should_run_submake('n --blah') | 
|  | False | 
|  | >>> should_run_submake('--blah n') | 
|  | False | 
|  | >>> should_run_submake('--blah') | 
|  | True | 
|  | >>> should_run_submake('--random') | 
|  | True | 
|  |  | 
|  | -q == --question | 
|  |  | 
|  | >>> should_run_submake('q') | 
|  | False | 
|  | >>> should_run_submake('q --blah') | 
|  | False | 
|  | >>> should_run_submake('--blah q') | 
|  | False | 
|  | >>> should_run_submake('--blah') | 
|  | True | 
|  | >>> should_run_submake('--random') | 
|  | True | 
|  |  | 
|  | Both --dry-run and --question | 
|  |  | 
|  | >>> should_run_submake('qn') | 
|  | False | 
|  | >>> should_run_submake('nq') | 
|  | False | 
|  | >>> should_run_submake('--quiant') | 
|  | True | 
|  | """ | 
|  | r = re.search(r'(?:^|\s)[^-]*(n|q)[^\s]*(\s|$)', make_flags) | 
|  | if not r: | 
|  | return True | 
|  | return not bool(r.groups()[0]) | 
|  |  | 
|  |  | 
|  | def main(argv): | 
|  | fuzzers_dir = os.path.abspath(os.path.dirname(__file__)) | 
|  |  | 
|  | parser = argparse.ArgumentParser(description=__doc__) | 
|  | parser.add_argument("fuzzer", help="fuzzer to run") | 
|  | parser.add_argument( | 
|  | "--retries", | 
|  | type=int, | 
|  | default=0, | 
|  | help="Retry a failed fuzzer n times.", | 
|  | ) | 
|  | args = parser.parse_args() | 
|  |  | 
|  | # Setup the logger with flush=False, it should be safe to use in a signal | 
|  | # handler. | 
|  | fuzzer_length = max(len(f) for f in os.listdir(fuzzers_dir)) | 
|  | logger = Logger(args.fuzzer, datetime.utcnow(), fuzzer_length) | 
|  |  | 
|  | # Need a signal handler on SIGCHLD otherwise get_resource doesn't return | 
|  | # anything. | 
|  | signal.signal(signal.SIGCHLD, lambda sig, frame: None) | 
|  |  | 
|  | fuzzer_dir = os.path.join(fuzzers_dir, args.fuzzer) | 
|  | assert os.path.exists(fuzzer_dir), fuzzer_dir | 
|  |  | 
|  | fuzzer_logdir = os.path.join( | 
|  | fuzzer_dir, "logs_{}".format(os.environ['XRAY_PART'])) | 
|  | if not os.path.exists(fuzzer_logdir): | 
|  | os.makedirs(fuzzer_logdir) | 
|  | assert os.path.exists(fuzzer_logdir) | 
|  |  | 
|  | # Update the environment for a specific part | 
|  | environment = get_environment_variables() | 
|  | for key, value in environment.items(): | 
|  | os.environ[key] = value | 
|  |  | 
|  | exit_code = -1 | 
|  | args.retries += 1 | 
|  | for retry_count in range(0, args.retries): | 
|  | logger.log('Running fuzzer attempt: {}', [retry_count]) | 
|  | exit_code = run_fuzzer( | 
|  | args.fuzzer, | 
|  | fuzzer_dir, | 
|  | fuzzer_logdir, | 
|  | logger, | 
|  | will_retry=retry_count < (args.retries - 1), | 
|  | ) | 
|  | if exit_code <= 0: | 
|  | break | 
|  | logger.log('WARNING: Fuzzer failed!') | 
|  |  | 
|  | return exit_code | 
|  |  | 
|  |  | 
|  | def run_fuzzer(fuzzer_name, fuzzer_dir, fuzzer_logdir, logger, will_retry): | 
|  | def log(msg, *a, **k): | 
|  | logger.log(msg, a, k, flush=True) | 
|  |  | 
|  | make_cmd = os.environ.get('MAKE', 'make') | 
|  | make_flags = os.environ.get('MAKEFLAGS', '') | 
|  | # Should run things? | 
|  | if not should_run_submake(make_flags): | 
|  | return 0 | 
|  |  | 
|  | fuzzer_runok = os.path.join( | 
|  | fuzzer_dir, "run.{}.ok".format(os.environ['XRAY_PART'])) | 
|  | if os.path.exists(fuzzer_runok): | 
|  | last_modified = datetime.fromtimestamp(os.stat(fuzzer_runok).st_mtime) | 
|  |  | 
|  | log( | 
|  | "Skipping as run.{}.ok exists (updated @ {})", | 
|  | os.environ['XRAY_PART'], last_modified.isoformat()) | 
|  |  | 
|  | return 0 | 
|  |  | 
|  | fuzzer_runok = os.path.join(fuzzer_dir, "run.ok") | 
|  | if os.path.exists(fuzzer_runok): | 
|  | last_modified = datetime.fromtimestamp(os.stat(fuzzer_runok).st_mtime) | 
|  |  | 
|  | log( | 
|  | "Skipping as run.ok exists (updated @ {})", | 
|  | last_modified.isoformat()) | 
|  |  | 
|  | return 0 | 
|  |  | 
|  | time_start = datetime.utcnow() | 
|  | log("Starting @ {}", time_start.isoformat()) | 
|  |  | 
|  | running_msg = "Running {} -C {} run (with MAKEFLAGS='{}')".format( | 
|  | make_cmd, | 
|  | fuzzer_dir, | 
|  | make_flags, | 
|  | ) | 
|  | log(running_msg) | 
|  |  | 
|  | log_suffix = ".{}.log".format(time_start.isoformat()).replace(":", "-") | 
|  | fuzzer_stdout = os.path.join(fuzzer_logdir, "stdout" + log_suffix) | 
|  | fuzzer_stderr = os.path.join(fuzzer_logdir, "stderr" + log_suffix) | 
|  |  | 
|  | # Write header to stdout/stderr to make sure they match. | 
|  | for fname in [fuzzer_stdout, fuzzer_stderr]: | 
|  | with open(fname, "w") as fd: | 
|  | fd.write("Build starting @ {}\n".format(time_start.isoformat())) | 
|  | fd.write(running_msg) | 
|  | fd.write("\n") | 
|  | fd.write("-" * 75) | 
|  | fd.write("\n") | 
|  | fd.flush() | 
|  | os.fsync(fd) | 
|  |  | 
|  | # Open the log files for appending | 
|  | stdout_fd = open(fuzzer_stdout, "a") | 
|  | stderr_fd = open(fuzzer_stderr, "a") | 
|  |  | 
|  | # Play nice with make's jobserver. | 
|  | # See https://www.gnu.org/software/make/manual/html_node/POSIX-Jobserver.html#POSIX-Jobserver  # noqa | 
|  | job_fds = [] | 
|  | if '--jobserver-fds' in make_flags or '--jobserver-auth' in make_flags: | 
|  | job_re = re.search( | 
|  | '--jobserver-(?:fds|auth)=([0-9]+),([0-9]+)', make_flags) | 
|  | assert job_re, make_flags | 
|  | job_rd, job_wr = job_re.groups() | 
|  |  | 
|  | # Make copies of jobserver FDs in case a retry is needed. | 
|  |  | 
|  | job_rd = int(job_rd) | 
|  | job_wr = int(job_wr) | 
|  | assert job_rd > 2, (job_rd, job_wr, make_flags) | 
|  | assert job_wr > 2, (job_rd, job_wr, make_flags) | 
|  | job_fds.append(job_rd) | 
|  | job_fds.append(job_wr) | 
|  |  | 
|  | p = None | 
|  | try: | 
|  | p = subprocess.Popen( | 
|  | [make_cmd, '-C', fuzzer_dir, 'run'], | 
|  | stdin=None, | 
|  | stdout=stdout_fd, | 
|  | stderr=stderr_fd, | 
|  | pass_fds=job_fds, | 
|  | ) | 
|  | while True: | 
|  | try: | 
|  | retcode = p.wait(timeout=10) | 
|  | p = None | 
|  | except subprocess.TimeoutExpired: | 
|  | retcode = None | 
|  |  | 
|  | if retcode is not None: | 
|  | break | 
|  | mem = get_memory()['mem'] | 
|  | log( | 
|  | "Still running (1m:{:0.2f}%, 5m:{:0.2f}%, 15m:{:0.2f}% Mem:{:0.1f}Gi used, {:0.1f}Gi free).\n{}", | 
|  | *get_load(), | 
|  | mem['used'] / 1e9, | 
|  | mem['available'] / | 
|  | 1e9,  # Using available so the numbers add up. | 
|  | PsTree.get(p.pid), | 
|  | ) | 
|  | except (Exception, KeyboardInterrupt, SystemExit): | 
|  | retcode = -1 | 
|  | tb = io.StringIO() | 
|  | traceback.print_exc(file=tb) | 
|  | log(tb.getvalue()) | 
|  |  | 
|  | # Prevent Ctrl-C so we exit properly... | 
|  | old_sigint_handler = signal.getsignal(signal.SIGINT) | 
|  |  | 
|  | def log_die(sig, frame): | 
|  | logger.log("Dieing!") | 
|  |  | 
|  | signal.signal(signal.SIGINT, log_die) | 
|  |  | 
|  | # Cleanup child process if they haven't already died. | 
|  | try: | 
|  | if p is not None: | 
|  | try: | 
|  | retcode = p.wait(1) | 
|  | except subprocess.TimeoutExpired: | 
|  | retcode = -1 | 
|  | p.kill() | 
|  | p.wait() | 
|  | log("Warning: Killed program which should have been dead!") | 
|  | except Exception: | 
|  | tb = io.StringIO() | 
|  | traceback.print_exc(file=tb) | 
|  | log(tb.getvalue()) | 
|  |  | 
|  | # Wait for all children to finish. | 
|  | try: | 
|  | while True: | 
|  | log("Child finished: {}", os.waitpid(-1, 0)) | 
|  | except ChildProcessError: | 
|  | pass | 
|  |  | 
|  | log("Finishing ({}).", get_usage()) | 
|  |  | 
|  | time_end = datetime.utcnow() | 
|  |  | 
|  | error_log = "\n".join(last_lines(open(fuzzer_stderr), 10000)) | 
|  | success_log = "\n".join(last_lines(open(fuzzer_stdout), 100)) | 
|  |  | 
|  | # Find the next X_sponge_log.xml file name... | 
|  | for i in range(0, 100): | 
|  | tsfilename = os.path.join(fuzzer_logdir, '{}_sponge_log.xml'.format(i)) | 
|  | if not os.path.exists(tsfilename): | 
|  | break | 
|  |  | 
|  | test_case = junit_xml.TestCase( | 
|  | name=fuzzer_name, | 
|  | timestamp=time_start.timestamp(), | 
|  | elapsed_sec=(time_end - time_start).total_seconds(), | 
|  | stdout=success_log, | 
|  | stderr=error_log, | 
|  | ) | 
|  |  | 
|  | if retcode != 0: | 
|  | test_case.add_failure_info( | 
|  | 'Fuzzer failed with exit code: {}'.format(retcode)) | 
|  |  | 
|  | with open(tsfilename, 'w') as f: | 
|  | ts = junit_xml.TestSuite(fuzzer_name, [test_case]) | 
|  | junit_xml.TestSuite.to_file(f, [ts]) | 
|  |  | 
|  | if retcode != 0: | 
|  | test_case.add_failure_info( | 
|  | 'Fuzzer failed with exit code: {}'.format(retcode), ) | 
|  |  | 
|  | # Log the last 10,000 lines of stderr on a failure | 
|  | log( | 
|  | """\ | 
|  | -------------------------------------------------------------------------- | 
|  | !Failed! @ {time_end} with exit code: {retcode} | 
|  | -------------------------------------------------------------------------- | 
|  | - STDOUT (see {stdout_fname} for full log): | 
|  | -------------------------------------------------------------------------- | 
|  | {stdout_log} | 
|  | -------------------------------------------------------------------------- | 
|  | - STDERR (see {stderr_fname} for full log): | 
|  | -------------------------------------------------------------------------- | 
|  | {stderr_log} | 
|  | -------------------------------------------------------------------------- | 
|  | !Failed! @ {time_end} with exit code: {retcode} | 
|  | -------------------------------------------------------------------------- | 
|  | """, | 
|  | retcode=retcode, | 
|  | stdout_fname=fuzzer_stdout, | 
|  | stdout_log='\n'.join(last_lines(open(fuzzer_stdout), 1000)), | 
|  | stderr_fname=fuzzer_stderr, | 
|  | stderr_log='\n'.join(last_lines(open(fuzzer_stderr), 1000)), | 
|  | time_end=time_end.isoformat()) | 
|  | else: | 
|  | # Log the last 100 lines of a successful run | 
|  | log( | 
|  | """\ | 
|  | Succeeded! @ {} | 
|  | -------------------------------------------------------------------------- | 
|  | {} | 
|  | -------------------------------------------------------------------------- | 
|  | Succeeded! @ {} | 
|  | """, time_end.isoformat(), success_log, time_end.isoformat()) | 
|  |  | 
|  | logger.flush() | 
|  | signal.signal(signal.SIGINT, old_sigint_handler) | 
|  | return retcode | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | if "--test" in sys.argv or len(sys.argv) == 1: | 
|  | import doctest | 
|  | doctest.testmod() | 
|  | else: | 
|  | sys.exit(main(sys.argv)) |