changeset 14413:5ef18e28df19

pure: provide more correct implementation of posixfile for Windows requires ctypes Why is posixfile a class? Because the implementation needs to use the Python library call os.fdopen [1], which sets the 'name' attribute on the Python file object it creates to the mostly meaningless string '<fdopen>', since file descriptors don't have a name. But users of posixfile depend on the name attribute [2] being set to a proper value, like Python's built-in 'open' function sets it on file objects. Python file's name attribute is read-only, so we can't just assign to it after the file object has alrady been created. To solve this problem, we save the name of the file on a wrapper object, and delegate the file function calls to the wrapped (private) file object using __getattr__. [1] http://docs.python.org/library/os.html#os.fdopen [2] http://docs.python.org/library/stdtypes.html#file.name
author Adrian Buehlmann <adrian@cadifra.com>
date Wed, 18 May 2011 09:12:27 +0200
parents 9ac479758d3b
children 90937dd4d94b
files mercurial/pure/osutil.py
diffstat 1 files changed, 128 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/pure/osutil.py	Mon May 23 20:35:10 2011 +0200
+++ b/mercurial/pure/osutil.py	Wed May 18 09:12:27 2011 +0200
@@ -8,8 +8,6 @@
 import os
 import stat as statmod
 
-posixfile = open
-
 def _mode_to_kind(mode):
     if statmod.S_ISREG(mode):
         return statmod.S_IFREG
@@ -57,3 +55,131 @@
             result.append((fn, _mode_to_kind(st.st_mode)))
     return result
 
+if os.name != 'nt':
+    posixfile = open
+else:
+    import ctypes, ctypes.util
+
+    _kernel32 = ctypes.windll.kernel32
+
+    _DWORD = ctypes.c_ulong
+    _LPCSTR = _LPSTR = ctypes.c_char_p
+    _HANDLE = ctypes.c_void_p
+
+    _INVALID_HANDLE_VALUE = _HANDLE(-1).value
+
+    def _crtname():
+        try:
+            # find_msvcrt was introduced in Python 2.6
+            return ctypes.util.find_msvcrt()
+        except AttributeError:
+            return 'msvcr80.dll' # CPython 2.5
+
+    _crt = ctypes.PyDLL(_crtname())
+
+    # CreateFile 
+    _FILE_SHARE_READ = 0x00000001
+    _FILE_SHARE_WRITE = 0x00000002
+    _FILE_SHARE_DELETE = 0x00000004
+
+    _CREATE_ALWAYS = 2
+    _OPEN_EXISTING = 3
+    _OPEN_ALWAYS = 4
+
+    _GENERIC_READ = 0x80000000
+    _GENERIC_WRITE = 0x40000000
+
+    _FILE_ATTRIBUTE_NORMAL = 0x80
+
+    # _open_osfhandle
+    _O_RDONLY = 0x0000
+    _O_RDWR = 0x0002
+    _O_APPEND = 0x0008
+
+    _O_TEXT = 0x4000
+    _O_BINARY = 0x8000
+
+    # types of parameters of C functions used (required by pypy)
+
+    _kernel32.CreateFileA.argtypes = [_LPCSTR, _DWORD, _DWORD, ctypes.c_void_p,
+        _DWORD, _DWORD, _HANDLE]
+    _kernel32.CreateFileA.restype = _HANDLE
+
+    _crt._open_osfhandle.argtypes = [_HANDLE, ctypes.c_int]
+    _crt._open_osfhandle.restype = ctypes.c_int
+
+    def _raiseioerror(name):
+        err = ctypes.WinError()
+        raise IOError(err.errno, '%s: %s' % (name, err.strerror))
+
+    class posixfile(object):
+        '''a file object aiming for POSIX-like semantics
+
+        CPython's open() returns a file that was opened *without* setting the
+        _FILE_SHARE_DELETE flag, which causes rename and unlink to abort.
+        This even happens if any hardlinked copy of the file is in open state.
+        We set _FILE_SHARE_DELETE here, so files opened with posixfile can be
+        renamed and deleted while they are held open.
+        Note that if a file opened with posixfile is unlinked, the file
+        remains but cannot be opened again or be recreated under the same name,
+        until all reading processes have closed the file.'''
+
+        def __init__(self, name, mode='r', bufsize=-1):
+            if 'b' in mode:
+                flags = _O_BINARY
+            else:
+                flags = _O_TEXT
+
+            m0 = mode[0]
+            if m0 == 'r' and not '+' in mode:
+                flags |= _O_RDONLY
+                access = _GENERIC_READ
+            else:
+                # work around http://support.microsoft.com/kb/899149 and
+                # set _O_RDWR for 'w' and 'a', even if mode has no '+'
+                flags |= _O_RDWR
+                access = _GENERIC_READ | _GENERIC_WRITE
+
+            if m0 == 'r':
+                creation = _OPEN_EXISTING
+            elif m0 == 'w':
+                creation = _CREATE_ALWAYS
+            elif m0 == 'a':
+                creation = _OPEN_ALWAYS
+                flags |= _O_APPEND
+            else:
+                raise ValueError("invalid mode: %s" % mode)
+
+            fh = _kernel32.CreateFileA(name, access,
+                    _FILE_SHARE_READ | _FILE_SHARE_WRITE | _FILE_SHARE_DELETE,
+                    None, creation, _FILE_ATTRIBUTE_NORMAL, None)
+            if fh == _INVALID_HANDLE_VALUE:
+                _raiseioerror(name)
+
+            # for CPython we must use the same CRT as Python uses,
+            # or the os.fdopen call below will abort with
+            #   "OSError: [Errno 9] Bad file descriptor"
+            fd = _crt._open_osfhandle(fh, flags)
+            if fd == -1:
+                _kernel32.CloseHandle(fh)
+                _raiseioerror(name)
+
+            f = os.fdopen(fd, mode, bufsize)
+            # unfortunately, f.name is '<fdopen>' at this point -- so we store
+            # the name on this wrapper. We cannot just assign to f.name,
+            # because that attribute is read-only.
+            object.__setattr__(self, 'name', name)
+            object.__setattr__(self, '_file', f)
+
+        def __iter__(self):
+            return self._file
+
+        def __getattr__(self, name):
+            return getattr(self._file, name)
+
+        def __setattr__(self, name, value):
+            '''mimics the read-only attributes of Python file objects
+            by raising 'TypeError: readonly attribute' if someone tries:
+              f = posixfile('foo.txt')
+              f.name = 'bla'  '''
+            return self._file.__setattr__(name, value)