feature request: mock --timeout

Michael E Brown Michael_E_Brown at dell.com
Tue Apr 24 05:51:10 UTC 2007


On Thu, Apr 19, 2007 at 01:14:32AM -0500, Michael E Brown wrote:
> On Mon, Apr 16, 2007 at 01:07:52PM -0500, Michael E Brown wrote:
> > On Mon, Apr 16, 2007 at 12:50:11PM -0500, Clark Williams wrote:
> > > -----BEGIN PGP SIGNED MESSAGE-----
> > > Hash: SHA1
> > > 
> > > Matt Domsch wrote:
> > > > I'd like to make a feature request for mock: the ability for it to
> > > > determine a job has taken too long and kill it.  mock --timeout N (with N
> > > > in minutes) is the UI I'm picturing.

Here is my patch. I've tested:
    -- builds without timeout
    -- builds with timeout, succesful build
    -- builds with timeout, takes too long

I accidentally developed the patch against 0.6.11. I think it is a one
line change to port to 0.6.12. Will do tomorrow if needed.

I dont think this changes any behaviour, but could somebody please check
the logging?
--
Michael

Patch for implementing a hard timeout for 'rpmbuild' command, to avoid
builds that hang.

To activate:
    commandline:  --rpmbuild_timeout=N
    config file:  config_opts["rpmbuild_timeout"]=N

where N==seconds to wait before timing out

signed-off-by: Michael E Brown <michael_e_brown at dell.com>

--- mock.py-ORIGINAL	2007-04-23 23:28:30.000000000 -0500
+++ mock.py-PATCHED	2007-04-24 00:46:56.000000000 -0500
@@ -26,9 +26,11 @@
     import rpmUtils.transaction
 import rpm
 import glob
+import popen2
 import shutil
 import types
 import grp
+import signal
 import stat
 import time
 from exceptions import Exception
@@ -58,6 +60,8 @@
     def __str__(self):
         return self.msg
 
+class commandTimeoutExpired(Error): pass
+
 class YumError(Error): 
     def __init__(self, msg):
         Error.__init__(self, msg)
@@ -379,11 +383,14 @@
         
         self.state("build")
 
-        (retval, output) = self.do_chroot(cmd)
-        
-        if retval != 0:
-            error(output)
-            raise BuildError, "Error building package from %s, See build log" % srpmfn
+        try:
+            (retval, output) = self.do_chroot(cmd, timeout=self.config['rpmbuild_timeout'])
+            
+            if retval != 0:
+                error(output)
+                raise BuildError, "Error building package from %s, See build log" % srpmfn
+        except commandTimeoutExpired:
+            raise BuildError, "Error building package from %s. Exceeded rpmbuild_timeout which was set to %s seconds." % (srpmfn, self.config['rpmbuild_timeout'])
         
         bd_out = self.rootdir + self.builddir 
         rpms = glob.glob(bd_out + '/RPMS/*.rpm')
@@ -490,13 +497,15 @@
         # poof, no more file
         if os.path.exists(mf):
             os.unlink(mf)
-        
 
-    def do(self, command):
+    def do(self, command, timeout=0):
         """execute given command outside of chroot"""
+        class alarmExc(Exception): pass
+        def alarmhandler(signum,stackframe):
+            raise alarmExc("timeout expired")
         
         retval = 0
-        msg = "Executing %s" % command
+        msg = "Executing timeout(%s): %s" % (timeout, command)
         self.debug(msg)
         self.root_log(msg)
 
@@ -507,25 +516,62 @@
         if self.state() == "build":
             logfile = self._build_log
 
