changeset 752:673ec182679d

Allow ''timeout'' parameter to be set on sh:exec, python:exec and python:distutils commands, which kills the commands after the given number of seconds. This makes it much easier to deal with runaway processes. Small fix to killing processes-- ''kill'' isn't an attribute of subprocess. It's an attribute of subprocess.Popen. This is a good solution for #380-- set limits on the processes running the steps, and you'll eventually get through the build. Other tickets for ignoring a build fill in the rest. Closes #380. Will update http://bitten.edgewall.org/wiki/Documentation/commands.html when this goes in.
author wbell
date Sat, 24 Apr 2010 13:21:29 +0000
parents 94a4be5a1f0f
children 545be0c8f405
files bitten/build/api.py bitten/build/pythontools.py bitten/build/shtools.py bitten/recipe.py doc/commands.txt
diffstat 5 files changed, 44 insertions(+), 12 deletions(-) [+]
line wrap: on
line diff
--- a/bitten/build/api.py
+++ b/bitten/build/api.py
@@ -110,7 +110,7 @@
             raise BuildError('Error executing %s: %s %s' % (args,
                                         e.__class__.__name__, str(e)))
 
-        log.debug('Executing %s, (pid = %s)', args, p.pid)
+        log.debug('Executing %s, (pid = %s, timeout = %s)', args, p.pid, timeout)
 
         if self.input:
             if isinstance(self.input, basestring):
@@ -130,7 +130,8 @@
 
         while True:
             if limit and limit < time.time():
-                if hasattr(subprocess, 'kill'): # Python 2.6+
+                if hasattr(p, 'kill'): # Python 2.6+
+                    log.debug('Killing process.')
                     p.kill()
                 raise TimeoutError('Command %s timed out' % self.executable)
             if p.poll() != None and self.returncode == None:
--- a/bitten/build/pythontools.py
+++ b/bitten/build/pythontools.py
@@ -43,7 +43,8 @@
         return python_path
     return sys.executable
 
-def distutils(ctxt, file_='setup.py', command='build', options=None):
+def distutils(ctxt, file_='setup.py', command='build',
+              options=None, timeout=None):
     """Execute a ``distutils`` command.
     
     :param ctxt: the build context
@@ -51,6 +52,8 @@
     :param file\_: name of the file defining the distutils setup
     :param command: the setup command to execute
     :param options: additional options to pass to the command
+    :param timeout: the number of seconds before the external process should
+                    be aborted (has same constraints as CommandLine)
     """
     if options:
         if isinstance(options, basestring):
@@ -58,12 +61,15 @@
     else:
         options = []
 
+    if timeout:
+        timeout = int(timeout)
+
     cmdline = CommandLine(_python_path(ctxt),
                           [ctxt.resolve(file_), command] + options,
                           cwd=ctxt.basedir)
     log_elem = xmlio.Fragment()
     error_logged = False
-    for out, err in cmdline.execute():
+    for out, err in cmdline.execute(timeout):
         if out is not None:
             log.info(out)
             log_elem.append(xmlio.Element('message', level='info')[out])
@@ -84,7 +90,8 @@
     if not error_logged and cmdline.returncode != 0:
         ctxt.error('distutils failed (%s)' % cmdline.returncode)
 
-def exec_(ctxt, file_=None, module=None, function=None, output=None, args=None):
+def exec_(ctxt, file_=None, module=None, function=None, 
+          output=None, args=None, timeout=None):
     """Execute a Python script.
     
     Either the `file_` or the `module` parameter must be provided. If
@@ -99,6 +106,8 @@
     :param function: name of the Python function to run
     :param output: name of the file to which output should be written
     :param args: extra arguments to pass to the script
+    :param timeout: the number of seconds before the external process should
+                    be aborted (has same constraints as CommandLine)
     """
     assert file_ or module, 'Either "file" or "module" attribute required'
     if function:
@@ -124,7 +133,8 @@
 
     from bitten.build import shtools
     returncode = shtools.execute(ctxt, executable=_python_path(ctxt),
-                                 file_=file_, output=output, args=args)
+                                 file_=file_, output=output, args=args,
+								 timeout=timeout)
     if returncode != 0:
         ctxt.error('Executing %s failed (error code %s)' % \
                        (file_ or function or module, returncode))
