# HG changeset patch # User Vadim Gelfer # Date 1146605400 25200 # Node ID 9b42304d9896acd903a536f359174131a09149f5 # Parent e5f5c21f4169b6a6575a33edd621fc29d09b97a7 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. diff --git a/mercurial/appendfile.py b/mercurial/appendfile.py --- 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 diff --git a/mercurial/bundlerepo.py b/mercurial/bundlerepo.py --- 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"): diff --git a/mercurial/revlog.py b/mercurial/revlog.py --- 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: diff --git a/mercurial/sshrepo.py b/mercurial/sshrepo.py --- 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 diff --git a/mercurial/util.py b/mercurial/util.py --- 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.""" diff --git a/mercurial/util_win32.py b/mercurial/util_win32.py --- 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)