chiark / gitweb /
: ${:=} not ${:=}
[autopkgtest.git] / virt-subproc / VirtSubproc.py
index 833cb5d0ea26aa0649035b363d6c9b0b34bf7431..bbe0521d6e4a3ca705e4aa0a7b231aff79a6ad8d 100644 (file)
@@ -1,7 +1,7 @@
 # VirtSubproc is part of autopkgtest
 # autopkgtest is a tool for testing Debian binary packages
 #
-# autopkgtest is Copyright (C) 2006 Canonical Ltd.
+# autopkgtest is Copyright (C) 2006-2007 Canonical Ltd.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -29,15 +29,25 @@ import urllib
 import signal
 import subprocess
 import traceback
+import re as regexp
 
 debuglevel = None
 progname = "<VirtSubproc>"
 devnull_read = file('/dev/null','r')
 caller = __main__
+copy_timeout = 300
 
 class Quit:
        def __init__(q,ec,m): q.ec = ec; q.m = m
 
+class Timeout: pass
+def alarm_handler(*a): raise Timeout()
+def timeout_start(to): signal.alarm(to)
+def timeout_stop(): signal.alarm(0)
+
+class FailedCmd:
+       def __init__(fc,e): fc.e = e
+
 def debug(m):
        if not debuglevel: return
        print >> sys.stderr, progname+": debug:", m
@@ -47,29 +57,52 @@ def bomb(m):
 
 def ok(): print 'ok'
 
-def cmdnumargs(c, ce, nargs=0):
-       if len(c) == nargs + 1: return
-       bomb("wrong number of arguments to command `%s'" % ce[0])
+def cmdnumargs(c, ce, nargs=0, noptargs=0):
+       if len(c) < 1+nargs:
+               bomb("too few arguments to command `%s'" % ce[0])
+       if noptargs is not None and len(c) > 1+nargs+noptargs:
+               bomb("too many arguments to command `%s'" % ce[0])
 
 def cmd_capabilities(c, ce):
        cmdnumargs(c, ce)
-       return caller.hook_capabilities()
+       return caller.hook_capabilities() + ['execute-debug',
+               'print-execute-command']
 
 def cmd_quit(c, ce):
        cmdnumargs(c, ce)
        raise Quit(0, '')
 
-def execute_raw(what, instr, *popenargs, **popenargsk):
+def cmd_close(c, ce):
+       cmdnumargs(c, ce)
+       if not downtmp: bomb("`close' when not open")
+       cleanup()
+
+def cmd_print_execute_command(c, ce):
+       cmdnumargs(c, ce)
+       if not downtmp: bomb("`print-execute-command' when not open")
+       if hasattr(caller,'hook_callerexeccmd'):
+               (cl,kvl) = caller.hook_callerexeccmd()
+       else:
+               cl = down
+               kvl = ['shstring']
+       return [','.join(map(urllib.quote, cl))] + kvl
+
+def preexecfn():
+       caller.hook_forked_inchild()
+
+def execute_raw(what, instr, timeout, *popenargs, **popenargsk):
        debug(" ++ %s" % string.join(popenargs[0]))
-       sp = subprocess.Popen(*popenargs, **popenargsk)
+       sp = subprocess.Popen(preexec_fn=preexecfn, *popenargs, **popenargsk)
        if instr is None: popenargsk['stdin'] = devnull_read
+       timeout_start(timeout)
        (out, err) = sp.communicate(instr)
+       timeout_stop()
        if err: bomb("%s unexpectedly produced stderr output `%s'" %
                        (what, err))
        status = sp.wait()
        return (status, out)
 
-def execute(cmd_string, cmd_list=[], downp=False, outp=False):
+def execute(cmd_string, cmd_list=[], downp=False, outp=False, timeout=0):
        cmdl = cmd_string.split()
 
        if downp: perhaps_down = down
@@ -81,7 +114,8 @@ def execute(cmd_string, cmd_list=[], downp=False, outp=False):
        cmd = cmdl + cmd_list
        if len(perhaps_down): cmd = perhaps_down + [' '.join(cmd)]
 
-       (status, out) = execute_raw(cmdl[0], None, cmd, stdout=stdout)
+       (status, out) = execute_raw(cmdl[0], None, timeout,
+                               cmd, stdout=stdout)
 
        if status: bomb("%s%s failed (exit status %d)" %
                        ((downp and "(down) " or ""), cmdl[0], status))
@@ -96,18 +130,12 @@ def cmd_open(c, ce):
        downtmp = caller.hook_open()
        return [downtmp]
 
-def cmd_close(c, ce):
-       global downtmp
+def cmd_revert(c, ce):
        cmdnumargs(c, ce)
