# Copyright (c) 2005 Divmod, Inc.
# Copyright (c) Twisted Matrix Laboratories.
# Copyright (c) Spyder Project Contributors
# Twisted is distributed under the MIT license.

"""
Filesystem-based interprocess mutex.

Taken from the Twisted project.
Distributed under the MIT license.

Changes by the Spyder Team to the original module:
  * Rewrite kill Windows function to make it more reliable.
  * Detect if the process that owns the lock is an Spyder one.
"""

__metaclass__ = type

import errno, os
from time import time as _uniquefloat

import psutil
from spyder.config.base import running_under_pytest
from spyder.py3compat import PY2, to_binary_string

def unique():
    if PY2:
        return str(long(_uniquefloat() * 1000))
    else:
        return str(int(_uniquefloat() * 1000))

from os import rename
if not os.name == 'nt':
    from os import kill
    from os import symlink
    from os import readlink
    from os import remove as rmlink
    _windows = False
else:
    _windows = True

    import ctypes
    from ctypes import wintypes

    # https://docs.microsoft.com/en-us/windows/desktop/ProcThread/process-security-and-access-rights
    PROCESS_QUERY_INFORMATION = 0x400

    # GetExitCodeProcess uses a special exit code to indicate that the
    # process is still running.
    STILL_ACTIVE = 259
    
    def _is_pid_running(pid):
        """Taken from https://www.madebuild.org/blog/?p=30"""
        kernel32 = ctypes.windll.kernel32
        handle = kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid)
        if handle == 0:
            return False
         
        # If the process exited recently, a pid may still exist for the
        # handle. So, check if we can get the exit code.
        exit_code = wintypes.DWORD()
        retval = kernel32.GetExitCodeProcess(handle,
                                             ctypes.byref(exit_code))
        is_running = (retval == 0)
        kernel32.CloseHandle(handle)
         
        # See if we couldn't get the exit code or the exit code indicates
        # that the process is still running.
        return is_running or exit_code.value == STILL_ACTIVE

    def kill(pid, signal):                    # analysis:ignore
        if not _is_pid_running(pid):
            raise OSError(errno.ESRCH, None)
        else:
            return
            
    _open = open

    # XXX Implement an atomic thingamajig for win32
    def symlink(value, filename):    #analysis:ignore
        newlinkname = filename+"."+unique()+'.newlink'
        newvalname = os.path.join(newlinkname, "symlink")
        os.mkdir(newlinkname)
        f = _open(newvalname, 'wb')
        f.write(to_binary_string(value))
        f.flush()
        f.close()
        try:
            rename(newlinkname, filename)
        except:
            try:
                os.remove(newvalname)
                os.rmdir(newlinkname)
            except (IOError, OSError):
                pass
            raise

    def readlink(filename):   #analysis:ignore
        try:
            fObj = _open(os.path.join(filename, 'symlink'), 'rb')
        except IOError as e:
            if e.errno == errno.ENOENT or e.errno == errno.EIO:
                raise OSError(e.errno, None)
            raise
        else:
            result = fObj.read().decode()
            fObj.close()
            return result

    def rmlink(filename):    #analysis:ignore
        os.remove(os.path.join(filename, 'symlink'))
        os.rmdir(filename)



class FilesystemLock:
    """
    A mutex.

    This relies on the filesystem property that creating
    a symlink is an atomic operation and that it will
    fail if the symlink already exists.  Deleting the
    symlink will release the lock.

    @ivar name: The name of the file associated with this lock.

    @ivar clean: Indicates whether this lock was released cleanly by its
        last owner.  Only meaningful after C{lock} has been called and
        returns True.

    @ivar locked: Indicates whether the lock is currently held by this
        object.
    """

    clean = None
    locked = False

    def __init__(self, name):
        self.name = name

    def lock(self):
        """
        Acquire this lock.

        @rtype: C{bool}
        @return: True if the lock is acquired, false otherwise.

        @raise: Any exception os.symlink() may raise, other than
        EEXIST.
        """
        clean = True
        while True:
            try:
                symlink(str(os.getpid()), self.name)
            except OSError as e:
                if _windows and e.errno in (errno.EACCES, errno.EIO):
                    # The lock is in the middle of being deleted because we're
                    # on Windows where lock removal isn't atomic.  Give up, we
                    # don't know how long this is going to take.
                    return False
                if e.errno == errno.EEXIST:
                    try:
                        pid = readlink(self.name)
                    except OSError as e:
                        if e.errno == errno.ENOENT:
                            # The lock has vanished, try to claim it in the
                            # next iteration through the loop.
                            continue
                        raise
                    except IOError as e:
                        if _windows and e.errno == errno.EACCES:
                            # The lock is in the middle of being
                            # deleted because we're on Windows where
                            # lock removal isn't atomic.  Give up, we
                            # don't know how long this is going to
                            # take.
                            return False
                        raise
                    try:
                        if kill is not None:
                            kill(int(pid), 0)
                        # Verify that the running process corresponds to
                        # a Spyder one
                        p = psutil.Process(int(pid))

                        # Valid names for main script
                        names = set(['spyder', 'spyder3', 'spyder.exe',
                                     'spyder3.exe', 'bootstrap.py',
                                     'spyder-script.py'])
                        if running_under_pytest():
                            names.add('runtests.py')

                        # Check the first three command line arguments
                        arguments = set(os.path.basename(arg)
                                        for arg in p.cmdline()[:3])
                        conditions = [names & arguments]
                        if not any(conditions):
                            raise(OSError(errno.ESRCH, 'No such process'))
                    except OSError as e:
                        if e.errno == errno.ESRCH:
                            # The owner has vanished, try to claim it in the
                            # next iteration through the loop.
                            try:
                                rmlink(self.name)
                            except OSError as e:
                                if e.errno == errno.ENOENT:
                                    # Another process cleaned up the lock.
                                    # Race them to acquire it in the next
                                    # iteration through the loop.
                                    continue
                                raise
                            clean = False
                            continue
                        raise
                    return False
                raise
            self.locked = True
            self.clean = clean
            return True

    def unlock(self):
        """
        Release this lock.

        This deletes the directory with the given name.

        @raise: Any exception os.readlink() may raise, or
        ValueError if the lock is not owned by this process.
        """
        pid = readlink(self.name)
        if int(pid) != os.getpid():
            raise ValueError("Lock %r not owned by this process" % (self.name,))
        rmlink(self.name)
        self.locked = False


def isLocked(name):
    """Determine if the lock of the given name is held or not.

    @type name: C{str}
    @param name: The filesystem path to the lock to test

    @rtype: C{bool}
    @return: True if the lock is held, False otherwise.
    """
    l = FilesystemLock(name)
    result = None
    try:
        result = l.lock()
    finally:
        if result:
            l.unlock()
    return not result


__all__ = ['FilesystemLock', 'isLocked']
