Timeout on subprocess readline in Python
Solution 1:
Thanks for all the answers!
I found a way to solve my problem by simply using select.poll to peek into standard output.
import select
...
scan_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
poll_obj = select.poll()
poll_obj.register(scan_process.stdout, select.POLLIN)
while(some_criterium and not time_limit):
poll_result = poll_obj.poll(0)
if poll_result:
line = scan_process.stdout.readline()
some_criterium = do_something(line)
update(time_limit)
Solution 2:
Here's a portable solution that enforces the timeout for reading a single line using asyncio
:
#!/usr/bin/env python3
import asyncio
import sys
from asyncio.subprocess import PIPE, STDOUT
async def run_command(*args, timeout=None):
# Start child process
# NOTE: universal_newlines parameter is not supported
process = await asyncio.create_subprocess_exec(*args,
stdout=PIPE, stderr=STDOUT)
# Read line (sequence of bytes ending with b'\n') asynchronously
while True:
try:
line = await asyncio.wait_for(process.stdout.readline(), timeout)
except asyncio.TimeoutError:
pass
else:
if not line: # EOF
break
elif do_something(line):
continue # While some criterium is satisfied
process.kill() # Timeout or some criterion is not satisfied
break
return await process.wait() # Wait for the child process to exit
if sys.platform == "win32":
loop = asyncio.ProactorEventLoop() # For subprocess' pipes on Windows
asyncio.set_event_loop(loop)
else:
loop = asyncio.get_event_loop()
returncode = loop.run_until_complete(run_command("cmd", "arg 1", "arg 2",
timeout=10))
loop.close()
Solution 3:
I used something a bit more general in Python (if I remember correctly, also pieced together from Stack Overflow questions, but I cannot recall which ones).
import thread
from threading import Timer
def run_with_timeout(timeout, default, f, *args, **kwargs):
if not timeout:
return f(*args, **kwargs)
try:
timeout_timer = Timer(timeout, thread.interrupt_main)
timeout_timer.start()
result = f(*args, **kwargs)
return result
except KeyboardInterrupt:
return default
finally:
timeout_timer.cancel()
Be warned, though. This uses an interrupt to stop whatever function you give it. This might not be a good idea for all functions and it also prevents you from closing the program with Ctrl + C during the timeout (i.e. Ctrl + C will be handled as a timeout).
You could use this and call it like:
scan_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while(some_criterium):
line = run_with_timeout(timeout, None, scan_process.stdout.readline)
if line is None:
break
else:
some_criterium = do_something(line)
It might be a bit overkill, though. I suspect there is a simpler option for your case that I don't know.
Solution 4:
While Tom's solution works, using select()
in the C idiom is more compact, this is the equivalent of your answer:
from select import select
scan_process = subprocess.Popen(command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1) # Line buffered
while some_criterium and not time_limit:
poll_result = select([scan_process.stdout], [], [], time_limit)[0]
The rest is the same.
See pydoc select.select
.
[Note: this is Unix-specific, as are some of the other answers.]
[Note 2: edited to add line buffering as per OP request]
[Note 3: the line buffering may not be reliable in all circumstances, leading to readline() blocking]