#!/usr/bin/env python """Usage: python suppress_output.py COMMAND... Run the given command and print "in progress..." messages to stderr, one per minute, as long as the command is producing some output. If the command has not recently produced any output, no further messages will be printed until it does. When the command exits with return code 0, exit with code 0. When the command exits with any other return code, print all output produced by the command and exit with the same return code. """ import sys import os import re import subprocess import signal import time import tempfile import shutil TIMEOUT = 60 def main(): command = sys.argv[1:] with tempfile.TemporaryFile("a+b", 0) as log: p = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT) # Rather than handling some signals ourselves, forward them. def forward_signal(signum, frame): p.send_signal(signum) signal.signal(signal.SIGINT, forward_signal) signal.signal(signal.SIGTERM, forward_signal) signal.signal(signal.SIGQUIT, forward_signal) signal.signal(signal.SIGHUP, forward_signal) # Wait for it to finish, and print something to indicate the # process is alive, but only if the log file has grown (to # allow continuous integration environments kill a hanging # process accurately if it produces no output). start_time = time.time() last_blip = time.time() last_log_size = log.tell() counter = [0] # Just poll it -- other approaches are more complex try: sleep_time = 0.001 while p.poll() is None: sleep_time = min(2*sleep_time, 0.5) time.sleep(sleep_time) if time.time() - last_blip > TIMEOUT: log_size = log.tell() if log_size > last_log_size: msg = " ... in progress ({0} elapsed)".format(elapsed(time.time() - start_time)) print(msg, file=sys.stderr) sys.stderr.flush() counter[0] += 1 last_blip = time.time() last_log_size = log_size ret = p.wait() except: # noqa: E722 p.terminate() raise if counter[0] > 0: if ret == 0: msg = " ... ok ({0} elapsed)".format(elapsed(time.time() - start_time)) print(msg, file=sys.stderr) sys.stderr.flush() else: msg = " ... failed ({0} elapsed, exit code {1})".format( elapsed(time.time() - start_time), ret) print(msg, file=sys.stderr) sys.stderr.flush() if ret != 0: log.seek(0) shutil.copyfileobj(log, sys.stdout.buffer) sys.exit(ret) def elapsed(t): if t < 0: sgn = '-' t = abs(t) else: sgn = '' if t < 60: return "{0}{1:.0f} s".format(sgn, round(t)) elif t < 3600: mins, secs = divmod(t, 60) return "{0}{1:.0f} min {2:.0f} s".format(sgn, mins, secs) else: hours, mins = divmod(t, 3600) mins, secs = divmod(mins, 60) return "{0}{1:.0f} h {2:.0f} min {3:.0f} s".format(sgn, hours, mins, secs) def test_elapsed(): assert elapsed(0.4) == '0 s' assert elapsed(30.3) == '30 s' assert elapsed(59.5) == '60 s' assert elapsed(60.5) == '1 min 0 s' assert elapsed(2*60 + 40.51) == '2 min 41 s' assert elapsed(60 * 59.999) == '59 min 60 s' assert elapsed(60 * 60.0) == '1 h 0 min 0 s' assert elapsed(266*3600 + 13*60 + 12.4243) == '266 h 13 min 12 s' def test_exitcode(): r0 = subprocess.call([sys.executable, __file__, sys.executable, '-c', 'import sys; sys.exit(0)']) assert r0 == 0 r1 = subprocess.call([sys.executable, __file__, sys.executable, '-c', 'import sys; sys.exit(1)']) assert r1 == 1 rs = subprocess.call([sys.executable, __file__, sys.executable, '-c', 'import os; os.kill(os.getpid(), 15)']) assert rs != 0 def test_suppress(tmpdir): p = subprocess.Popen([sys.executable, __file__, sys.executable, '-c', 'import sys; ' 'sys.stdout.write("OUT"); ' 'sys.stderr.write("ERR"); ' 'sys.exit(0)'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() assert p.returncode == 0 assert out == b'' assert err == b'' p = subprocess.Popen([sys.executable, __file__, sys.executable, '-c', 'import sys; ' 'sys.stdout.write("OUT"); ' 'sys.stdout.flush(); ' 'sys.stderr.write("ERR"); ' 'sys.exit(1)'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() assert p.returncode == 1 assert out == b'OUTERR' assert err == b'' def run_script_fast(path, script): fn = os.path.join(path, 'suppress_output.py') with open(__file__, 'rb') as f: text = f.read() text = text.replace(b'TIMEOUT = 60', b'TIMEOUT = 1') with open(fn, 'wb') as f: f.write(text) p = subprocess.Popen([sys.executable, fn, sys.executable, '-c', script], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() return p.returncode, out, err def test_suppress_long_ok(tmpdir): returncode, out, err = run_script_fast(str(tmpdir), 'import sys, time; ' 'sys.stdout.write("OUT"); ' 'time.sleep(1.5); ' 'sys.stderr.write("ERR"); ' 'sys.exit(0)') assert returncode == 0 assert out == b'' assert re.match(rb'^ \.\.\. in progress \([0-9 sminh]* elapsed\)\n' rb' \.\.\. ok \([0-9 sminh]* elapsed\)\n$', err, re.S) def test_suppress_long_failed(tmpdir): returncode, out, err = run_script_fast(str(tmpdir), 'import sys, time; ' 'sys.stdout.write("OUT"); ' 'time.sleep(1.5); ' 'sys.stdout.flush(); ' 'sys.stderr.write("ERR"); ' 'sys.exit(1)') assert returncode == 1 assert out == b'OUTERR' assert re.match(rb'^ \.\.\. in progress \([0-9 sminh]* elapsed\)\n' rb' \.\.\. failed \([0-9 sminh]* elapsed, exit code 1\)\n$', err, re.S) if __name__ == "__main__": main()