fix file handling bugs on windows.
authorVadim Gelfer <vadim.gelfer@gmail.com>
Tue, 02 May 2006 14:30:00 -0700
changeset 2176 9b42304d9896
parent 2129 e5f5c21f4169
child 2177 6886bc0b77af
fix file handling bugs on windows. add util.posixfile class that has posix semantics on windows. fix util.rename so it works with stupid windows delete semantics.
mercurial/appendfile.py
mercurial/bundlerepo.py
mercurial/revlog.py
mercurial/sshrepo.py
mercurial/util.py
mercurial/util_win32.py
--- a/mercurial/appendfile.py	Tue Apr 25 23:28:40 2006 +0200
+++ b/mercurial/appendfile.py	Tue May 02 14:30:00 2006 -0700
@@ -6,7 +6,7 @@
 # of the GNU General Public License, incorporated herein by reference.
 
 from demandload import *
-demandload(globals(), "cStringIO changelog errno manifest os tempfile")
+demandload(globals(), "cStringIO changelog errno manifest os tempfile util")
 
 # writes to metadata files are ordered.  reads: changelog, manifest,
 # normal files.  writes: normal files, manifest, changelog.
@@ -36,19 +36,21 @@
     def __init__(self, fp, tmpname):
         if tmpname:
             self.tmpname = tmpname
-            self.tmpfp = open(self.tmpname, 'ab+')
+            self.tmpfp = util.posixfile(self.tmpname, 'ab+')
         else:
             fd, self.tmpname = tempfile.mkstemp()
-            self.tmpfp = os.fdopen(fd, 'ab+')
+            os.close(fd)
+            self.tmpfp = util.posixfile(self.tmpname, 'ab+')
         self.realfp = fp
         self.offset = fp.tell()
         # real file is not written by anyone else. cache its size so
         # seek and read can be fast.
-        self.realsize = os.fstat(fp.fileno()).st_size
+        self.realsize = util.fstat(fp).st_size
+        self.name = fp.name
 
     def end(self):
         self.tmpfp.flush() # make sure the stat is correct
-        return self.realsize + os.fstat(self.tmpfp.fileno()).st_size
+        return self.realsize + util.fstat(self.tmpfp).st_size
 
     def tell(self):
         return self.offset
--- a/mercurial/bundlerepo.py	Tue Apr 25 23:28:40 2006 +0200
+++ b/mercurial/bundlerepo.py	Tue May 02 14:30:00 2006 -0700
@@ -160,7 +160,7 @@
     def __init__(self, ui, path, bundlename):
         localrepo.localrepository.__init__(self, ui, path)
         f = open(bundlename, "rb")
-        s = os.fstat(f.fileno())
+        s = util.fstat(f)
         self.bundlefile = f
         header = self.bundlefile.read(6)
         if not header.startswith("HG"):
--- a/mercurial/revlog.py	Tue Apr 25 23:28:40 2006 +0200
+++ b/mercurial/revlog.py	Tue May 02 14:30:00 2006 -0700
@@ -14,7 +14,7 @@
 from i18n import gettext as _
 from demandload import demandload
 demandload(globals(), "binascii changegroup errno heapq mdiff os")
-demandload(globals(), "sha struct zlib")
+demandload(globals(), "sha struct util zlib")
 
 # revlog version strings
 REVLOGV0 = 0
@@ -322,7 +322,7 @@
             i = ""
         else:
             try:
-                st = os.fstat(f.fileno())
+                st = util.fstat(f)
             except AttributeError, inst:
                 st = None
             else:
--- a/mercurial/sshrepo.py	Tue Apr 25 23:28:40 2006 +0200
+++ b/mercurial/sshrepo.py	Tue May 02 14:30:00 2006 -0700
@@ -57,7 +57,7 @@
 
     def readerr(self):
         while 1:
-            size = os.fstat(self.pipee.fileno())[stat.ST_SIZE]
+            size = util.fstat(self.pipee).st_size
             if size == 0: break
             l = self.pipee.readline()
             if not l: break