-        pipe = os.popen('{ ' + command + '; } 2>&1', 'r')
-        output = ""
-        for line in pipe:
-            logfile.write(line)
-            if self.config['debug']:
-                print line[:-1]
-                sys.stdout.flush()
-            logfile.flush()
-            output += line
-        status = pipe.close()
-        if status is None:
-            status = 0
-        
-        if os.WIFEXITED(status):
-            retval = os.WEXITSTATUS(status)
+        output=""
+        (r,w) = os.pipe()
+        pid = os.fork()
+        if pid: #parent
+            rpid = ret = 0
+            os.close(w)
+            oldhandler=signal.signal(signal.SIGALRM,alarmhandler)
+            starttime = time.time()
+            # timeout=0 means disable alarm signal. no timeout
+            signal.alarm(timeout)
 
-        return (retval, output)
+            try:
+                # read output from child
+                r = os.fdopen(r, "r")
+                for line in r:
+                    logfile.write(line)
+                    if self.config['debug']:
+                        print line[:-1]
+                        sys.stdout.flush()
+                    logfile.flush()
+                    output += line
+
+                # close read handle, get child return status, etc
+                r.close()
+                (rpid, ret) = os.waitpid(pid, 0)
+                signal.alarm(0)
+                signal.signal(signal.SIGALRM,oldhandler)
+
+            except alarmExc:
+                os.kill(-pid, signal.SIGTERM)
+                time.sleep(1)
+                os.kill(-pid, signal.SIGKILL)
+                (rpid, ret) = os.waitpid(pid, 0)
+                signal.signal(signal.SIGALRM,oldhandler)
+                raise commandTimeoutExpired( "Timeout(%s) exceeded for command: %s" % (timeout, command))
+
+            # mask and return just return value, plus child output
+            return ((ret & 0xFF00) >> 8, output)
+
+        else: #child
+            os.close(r)
+            # become process group leader so that our parent
+            # can kill our children
+            os.setpgrp()  
+
+            child = popen2.Popen4(command)
+            child.tochild.close()
+
+            w = os.fdopen(w, "w")
+            for line in child.fromchild:
+                w.write(line)
+            w.close()
+            os._exit( (retval & 0xFF00) >> 8 )
+    
 
-    def do_chroot(self, command, fatal = False, exitcode=None):
+    def do_chroot(self, command, fatal = False, exitcode=None, timeout=0):
         """execute given command in root"""
         cmd = ""
         
@@ -539,7 +585,7 @@
                                                  self.rootdir,
                                                  self.config['runuser'],
                                                  command)
-        (ret, output) = self.do(cmd)
+        (ret, output) = self.do(cmd, timeout=timeout)
         if (ret != 0) and fatal:
             self.close()
             if exitcode:
@@ -778,6 +824,8 @@
                       default=False, help="Turn on build-root caching")
     parser.add_option("--rebuildcache", action ="store_true", dest="rebuild_cache",
                       default=False, help="Force rebuild of build-root cache")
+    parser.add_option("--rpmbuild_timeout", action="store", dest="rpmbuild_timeout", type="int",
+                      default=None, help="Fail build if rpmbuild takes longer than 'timeout' seconds ")
     
     return parser.parse_args()
 
@@ -789,6 +837,7 @@
     config_opts['rm'] = '/usr/sbin/mock-helper rm'
     config_opts['mknod'] = '/usr/sbin/mock-helper mknod'
     config_opts['yum'] = '/usr/sbin/mock-helper yum'
+    config_opts['rpmbuild_timeout'] = 0
     config_opts['runuser'] = '/sbin/runuser'
     config_opts['chroot_setup_cmd'] = 'install buildsys-build'
     config_opts['chrootuser'] = 'mockbuild'
@@ -845,6 +894,8 @@
         config_opts['statedir'] = options.statedir
     if options.uniqueext:
         config_opts['unique-ext'] = options.uniqueext
+    if options.rpmbuild_timeout is not None:
+        config_opts['rpmbuild_timeout'] = options.rpmbuild_timeout
 
 def do_clean(config_opts, init=0):
         my = None
@@ -972,7 +1023,7 @@
     
     # cmdline options override config options
     set_config_opts_per_cmdline(config_opts, options)
-    
+
     # do whatever we're here to do
     if args[0] == 'clean':
         # unset a --no-clean




More information about the buildsys mailing list