-       if not downtmp: bomb("`close' when not open")
-       cleanup()
-
-def cmd_stop(c, ce):
-       global downtmp
-       cmdnumargs(c, ce, 1)
-       if not downtmp: bomb("`stop' when not open")
-       caller.hook_stop()
-       cleanup()
+       if not downtmp: bomb("`revert' when not open")
+       if not 'revert' in caller.hook_capabilities():
+               bomb("`revert' when `revert' not advertised")
+       caller.hook_revert()
 
 def down_python_script(gobody, functions=''):
        # Many things are made much harder by the inability of
@@ -119,10 +147,10 @@ def down_python_script(gobody, functions=''):
                        "import os\n"
                        "def setfd(fd,fnamee,write,mode=0666):\n"
                        "       fname = urllib.unquote(fnamee)\n"
-                       "       if write: rw = os.O_WRONLY|os.O_CREAT\n"
+                       "       if write: rw = os.O_WRONLY|os.O_CREAT|os.O_TRUNC\n"
                        "       else: rw = os.O_RDONLY\n"
                        "       nfd = os.open(fname, rw, mode)\n"
-                       "       os.dup2(nfd,fd)\n"
+                       "       if fd >= 0: os.dup2(nfd,fd)\n"
                        + functions +
                        "def go():\n" )
        script += (     "       os.environ['TMPDIR']= urllib.unquote('%s')\n" %
@@ -134,17 +162,48 @@ def down_python_script(gobody, functions=''):
        debug("+P ...\n"+script)
 
        scripte = urllib.quote(script)
-       cmdl = down + ['python','-c',
-               "'import urllib; s = urllib.unquote(%s); exec s'" %
-                       ('"%s"' % scripte)]
+       cmdl = down + ["exec python -c 'import urllib; s = urllib.unquote(%s);"
+                      " exec s'" % ('"%s"' % scripte)]
        return cmdl
 
 def cmd_execute(c, ce):
-       cmdnumargs(c, ce, 5)
+       cmdnumargs(c, ce, 5, None)
+       debug_re = regexp.compile('debug=(\d+)\-(\d+)$')
+       debug_g = None
+       timeout = 0
+       envs = []
+       for kw in ce[6:]:
+               if kw.startswith('debug='):
+                       if debug_g: bomb("multiple debug= in execute")
+                       m = debug_re.match(kw)
+                       if not m: bomb("invalid execute debug arg `%s'" % kw)
+                       debug_g = m.groups()
+               elif kw.startswith('timeout='):
+                       try: timeout = int(kw[8:],0)
+                       except ValueError: bomb("invalid timeout arg `%s'" %kw)
+               elif kw.startswith('env='):
+                       es = kw[4:]; eq = es.find('=')
+                       if eq <= 0: bomb("invalid env arg `%s'" % kw)
+                       envs.append((es[:eq], es[eq+1:]))
+               else: bomb("invalid execute kw arg `%s'" % kw)
+               
        gobody = "      import sys\n"
+       stdout = None
+       tfd = None
+       if debug_g:
+               (tfd,hfd) = m.groups()
+               tfd = int(tfd)
+               gobody += "     os.dup2(1,%d)\n" % tfd
+               stdout = int(hfd)
        for ioe in range(3):
+               ioe_tfd = ioe
+               if ioe == tfd: ioe_tfd = -1
                gobody += "     setfd(%d,'%s',%d)\n" % (
-                       ioe, ce[ioe+2], ioe>0 )
+                       ioe_tfd, ce[ioe+2], ioe>0 )
+       for e in envs:
+               gobody += ("    os.environ[urllib.unquote('%s')]"
+                          " = urllib.unquote('%s')\n"
+                               % tuple(map(urllib.quote, e)))
        gobody += "     os.chdir(urllib.unquote('" + ce[5] +"'))\n"
        gobody += "     cmd = '%s'\n" % ce[1]
        gobody += ("    cmd = cmd.split(',')\n"
@@ -156,14 +215,24 @@ def cmd_execute(c, ce):
                "                       mode = status.st_mode | 0111\n"
                "                       os.chmod(c0, mode)\n"
                "       try: os.execvp(c0, cmd)\n"
-               "       except OSError, e:\n"
+               "       except (IOError,OSError), e:\n"
                "               print >>sys.stderr, \"%s: %s\" % (\n"
                "                       (c0, os.strerror(e.errno)))\n"
                "               os._exit(127)\n")
        cmdl = down_python_script(gobody)
 