--- a/mercurial/util.py	Tue Apr 25 23:28:40 2006 +0200
+++ b/mercurial/util.py	Tue May 02 14:30:00 2006 -0700
@@ -406,8 +406,18 @@
     """forcibly rename a file"""
     try:
         os.rename(src, dst)
-    except:
-        os.unlink(dst)
+    except OSError, err:
+        # on windows, rename to existing file is not allowed, so we
+        # must delete destination first. but if file is open, unlink
+        # schedules it for delete but does not delete it. rename
+        # happens immediately even for open files, so we create
+        # temporary file, delete it, rename destination to that name,
+        # then delete that. then rename is safe to do.
+        fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
+        os.close(fd)
+        os.unlink(temp)
+        os.rename(dst, temp)
+        os.unlink(temp)
         os.rename(src, dst)
 
 def unlink(f):
@@ -449,90 +459,13 @@
         or os.pardir in parts):
         raise Abort(_("path contains illegal component: %s\n") % path)
 
-def opener(base, audit=True):
-    """
-    return a function that opens files relative to base
-
-    this function is used to hide the details of COW semantics and
-    remote file access from higher level code.
-    """
-    p = base
-    audit_p = audit
-
-    def mktempcopy(name):
-        d, fn = os.path.split(name)
-        fd, temp = tempfile.mkstemp(prefix=fn, dir=d)
-        fp = os.fdopen(fd, "wb")
-        try:
-            fp.write(file(name, "rb").read())
-        except:
-            try: os.unlink(temp)
-            except: pass
-            raise
-        fp.close()
-        st = os.lstat(name)
-        os.chmod(temp, st.st_mode)
-        return temp
-
-    class atomictempfile(file):
-        """the file will only be copied when rename is called"""
-        def __init__(self, name, mode):
-            self.__name = name
-            self.temp = mktempcopy(name)
-            file.__init__(self, self.temp, mode)
-        def rename(self):
-            if not self.closed:
-                file.close(self)
-                rename(self.temp, self.__name)
-        def __del__(self):
-            if not self.closed:
-                try:
-                    os.unlink(self.temp)
-                except: pass
-                file.close(self)
-
-    class atomicfile(atomictempfile):
-        """the file will only be copied on close"""
-        def __init__(self, name, mode):
-            atomictempfile.__init__(self, name, mode)
-        def close(self):
-            self.rename()
-        def __del__(self):
-            self.rename()
-
-    def o(path, mode="r", text=False, atomic=False, atomictemp=False):
-        if audit_p:
-            audit_path(path)
-        f = os.path.join(p, path)
-
-        if not text:
-            mode += "b" # for that other OS
-
-        if mode[0] != "r":
-            try:
-                nlink = nlinks(f)
-            except OSError:
-                d = os.path.dirname(f)
-                if not os.path.isdir(d):
-                    os.makedirs(d)
-            else:
-                if atomic:
-                    return atomicfile(f, mode)
-                elif atomictemp:
-                    return atomictempfile(f, mode)
-                if nlink > 1:
-                    rename(mktempcopy(f), f)
-        return file(f, mode)
-
-    return o
-
 def _makelock_file(info, pathname):
     ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
     os.write(ld, info)
     os.close(ld)
 
 def _readlock_file(pathname):
-    return file(pathname).read()
+    return posixfile(pathname).read()
 
 def nlinks(pathname):
     """Return number of hardlinks for the given file."""
@@ -544,6 +477,15 @@
     def os_link(src, dst):
         raise OSError(0, _("Hardlinks not supported"))
 
+def fstat(fp):
+    '''stat file object that may not have fileno method.'''
+    try:
+        return os.fstat(fp.fileno())
+    except AttributeError:
+        return os.stat(fp.name)
+
+posixfile = file
+
 # Platform specific variants
 if os.name == 'nt':
     demandload(globals(), "msvcrt")
@@ -722,6 +664,84 @@
             return _("stopped by signal %d") % val, val
         raise ValueError(_("invalid exit code"))
 
