1 # VirtSubproc is part of autopkgtest
2 # autopkgtest is a tool for testing Debian binary packages
4 # autopkgtest is Copyright (C) 2006-2007 Canonical Ltd.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 # See the file CREDITS for a full list of credits information (often
21 # installed as /usr/share/doc/autopkgtest/CREDITS).
36 progname = "<VirtSubproc>"
37 devnull_read = file('/dev/null','r')
42 def __init__(q,ec,m): q.ec = ec; q.m = m
45 def alarm_handler(*a): raise Timeout()
46 def timeout_start(to): signal.alarm(to)
47 def timeout_stop(): signal.alarm(0)
50 def __init__(fc,e): fc.e = e
53 if not debuglevel: return
54 print >> sys.stderr, progname+": debug:", m
57 raise Quit(12, progname+": failure: %s" % m)
61 def cmdnumargs(c, ce, nargs=0, noptargs=0):
63 bomb("too few arguments to command `%s'" % ce[0])
64 if noptargs is not None and len(c) > 1+nargs+noptargs:
65 bomb("too many arguments to command `%s'" % ce[0])
67 def cmd_capabilities(c, ce):
69 return caller.hook_capabilities() + ['execute-debug']
77 if not downtmp: bomb("`close' when not open")
80 def cmd_print_auxverb_command(c, ce): return print_command('auxverb', c, ce)
81 def cmd_print_shstring_command(c, ce): return print_command('shstring', c, ce)
83 def print_command(which, c, ce):
86 if not downtmp: bomb("`print-%s-command' when not open" % which)
89 cl = ['sh','-c','exec "$@"','x'] + cl
90 return [','.join(map(urllib.quote, cl))]
93 caller.hook_forked_inchild()
95 def execute_raw(what, instr, timeout, *popenargs, **popenargsk):
96 debug(" ++ %s" % string.join(popenargs[0]))
97 sp = subprocess.Popen(preexec_fn=preexecfn, *popenargs, **popenargsk)
98 if instr is None: popenargsk['stdin'] = devnull_read
99 timeout_start(timeout)
100 (out, err) = sp.communicate(instr)
102 if err: bomb("%s unexpectedly produced stderr output `%s'" %
107 def execute(cmd_string, cmd_list=[], downp=False, outp=False, timeout=0):
108 cmdl = cmd_string.split()
110 if downp: perhaps_down = downs['auxverb']
111 else: perhaps_down = []
113 if outp: stdout = subprocess.PIPE
116 cmd = cmdl + cmd_list
117 if len(perhaps_down): cmd = perhaps_down + cmd
119 (status, out) = execute_raw(cmdl[0], None, timeout,
122 if status: bomb("%s%s failed (exit status %d)" %
123 ((downp and "(down) " or ""), cmdl[0], status))
125 if outp and out and out[-1]=='\n': out = out[:-1]
131 if downtmp: bomb("`open' when already open")
134 downtmp = caller.hook_downtmp()
137 def downtmp_mktemp():
139 return execute('mktemp -t -d', downp=True, outp=True)
141 def downtmp_remove():
143 execute('rm -rf --', [downtmp], downp=True)
145 perl_quote_re = regexp.compile('[^-+=_.,;:() 0-9a-zA-Z]')
146 def perl_quote_1chargroup(m): return '\\x%02x' % ord(m.group(0))
147 def perl_quote(s): return '"'+perl_quote_re.sub(perl_quote_1chargroup, s)+'"'
150 global down, downkind, downs
151 debug("downkind = %s, down = %s" % (downkind, `down`))
152 if downkind == 'auxverb':
153 downs = { 'auxverb': down,
154 'shstring': down + ['sh','-c'] }
155 elif downkind == 'shstring':
156 downs = { 'shstring': down,
157 'auxverb': ['perl','-e','''
158 @cmd=('''+(','.join(map(perl_quote,down)))+''');
159 my $shstring = pop @ARGV;
160 s/'/'\\\\''/g foreach @ARGV;
161 push @cmd, "'$_'" foreach @ARGV;
166 debug("downs = %s" % `downs`)
169 global downtmp, downs
170 debug("downtmp = %s" % (downtmp))
173 def cmd_revert(c, ce):
176 if not downtmp: bomb("`revert' when not open")
177 if not 'revert' in caller.hook_capabilities():
178 bomb("`revert' when `revert' not advertised")
181 downtmp = caller.hook_downtmp()
184 def down_python_script(gobody, functions=''):
185 # Many things are made much harder by the inability of
186 # dchroot, ssh, et al, to cope without mangling the arguments.
187 # So we run a sub-python on the testbed and feed it a script
188 # on stdin. The sub-python decodes the arguments.
190 script = ( "import urllib\n"
193 "def setfd(fd,fnamee,write,mode=0666):\n"
194 " fname = urllib.unquote(fnamee)\n"
195 " if write: rw = os.O_WRONLY|os.O_CREAT|os.O_TRUNC\n"
196 " else: rw = os.O_RDONLY\n"
197 " nfd = os.open(fname, rw, mode)\n"
198 " if fd >= 0: os.dup2(nfd,fd)\n"
201 script += ( " os.environ['TMPDIR']= urllib.unquote('%s')\n" %
202 urllib.quote(downtmp) )
203 script += ( " os.chdir(os.environ['TMPDIR'])\n" )
207 debug("+P ...\n"+script)
209 scripte = urllib.quote(script)
210 cmdl = (downs['shstring'] +
211 ["exec python -c 'import urllib; s = urllib.unquote(%s);"
212 " exec s'" % ('"%s"' % scripte)])
215 def cmd_execute(c, ce):
216 cmdnumargs(c, ce, 5, None)
217 debug_re = regexp.compile('debug=(\d+)\-(\d+)$')
222 if kw.startswith('debug='):
223 if debug_g: bomb("multiple debug= in execute")
224 m = debug_re.match(kw)
225 if not m: bomb("invalid execute debug arg `%s'" % kw)
227 elif kw.startswith('timeout='):
228 try: timeout = int(kw[8:],0)
229 except ValueError: bomb("invalid timeout arg `%s'" %kw)
230 elif kw.startswith('env='):
231 es = kw[4:]; eq = es.find('=')
232 if eq <= 0: bomb("invalid env arg `%s'" % kw)
233 envs.append((es[:eq], es[eq+1:]))
234 else: bomb("invalid execute kw arg `%s'" % kw)
236 gobody = " import sys\n"
240 (tfd,hfd) = m.groups()
242 gobody += " os.dup2(1,%d)\n" % tfd
246 if ioe == tfd: ioe_tfd = -1
247 gobody += " setfd(%d,'%s',%d)\n" % (
248 ioe_tfd, ce[ioe+2], ioe>0 )
250 gobody += (" os.environ[urllib.unquote('%s')]"
251 " = urllib.unquote('%s')\n"
252 % tuple(map(urllib.quote, e)))
253 gobody += " os.chdir(urllib.unquote('" + ce[5] +"'))\n"
254 gobody += " cmd = '%s'\n" % ce[1]
255 gobody += (" cmd = cmd.split(',')\n"
256 " cmd = map(urllib.unquote, cmd)\n"
259 " if not os.access(c0, os.X_OK):\n"
260 " status = os.stat(c0)\n"
261 " mode = status.st_mode | 0111\n"
262 " os.chmod(c0, mode)\n"
263 " try: os.execvp(c0, cmd)\n"
264 " except (IOError,OSError), e:\n"
265 " print >>sys.stderr, \"%s: %s\" % (\n"
266 " (c0, os.strerror(e.errno)))\n"
268 cmdl = down_python_script(gobody)
272 if type(stdout) == type(2): stdout_copy = os.dup(stdout)
274 (status, out) = execute_raw('sub-python', None,
275 timeout, cmdl, stdout=stdout_copy,
276 stdin=devnull_read, stderr=subprocess.PIPE)
278 raise FailedCmd(['timeout'])
280 if stdout_copy is not None: os.close(stdout_copy)
282 if out: bomb("sub-python unexpected produced stdout"
283 " visible to us `%s'" % out)
286 def copyupdown(c, ce, upp):
295 if not sd[0] or not sd[1]:
296 bomb("%s paths must be nonempty" % wh)
297 dirsp = sd[0][-1]=='/'
298 functions = "import errno\n"
299 if dirsp != (sd[1][-1]=='/'):
300 bomb("% paths must agree about directoryness"
301 " (presence or absence of trailing /)" % wh)
303 deststdout = devnull_read
304 srcstdin = devnull_read
308 deststdout = file(sd[idst], 'w')
310 srcstdin = file(sd[isrc], 'r')
311 status = os.fstat(srcstdin.fileno())
312 if status.st_mode & 0111: modestr = ',0777'
313 gobody = " setfd(%s,'%s',%s%s)\n" % (
314 1-upp, sde[iremote], not upp, modestr)
315 gobody += " os.execvp('cat', ['cat'])\n"
318 gobody = " dir = urllib.unquote('%s')\n" % sde[iremote]
320 try: os.mkdir(sd[ilocal])
321 except (IOError,OSError), oe:
322 if oe.errno != errno.EEXIST: raise
324 gobody += (" try: os.mkdir(dir)\n"
325 " except (IOError,OSError), oe:\n"
326 " if oe.errno != errno.EEXIST: raise\n")
327 gobody +=( " os.chdir(dir)\n"
328 " tarcmd = 'tar -f -'.split()\n")
329 localcmdl = 'tar -f -'.split()
330 taropts = [None, None]
331 taropts[isrc] = '-c .'
332 taropts[idst] = '-p -x --no-same-owner'
333 gobody += " tarcmd += '%s'.split()\n" % taropts[iremote]
334 localcmdl += ['-C',sd[ilocal]]
335 localcmdl += taropts[ilocal].split()
336 gobody += " os.execvp('tar', tarcmd)\n";
338 downcmdl = down_python_script(gobody, functions)
340 if upp: cmdls = (downcmdl, localcmdl)
341 else: cmdls = (localcmdl, downcmdl)
343 debug(`["cmdls", `cmdls`]`)
344 debug(`["srcstdin", `srcstdin`, "deststdout", `deststdout`, "devnull_read", devnull_read]`)
346 subprocs = [None,None]
347 debug(" +< %s" % string.join(cmdls[0]))
348 subprocs[0] = subprocess.Popen(cmdls[0], stdin=srcstdin,
349 stdout=subprocess.PIPE, preexec_fn=preexecfn)
350 debug(" +> %s" % string.join(cmdls[1]))
351 subprocs[1] = subprocess.Popen(cmdls[1], stdin=subprocs[0].stdout,
352 stdout=deststdout, preexec_fn=preexecfn)
353 subprocs[0].stdout.close()
354 timeout_start(copy_timeout)
356 debug(" +"+"<>"[sdn]+"?");
357 status = subprocs[sdn].wait()
358 if not (status==0 or (sdn==0 and status==-13)):
360 bomb("%s %s failed, status %d" %
361 (wh, ['source','destination'][sdn], status))
364 def cmd_copydown(c, ce): copyupdown(c, ce, False)
365 def cmd_copyup(c, ce): copyupdown(c, ce, True)
369 ce = sys.stdin.readline()
370 if not ce: bomb('end of file - caller quit?')
371 ce = ce.rstrip().split()
372 c = map(urllib.unquote, ce)
373 if not c: bomb('empty commands are not permitted')
374 debug('executing '+string.join(ce))
375 c_lookup = c[0].replace('-','_')
376 try: f = globals()['cmd_'+c_lookup]
377 except KeyError: bomb("unknown command `%s'" % ce[0])
382 except FailedCmd, fc:
386 signal_list = [ signal.SIGHUP, signal.SIGTERM,
387 signal.SIGINT, signal.SIGPIPE ]
390 for signum in signal_list: signal.signal(signum, f)
393 global downtmp, cleaning
395 sethandlers(signal.SIG_DFL)
398 caller.hook_cleanup()
409 print >> sys.stderr, q.m
411 print >> sys.stderr, "Unexpected cleanup error:"
412 traceback.print_exc()
413 print >> sys.stderr, ''
415 print >> sys.stderr, ("while cleaning up"
416 " because of another error:")
421 global downtmp, cleaning
423 def handler(sig, *any):
425 os.kill(os.getpid(), sig)
430 while True: command()
433 if q.m: print >> sys.stderr, q.m
437 print >> sys.stderr, "Unexpected error:"
438 traceback.print_exc()
442 signal.signal(signal.SIGALRM, alarm_handler)