feature request: mock --timeout

Michael E Brown Michael_E_Brown at dell.com
Tue Apr 24 13:55:42 UTC 2007


On Tue, Apr 24, 2007 at 08:41:36AM -0500, Michael E Brown wrote:
> On Tue, Apr 24, 2007 at 01:28:03AM -0500, Michael E Brown wrote:
> > On Tue, Apr 24, 2007 at 12:51:10AM -0500, Michael E Brown wrote:
> > > 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.
> > > 
> > 
> > And here is the same patch against (what I hope is) the mock-0-6-branch.
> > 
> > I ran:
> >  $ git-format-patch origin/mock-0-6-branch
> >  0001-Patch-for-implementing-a-hard-timeout-for-rpmbuild.patch
> > 
> > I dont have a machine set up to test this patch. it was a one line fix
> > from the previous submission.
> > 
> > I'm not too handy with git yet, so I hope this is right... :)
> 
> Obviously not too handy with mutt, either, as I forgot to add the patch.

Ok, It wasnt me that time, as I had the patch there, honest! Looks like
something stripped it off :( Lets try again a different way.

(sorry for all the extra traffic.)
--
Michael

> From 40724405ee7c38ec5a4ba7ccecb7ae0bc7889d65 Mon Sep 17 00:00:00 2001
> From: Michael E Brown <mebrown at michaels-house.net>
> Date: Tue, 24 Apr 2007 01:00:05 -0500
> Subject: [PATCH] implementing a hard timeout for 'rpmbuild' command

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

where N==seconds to wait before timing out
---
 mock.py |  105 ++++++++++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 78 insertions(+), 27 deletions(-)

diff --git a/mock.py b/mock.py
index 497a95f..370f48c 100644
--- a/mock.py
+++ b/mock.py
@@ -26,9 +26,11 @@ except:
     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 @@ class Error(Exception):
     def __str__(self):
         return self.msg
 
+class commandTimeoutExpired(Error): pass
+
 class YumError(Error): 
     def __init__(self, msg):
         Error.__init__(self, msg)
@@ -378,11 +382,14 @@ class Root:
         
         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')
@@ -489,13 +496,15 @@ class Root:
         # 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)
 
@@ -506,25 +515,62 @@ class Root:
         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'] or self.config['verbose']:
-                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'] or self.config['verbose']:
+                        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 = ""
         
@@ -538,7 +584,7 @@ class Root:
                                                  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:
@@ -785,6 +831,8 @@ def command_parse():
                       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()
 
@@ -796,6 +844,7 @@ def setup_default_config_opts(config_opts):
     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'
@@ -852,6 +901,8 @@ def set_config_opts_per_cmdline(config_opts, options):
         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
@@ -979,7 +1030,7 @@ def main():
     
     # 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
-- 
1.5.0.6




More information about the buildsys mailing list