+def opener(base, audit=True):
+    """
+    return a function that opens files relative to base
+
+    this function is used to hide the details of COW semantics and
+    remote file access from higher level code.
+    """
+    p = base
+    audit_p = audit
+
+    def mktempcopy(name):
+        d, fn = os.path.split(name)
+        fd, temp = tempfile.mkstemp(prefix=fn, dir=d)
+        os.close(fd)
+        fp = posixfile(temp, "wb")
+        try:
+            fp.write(posixfile(name, "rb").read())
+        except:
+            try: os.unlink(temp)
+            except: pass
+            raise
+        fp.close()
+        st = os.lstat(name)
+        os.chmod(temp, st.st_mode)
+        return temp
+
+    class atomictempfile(posixfile):
+        """the file will only be copied when rename is called"""
+        def __init__(self, name, mode):
+            self.__name = name
+            self.temp = mktempcopy(name)
+            posixfile.__init__(self, self.temp, mode)
+        def rename(self):
+            if not self.closed:
+                posixfile.close(self)
+                rename(self.temp, self.__name)
+        def __del__(self):
+            if not self.closed:
+                try:
+                    os.unlink(self.temp)
+                except: pass
+                posixfile.close(self)
+
+    class atomicfile(atomictempfile):
+        """the file will only be copied on close"""
+        def __init__(self, name, mode):
+            atomictempfile.__init__(self, name, mode)
+        def close(self):
+            self.rename()
+        def __del__(self):
+            self.rename()
+
+    def o(path, mode="r", text=False, atomic=False, atomictemp=False):
+        if audit_p:
+            audit_path(path)
+        f = os.path.join(p, path)
+
+        if not text:
+            mode += "b" # for that other OS
+
+        if mode[0] != "r":
+            try:
+                nlink = nlinks(f)
+            except OSError:
+                d = os.path.dirname(f)
+                if not os.path.isdir(d):
+                    os.makedirs(d)
+            else:
+                if atomic:
+                    return atomicfile(f, mode)
+                elif atomictemp:
+                    return atomictempfile(f, mode)
+                if nlink > 1:
+                    rename(mktempcopy(f), f)
+        return posixfile(f, mode)
+
+    return o
+
 class chunkbuffer(object):
     """Allow arbitrary sized chunks of data to be efficiently read from an
     iterator over chunks of arbitrary size."""
--- a/mercurial/util_win32.py	Tue Apr 25 23:28:40 2006 +0200
+++ b/mercurial/util_win32.py	Tue May 02 14:30:00 2006 -0700
@@ -16,9 +16,9 @@
 from demandload import *
 from i18n import gettext as _
 demandload(globals(), 'errno os pywintypes win32con win32file win32process')
-demandload(globals(), 'winerror')
+demandload(globals(), 'cStringIO winerror')
 
