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

Adrian Buehlmann adrian at cadifra.com
Sun Jan 23 09:12:02 CST 2011


On 2011-01-23 15:17, Adrian Buehlmann wrote:
> # HG changeset patch
> # User Adrian Buehlmann <adrian at cadifra.com>
> # Date 1295790385 -3600
> # Branch stable
> # Node ID 99f2b980756d1c22775cf6ec6e93c646bc910a55
> # 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
> @@ -7,7 +7,7 @@
>  
>  from i18n import _
>  import osutil, error
> -import errno, msvcrt, os, re, sys, random, subprocess
> +import errno, msvcrt, os, re, sys, random, subprocess, ctypes
>  
>  nulldev = 'NUL:'
>  umask = 002
> @@ -20,6 +20,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.c_uint),
> +        ('dwHighDateTime',      ctypes.c_uint),
> +    ]
> +
> +class BY_HANDLE_FILE_INFORMATION(ctypes.Structure):
> +    _fields_ = [
> +        ('dwFileAttributes',        ctypes.c_uint),
> +        ('ftCreationTime',          FILETIME),
> +        ('ftLastAccessTime',        FILETIME),
> +        ('ftLastWriteTime',         FILETIME),
> +        ('dwVolumeSerialNumber',    ctypes.c_uint),
> +        ('nFileSizeHigh',           ctypes.c_uint),
> +        ('nFileSizeLow',            ctypes.c_uint),
> +        ('nNumberOfLinks',          ctypes.c_uint),
> +        ('nFileIndexHigh',          ctypes.c_uint),
> +        ('nFileIndexLow',           ctypes.c_uint),
> +    ]
> +

I just found out that we can use:

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),
    ]

(Otherwise works pretty well so far. I found no noticeable speed change
on Python 2.6.5 32 bit)

> +_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