changeset 2176:9b42304d9896

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.
author Vadim Gelfer <vadim.gelfer@gmail.com>
date Tue, 02 May 2006 14:30:00 -0700
parents e5f5c21f4169
children 6886bc0b77af
files mercurial/appendfile.py mercurial/bundlerepo.py mercurial/revlog.py mercurial/sshrepo.py mercurial/util.py mercurial/util_win32.py
diffstat 6 files changed, 224 insertions(+), 93 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/appendfile.py
+++ b/mercurial/appendfile.py
@@ -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 @@ class appendfile(object):
     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
+++ b/mercurial/bundlerepo.py
@@ -160,7 +160,7 @@ class bundlerepository(localrepo.localre
     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
+++ b/mercurial/revlog.py
@@ -14,7 +14,7 @@ from node import *
 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 @@ class revlog(object):
             i = ""
         else:
             try:
-                st = os.fstat(f.fileno())
+                st = util.fstat(f)
             except AttributeError, inst:
                 st = None
             else:
--- a/mercurial/sshrepo.py
+++ b/mercurial/sshrepo.py
@@ -57,7 +57,7 @@ class sshrepository(remoterepository):
 
     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
+++ b/mercurial/util.py
@@ -406,8 +406,18 @@ def rename(src, dst):
     """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 @@ def audit_path(path):
         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 @@ else:
     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 @@ else:
             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
+++ b/mercurial/util_win32.py
@@ -16,9 +16,9 @@ import win32api
 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 @@ class WinError(OSError):
         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 @@ class WinError(OSError):
 
     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 @@ def os_link(src, dst):
     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 @@ def system_rcpath_win32():
     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)