#!/usr/bin/python3

# Copyright 2009-2016 Peter Poeml
# 
#    Licensed under the Apache License, Version 2.0 (the "License");
#    you may not use this file except in compliance with the License.
#    You may obtain a copy of the License at
# 
#        http://www.apache.org/licenses/LICENSE-2.0
# 
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS,
#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#    See the License for the specific language governing permissions and
#    limitations under the License.
#
#
#
# withlock -- https://github.com/poeml/withlock
#
#
# A locking wrapper that make sure that a program isn't run more than once.
# It creates locks that are valid only while the wrapper is running, and thus
# will never require a cleanup, e.g. after a reboot.

# Parts of the locking strategy, and parts of the usage semantics, of this
# script were inspired from with-lock-ex.c, which was written by Ian Jackson and
# placed in the public domain by him.
#
#
# Usage is simple. Instead of your command 
#   CMD ARGS...
# you simply use
#   withlock LOCKFILE CMD ARGS...
#
# See --help output for more options.

# Modernize features for Python 2 and 3 compatibility
from __future__ import absolute_import
from __future__ import print_function

__version__ = '0.5'

import os
import os.path
import errno
import sys
import time
import stat
import fcntl
import signal
import atexit
from optparse import OptionParser

got_lock = False

class SignalInterrupt(Exception):
    """Exception raised on SIGTERM and SIGHUP."""


def catchterm(*args):
    raise SignalInterrupt


for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
    num = getattr(signal, name, None)
    if num: signal.signal(num, catchterm)


def cleanup(lockfile, verbose):
    global got_lock
    if got_lock:
        if verbose:
            sys.stderr.write('removing lockfile: %r\n' % lockfile)

        try:
            os.unlink(lockfile)
        except:
            pass


def main():

    usage = 'usage: %prog [options] LOCKFILE CMD ARGS...'
    version = '%prog ' + __version__

    parser = OptionParser(usage=usage, version=version)
    parser.disable_interspersed_args()

    parser.add_option('-w', '--wait',
                      dest='wait',
                      help="wait for maximum SECONDS until the lock is acquired",
                      metavar="SECONDS")

    parser.add_option("-q", "--quiet",
                      action="store_true", dest="quiet", default=False,
                      help="if lock can't be acquired immediately, silently "
                           "quit without error")

    parser.add_option("-v", "--verbose",
                      action="store_true", dest="verbose", default=False,
                      help="print debug messages to stderr")



    (options, args) = parser.parse_args()

    usage = usage.replace('%prog', os.path.basename(sys.argv[0]))


    if len(args) < 2:
        sys.exit(usage)

    lockfile = args[0]
    cmd = args[1:]
    waited = 0
    if options.wait:
        options.wait = int(options.wait)

    if options.verbose:
        sys.stderr.write('lockfile: %r\n' % lockfile)
        sys.stderr.write('cmd: %r\n' % cmd)

    # check that symlink attacks in the lockfile directory are not possible
    lockdir = os.path.dirname(os.path.realpath(lockfile)) or os.curdir
    lockdir_mode = stat.S_IMODE(os.stat(lockdir).st_mode)
    if options.verbose:
        sys.stderr.write('lockdir: %r\n' % lockdir)
        sys.stderr.write('lockdir mode: %s\n' % oct(lockdir_mode))
    if (lockdir_mode & stat.S_IWGRP) or (lockdir_mode & stat.S_IWOTH):
        sys.stderr.write('withlock: the destination directory for %r is %r, \n'
                         '          which is writable by other users. That allows for symlink attacks. \n'
                         '          Choose another directory.\n' \
                                % (lockfile, lockdir))
        sys.exit(3)


    prev_umask = os.umask(0o066)

    global got_lock
    while 1 + 1 == 2:

        try:
            lock = open(lockfile, 'w')
            fcntl.lockf(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
            got_lock = True
            break
        except IOError as e:
            if e.errno in [ errno.EAGAIN, 
                            errno.EACCES, 
                            errno.EWOULDBLOCK, 
                            errno.EBUSY ]:
                if options.wait:
                    if waited >= options.wait:
                        if options.quiet:
                            if options.verbose:
                                sys.stderr.write('quitting silently\n')
                            sys.exit(0)
                        else:
                            sys.stderr.write('could not acquire lock\n')
                            sys.exit(1)
                    if options.verbose and waited == 0:
                        sys.stderr.write('waiting up to %ss for lock\n' \
                                            % options.wait)
                    time.sleep(1)
                    waited += 1
                    continue
                elif options.quiet:
                    if options.verbose:
                        sys.stderr.write('quitting silently\n')
                    sys.exit(0)
                else:
                    sys.exit('could not acquire lock')
                
        try:
            os.stat(lockfile)
            break
        except OSError as e: 
            if e.errno == errno.ENOENT:
                sys.exit('====== could not stat %s, which we have just locked' \
                            % lockfile)


    atexit.register(cleanup, lockfile, options.verbose)
    os.umask(prev_umask)

    import subprocess
    rc = subprocess.call(' '.join(cmd), shell=True)
    sys.stdout.flush()
    sys.stderr.flush()

    if options.verbose:
        sys.stderr.write('command terminated with exit code %s\n' % rc)

    sys.exit(rc)


if __name__ == '__main__':

    try:
        main()

    except SignalInterrupt:
        print('killed!', file=sys.stderr)

    except KeyboardInterrupt:
        print('interrupted!', file=sys.stderr)