-       (status, out) = execute_raw('sub-python', None, cmdl,
+       stdout_copy = None
+       try:
+               if type(stdout) == type(2): stdout_copy = os.dup(stdout)
+               try:
+                       (status, out) = execute_raw('sub-python', None,
+                               timeout, cmdl, stdout=stdout_copy,
                                stdin=devnull_read, stderr=subprocess.PIPE)
+               except Timeout:
+                       raise FailedCmd(['timeout'])
+       finally:
+               if stdout_copy is not None: os.close(stdout_copy)
+
        if out: bomb("sub-python unexpected produced stdout"
                        " visible to us `%s'" % out)
        return [`status`]
@@ -187,7 +256,6 @@ def copyupdown(c, ce, upp):
        localfd = None
        deststdout = devnull_read
        srcstdin = devnull_read
-       preexecfns = [None, None]
        if not dirsp:
                modestr = ''
                if upp:
@@ -204,11 +272,11 @@ def copyupdown(c, ce, upp):
                gobody = "      dir = urllib.unquote('%s')\n" % sde[iremote]
                if upp:
                        try: os.mkdir(sd[ilocal])
-                       except OSError, oe:
+                       except (IOError,OSError), oe:
                                if oe.errno != errno.EEXIST: raise
                else:
                        gobody += ("    try: os.mkdir(dir)\n"
-                               "       except OSError, oe:\n"
+                               "       except (IOError,OSError), oe:\n"
                                "               if oe.errno != errno.EEXIST: raise\n")
                gobody +=( "    os.chdir(dir)\n"
                        "       tarcmd = 'tar -f -'.split()\n")
@@ -232,14 +300,20 @@ def copyupdown(c, ce, upp):
        subprocs = [None,None]
        debug(" +< %s" % string.join(cmdls[0]))
        subprocs[0] = subprocess.Popen(cmdls[0], stdin=srcstdin,
-                       stdout=subprocess.PIPE, preexec_fn=preexecfns[0])
+                       stdout=subprocess.PIPE, preexec_fn=preexecfn)
        debug(" +> %s" % string.join(cmdls[1]))
        subprocs[1] = subprocess.Popen(cmdls[1], stdin=subprocs[0].stdout,
-                       stdout=deststdout, preexec_fn=preexecfns[1])
+                       stdout=deststdout, preexec_fn=preexecfn)
+       subprocs[0].stdout.close()
+       timeout_start(copy_timeout)
        for sdn in [1,0]:
+               debug(" +"+"<>"[sdn]+"?");
                status = subprocs[sdn].wait()
-               if status: bomb("%s %s failed, status %d" %
-                       (wh, ['source','destination'][sdn], status))
+               if not (status==0 or (sdn==0 and status==-13)):
+                       timeout_stop()
+                       bomb("%s %s failed, status %d" %
+                               (wh, ['source','destination'][sdn], status))
+       timeout_stop()
 
 def cmd_copydown(c, ce): copyupdown(c, ce, False)
 def cmd_copyup(c, ce): copyupdown(c, ce, True)
@@ -252,16 +326,27 @@ def command():
        c = map(urllib.unquote, ce)
        if not c: bomb('empty commands are not permitted')
        debug('executing '+string.join(ce))
-       try: f = globals()['cmd_'+c[0]]
+       c_lookup = c[0].replace('-','_')
+       try: f = globals()['cmd_'+c_lookup]
        except KeyError: bomb("unknown command `%s'" % ce[0])
-       r = f(c, ce)
-       if not r: r = []
-       r.insert(0, 'ok')
-       ru = map(urllib.quote, r)
-       print string.join(ru)
+       try:
+               r = f(c, ce)
+               if not r: r = []
+               r.insert(0, 'ok')
+       except FailedCmd, fc:
+               r = fc.e
+       print string.join(r)
+
+signal_list = [        signal.SIGHUP, signal.SIGTERM,
+               signal.SIGINT, signal.SIGPIPE ]
+
+def sethandlers(f):
+       for signum in signal_list: signal.signal(signum, f)
 
 def cleanup():
        global downtmp, cleaning
+       debug("cleanup...");
+       sethandlers(signal.SIG_DFL)
        cleaning = True
        if downtmp: caller.hook_cleanup()
        cleaning = False
@@ -288,12 +373,7 @@ def error_cleanup():
 def prepare():
        global downtmp, cleaning
        downtmp = None
-       signal_list = [ signal.SIGHUP, signal.SIGTERM,
-                       signal.SIGINT, signal.SIGPIPE ]
-       def sethandlers(f):
-               for signum in signal_list: signal.signal(signum, f)
        def handler(sig, *any):
-               sethandlers(signal.SIG_DFL)
                cleanup()
                os.kill(os.getpid(), sig)
        sethandlers(handler)
@@ -312,6 +392,7 @@ def mainloop():
                sys.exit(16)
 
 def main():
+       signal.signal(signal.SIGALRM, alarm_handler)
        debug("down = %s" % string.join(down))
        ok()
        prepare()