#!/usr/bin/python3 '''Only allow one of something to run at a time''' from __future__ import print_function import os import pwd import sys import errno import subprocess def usage(retval): '''Output a usage message''' if retval == 0: file_ = sys.stdout else: file_ = sys.stderr file_.write("Usage: %s --command command --string string\n" % sys.argv[0]) sys.exit(retval) def start_command(verbose, command, filename): '''Start the command - return a shell-compatible exit status''' if verbose: print('about to start command') sys.stdout.flush() try: subp = subprocess.Popen(command, shell=True) pid = subp.pid with open(filename, 'w') as file_: file_.write('{}\n'.format(pid)) subp.communicate() return subp.returncode finally: os.unlink(filename) def pid_exists(verbose, pid): '''Return True iff there is a process running corresponding to the given pid''' command = ['ps', '-p', str(pid)] with open(os.devnull, 'wb') as devnull: if subprocess.call(command, stdout=devnull, stderr=devnull) == 0: if verbose: print('pid does exist') return True else: if verbose: print('pid does not exist') return False def get_just_one_dir(): '''Build the directory to use, creating it if it doesn't yet exist''' uid = os.geteuid() pwent = pwd.getpwuid(uid) home_dir = pwent.pw_dir just_one_dir = os.path.join(home_dir, '.just-one') try: os.mkdir(just_one_dir, 7 * 64 + 5 * 8 + 5) except (OSError, IOError): _unused, mkdir_extra, _unused = sys.exc_info() if mkdir_extra.errno == errno.EEXIST: pass else: raise return just_one_dir class Options(object): # pylint: disable=too-few-public-methods '''Deal with command line options''' def __init__(self): self.command = None self.string = None self.verbose = True def parse(self): '''Parse sys.argv''' while sys.argv[1:]: if sys.argv[1] == '--command': self.command = sys.argv[2] del sys.argv[1] elif sys.argv[1] == '--string': self.string = sys.argv[2] del sys.argv[1] elif sys.argv[1] == '--verbose': self.verbose = True elif sys.argv[1] == '--quiet': self.verbose = False elif sys.argv[1] in ['--help', '-h']: usage(0) else: sys.stderr.write("%s: Unrecognized option: %s\n" % (sys.argv[0], sys.argv[1])) usage(1) del sys.argv[1] required_missing = False if not self.command: sys.stderr.write("%s: --command command is a required option\n" % sys.argv[0]) required_missing = True if not self.string: sys.stderr.write("%s: --string string is a required option\n" % sys.argv[0]) required_missing = True if required_missing: sys.stderr.write('%s: one or more required options missing\n' % sys.argv[0]) usage(1) def run_one(verbose, command, string): '''Run the specified process, but only if it isn't already running''' if verbose: print('about to mkdir') just_one_dir = get_just_one_dir() if verbose: print('about to get pid') filename = os.path.join(just_one_dir, string) pid = None try: file_ = open(filename, 'r') except (OSError, IOError): _unused, open_extra, _unused = sys.exc_info() if open_extra.errno == errno.ENOENT: pass else: raise else: pid = int(file_.readline()) file_.close() if pid: if verbose: print('got pid %d' % pid) if pid_exists(verbose, pid): sys.stderr.write("%s: %s is locked\n" % (sys.argv[0], string)) exit_code = 1 else: exit_code = start_command(verbose, command, filename) else: exit_code = start_command(verbose, command, filename) return exit_code def main(): '''Main function''' options = Options() options.parse() exit_code = run_one(verbose=options.verbose, command=options.command, string=options.string) sys.exit(exit_code) if __name__ == '__main__': main()