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