-class WinError(OSError):
+class WinError:
     winerror_map = {
         winerror.ERROR_ACCESS_DENIED: errno.EACCES,
         winerror.ERROR_ACCOUNT_DISABLED: errno.EACCES,
@@ -105,7 +105,7 @@
         winerror.ERROR_OUTOFMEMORY: errno.ENOMEM,
         winerror.ERROR_PASSWORD_EXPIRED: errno.EACCES,
         winerror.ERROR_PATH_BUSY: errno.EBUSY,
-        winerror.ERROR_PATH_NOT_FOUND: errno.ENOTDIR,
+        winerror.ERROR_PATH_NOT_FOUND: errno.ENOENT,
         winerror.ERROR_PIPE_BUSY: errno.EBUSY,
         winerror.ERROR_PIPE_CONNECTED: errno.EPIPE,
         winerror.ERROR_PIPE_LISTENING: errno.EPIPE,
@@ -129,6 +129,19 @@
 
     def __init__(self, err):
         self.win_errno, self.win_function, self.win_strerror = err
+        if self.win_strerror.endswith('.'):
+            self.win_strerror = self.win_strerror[:-1]
+
+class WinIOError(WinError, IOError):
+    def __init__(self, err, filename=None):
+        WinError.__init__(self, err)
+        IOError.__init__(self, self.winerror_map.get(self.win_errno, 0),
+                         self.win_strerror)
+        self.filename = filename
+
+class WinOSError(WinError, OSError):
+    def __init__(self, err):
+        WinError.__init__(self, err)
         OSError.__init__(self, self.winerror_map.get(self.win_errno, 0),
                          self.win_strerror)
 
@@ -137,7 +150,7 @@
     try:
         win32file.CreateHardLink(dst, src)
     except pywintypes.error, details:
-        raise WinError(details)
+        raise WinOSError(details)
 
 def nlinks(pathname):
     """Return number of hardlinks for the given file."""
@@ -169,3 +182,99 @@
     proc = win32api.GetCurrentProcess()
     filename = win32process.GetModuleFileNameEx(proc, 0)
     return [os.path.join(os.path.dirname(filename), 'mercurial.ini')]
+
+class posixfile(object):
+    '''file object with posix-like semantics.  on windows, normal
+    files can not be deleted or renamed if they are open. must open
+    with win32file.FILE_SHARE_DELETE. this flag does not exist on
+    windows <= nt.'''
+
+    # tried to use win32file._open_osfhandle to pass fd to os.fdopen,
+    # but does not work at all. wrap win32 file api instead.
+
+    def __init__(self, name, mode='rb'):
+        access = 0
+        if 'r' in mode or '+' in mode:
+            access |= win32file.GENERIC_READ
+        if 'w' in mode or 'a' in mode:
+            access |= win32file.GENERIC_WRITE
+        if 'r' in mode:
+            creation = win32file.OPEN_EXISTING
+        elif 'a' in mode:
+            creation = win32file.OPEN_ALWAYS
+        else:
+            creation = win32file.CREATE_ALWAYS
+        try:
+            self.handle = win32file.CreateFile(name,
+                                               access,
+                                               win32file.FILE_SHARE_READ |
+                                               win32file.FILE_SHARE_WRITE |
+                                               win32file.FILE_SHARE_DELETE,
+                                               None,
+                                               creation,
+                                               win32file.FILE_ATTRIBUTE_NORMAL,
+                                               0)
+        except pywintypes.error, err:
+            raise WinIOError(err, name)
+        self.closed = False
+        self.name = name
+        self.mode = mode
+
+    def read(self, count=-1):
+        try:
+            cs = cStringIO.StringIO()
+            while count:
+                wincount = int(count)
+                if wincount == -1:
+                    wincount = 1048576
+                val, data = win32file.ReadFile(self.handle, wincount)
+                if not data: break
+                cs.write(data)
+                if count != -1:
+                    count -= len(data)
+            return cs.getvalue()
+        except pywintypes.error, err:
+            raise WinIOError(err)
+
+    def write(self, data):
+        try:
+            if 'a' in self.mode:
+                win32file.SetFilePointer(self.handle, 0, win32file.FILE_END)
+            nwrit = 0
+            while nwrit < len(data):
+                val, nwrit = win32file.WriteFile(self.handle, data)
+                data = data[nwrit:]
+        except pywintypes.error, err:
+            raise WinIOError(err)
+
+    def seek(self, pos, whence=0):
+        try:
+            win32file.SetFilePointer(self.handle, int(pos), whence)
+        except pywintypes.error, err:
+            raise WinIOError(err)
+
+    def tell(self):
+        try:
+            return win32file.SetFilePointer(self.handle, 0,
+                                            win32file.FILE_CURRENT)
+        except pywintypes.error, err:
+            raise WinIOError(err)
+
+    def close(self):
+        if not self.closed:
+            self.handle = None
+            self.closed = True
+
+    def flush(self):
+        try:
+            win32file.FlushFileBuffers(self.handle)
+        except pywintypes.error, err:
+            raise WinIOError(err)
+
+    def truncate(self, pos=0):
+        try:
+            win32file.SetFilePointer(self.handle, int(pos),
+                                     win32file.FILE_BEGIN)
+            win32file.SetEndOfFile(self.handle)
+        except pywintypes.error, err:
+            raise WinIOError(err)