[PATCH STABLE] implement nlinks() for _WIN32 in osutil.c (issue1922)

Adrian Buehlmann adrian at cadifra.com
Sat Jan 22 12:46:56 CST 2011


# HG changeset patch
# User Adrian Buehlmann <adrian at cadifra.com>
# Date 1295720495 -3600
# Branch stable
# Node ID 6441aef24506a94c3e19c6eb81fc0a8d74132214
# Parent  d0e0d3d43e1439d63564ab4dddfe0daa69ae2d86
implement nlinks() for _WIN32 in osutil.c (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 C implementation of nlinks() in osutil.c for
Windows, so we don't depend on the pywin32 package being installed for
nlinks().

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)

diff --git a/mercurial/osutil.c b/mercurial/osutil.c
--- a/mercurial/osutil.c
+++ b/mercurial/osutil.c
@@ -517,6 +517,53 @@ bail:
 	PyMem_Free(name);
 	return file_obj;
 }
+
+static PyObject *nlinks(PyObject *self, PyObject *args)
+{
+	char *name = NULL;
+	PyObject *res = NULL;
+	HANDLE fh = INVALID_HANDLE_VALUE;
+	long n = 1000;
+	BY_HANDLE_FILE_INFORMATION fi;
+
+	if (!PyArg_ParseTuple(args, "et:nlinks",
+			      Py_FileSystemDefaultEncoding,
+			      &name))
+		return NULL;
+
+	fh = CreateFile(name, 0,
+			FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+			NULL,
+			OPEN_EXISTING,
+			0,
+			NULL);
+
+	if (fh == INVALID_HANDLE_VALUE) {
+		PyErr_SetFromWindowsErrWithFilename(GetLastError(), name);
+		goto bail;
+	}
+
+	fi.nNumberOfLinks = 1000;
+	if (!GetFileInformationByHandle(fh, &fi)) {
+		PyErr_SetFromWindowsErrWithFilename(GetLastError(), name);
+		goto bail;
+	}
+
+	/*
+	nNumberOfLinks is a DWORD, which is a 32 bit unsigned
+	The return value is a Python int, which is signed
+	*/
+	if (fi.nNumberOfLinks > (DWORD)PyInt_GetMax())
+		n = PyInt_GetMax();
+	else
+		n = (long)fi.nNumberOfLinks;
+	res = PyInt_FromLong(n);
+bail:
+	if (fh != INVALID_HANDLE_VALUE)
+		CloseHandle(fh);
+	PyMem_Free(name);
+	return res;
+}
 #endif
 
 static char osutil_doc[] = "Native operating system services.";
@@ -528,6 +575,8 @@ static PyMethodDef methods[] = {
 	{"posixfile", (PyCFunction)posixfile, METH_VARARGS | METH_KEYWORDS,
 	 "Open a file with POSIX-like semantics.\n"
 "On error, this function may raise either a WindowsError or an IOError."},
+	{"nlinks", nlinks, METH_VARARGS,
+	 "return number of hardlinks for the given file\n"},
 #endif
 	{NULL, NULL}
 };
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
@@ -20,6 +20,13 @@ def posixfile(name, mode='r', buffering=
         raise IOError(err.errno, '%s: %s' % (name, err.strerror))
 posixfile.__doc__ = osutil.posixfile.__doc__
 
+def nlinks(name):
+    try:
+        return osutil.nlinks(name)
+    except WindowsError, err:
+        raise OSError(err.errno, '%s: %s' % (name, err.strerror))
+nlinks.__doc__ = osutil.nlinks.__doc__
+
 class winstdout(object):
     '''stdout on windows misbehaves if sent through a pipe'''
 


More information about the Mercurial-devel mailing list