--- a/bitten/build/shtools.py
+++ b/bitten/build/shtools.py
@@ -22,7 +22,8 @@
 
 __docformat__ = 'restructuredtext en'
 
-def exec_(ctxt, executable=None, file_=None, output=None, args=None, dir_=None):
+def exec_(ctxt, executable=None, file_=None, output=None, args=None,
+          dir_=None, timeout=None):
     """Execute a program or shell script.
     
     :param ctxt: the build context
@@ -33,12 +34,15 @@
     :param output: name of the file to which the output of the script should be
                    written
     :param args: command-line arguments to pass to the script
+    :param timeout: the number of seconds before the external process should
+                    be aborted (has same constraints as CommandLine)
     """
     assert executable or file_, \
         'Either "executable" or "file" attribute required'
 
     returncode = execute(ctxt, executable=executable, file_=file_,
-                         output=output, args=args, dir_=dir_)
+                         output=output, args=args, dir_=dir_,
+                         timeout=timeout)
     if returncode != 0:
         ctxt.error('Executing %s failed (error code %s)' % (executable or file_,
                                                             returncode))
@@ -69,7 +73,7 @@
                    % (executable or file_, returncode))
 
 def execute(ctxt, executable=None, file_=None, input_=None, output=None,
-            args=None, dir_=None, filter_=None):
+            args=None, dir_=None, filter_=None, timeout=None):
     """Generic external program execution.
     
     This function is not itself bound to a recipe command, but rather used from
@@ -86,6 +90,8 @@
                    written
     :param args: command-line arguments to pass to the script
     :param filter\_: function to filter out messages from the executable stdout
+    :param timeout: the number of seconds before the external process should
+                    be aborted (has same constraints as CommandLine)
     """
     if args:
         if isinstance(args, basestring):
@@ -136,11 +142,14 @@
     if not filter_:
         filter_=lambda s: s
 
+    if timeout:
+        timeout = int(timeout)
+
     try:
         cmdline = CommandLine(executable, args, input=input_file,
                               cwd=dir_, shell=shell)
         log_elem = xmlio.Fragment()
-        for out, err in cmdline.execute():
+        for out, err in cmdline.execute(timeout=timeout):
             if out is not None:
                 log.info(out)
                 info = filter_(out)
--- a/bitten/recipe.py
+++ b/bitten/recipe.py
@@ -25,7 +25,7 @@
     from sets import Set as set
 
 from pkg_resources import WorkingSet
-from bitten.build import BuildError
+from bitten.build import BuildError, TimeoutError
 from bitten.build.config import Configuration
 from bitten.util import xmlio
 
@@ -226,7 +226,7 @@
         for child in self._elem:
             try:
                 ctxt.run(self, child.namespace, child.name, child.attr)
-            except (BuildError, InvalidRecipeError), e:
+            except (BuildError, InvalidRecipeError, TimeoutError), e:
                 ctxt.error(e)
         if time.time() < last_finish + 1:
             # Add a delay to make sure steps appear in correct order
--- a/doc/commands.txt
+++ b/doc/commands.txt
@@ -105,6 +105,10 @@
 +----------------+-----------------------------------------------------------+
 | ``args``       | Any arguments to pass to the executable or script         |
 +----------------+-----------------------------------------------------------+
+| `timeout`      | Limits the runtime of this command to the specified       |
+|                | number of seconds, after which it will be terminated.     |
++----------------+-----------------------------------------------------------+
+
 
 Either ``executable`` or ``file`` must be specified.
 
@@ -651,6 +655,10 @@
 | ``output``     | Path to a file where any output by the script should be   |
 |                | recorded.                                                 |
 +----------------+-----------------------------------------------------------+
+| ``timeout``    | Limits the runtime of this command to the specified       |
+|                | number of seconds, after which it will be terminated.     |
++----------------+-----------------------------------------------------------+
+
 
 Either `file` or `module` must be specified.
 
@@ -692,6 +700,10 @@
 | `options`      | Additional options to pass to the command, separated by   |
 |                | spaces                                                    |
 +----------------+-----------------------------------------------------------+
+| `timeout`      | Limits the runtime of this command to the specified       |
+|                | number of seconds, after which it will be terminated.     |
++----------------+-----------------------------------------------------------+
+
 
 Examples
 --------
Copyright (C) 2012-2017 Edgewall Software