diff --git a/.gitignore b/.gitignore index d35cddebd..a672c3f16 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ nbproject .DS_Store /*egg-info /.tox +.cache diff --git a/AUTHORS b/AUTHORS index e2c3293cc..b06c5b46f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,5 +15,6 @@ Contributors are: -Jonathan Chu -Vincent Driessen -Phil Elson +-Daniele Esposti Portions derived from other open source works and are clearly marked. diff --git a/git/test/test_util.py b/git/test/test_util.py index e07417b4b..6ba3d0d40 100644 --- a/git/test/test_util.py +++ b/git/test/test_util.py @@ -5,6 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php import tempfile +import gc from git.test.lib import ( TestBase, @@ -80,6 +81,7 @@ def test_lock_file(self): # auto-release on destruction del(other_lock_file) + gc.collect() lock_file._obtain_lock_or_raise() lock_file._release_lock() diff --git a/git/util.py b/git/util.py index c96a6b087..037fd91b5 100644 --- a/git/util.py +++ b/git/util.py @@ -48,6 +48,56 @@ #{ Utility Methods +if platform.system() == 'Windows': + # This code is a derivative work of Portalocker http://code.activestate.com/recipes/65203/ + import win32con + import win32file + import pywintypes + + LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK + LOCK_SH = 0 # the default + LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY + LOCK_UN = 1 << 2 + + __overlapped = pywintypes.OVERLAPPED() + + def flock(fd, flags=0): + hfile = win32file._get_osfhandle(fd) + + if flags & LOCK_UN != 0: + # Unlock file descriptor + try: + win32file.UnlockFileEx(hfile, 0, -0x10000, __overlapped) + except pywintypes.error as exc_value: + # error: (158, 'UnlockFileEx', 'The segment is already unlocked.') + # To match the 'posix' implementation, silently ignore this error + if exc_value[0] == 158: + pass + else: + # Q: Are there exceptions/codes we should be dealing with here? + raise + + elif flags & LOCK_EX != 0: + # Lock file + try: + win32file.LockFileEx(hfile, flags, 0, -0x10000, __overlapped) + except pywintypes.error as exc_value: + if exc_value[0] == 33: + # error: (33, 'LockFileEx', + # 'The process cannot access the file because another process has locked + # a portion of the file.') + raise IOError(33, exc_value[2]) + else: + # Q: Are there exceptions/codes we should be dealing with here? + raise + + else: + raise NotImplementedError("Unsupported set of bitflags {}".format(bin(flags))) + + +else: + from fcntl import flock, LOCK_UN, LOCK_EX, LOCK_NB + def unbare_repo(func): """Methods with this decorator raise InvalidGitRepositoryError if they @@ -555,9 +605,10 @@ class LockFile(object): As we are a utility class to be derived from, we only use protected methods. Locks will automatically be released on destruction""" - __slots__ = ("_file_path", "_owns_lock") + __slots__ = ("_file_path", "_owns_lock", "_file_descriptor") def __init__(self, file_path): + self._file_descriptor = None self._file_path = file_path self._owns_lock = False @@ -579,20 +630,21 @@ def _obtain_lock_or_raise(self): :raise IOError: if a lock was already present or a lock file could not be written""" if self._has_lock(): return + lock_file = self._lock_file_path() - if os.path.isfile(lock_file): - raise IOError("Lock for file %r did already exist, delete %r in case the lock is illegal" % - (self._file_path, lock_file)) + # Create file and lock try: - flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL + flags = os.O_CREAT if is_win: flags |= os.O_SHORT_LIVED fd = os.open(lock_file, flags, 0) - os.close(fd) except OSError as e: raise IOError(str(e)) + flock(fd, LOCK_EX | LOCK_NB) + + self._file_descriptor = fd self._owns_lock = True def _obtain_lock(self): @@ -605,14 +657,21 @@ def _release_lock(self): if not self._has_lock(): return + fd = self._file_descriptor + lock_file = self._lock_file_path() + + flock(fd, LOCK_UN) + os.close(fd) + # if someone removed our file beforhand, lets just flag this issue # instead of failing, to make it more usable. - lfp = self._lock_file_path() try: - rmfile(lfp) + rmfile(lock_file) except OSError: pass + self._owns_lock = False + self._file_descriptor = None class BlockingLockFile(LockFile): @@ -647,7 +706,7 @@ def _obtain_lock(self): try: super(BlockingLockFile, self)._obtain_lock() except IOError: - # synity check: if the directory leading to the lockfile is not + # sanity check: if the directory leading to the lockfile is not # readable anymore, raise an execption curtime = time.time() if not os.path.isdir(os.path.dirname(self._lock_file_path())): diff --git a/setup.py b/setup.py index c7dd25fcc..43b0505bd 100755 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ import logging import os import sys +import platform from os import path with open(path.join(path.dirname(__file__), 'VERSION')) as v: @@ -21,6 +22,10 @@ with open('requirements.txt') as reqs_file: requirements = reqs_file.read().splitlines() +if platform.system() == 'Windows': + with open('win32-requirements.txt') as reqs_file: + requirements += reqs_file.read().splitlines() + class build_py(_build_py): @@ -65,6 +70,8 @@ def _stamp_version(filename): print("WARNING: Couldn't find version line in file %s" % filename, file=sys.stderr) install_requires = ['gitdb >= 0.6.4'] +if platform.system() == 'Windows': + install_requires.append("pypiwin32 >= 219") extras_require = { ':python_version == "2.6"': ['ordereddict'], } diff --git a/win32-requirements.txt b/win32-requirements.txt new file mode 100644 index 000000000..0008dcffd --- /dev/null +++ b/win32-requirements.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pypiwin32 >= 219