blob: 1b18a035d2400d2ad1cea53b0880e5a94cb1a82e [file] [log] [blame]
mbligh63073c92008-03-31 16:49:32 +00001#!/usr/bin/python
2#
3# Copyright 2008 Google Inc. Released under the GPL v2
4
5import os, pickle, random, re, select, shutil, signal, StringIO, subprocess
mbligh0a297572008-04-02 00:13:45 +00006import sys, time, textwrap, urllib, urlparse
mbligh63073c92008-03-31 16:49:32 +00007import error
mbligh6231cd62008-02-02 19:18:33 +00008
mblighde0d47e2008-03-28 14:37:18 +00009
10def read_keyval(path):
11 """
12 Read a key-value pair format file into a dictionary, and return it.
13 Takes either a filename or directory name as input. If it's a
14 directory name, we assume you want the file to be called keyval.
15 """
16 if os.path.isdir(path):
17 path = os.path.join(path, 'keyval')
18 keyval = {}
19 for line in open(path):
20 line = re.sub('#.*', '', line.rstrip())
21 if not re.search(r'^\w+=', line):
22 raise ValueError('Invalid format line: %s' % line)
23 key, value = line.split('=', 1)
24 if re.search('^\d+$', value):
25 value = int(value)
26 elif re.search('^(\d+\.)?\d+$', value):
27 value = float(value)
28 keyval[key] = value
29 return keyval
30
31
mbligh6231cd62008-02-02 19:18:33 +000032def write_keyval(path, dictionary):
33 if os.path.isdir(path):
34 path = os.path.join(path, 'keyval')
35 keyval = open(path, 'a')
36 try:
37 for key, value in dictionary.iteritems():
38 if re.search(r'\W', key):
39 raise ValueError('Invalid key: %s' % key)
40 keyval.write('%s=%s\n' % (key, value))
41 finally:
42 keyval.close()
43
44
45def is_url(path):
46 """Return true if path looks like a URL"""
47 # for now, just handle http and ftp
48 url_parts = urlparse.urlparse(path)
49 return (url_parts[0] in ('http', 'ftp'))
50
51
52def get_file(src, dest, permissions=None):
53 """Get a file from src, which can be local or a remote URL"""
54 if (src == dest):
55 return
56 if (is_url(src)):
57 print 'PWD: ' + os.getcwd()
58 print 'Fetching \n\t', src, '\n\t->', dest
59 try:
60 urllib.urlretrieve(src, dest)
61 except IOError, e:
mbligh63073c92008-03-31 16:49:32 +000062 raise error.AutotestError('Unable to retrieve %s (to %s)'
mbligh6231cd62008-02-02 19:18:33 +000063 % (src, dest), e)
64 else:
65 shutil.copyfile(src, dest)
66 if permissions:
67 os.chmod(dest, permissions)
68 return dest
69
70
71def unmap_url(srcdir, src, destdir='.'):
72 """
73 Receives either a path to a local file or a URL.
74 returns either the path to the local file, or the fetched URL
75
76 unmap_url('/usr/src', 'foo.tar', '/tmp')
77 = '/usr/src/foo.tar'
78 unmap_url('/usr/src', 'http://site/file', '/tmp')
79 = '/tmp/file'
80 (after retrieving it)
81 """
82 if is_url(src):
83 url_parts = urlparse.urlparse(src)
84 filename = os.path.basename(url_parts[2])
85 dest = os.path.join(destdir, filename)
86 return get_file(src, dest)
87 else:
88 return os.path.join(srcdir, src)
89
90
91def update_version(srcdir, preserve_srcdir, new_version, install,
92 *args, **dargs):
93 """
94 Make sure srcdir is version new_version
95
96 If not, delete it and install() the new version.
97
98 In the preserve_srcdir case, we just check it's up to date,
99 and if not, we rerun install, without removing srcdir
100 """
101 versionfile = os.path.join(srcdir, '.version')
102 install_needed = True
103
104 if os.path.exists(versionfile):
105 old_version = pickle.load(open(versionfile))
106 if old_version == new_version:
107 install_needed = False
108
109 if install_needed:
110 if not preserve_srcdir and os.path.exists(srcdir):
111 shutil.rmtree(srcdir)
112 install(*args, **dargs)
113 if os.path.exists(srcdir):
114 pickle.dump(new_version, open(versionfile, 'w'))
mbligh462c0152008-03-13 15:37:10 +0000115
116
mbligh63073c92008-03-31 16:49:32 +0000117def run(command, timeout=None, ignore_status=False,
118 stdout_tee=None, stderr_tee=None):
119 """
120 Run a command on the host.
121
122 Args:
123 command: the command line string
124 timeout: time limit in seconds before attempting to
125 kill the running process. The run() function
126 will take a few seconds longer than 'timeout'
127 to complete if it has to kill the process.
128 ignore_status: do not raise an exception, no matter what
129 the exit code of the command is.
130 stdout_tee: optional file-like object to which stdout data
131 will be written as it is generated (data will still
132 be stored in result.stdout)
133 stderr_tee: likewise for stderr
134
135 Returns:
136 a CmdResult object
137
138 Raises:
139 AutoservRunError: the exit code of the command
140 execution was not 0
141 """
142 return join_bg_job(run_bg(command), timeout, ignore_status,
143 stdout_tee, stderr_tee)
144
145
146def run_bg(command):
147 """Run the command in a subprocess and return the subprocess."""
148 result = CmdResult(command)
149 sp = subprocess.Popen(command, stdout=subprocess.PIPE,
150 stderr=subprocess.PIPE,
151 shell=True, executable="/bin/bash")
152 return sp, result
153
154
155def join_bg_job(bg_job, timeout=None, ignore_status=False,
156 stdout_tee=None, stderr_tee=None):
157 """Join the subprocess with the current thread. See run description."""
158 sp, result = bg_job
159 stdout_file = StringIO.StringIO()
160 stderr_file = StringIO.StringIO()
161
162 try:
163 # We are holding ends to stdin, stdout pipes
164 # hence we need to be sure to close those fds no mater what
165 start_time = time.time()
166 ret = _wait_for_command(sp, start_time, timeout, stdout_file,
167 stderr_file, stdout_tee, stderr_tee)
168 result.exit_status = ret
169
170 result.duration = time.time() - start_time
171 # don't use os.read now, so we get all the rest of the output
172 _process_output(sp.stdout, stdout_file, stdout_tee,
173 use_os_read=False)
174 _process_output(sp.stderr, stderr_file, stderr_tee,
175 use_os_read=False)
176 finally:
177 # close our ends of the pipes to the sp no matter what
178 sp.stdout.close()
179 sp.stderr.close()
180
181 result.stdout = stdout_file.getvalue()
182 result.stderr = stderr_file.getvalue()
183
184 if not ignore_status and result.exit_status > 0:
185 raise error.AutoservRunError("command execution error", result)
186
187 return result
188
189
190def _wait_for_command(subproc, start_time, timeout, stdout_file, stderr_file,
191 stdout_tee, stderr_tee):
192 if timeout:
193 stop_time = start_time + timeout
194 time_left = stop_time - time.time()
195 else:
196 time_left = None # so that select never times out
197 while not timeout or time_left > 0:
198 # select will return when stdout is ready (including when it is
199 # EOF, that is the process has terminated).
200 ready, _, _ = select.select([subproc.stdout, subproc.stderr],
201 [], [], time_left)
202 # os.read() has to be used instead of
203 # subproc.stdout.read() which will otherwise block
204 if subproc.stdout in ready:
205 _process_output(subproc.stdout, stdout_file,
206 stdout_tee)
207 if subproc.stderr in ready:
208 _process_output(subproc.stderr, stderr_file,
209 stderr_tee)
210
211 exit_status_indication = subproc.poll()
212
213 if exit_status_indication is not None:
214 return exit_status_indication
215 if timeout:
216 time_left = stop_time - time.time()
217
218 # the process has not terminated within timeout,
219 # kill it via an escalating series of signals.
220 if exit_status_indication is None:
221 nuke_subprocess(subproc)
222 raise error.AutoservRunError('Command not complete within %s seconds'
223 % timeout, None)
224
225
226def _process_output(pipe, fbuffer, teefile=None, use_os_read=True):
227 if use_os_read:
228 data = os.read(pipe.fileno(), 1024)
229 else:
230 data = pipe.read()
231 fbuffer.write(data)
232 if teefile:
233 teefile.write(data)
234 teefile.flush()
235
236
237def nuke_subprocess(subproc):
238 # the process has not terminated within timeout,
239 # kill it via an escalating series of signals.
240 signal_queue = [signal.SIGTERM, signal.SIGKILL]
241 for sig in signal_queue:
242 try:
243 os.kill(subproc.pid, sig)
244 # The process may have died before we could kill it.
245 except OSError:
246 pass
247
248 for i in range(5):
249 rc = subproc.poll()
250 if rc != None:
251 return
252 time.sleep(1)
253
254
255def nuke_pid(pid):
256 # the process has not terminated within timeout,
257 # kill it via an escalating series of signals.
258 signal_queue = [signal.SIGTERM, signal.SIGKILL]
259 for sig in signal_queue:
260 try:
261 os.kill(pid, sig)
262
263 # The process may have died before we could kill it.
264 except OSError:
265 pass
266
267 try:
268 for i in range(5):
269 status = os.waitpid(pid, os.WNOHANG)[0]
270 if status == pid:
271 return
272 time.sleep(1)
273
274 if status != pid:
275 raise error.AutoservRunError('Could not kill %d'
276 % pid, None)
277
278 # the process died before we join it.
279 except OSError:
280 pass
281
282
283def _process_output(pipe, fbuffer, teefile=None, use_os_read=True):
284 if use_os_read:
285 data = os.read(pipe.fileno(), 1024)
286 else:
287 data = pipe.read()
288 fbuffer.write(data)
289 if teefile:
290 teefile.write(data)
291 teefile.flush()
292
293
294def system(command, timeout=None, ignore_status=False):
295 return run(command, timeout, ignore_status,
296 stdout_tee=sys.stdout, stderr_tee=sys.stderr).exit_status
297
298
299def system_output(command, timeout=None, ignore_status=False):
300 out = run(command, timeout, ignore_status).stdout
301 if out[-1:] == '\n': out = out[:-1]
302 return out
303
304
305class CmdResult(object):
306 """
307 Command execution result.
308
309 command: String containing the command line itself
310 exit_status: Integer exit code of the process
311 stdout: String containing stdout of the process
312 stderr: String containing stderr of the process
313 duration: Elapsed wall clock time running the process
314 """
315
316
317 def __init__(self, command = None):
318 self.command = command
319 self.exit_status = None
320 self.stdout = ""
321 self.stderr = ""
322 self.duration = 0
323
324
325 def __repr__(self):
326 wrapper = textwrap.TextWrapper(width = 78,
327 initial_indent="\n ",
328 subsequent_indent=" ")
329
330 stdout = self.stdout.rstrip()
331 if stdout:
332 stdout = "\nstdout:\n%s" % stdout
333
334 stderr = self.stderr.rstrip()
335 if stderr:
336 stderr = "\nstderr:\n%s" % stderr
337
338 return ("* Command: %s\n"
339 "Exit status: %s\n"
340 "Duration: %s\n"
341 "%s"
342 "%s"
343 % (wrapper.fill(self.command), self.exit_status,
344 self.duration, stdout, stderr))
345
346
mbligh462c0152008-03-13 15:37:10 +0000347class run_randomly:
mbligh63073c92008-03-31 16:49:32 +0000348
349
mbligh462c0152008-03-13 15:37:10 +0000350 def __init__(self):
351 self.test_list = []
352
353
354 def add(self, *args, **dargs):
355 test = (args, dargs)
356 self.test_list.append(test)
357
358
359 def run(self, fn):
360 while self.test_list:
361 test_index = random.randint(0, len(self.test_list)-1)
362 (args, dargs) = self.test_list.pop(test_index)
363 fn(*args, **dargs)