Revision 7614 (by gradha, 2006/12/02 18:13:42) Whitespace cleanup.
#!/usr/bin/env python
# -*- mode:Python; tab-width: 3 -*-
"""
Small module containing methods to avoid multiple runs of the same
script/binary. It also provides a simple function which runs external
binaries in a blocking fashion and returns the results. I'm afraid
the whole script is Unix dependant and non portable to Windows.

This script is gift-ware, it's given to you freely as a gift.
You may use, modify, redistribute, and generally hack it about
in any way you like, and you do not have to give me anything in
return. However, if you like this script you are encouraged to thank
me emailing me your opinion about it, requests for more features,
or directly diffs which implement them. If you redistribute parts
of this script or use it somewhere successfully, it would be nice if
you mentioned me somewhere in the credits, but you are not required
to do this.

Of course, I do not accept responsibility for any effects, adverse
or otherwise, that this code may have on you, your computer, your
sanity, your dog and anything else that you can think of.

Use it at your own risk.

Written by Grzegorz Adam Hankiewicz <gradha@users.sourceforge.net>.
Download this and other things from http://gradha.sdf-eu.org/.

$Id: singleton.py 7614 2006-12-02 18:13:42Z gradha $
"""


import os
import popen2
import sys
import time


def still_running(pid_as_string):
   """Returns zero if the process is not running."""
   child_stdin, child_stdout_and_stderr = os.popen4(["ps", "ux"])
   child_stdin.close()
   # filter processes with the given pid
   lines = filter(lambda x: x.find(" %s " % pid_as_string) >= 0,
      child_stdout_and_stderr.readlines())
   child_stdout_and_stderr.close()
   # second test, filter with the current binary name
   lines = filter(lambda x: x.find(os.path.basename(sys.argv[0])) >= 0, lines)
   return len(lines)


def run_if_possible(entry_point):
   """Given a function, it is called if there's no previous process running"""
   permission = 0
   lock_path = "%s.lock" % os.path.realpath(sys.argv[0])
   if os.path.isfile(lock_path):
      input_file = open(lock_path, "rt")
      pid = input_file.readline() # it's a string! not an integer
      input_file.close()
      if still_running(pid):
         sys.stderr.write("Process '%s:%s' is still running\n" % (pid,
            sys.argv[0]))
      else:
         sys.stderr.write("Found stale lock '%s'\n" % lock_path)
         permission = 1
   else:
      permission = 1

   if permission:
      output_file = open(lock_path, "wt")
      output_file.write("%d" % os.getpid())
      output_file.close()

      try:
         entry_point()
      finally:
         try: os.unlink(lock_path)
         except IOError: pass


def run_external(command_list):
   """func([command_list]) -> (exit_code, [stdout_stderr_lines]).

   command_list is really a single command with parameters. This
      avoids problems with parameters having spaces in them. Example:
      run_external(["touch", "my file"]).

   exit_code is the typical return value of os.wait()[1]. Zero
      means that all went correctly, nonzero means there was
      a problem.
   stdout_stderr_lines is a list of lines with the text produced
      by the command, both normal and error output. If the program
      exits with a strange error code, this will be appended as
      text to the list line of this variable. Note that output
      lines will contain line terminators.

   An empty command list will return (0, []).

   If command_list is not a sizeable object, TypeError will be
   raised.
   """
   if len(command_list) < 1:
      return (0, [])

   output = []
   command = popen2.Popen4(command_list)
   command.tochild.close()
   output.extend(command.fromchild.readlines())
   command.fromchild.close()
   exit_code = command.wait()
   if exit_code:
      output.append("*** command `%s' exited with %d ***\n" %
         (command_list, exit_code))
   return (exit_code, output)


def run_forgiving(command_list, retries, seconds_between):
   """func([command_list], num, num) -> (exit_code, [stdout_stderr_lines]).

   command_list is really a single command with parameters. This
      avoids problems with parameters having spaces in them. Example:
      run_external(["touch", "my file"]).
   retries can be zero or positive integer which says how many
      times the same command should be retried if it fails. Useful
      for running commands which depend on external factors like
      network connectivity and which usually can be retried and
      will be ok.
   seconds_between can be zero or positive number stating how
      many seconds I have to wait between retries (if any).

   exit_code is the typical return value of os.wait()[1]. Zero
      means that all went correctly, nonzero means there was a
      problem. If the command was retried, only the last exit_code
      will be returned.
   stdout_stderr_lines is a list of lines with the text produced
      by the command, both normal and error output. If the program
      exits with a strange error code, this will be appended as text
      to the list line of this variable. Note that output lines
      will contain line terminators. If the command was retried
      several times, this will contain the combined output of all
      tried commands.

   An empty command list will return (0, []).

   If command_list is not a sizeable object, TypeError will be
   raised. The same will happen for retries and seconds_between if
   they are not numeric.
   If retries or seconds_between are not bigger or equal zero
   AssertionError will be raised with an out of range message.
   """
   assert retries >= 0, "retries out of range"
   assert seconds_between >= 0, "seconds_between out of range"

   exit_code, full_output = run_external(command_list)

   while retries > 0 and exit_code != 0:
      time.sleep(seconds_between)
      exit_code, additional_output = run_external(command_list)
      full_output.extend(additional_output)
      retries = retries - 1

   return exit_code, full_output