Reputation: 481
I want to run a process which may produce a lot of output for up to timeout seconds, capturing the stdout
/stderr
. Using capture()
and PIPE
as stdout
/stderr
is prone to deadlocking according to the documentation for subprocess
.
Now, I'm using poll()
anyways -- because I want to be able to kill the process after the timeout -- but I still don't know how to avoid the deadlock using PIPE. How do I do that?
Currently I'm just working around by creating tempfiles:
#because of the shitty api, this has to be a file, because std.PIPE is prone to deadlocking with a lot of output, and I can't figure out what to do about it
out, outfile = tempfile.mkstemp()
err, errfile = tempfile.mkstemp()
now = datetime.datetime.now().strftime('%H:%M, %Ss')
print "Running '" + exe + "' with a timeout of ", timeout , "s., starting at ", now
p = subprocess.Popen(args = exe,
stdout = out,
#for some reason, err isn't working if the process is killed by the kernel for, say, using too much memory.
stderr = err,
cwd = dir)
start = time.time()
# take care of infinite loops
sleepDuration = 0.25
time.sleep(0.1)
lastPrintedDuration = 0
duration = 0
while p.poll() is None:
duration = time.time() - start
if duration > lastPrintedDuration + 1:
lastPrintedDuration += 1
#print '.',
sys.stdout.flush()
if duration >= timeout:
p.kill()
raise Exception("Killed after " + str(duration) + "s.")
time.sleep(sleepDuration)
if p.returncode is not 0:
with open(errfile, 'r') as f:
e = f.read()
#fix empty error messages
if e == '':
e = 'Program crashed, or was killed by kernel.'
f.close()
os.close(out)
os.close(err)
os.unlink(outfile)
os.unlink(errfile)
print "Error after " + str(duration) + 's: ',
print "'" + e + "'"
raw_input('test')
raise Exception(e)
else:
print "completed in " + str(duration) + 's.'
os.close(out)
os.close(err)
os.unlink(outfile)
os.unlink(errfile)
But even this fails to capture errors if the process is killed by, say, the kernel (out of memory, etc.).
What's the ideal solution to this problem?
Upvotes: 3
Views: 1703
Reputation: 2784
The trouble with non-blocking mode is that you end up busy-waiting for I/O. The more conventional approach is to use one of the select calls. Even if you have only one file descriptor to read/write, you can stick your desired timeout on it, so you regain control after the specified interval with no further I/O.
Upvotes: 2
Reputation: 208475
Instead of using files for the output, go back to using pipes but use the fcntl module to put p.stdout
and p.stderr
into non-blocking mode. This will cause p.stdout.read()
and p.stderr.read()
to return whatever data is available or raise an IOError
if there is no data, instead of blocking:
import fcntl, os
p = subprocess.Popen(args = exe,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
cwd = dir)
fcntl.fcntl(p.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
fcntl.fcntl(p.stderr.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
outdata, errdata = '', ''
while p.poll() is None:
try:
outdata += p.stdout.read()
except IOError:
pass
try:
errdata += p.stderr.read()
except IOError:
pass
time.sleep(sleepDuration)
As glglgl pointed out in comments, you should do some additional checking in the except IOError
clause to make sure that it is not actually a real error.
Upvotes: 4