Revision c1dd99d4 lib/utils.py
b/lib/utils.py | ||
---|---|---|
28 | 28 |
|
29 | 29 |
|
30 | 30 |
import os |
31 |
import sys |
|
31 | 32 |
import time |
32 | 33 |
import subprocess |
33 | 34 |
import re |
... | ... | |
119 | 120 |
output = property(_GetOutput, None, None, "Return full output") |
120 | 121 |
|
121 | 122 |
|
122 |
def RunCmd(cmd, env=None, output=None, cwd='/'): |
|
123 |
def _BuildCmdEnvironment(env): |
|
124 |
"""Builds the environment for an external program. |
|
125 |
|
|
126 |
""" |
|
127 |
cmd_env = os.environ.copy() |
|
128 |
cmd_env["LC_ALL"] = "C" |
|
129 |
if env is not None: |
|
130 |
cmd_env.update(env) |
|
131 |
return cmd_env |
|
132 |
|
|
133 |
|
|
134 |
def RunCmd(cmd, env=None, output=None, cwd="/"): |
|
123 | 135 |
"""Execute a (shell) command. |
124 | 136 |
|
125 | 137 |
The command should not read from its standard input, as it will be |
126 | 138 |
closed. |
127 | 139 |
|
128 |
@type cmd: string or list
|
|
140 |
@type cmd: string or list |
|
129 | 141 |
@param cmd: Command to run |
130 | 142 |
@type env: dict |
131 |
@param env: Additional environment |
|
143 |
@param env: Additional environment variables
|
|
132 | 144 |
@type output: str |
133 | 145 |
@param output: if desired, the output of the command can be |
134 | 146 |
saved in a file instead of the RunResult instance; this |
... | ... | |
144 | 156 |
if no_fork: |
145 | 157 |
raise errors.ProgrammerError("utils.RunCmd() called with fork() disabled") |
146 | 158 |
|
147 |
if isinstance(cmd, list): |
|
159 |
if isinstance(cmd, basestring): |
|
160 |
strcmd = cmd |
|
161 |
shell = True |
|
162 |
else: |
|
148 | 163 |
cmd = [str(val) for val in cmd] |
149 |
strcmd = " ".join(cmd)
|
|
164 |
strcmd = ShellQuoteArgs(cmd)
|
|
150 | 165 |
shell = False |
166 |
|
|
167 |
if output: |
|
168 |
logging.debug("RunCmd %s, output file '%s'", strcmd, output) |
|
151 | 169 |
else: |
152 |
strcmd = cmd |
|
153 |
shell = True |
|
154 |
logging.debug("RunCmd '%s'", strcmd) |
|
170 |
logging.debug("RunCmd %s", strcmd) |
|
155 | 171 |
|
156 |
cmd_env = os.environ.copy() |
|
157 |
cmd_env["LC_ALL"] = "C" |
|
158 |
if env is not None: |
|
159 |
cmd_env.update(env) |
|
172 |
cmd_env = _BuildCmdEnvironment(env) |
|
160 | 173 |
|
161 | 174 |
try: |
162 | 175 |
if output is None: |
... | ... | |
181 | 194 |
return RunResult(exitcode, signal_, out, err, strcmd) |
182 | 195 |
|
183 | 196 |
|
197 |
def StartDaemon(cmd, env=None, cwd="/", output=None, output_fd=None, |
|
198 |
pidfile=None): |
|
199 |
"""Start a daemon process after forking twice. |
|
200 |
|
|
201 |
@type cmd: string or list |
|
202 |
@param cmd: Command to run |
|
203 |
@type env: dict |
|
204 |
@param env: Additional environment variables |
|
205 |
@type cwd: string |
|
206 |
@param cwd: Working directory for the program |
|
207 |
@type output: string |
|
208 |
@param output: Path to file in which to save the output |
|
209 |
@type output_fd: int |
|
210 |
@param output_fd: File descriptor for output |
|
211 |
@type pidfile: string |
|
212 |
@param pidfile: Process ID file |
|
213 |
@rtype: int |
|
214 |
@return: Daemon process ID |
|
215 |
@raise errors.ProgrammerError: if we call this when forks are disabled |
|
216 |
|
|
217 |
""" |
|
218 |
if no_fork: |
|
219 |
raise errors.ProgrammerError("utils.StartDaemon() called with fork()" |
|
220 |
" disabled") |
|
221 |
|
|
222 |
if output and not (bool(output) ^ (output_fd is not None)): |
|
223 |
raise errors.ProgrammerError("Only one of 'output' and 'output_fd' can be" |
|
224 |
" specified") |
|
225 |
|
|
226 |
if isinstance(cmd, basestring): |
|
227 |
cmd = ["/bin/sh", "-c", cmd] |
|
228 |
|
|
229 |
strcmd = ShellQuoteArgs(cmd) |
|
230 |
|
|
231 |
if output: |
|
232 |
logging.debug("StartDaemon %s, output file '%s'", strcmd, output) |
|
233 |
else: |
|
234 |
logging.debug("StartDaemon %s", strcmd) |
|
235 |
|
|
236 |
cmd_env = _BuildCmdEnvironment(env) |
|
237 |
|
|
238 |
# Create pipe for sending PID back |
|
239 |
(pidpipe_read, pidpipe_write) = os.pipe() |
|
240 |
try: |
|
241 |
try: |
|
242 |
# Create pipe for sending error messages |
|
243 |
(errpipe_read, errpipe_write) = os.pipe() |
|
244 |
try: |
|
245 |
try: |
|
246 |
# First fork |
|
247 |
pid = os.fork() |
|
248 |
if pid == 0: |
|
249 |
try: |
|
250 |
# Child process, won't return |
|
251 |
_RunCmdDaemonChild(errpipe_read, errpipe_write, |
|
252 |
pidpipe_read, pidpipe_write, |
|
253 |
cmd, cmd_env, cwd, |
|
254 |
output, output_fd, pidfile) |
|
255 |
finally: |
|
256 |
# Well, maybe child process failed |
|
257 |
os._exit(1) |
|
258 |
finally: |
|
259 |
_CloseFDNoErr(errpipe_write) |
|
260 |
|
|
261 |
# Wait for daemon to be started (or an error message to arrive) and read |
|
262 |
# up to 100 KB as an error message |
|
263 |
errormsg = RetryOnSignal(os.read, errpipe_read, 100 * 1024) |
|
264 |
finally: |
|
265 |
_CloseFDNoErr(errpipe_read) |
|
266 |
finally: |
|
267 |
_CloseFDNoErr(pidpipe_write) |
|
268 |
|
|
269 |
# Read up to 128 bytes for PID |
|
270 |
pidtext = RetryOnSignal(os.read, pidpipe_read, 128) |
|
271 |
finally: |
|
272 |
_CloseFDNoErr(pidpipe_read) |
|
273 |
|
|
274 |
# Try to avoid zombies by waiting for child process |
|
275 |
try: |
|
276 |
os.waitpid(pid, 0) |
|
277 |
except OSError: |
|
278 |
pass |
|
279 |
|
|
280 |
if errormsg: |
|
281 |
raise errors.OpExecError("Error when starting daemon process: %r" % |
|
282 |
errormsg) |
|
283 |
|
|
284 |
try: |
|
285 |
return int(pidtext) |
|
286 |
except (ValueError, TypeError), err: |
|
287 |
raise errors.OpExecError("Error while trying to parse PID %r: %s" % |
|
288 |
(pidtext, err)) |
|
289 |
|
|
290 |
|
|
291 |
def _RunCmdDaemonChild(errpipe_read, errpipe_write, |
|
292 |
pidpipe_read, pidpipe_write, |
|
293 |
args, env, cwd, |
|
294 |
output, fd_output, pidfile): |
|
295 |
"""Child process for starting daemon. |
|
296 |
|
|
297 |
""" |
|
298 |
try: |
|
299 |
# Close parent's side |
|
300 |
_CloseFDNoErr(errpipe_read) |
|
301 |
_CloseFDNoErr(pidpipe_read) |
|
302 |
|
|
303 |
# First child process |
|
304 |
os.chdir("/") |
|
305 |
os.umask(077) |
|
306 |
os.setsid() |
|
307 |
|
|
308 |
# And fork for the second time |
|
309 |
pid = os.fork() |
|
310 |
if pid != 0: |
|
311 |
# Exit first child process |
|
312 |
os._exit(0) # pylint: disable-msg=W0212 |
|
313 |
|
|
314 |
# Make sure pipe is closed on execv* (and thereby notifies original process) |
|
315 |
SetCloseOnExecFlag(errpipe_write, True) |
|
316 |
|
|
317 |
# List of file descriptors to be left open |
|
318 |
noclose_fds = [errpipe_write] |
|
319 |
|
|
320 |
# Open PID file |
|
321 |
if pidfile: |
|
322 |
try: |
|
323 |
# TODO: Atomic replace with another locked file instead of writing into |
|
324 |
# it after creating |
|
325 |
fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600) |
|
326 |
|
|
327 |
# Lock the PID file (and fail if not possible to do so). Any code |
|
328 |
# wanting to send a signal to the daemon should try to lock the PID |
|
329 |
# file before reading it. If acquiring the lock succeeds, the daemon is |
|
330 |
# no longer running and the signal should not be sent. |
|
331 |
LockFile(fd_pidfile) |
|
332 |
|
|
333 |
os.write(fd_pidfile, "%d\n" % os.getpid()) |
|
334 |
except Exception, err: |
|
335 |
raise Exception("Creating and locking PID file failed: %s" % err) |
|
336 |
|
|
337 |
# Keeping the file open to hold the lock |
|
338 |
noclose_fds.append(fd_pidfile) |
|
339 |
|
|
340 |
SetCloseOnExecFlag(fd_pidfile, False) |
|
341 |
else: |
|
342 |
fd_pidfile = None |
|
343 |
|
|
344 |
# Open /dev/null |
|
345 |
fd_devnull = os.open(os.devnull, os.O_RDWR) |
|
346 |
|
|
347 |
assert not output or (bool(output) ^ (fd_output is not None)) |
|
348 |
|
|
349 |
if fd_output is not None: |
|
350 |
pass |
|
351 |
elif output: |
|
352 |
# Open output file |
|
353 |
try: |
|
354 |
# TODO: Implement flag to set append=yes/no |
|
355 |
fd_output = os.open(output, os.O_WRONLY | os.O_CREAT, 0600) |
|
356 |
except EnvironmentError, err: |
|
357 |
raise Exception("Opening output file failed: %s" % err) |
|
358 |
else: |
|
359 |
fd_output = fd_devnull |
|
360 |
|
|
361 |
# Redirect standard I/O |
|
362 |
os.dup2(fd_devnull, 0) |
|
363 |
os.dup2(fd_output, 1) |
|
364 |
os.dup2(fd_output, 2) |
|
365 |
|
|
366 |
# Send daemon PID to parent |
|
367 |
RetryOnSignal(os.write, pidpipe_write, str(os.getpid())) |
|
368 |
|
|
369 |
# Close all file descriptors except stdio and error message pipe |
|
370 |
CloseFDs(noclose_fds=noclose_fds) |
|
371 |
|
|
372 |
# Change working directory |
|
373 |
os.chdir(cwd) |
|
374 |
|
|
375 |
if env is None: |
|
376 |
os.execvp(args[0], args) |
|
377 |
else: |
|
378 |
os.execvpe(args[0], args, env) |
|
379 |
except: # pylint: disable-msg=W0702 |
|
380 |
try: |
|
381 |
# Report errors to original process |
|
382 |
buf = str(sys.exc_info()[1]) |
|
383 |
|
|
384 |
RetryOnSignal(os.write, errpipe_write, buf) |
|
385 |
except: # pylint: disable-msg=W0702 |
|
386 |
# Ignore errors in error handling |
|
387 |
pass |
|
388 |
|
|
389 |
os._exit(1) # pylint: disable-msg=W0212 |
|
390 |
|
|
391 |
|
|
184 | 392 |
def _RunCmdPipe(cmd, env, via_shell, cwd): |
185 | 393 |
"""Run a command and return its output. |
186 | 394 |
|
Also available in: Unified diff