blob: 9ed621806f39a4224387accec53c126f70d534f4 [file] [log] [blame]
mblighdcd57a82007-07-11 23:06:47 +00001#!/usr/bin/python
2#
3# Copyright 2007 Google Inc. Released under the GPL v2
4
5"""Miscellaneous small functions.
6"""
7
8__author__ = """mbligh@google.com (Martin J. Bligh),
9poirier@google.com (Benjamin Poirier),
10stutsman@google.com (Ryan Stutsman)"""
11
12
13import atexit
14import os
15import os.path
16import select
17import shutil
18import signal
19import StringIO
20import subprocess
21import tempfile
22import time
23import types
24import urllib
25
26import hosts
27import errors
28
29
30__tmp_dirs= []
31
32
33def sh_escape(command):
34 """Escape special characters from a command so that it can be passed
35 as a double quoted (" ") string.
36
37 Args:
38 command: the command string to escape.
39
40 Returns:
41 The escaped command string. The required englobing double
42 quotes are NOT added and so should be added at some point by
43 the caller.
44
45 See also: http://www.tldp.org/LDP/abs/html/escapingsection.html
46 """
47 command= command.replace("\\", "\\\\")
48 command= command.replace("$", r'\$')
49 command= command.replace('"', r'\"')
50 command= command.replace('`', r'\`')
51 return command
52
53
54def scp_remote_escape(filename):
55 """Escape special characters from a filename so that it can be passed
56 to scp (within double quotes) as a remote file.
57
58 Bis-quoting has to be used with scp for remote files, "bis-quoting"
59 as in quoting x 2
60 scp does not support a newline in the filename
61
62 Args:
63 filename: the filename string to escape.
64
65 Returns:
66 The escaped filename string. The required englobing double
67 quotes are NOT added and so should be added at some point by
68 the caller.
69 """
70 escape_chars= r' !"$&' "'" r'()*,:;<=>?[\]^`{|}'
71
72 new_name= []
73 for char in filename:
74 if char in escape_chars:
75 new_name.append("\\%s" % (char,))
76 else:
77 new_name.append(char)
78
79 return sh_escape("".join(new_name))
80
81
82def get(location):
83 """Get a file or directory to a local temporary directory.
84
85 Args:
86 location: the source of the material to get. This source may
87 be one of:
88 * a local file or directory
89 * a URL (http or ftp)
90 * a python file-like object
91
92 Returns:
93 The location of the file or directory where the requested
94 content was saved. This will be contained in a temporary
95 directory on the local host.
96 """
97 tmpdir = get_tmp_dir()
98
99 # location is a file-like object
100 if hasattr(location, "read"):
101 tmpfile = os.path.join(tmpdir, "file")
102 tmpfileobj = file(tmpfile, 'w')
103 shutil.copyfileobj(location, tmpfileobj)
104 tmpfileobj.close()
105 return tmpfile
106
107 if isinstance(location, types.StringTypes):
108 # location is a URL
109 if location.startswith('http') or location.startswith('ftp'):
110 tmpfile = os.path.join(tmpdir, os.path.basename(location))
111 urllib.urlretrieve(location, tmpfile)
112 return tmpfile
113 # location is a local path
114 elif os.path.exists(os.path.abspath(location)):
115 tmpfile = os.path.join(tmpdir, os.path.basename(location))
116 if os.path.isdir(location):
117 tmpfile += '/'
118 shutil.copytree(location, tmpfile, symlinks=True)
119 return tmpfile
120 shutil.copyfile(location, tmpfile)
121 return tmpfile
122 # location is just a string, dump it to a file
123 else:
124 tmpfd, tmpfile = tempfile.mkstemp(dir=tmpdir)
125 tmpfileobj = os.fdopen(tmpfd, 'w')
126 tmpfileobj.write(location)
127 tmpfileobj.close()
128 return tmpfile
129
130
131def run(command, timeout=None):
132 """Run a command on the host.
133
134 Args:
135 command: the command line string
136 timeout: time limit in seconds before attempting to
137 kill the running process. The run() function
138 will take a few seconds longer than 'timeout'
139 to complete if it has to kill the process.
140
141 Returns:
142 a hosts.CmdResult object
143
144 Raises:
145 AutoservRunError: the exit code of the command
146 execution was not 0
147
148 TODO(poirier): Add a "tee" option to send the command's
149 stdout and stderr to python's stdout and stderr? At
150 the moment, there is no way to see the command's
151 output as it is running.
152 TODO(poirier): Should a timeout raise an exception? Should
153 exceptions be raised at all?
154 """
155 result= hosts.CmdResult()
156 result.command= command
157 sp= subprocess.Popen(command, stdout=subprocess.PIPE,
158 stderr=subprocess.PIPE, close_fds=True, shell=True,
159 executable="/bin/bash")
160
161 start_time= time.time()
162 if timeout:
163 stop_time= start_time + timeout
164 time_left= stop_time - time.time()
165 while time_left > 0:
166 # select will return when stdout is ready
167 # (including when it is EOF, that is the
168 # process has terminated).
169 (retval, tmp, tmp) = select.select(
170 [sp.stdout], [], [], time_left)
171 if len(retval):
172 # os.read() has to be used instead of
173 # sp.stdout.read() which will
174 # otherwise block
175 result.stdout += os.read(
176 sp.stdout.fileno(), 1024)
177
178 (pid, exit_status_indication) = os.waitpid(
179 sp.pid, os.WNOHANG)
180 if pid:
181 stop_time= time.time()
182 time_left= stop_time - time.time()
183
184 # the process has not terminated within timeout,
185 # kill it via an escalating series of signals.
186 if not pid:
187 signal_queue = [signal.SIGTERM, signal.SIGKILL]
188 for sig in signal_queue:
189 try:
190 os.kill(sp.pid, sig)
191 # handle race condition in which
192 # process died before we could kill it.
193 except OSError:
194 pass
195
196 for i in range(5):
197 (pid, exit_status_indication
198 ) = os.waitpid(sp.pid,
199 os.WNOHANG)
200 if pid:
201 break
202 else:
203 time.sleep(1)
204 if pid:
205 break
206 else:
207 exit_status_indication = os.waitpid(sp.pid, 0)[1]
208
209 result.duration = time.time() - start_time
210 result.aborted = exit_status_indication & 127
211 if result.aborted:
212 result.exit_status= None
213 else:
214 result.exit_status= exit_status_indication / 256
215 result.stdout += sp.stdout.read()
216 result.stderr = sp.stderr.read()
217
218 if result.exit_status > 0:
219 raise errors.AutoservRunError("command execution error",
220 result)
221
222 return result
223
224
225def get_tmp_dir():
226 """Return the pathname of a directory on the host suitable
227 for temporary file storage.
228
229 The directory and its content will be deleted automatically
230 at the end of the program execution if they are still present.
231 """
232 global __tmp_dirs
233
234 dir_name= tempfile.mkdtemp(prefix="autoserv-")
235 __tmp_dirs.append(dir_name)
236 return dir_name
237
238
239@atexit.register
240def __clean_tmp_dirs():
241 """Erase temporary directories that were created by the get_tmp_dir()
242 function and that are still present.
243 """
244 global __tmp_dirs
245
246 for dir in __tmp_dirs:
247 shutil.rmtree(dir)
248 __tmp_dirs= []