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).
35 from Autopkgtest import *
38 progname = "<VirtSubproc>"
39 devnull_read = file('/dev/null','r')
44 def __init__(q,ec,m): q.ec = ec; q.m = m
47 def alarm_handler(*a): raise Timeout()
48 def timeout_start(to): signal.alarm(to)
49 def timeout_stop(): signal.alarm(0)
52 def __init__(fc,e): fc.e = e
55 if not debuglevel: return
56 print >> sys.stderr, progname+": debug:", m
59 raise Quit(12, progname+": failure: %s" % m)
63 def cmdnumargs(c, ce, nargs=0, noptargs=0):
65 bomb("too few arguments to command `%s'" % ce[0])
66 if noptargs is not None and len(c) > 1+nargs+noptargs:
67 bomb("too many arguments to command `%s'" % ce[0])
69 def cmd_capabilities(c, ce):
71 return caller.hook_capabilities() + ['execute-debug']
79 if not downtmp: bomb("`close' when not open")
82 def cmd_print_auxverb_command(c, ce): return print_command('auxverb', c, ce)
83 def cmd_print_shstring_command(c, ce): return print_command('shstring', c, ce)
85 def print_command(which, c, ce):
88 if not downtmp: bomb("`print-%s-command' when not open" % which)
91 cl = ['sh','-c','exec "$@"','x'] + cl
92 return [','.join(map(urllib.quote, cl))]
95 caller.hook_forked_inchild()
97 def execute_raw(what, instr, timeout, *popenargs, **popenargsk):
98 debug(" ++ %s" % string.join(popenargs[0]))
99 sp = subprocess.Popen(preexec_fn=preexecfn, *popenargs, **popenargsk)
100 if instr is None: popenargsk['stdin'] = devnull_read
101 timeout_start(timeout)
102 (out, err) = sp.communicate(instr)
104 if err: bomb("%s unexpectedly produced stderr output `%s'" %
109 def execute(cmd_string, cmd_list=[], downp=False, outp=False, timeout=0):
110 cmdl = cmd_string.split()
112 if downp: perhaps_down = downs['auxverb']
113 else: perhaps_down = []
115 if outp: stdout = subprocess.PIPE
118 cmd = cmdl + cmd_list
119 if len(perhaps_down): cmd = perhaps_down + cmd
121 (status, out) = execute_raw(cmdl[0], None, timeout,
124 if status: bomb("%s%s failed (exit status %d)" %
125 ((downp and "(down) " or ""), cmdl[0], status))
127 if outp and out and out[-1]=='\n': out = out[:-1]
133 if downtmp: bomb("`open' when already open")
136 downtmp = caller.hook_downtmp()
139 def downtmp_mktemp():
141 return execute('mktemp -t -d', downp=True, outp=True)
143 def downtmp_remove():
145 execute('rm -rf --', [downtmp], downp=True)
147 perl_quote_re = regexp.compile('[^-+=_.,;:() 0-9a-zA-Z]')
148 def perl_quote_1chargroup(m): return '\\x%02x' % ord(m.group(0))
149 def perl_quote(s): return '"'+perl_quote_re.sub(perl_quote_1chargroup, s)+'"'
152 global down, downkind, downs
153 debug("downkind = %s, down = %s" % (downkind, `down`))
154 if downkind == 'auxverb':
155 downs = { 'auxverb': down,
156 'shstring': down + ['sh','-c'] }
157 elif downkind == 'shstring':
158 downs = { 'shstring': down,
159 'auxverb': ['perl','-e','''
160 @cmd=('''+(','.join(map(perl_quote,down)))+''');
161 my $shstring = pop @ARGV;
162 s/'/'\\\\''/g foreach @ARGV;
163 push @cmd, "'$_'" foreach @ARGV;
168 debug("downs = %s" % `downs`)
171 global downtmp, downs
172 debug("downtmp = %s" % (downtmp))
175 def cmd_revert(c, ce):
178 if not downtmp: bomb("`revert' when not open")
179 if not 'revert' in caller.hook_capabilities():
180 bomb("`revert' when `revert' not advertised")
183 downtmp = caller.hook_downtmp()
186 def cmd_execute(c, ce):
187 cmdnumargs(c, ce, 5, None)
188 if not downtmp: bomb("`execute' when not open" % which)
189 debug_re = regexp.compile('debug=(\d+)\-(\d+)$')
194 if kw.startswith('debug='):
195 if debug_g: bomb("multiple debug= in execute")
196 m = debug_re.match(kw)
197 if not m: bomb("invalid execute debug arg `%s'" % kw)
199 elif kw.startswith('timeout='):
200 try: timeout = int(kw[8:],0)
201 except ValueError: bomb("invalid timeout arg `%s'" %kw)
202 elif kw.startswith('env='):
203 es = kw[4:]; eq = es.find('=')
204 if eq <= 0: bomb("invalid env arg `%s'" % kw)
205 envs.append((es[:eq], es[eq+1:]))
206 else: bomb("invalid execute kw arg `%s'" % kw)
208 rune = 'set -e; exec '
216 rune += " %d%s%s" % (ioe, '<>'[ioe>0],
217 shellquote_arg(ce[ioe+2]))
219 (tfd,hfd) = m.groups()
221 rune += " %d>&3 3>&-" % tfd
226 rune += 'cd %s; ' % shellquote_arg(ce[5])
229 (en, ev) = map(urllib.unquote,e)
230 rune += "%s=%s " % (en, shellquote_arg(ev))
232 rune += 'exec ' + shellquote_cmdl(map(urllib.unquote, ce[1].split(',')))
234 cmdl = downs['shstring'] + [rune]
238 if type(stdout) == type(2):
239 stdout_copy = os.dup(stdout)
241 (status, out) = execute_raw('target-cmd', None,
242 timeout, cmdl, stdout=stdout_copy,
243 stdin=devnull_read, stderr=subprocess.PIPE)
245 raise FailedCmd(['timeout'])
247 if stdout_copy is not None: os.close(stdout_copy)
249 if out: bomb("target command unexpected produced stdout"
250 " visible to us `%s'" % out)
253 def copyupdown(c, ce, upp):
255 if not downtmp: bomb("`copyup'/`copydown' when not open" % which)
262 if not sd[0] or not sd[1]:
263 bomb("%s paths must be nonempty" % wh)
264 dirsp = sd[0][-1]=='/'
265 functions = "import errno\n"
266 if dirsp != (sd[1][-1]=='/'):
267 bomb("% paths must agree about directoryness"
268 " (presence or absence of trailing /)" % wh)
270 deststdout = devnull_read
271 srcstdin = devnull_read
272 remfileq = shellquote_arg(sd[iremote])
275 rune = 'cat %s%s' % ('><'[upp], remfileq)
277 deststdout = file(sd[idst], 'w')
279 srcstdin = file(sd[isrc], 'r')
280 status = os.fstat(srcstdin.fileno())
281 if status.st_mode & 0111:
282 rune += '; chmod +x -- %s' % (remfileq)
285 taropts = [None, None]
286 taropts[isrc] = '-c .'
287 taropts[idst] = '-p -x --no-same-owner'
289 rune = 'cd %s; tar %s -f -' % (remfileq, taropts[iremote])
291 try: os.mkdir(sd[ilocal])
292 except (IOError,OSError), oe:
293 if oe.errno != errno.EEXIST: raise
295 rune = ('if ! test -d %s; then mkdir -- %s; fi; ' % (
299 localcmdl = ['tar','-C',sd[ilocal]] + (
300 ('%s -f -' % taropts[ilocal]).split()
302 rune = 'set -e; ' + rune
303 downcmdl = downs['shstring'] + [rune]
305 if upp: cmdls = (downcmdl, localcmdl)
306 else: cmdls = (localcmdl, downcmdl)
308 debug(`["cmdls", `cmdls`]`)
309 debug(`["srcstdin", `srcstdin`, "deststdout", `deststdout`, "devnull_read", devnull_read]`)
311 subprocs = [None,None]
312 debug(" +< %s" % string.join(cmdls[0]))
313 subprocs[0] = subprocess.Popen(cmdls[0], stdin=srcstdin,
314 stdout=subprocess.PIPE, preexec_fn=preexecfn)
315 debug(" +> %s" % string.join(cmdls[1]))
316 subprocs[1] = subprocess.Popen(cmdls[1], stdin=subprocs[0].stdout,
317 stdout=deststdout, preexec_fn=preexecfn)
318 subprocs[0].stdout.close()
319 timeout_start(copy_timeout)
321 debug(" +"+"<>"[sdn]+"?");
322 status = subprocs[sdn].wait()
323 if not (status==0 or (sdn==0 and status==-13)):
325 bomb("%s %s failed, status %d" %
326 (wh, ['source','destination'][sdn], status))
329 def cmd_copydown(c, ce): copyupdown(c, ce, False)
330 def cmd_copyup(c, ce): copyupdown(c, ce, True)
334 ce = sys.stdin.readline()
335 if not ce: bomb('end of file - caller quit?')
336 ce = ce.rstrip().split()
337 c = map(urllib.unquote, ce)
338 if not c: bomb('empty commands are not permitted')
339 debug('executing '+string.join(ce))
340 c_lookup = c[0].replace('-','_')
341 try: f = globals()['cmd_'+c_lookup]
342 except KeyError: bomb("unknown command `%s'" % ce[0])
347 except FailedCmd, fc:
351 signal_list = [ signal.SIGHUP, signal.SIGTERM,
352 signal.SIGINT, signal.SIGPIPE ]
355 for signum in signal_list: signal.signal(signum, f)
358 global downtmp, cleaning
360 sethandlers(signal.SIG_DFL)
363 caller.hook_cleanup()
374 print >> sys.stderr, q.m
376 print >> sys.stderr, "Unexpected cleanup error:"
377 traceback.print_exc()
378 print >> sys.stderr, ''
380 print >> sys.stderr, ("while cleaning up"
381 " because of another error:")
386 global downtmp, cleaning
388 def handler(sig, *any):
390 os.kill(os.getpid(), sig)
395 while True: command()
398 if q.m: print >> sys.stderr, q.m
402 print >> sys.stderr, "Unexpected error:"
403 traceback.print_exc()
407 signal.signal(signal.SIGALRM, alarm_handler)