Categories
Software & Development

Python start process with timeout

The typical scenario to start process and wait for the result takes only a few seconds to write  in other object oriented languages (other than Python) It looks like:

Process p = new Process(command = "my command", redirect_output = True)
p.Wait(timeout = 10)
return p.stdout.result

In Python there is no such functionality provided. You have to check the process using process.poll() method. If there is too much of text in buffer you might achieve situation where your application is deadlocked.

To avoid the deadlock you have to call process.communicate(). However, if there is nothing in stdout the (main) thread waits until the process is finished or the process prints something to stdout. There are a few solutions available that does not work for all scenarios or require unix only modules.

This solution should be platform independent, should work in every situation and should give the process one second to finish gracefully by sending ctrl+c and ctrl+break signals:

def get_cmd_output(self, cmd, timeout = -1, shell = False, cwd = None):

class ProcessHandler:
# there is no subprocess.CREATE_NEW_PROCESS_GROUP available so let's declare it with all necessary constants..
CREATE_NEW_PROCESS_GROUP = 512
CTRL_C_EVENT = 0
CTRL_BREAK_EVENT = 1

def __init__ (self, process, timeout) #, logger):
self.process = process
self.timer = threading.Timer(timeout, self)
self.cancelled = False
self.logger = logger
self.__dict__["result"] = ""

if self.timer.interval > 0:
self.timer.start()

def __setattr__(self, name, val):
# if we set result value then we have to cancel timer
if name == 'result':
self.timer.cancel()

self.__dict__[name] = val

def __getattr__(self, name):
if name == 'result':
if self.cancelled:
return None

return self.__dict__[name]

def __call__ (self):
#self.logger.log("Command timeout (%s seconds) for `%s`" % (timeout, cmd))
self.cancelled = True

# send keyboard interrupt
if platform.system() == 'Windows':
self.logger.log("Sending break signal to windows process")
GenerateConsoleCtrlEvent = ctypes.windll.kernel32.GenerateConsoleCtrlEvent
GenerateConsoleCtrlEvent(ProcessHandler.CTRL_BREAK_EVENT, p.pid)
GenerateConsoleCtrlEvent(ProcessHandler.CTRL_C_EVENT, p.pid)
else:
self.logger.log("Sending break signal to non-windows process")
p.send_signal(ProcessHandler.CTRL_BREAK_EVENT)
p.send_signal(ProcessHandler.CTRL_C_EVENT)

# let's wait 1 second before finally killing the process
time.sleep(1)

# killing brutally
if p.poll() is None:
#self.logger.log("Killing process brutally...")
p.kill()

cmd_split = cmd.split(" ")

p = subprocess.Popen(cmd_split, shell = shell, cwd = cwd, stdout = subprocess.PIPE, stderr = subprocess.PIPE, creationflags = ProcessHandler.CREATE_NEW_PROCESS_GROUP)
h = ProcessHandler(p, timeout) #, logger)
h.result = p.stdout.read()

return h.result