# The contents of this file are subject to the BitTorrent Open Source License
# Version 1.1 (the License).  You may not copy or use this file, in either
# source code or executable form, except in compliance with the License.  You
# may obtain a copy of the License at http://www.bittorrent.com/license/.
#
# Software distributed under the License is distributed on an AS IS basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied.  See the License
# for the specific language governing rights and limitations under the
# License.

# Author: David Harrison

if __name__ == "__main__":
    def _(s):
        return s
else:
    from BitTorrent.translation import _

import os

if os.name == 'nt':
    import win32file, win32event, win32api, winerror
    from win32file import CreateFile, CreateFileW
    import pywintypes

elif os.name == 'posix':
    import fcntl
    from BitTorrent import platform
    from fcntl import flock


class NamedMutex(object):
    """Reasonably cross-platform, cross-process mutex.

       It does not implement mutual exclusion between threads in the
       same process.  Use threading.Lock or threading.RLock for mutual
       exclusion between threads.
    """
    # In Unix:
    #   Same semantics as directly locking a file: mutual exclusion is
    #   provided when a process can lock the file.  If
    #   the file does not exist, it is created. 
    #
    # In NT:
    #   Uses Windows CreateMutex.
    # obtain_mutex = 1
    # mutex = win32event.CreateMutex(None, obtain_mutex, name)

    def __init__(self, name):
        self._mylock = False
        self._name = name
        self._mutex = None 
        if os.name in ('posix','max'):
            # We don't use config["data_dir"] to keep mutex separate from 
            # other configuration data since bittorrent.py will have a 
            # separate config directory from bittorrent console and curses 
            # apps.
            ddir = platform.get_dot_dir()
            self._path = os.path.join( ddir, "mutex", name )
            if not os.path.exists(ddir):
                os.mkdir(ddir, 0700)

    def owner(self):
        return self._mylock
    
    def acquire(self, wait=True):  
        """Acquires mutual exclusion.  Returns true iff acquired."""
        if os.name == 'nt':

            # Gotcha: Not checking self._mylock in windows, because it is
            # possible to acquire the mutex from more than one object with
            # the same name.  This needs some work to make the semantics
            # exactly the same between Windows and Unix.
            obtain_mutex = 1
            self._mutex = win32event.CreateMutex(None,obtain_mutex,self._name)
            if self._mutex is None:
                return False
            lasterror = win32api.GetLastError()
            if lasterror == winerror.ERROR_ALREADY_EXISTS:
                if wait:
                   # mutex exists and has been opened(not created, not locked).
                   r = win32event.WaitForSingleObject(self._mutex, 
                                                      win32event.INFINITE)
                else:
                   r = win32event.WaitForSingleObject(self._mutex, 0)

                # WAIT_OBJECT_0 means the mutex was obtained
                # WAIT_ABANDONED means the mutex was obtained, 
                # and it had previously been abandoned
                if (r != win32event.WAIT_OBJECT_0 and 
                    r != win32event.WAIT_ABANDONED):
                    return False
    
        elif os.name in ('posix','max'):
            if self._mylock: 
                return True
            
            if os.path.exists(self._path) and not os.path.isfile(self._path):
                raise BTFailure( 
                    "Cannot lock file that is not regular file." )

            (dir,name) = os.path.split(self._path)
            if not os.path.exists(dir):
                os.mkdir(dir, 0700)  # mode=0700 = allow user access.

            while True:  # <--- for file deletion race condition (see __del__)

                # UNIX does not support O_TEMPORARY!! Blech. This complicates
                # file deletion (see "while True" above and path checking after
                # "flock").
                #self._mutex = os.open( self._path, os.O_CREAT|os.O_TEMPORARY )
                self._mutex = open( self._path, "w" )
                if wait:
                    flags = fcntl.LOCK_EX
                else:
                    flags = fcntl.LOCK_EX | fcntl.LOCK_NB
                try:
                    flock( self._mutex.fileno(), flags)
                except IOError:
                    return False

                # race condition: __del__ may have deleted the file.
                if not os.path.exists( self._path ):
                    self._mutex.close()
                else:
                    break
    
        else:
            # dangerous, but what should we do if the platform neither
            # supports named mutexes nor file locking?   --Dave
            pass
            
        self._mylock = True
        return True
  
    def release(self):
        # unlock
        assert self._mylock
        if os.name == 'nt':
            win32event.ReleaseMutex(self._mutex)
            # Error code 123? 
            #lasterror = win32api.GetLastError()
            #if lasterror != 0:
            #    raise IOError( _("Could not release mutex %s due to "
            #                     "error windows code %d.") % 
            #                    (self._name,lasterror) )
        elif os.name == 'posix':
            self._mylock = False
            if not os.path.exists(self._path):
                raise IOError( _("Non-existent file: %s") % self._path )
            flock( self._mutex.fileno(), fcntl.LOCK_UN )
            self._mutex.close()

    def __del__(self):

        if os.name == 'nt':
            if self._mutex is not None:
                # Gotchas: Don't close the handle before releasing it or the
                # mutex won't release until the python script exits.  It's 
                # safe to call ReleaseMutex even if the local process hasn't 
                # acquired the mutex.  If this process doesn't have mutex then 
                # the call fails.  Note that in Windows, mutexes are literally 
                # per process.  Multiple mutexes created with the same name
                # from the same process will be treated as one with respect to 
                # release and acquire.
                self.release()

                # windows will destroy the mutex when the last handle to that
                # mutex is closed.
                win32api.CloseHandle(self._mutex)
                del self._mutex
   
        elif os.name == 'posix':
            if self._mylock:
                self.release()

            # relock file non-blocking to see if anyone else was 
            # waiting on it.  (A race condition exists where another process
            # could have just opened but not yet locked the file.  This process
            # then deletes the file.  When the other process resumes, the
            # flock call fails.  This is however not a particularly bad
            # race condition since acquire() simply repeats the open & flock.)
            if os.path.exists(self._path) and self.acquire(False):

                try:
                    os.remove(self._path)
                except:
                    pass

                # Strange to unlock the file after it has been deleted.
                # Works on all UNIX platforms?  If I release before
                # deleting then I introduce a race condition where the
                # another process acquires the lock then
                # this process deletes the file.
                flock( self._mutex.fileno(), fcntl.LOCK_UN )
                self._mutex.close()

if __name__ == "__main__":
    # perform unit tests.
    n_tests = n_tests_passed = 0
    n_tests += 1
    mutex = NamedMutex("blah")    
    if mutex.acquire() and mutex._mutex is not None and mutex._mylock:
        n_tests_passed += 1
    else:
        print "FAIL! Failed to acquire mutex on a new NamedMutex."

    # attempt to acquire again.  Since already acquired, it should fail.
    #n_tests += 1
    #if not mutex.acquire():
    #    n_tests_passed += 1
    #else:
    #    print "FAIL! Second acquire should return false but returned true."

    n_tests += 1
    mutex.release()
    if mutex._mutex is not None and not mutex._mylock:
        n_tests_passed += 1
    else:
        print "FAIL! Did not properly release mutex."

    n_tests += 1
    if mutex.acquire():
        if mutex._mutex is not None and mutex._mylock:
            n_tests_passed += 1
        else: 
            print ( "FAIL! After calling acquire on a released NamedMutex, "
                    "either mutex._mutex is None or mutex._mylock is false." )
    else:
        print "FAIL! Failed to acquire mutex a released NamedMutex."


    # okay.  I should add more tests.

    del mutex

    if n_tests == n_tests_passed:
        print "Passed all %d tests." % n_tests
