'''
Contains two implementations of the classic unix tee command to tee output
from this python process (and all of its subprocesses) into a file and keep
directing output to stdout and stderr.
'''
import contextlib
import os
import subprocess
import sys
import time
from functools import partial
from multiprocessing import Process, Pipe
from Queue import Queue
from threading import Thread
def _unbuffer():
if sys.stdout is sys.__stdout__:
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
def _python_tee(filepath, infd, exit_signal_pipe, append=False):
'''
Python only implementation of tee used if unable to locate the system tee
program. Aims to be cross-platform.
'''
flag = 'w'
if append:
flag += '+'
redir = open(filepath, flag)
inpipe = os.fdopen(infd, 'r', 0)
queue = Queue(1)
def finish_up(queue):
# We have recieved instructions to terminate from our parent
# process. Every .1 second check if we have finished otherwise
# continue blocking read.
if queue.full():
redir.close()
return
queue.put(True)
time.sleep(.1)
finish_up(queue)
def wait_exit(queue):
# Wait until we recieve the signal to exit.
exit_signal_pipe.recv()
finish_up(queue)
def block_read(queue):
for string in iter(partial(inpipe.read, 1), b''):
redir.write(string)
sys.stdout.write(string)
if not queue.empty():
queue.get()
read_thread = Thread(target=block_read, args=(queue,))
read_thread.setDaemon(True)
read_thread.start()
wait_exit(queue)
[docs]def python_tee(filepath, append=False, stdout=True, stderr=True):
'''
Python implementation of tee.
Spawns a multiprocessing python process, a full flegged python process to
avoid the GIL problem since the subprocess will likely spend a lot of time
spinning while reading single char bytes.
'''
if stdout:
original_stdout = os.dup(sys.stdout.fileno())
# Unbuffer normal stdout
_unbuffer()
if stderr:
original_stderr= os.dup(sys.stderr.fileno())
(r, w) = os.pipe()
(exit_pipe_read, exit_pipe_write) = Pipe()
py_tee = Process(
target=_python_tee,
args=(filepath, r, exit_pipe_read, append))
py_tee.start()
if stdout:
os.dup2(w, sys.stdout.fileno())
if stderr:
os.dup2(w, sys.stderr.fileno())
# Let the body run
yield
# Close the write side of the pipe and wait for python tee to finish
# reading pipe.
os.close(w)
# Tell the subprocess that it's time to close now.
exit_pipe_write.send(True)
# Wait for the subprocess to close gracefully and flush it's pipe.
py_tee.join()
os.close(r)
if stdout:
os.dup2(original_stdout, sys.stdout.fileno())
os.close(original_stdout)
if stderr:
os.dup2(original_stderr, sys.stdout.fileno())
os.close(original_stderr)
[docs]def system_tee(filepath, append=False, stdout=True, stderr=True):
'''
An implementation of tee using the system available program tee as
a subprocess.
'''
def _unbuffer():
if sys.stdout is sys.__stdout__:
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
# Copy a reference to stdout and stderr
if stdout:
original_stdout = os.dup(sys.stdout.fileno())
if stderr:
original_stderr = os.dup(sys.stderr.fileno())
# Unbuffer normal stdout
_unbuffer()
# Build the arguments list for tee
args = ['tee']
if append:
args.append('-a')
args.append(filepath)
# Start up tee
tee_process = subprocess.Popen(args, stdin=subprocess.PIPE)
if stdout:
os.dup2(tee_process.stdin.fileno(), sys.stdout.fileno())
if stderr:
os.dup2(tee_process.stdin.fileno(), sys.stderr.fileno())
yield
# Restore original stdout and stderr
if stdout:
os.dup2(original_stdout, sys.stdout.fileno())
# Close backup stdout and stderr
os.close(original_stdout)
if stderr:
os.dup2(original_stderr, sys.stderr.fileno())
os.close(original_stderr)
# Close tee's file descriptors, wait for it to finish, and reap it
tee_process.communicate()
[docs]@contextlib.contextmanager
def tee(*args, **kwargs):
'''
A context manager for the tee command. Tries to default to the tee program
for the system for performance reasons, but if it is unavailable, will
use a pure python implementaion.
An example of usage:
>>> with tee('stdout', stdout=True, stderr=False):
>>> print ('This is going to both the file and stdout')
'''
# Test if there is a system tee program we'll use that if there is.
if any(os.access(os.path.join(path, 'tee'), os.X_OK) for path in
os.environ["PATH"].split(os.pathsep)):
return system_tee(*args, **kwargs)
else:
# Otherwise default to the slower python version.
return python_tee(*args, **kwargs)
if __name__ == '__main__':
with tee('test.out'):
print 'ayyy'
subprocess.call('echo waddup', shell=True)
print >> sys.stderr, 'uh oh'