[PATCH STABLE V3] windows: implement nlinks() using Python's ctypes (issue1922)

Adrian Buehlmann adrian at cadifra.com
Sun Jan 23 09:26:18 CST 2011


# HG changeset patch
# User Adrian Buehlmann <adrian at cadifra.com>
# Date 1295795860 -3600
# Branch stable
# Node ID 6ce88152907de5e6362eff7bf0c64d453b9d2505
# Parent  d0e0d3d43e1439d63564ab4dddfe0daa69ae2d86
windows: implement nlinks() using Python's ctypes (issue1922)

If the pywin32 package was not installed, the import of win32 in
windows.py silently failed and (before this patch) util.nlinks was then
used as a fallback when running on Windows.

util.nlinks() returned 0 for all files when called on Windows, because
Python's

    os.lstat(name).st_nlink

is 0 on Windows for all files.

If nlinks() returns 0, util.opener failed to break up hardlinks, which
could lead to repository corruption when committing or pushing to a
hardlinked clone (hg verify detects it).

We now provide our own nlinks() in windows.py by using Python's ctypes
library, so we don't depend on the pywin32 package being installed for
nlinks().

  ** Since Python's ctypes were introduced in Python 2.5, we now
  ** require Python 2.5 or later for Mercurial on Windows

Using ctypes also has the benefit that nlinks() also works correctly
for the pure Python Mercurial.

And we force breaking up hardlinks on every append file access in the
opener if nlinks() returns < 1, thus making sure that we can't cause
any hardlink repository corruption.

It would have been possible to simply require the pywin32 package on
Windows and abort with an import error if it's not installed, but such
a policy change should be avoided on the stable branch. Previous packages
like for example

    mercurial-1.7.3-1.win32-py2.6.exe

didn't make it obvious that pywin32 was needed as a dependency. It just
silently caused repository corruption in hardlinked clones if pywin32
was not installed.

(This patch is supposed to completely fix issue1922)

(Based on contributions by Aaron Cohen <aaron at assonance.org>)

diff --git a/mercurial/posix.py b/mercurial/posix.py
--- a/mercurial/posix.py
+++ b/mercurial/posix.py
@@ -23,6 +23,10 @@ def openhardlinks():
     '''return true if it is safe to hold open file handles to hardlinks'''
     return True
 
+def nlinks(name):
+    """return number of hardlinks for the given file"""
+    return os.lstat(name).st_nlink
+
 def rcfiles(path):
     rcs = [os.path.join(path, 'hgrc')]
     rcdir = os.path.join(path, 'hgrc.d')
diff --git a/mercurial/util.py b/mercurial/util.py
--- a/mercurial/util.py
+++ b/mercurial/util.py
@@ -550,10 +550,6 @@ class path_auditor(object):
         # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
         self.auditeddir.update(prefixes)
 
-def nlinks(pathname):
-    """Return number of hardlinks for the given file."""
-    return os.lstat(pathname).st_nlink
-
 if hasattr(os, 'link'):
     os_link = os.link
 else:
@@ -913,6 +909,8 @@ class opener(object):
                     # shares if the file is open.
                     fd = open(f)
                     nlink = nlinks(f)
+                    if nlink < 1:
+                        nlink = 2 # force mktempcopy (issue1922)
                     fd.close()
             except (OSError, IOError):
                 nlink = 0
diff --git a/mercurial/win32.py b/mercurial/win32.py
--- a/mercurial/win32.py
+++ b/mercurial/win32.py
@@ -41,10 +41,6 @@ def _getfileinfo(pathname):
     finally:
         fh.Close()
 
-def nlinks(pathname):
-    """Return number of hardlinks for the given file."""
-    return _getfileinfo(pathname)[7]
-
 def samefile(fpath1, fpath2):
     """Returns whether fpath1 and fpath2 refer to the same file. This is only
     guaranteed to work for files, not directories."""
diff --git a/mercurial/windows.py b/mercurial/windows.py
--- a/mercurial/windows.py
+++ b/mercurial/windows.py
@@ -8,6 +8,7 @@
 from i18n import _
 import osutil, error
 import errno, msvcrt, os, re, sys, random, subprocess
+import ctypes, ctypes.wintypes
 
 nulldev = 'NUL:'
 umask = 002
@@ -20,6 +21,57 @@ def posixfile(name, mode='r', buffering=
         raise IOError(err.errno, '%s: %s' % (name, err.strerror))
 posixfile.__doc__ = osutil.posixfile.__doc__
 
+class FILETIME(ctypes.Structure):
+    _fields_ = [
+        ('dwLowDateTime',       ctypes.wintypes.DWORD),
+        ('dwHighDateTime',      ctypes.wintypes.DWORD),
+    ]
+
+class BY_HANDLE_FILE_INFORMATION(ctypes.Structure):
+    _fields_ = [
+        ('dwFileAttributes',        ctypes.wintypes.DWORD),
+        ('ftCreationTime',          FILETIME),
+        ('ftLastAccessTime',        FILETIME),
+        ('ftLastWriteTime',         FILETIME),
+        ('dwVolumeSerialNumber',    ctypes.wintypes.DWORD),
+        ('nFileSizeHigh',           ctypes.wintypes.DWORD),
+        ('nFileSizeLow',            ctypes.wintypes.DWORD),
+        ('nNumberOfLinks',          ctypes.wintypes.DWORD),
+        ('nFileIndexHigh',          ctypes.wintypes.DWORD),
+        ('nFileIndexLow',           ctypes.wintypes.DWORD),
+    ]
+
+_FILE_SHARE_READ = 0x00000001
+_FILE_SHARE_WRITE = 0x00000002
+_FILE_SHARE_DELETE = 0x00000004
+
+_OPEN_EXISTING = 3
+
+def _raiseoserror(name):
+    err = ctypes.WinError()
+    raise OSError(err.errno, '%s: %s' % (name, err.strerror))
+
+def _getfileinfo(name):
+    k = ctypes.windll.kernel32
+    fh = k.CreateFileA(name,
+            0, _FILE_SHARE_READ | _FILE_SHARE_WRITE | _FILE_SHARE_DELETE,
+            None, _OPEN_EXISTING, 0, None)
+    if fh == -1:
+        _raiseoserror(name)
+
+    try:
+        fi = BY_HANDLE_FILE_INFORMATION()
+        if (not k.GetFileInformationByHandle(fh, ctypes.pointer(fi))):
+            _raiseoserror(name)
+        return fi
+    finally:
+        if fh != -1:
+            k.CloseHandle(fh)
+
+def nlinks(name):
+    '''return number of hardlinks for the given file'''
+    return _getfileinfo(name).nNumberOfLinks
+
 class winstdout(object):
     '''stdout on windows misbehaves if sent through a pipe'''
 


More information about the Mercurial-devel mailing list