[PATCH 3 of 5 rfc] posix: give checkexec a fast path; keep the check files and test read only

Mads Kiilerich mads at kiilerich.com
Sat Oct 24 11:32:01 CDT 2015


# HG changeset patch
# User Mads Kiilerich <madski at unity3d.com>
# Date 1421194526 -3600
#      Wed Jan 14 01:15:26 2015 +0100
# Branch stable
# Node ID 5921bcc389073dfdb4a44284db5ee00bcbb0c499
# Parent  489575343604ce4558ee18007714844f42b0fff5
posix: give checkexec a fast path; keep the check files and test read only

Before, Mercurial would create a new temporary file every time, stat it, change
its exec mode, stat it again, and delete it. Most of this dance was done to
handle the rare and not-so-essential case of VFAT mounts on unix. The cost of
that was paid by the much more common and important case of using normal file
systems. These temporary files were created in the working directory and could
cause confusion for other tools monitoring the file system.

Instead, try to create .hg/cache/checkisexec and .hg/cache/checknoexec if they
don't exist. Best case, the exec check can be done with two stat calls. Worst
case is on file systems without support for the exec bit where there will be
one extra stat.

It is possible that this different test algorithm in some cases will give
different behaviour. Again, I think it will be rare and special cases and I
think it is worth the risk.

test-clone.t happens to show the situation where checkisexec is left behind
from the old style check, while checknoexec only will be created next time a
exec check will be performed.

diff --git a/mercurial/posix.py b/mercurial/posix.py
--- a/mercurial/posix.py
+++ b/mercurial/posix.py
@@ -153,18 +153,59 @@ def checkexec(path):
     try:
         EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
         cachedir = os.path.join(path, '.hg', 'cache')
-        if not os.path.isdir(cachedir):
-            cachedir = path
-        fh, fn = tempfile.mkstemp(dir=cachedir, prefix='hg-checkexec-')
+        if os.path.isdir(cachedir):
+            checkisexec = os.path.join(cachedir, 'checkisexec')
+            checknoexec = os.path.join(cachedir, 'checknoexec')
+
+            try:
+                m = os.stat(checkisexec).st_mode
+            except OSError as e:
+                if e.errno != errno.ENOENT:
+                    raise
+                # checkisexec does not exist - fall through to create and check
+            else:
+                if not m & EXECFLAGS:
+                    # checkisexec exists but is not exec
+                    return False
+                # ensure checkisexec exists, check if it actually is exec
+                try:
+                    m = os.stat(checknoexec).st_mode
+                except OSError as e:
+                    if e.errno != errno.ENOENT:
+                        raise
+                    file(checknoexec, 'w').close() # might raise
+                    m = os.stat(checknoexec).st_mode
+                if m & EXECFLAGS:
+                    # checknoexec exists but is exec
+                    os.unlink(checknoexec)
+                    return False
+                # check-no-exec exists and is not exec
+                return True
+
+            # check using one file, leave it as checkisexec
+            checkdir = cachedir
+        else:
+            checkdir = path
+            checkisexec = None
+        fh, fn = tempfile.mkstemp(dir=checkdir, prefix='hg-checkexec-')
         try:
             os.close(fh)
             m = os.stat(fn).st_mode
             if m & EXECFLAGS:
                 return False
             os.chmod(fn, m & 0o777 | EXECFLAGS)
-            return os.stat(fn).st_mode & EXECFLAGS
+            if os.stat(fn).st_mode & EXECFLAGS:
+                if checkisexec is not None:
+                    try:
+                        os.rename(fn, checkisexec)
+                        fn = None
+                    except OSError:
+                        pass # probably because .hg/cache doesn't exist (yet)
+                return True
+            return False
         finally:
-            os.unlink(fn)
+            if fn is not None:
+                os.unlink(fn)
     except (IOError, OSError):
         # we don't care, the user probably won't be able to commit anyway
         return False
diff --git a/tests/test-clone.t b/tests/test-clone.t
--- a/tests/test-clone.t
+++ b/tests/test-clone.t
@@ -31,6 +31,8 @@ Trigger branchcache creation:
   default                       10:a7949464abda
   $ ls .hg/cache
   branch2-served
+  checkisexec
+  checknoexec
   rbc-names-v1
   rbc-revs-v1
 
@@ -45,6 +47,7 @@ Ensure branchcache got copied over:
 
   $ ls .hg/cache
   branch2-served
+  checkisexec
 
   $ cat a
   a
diff --git a/tests/test-hardlinks.t b/tests/test-hardlinks.t
--- a/tests/test-hardlinks.t
+++ b/tests/test-hardlinks.t
@@ -211,6 +211,8 @@ r4 has hardlinks in the working dir (not
   2 r4/.hg/00changelog.i
   2 r4/.hg/branch
   2 r4/.hg/cache/branch2-served
+  2 r4/.hg/cache/checkisexec
+  2 r4/.hg/cache/checknoexec
   2 r4/.hg/cache/rbc-names-v1
   2 r4/.hg/cache/rbc-revs-v1
   2 r4/.hg/dirstate
@@ -246,6 +248,8 @@ Update back to revision 11 in r4 should 
   2 r4/.hg/00changelog.i
   1 r4/.hg/branch
   2 r4/.hg/cache/branch2-served
+  2 r4/.hg/cache/checkisexec
+  2 r4/.hg/cache/checknoexec
   2 r4/.hg/cache/rbc-names-v1
   2 r4/.hg/cache/rbc-revs-v1
   1 r4/.hg/dirstate
diff --git a/tests/test-tags.t b/tests/test-tags.t
--- a/tests/test-tags.t
+++ b/tests/test-tags.t
@@ -665,6 +665,7 @@ Missing tags2* files means the cache was
 
   $ ls tagsclient/.hg/cache
   branch2-served
+  checkisexec
   hgtagsfnodes1
   rbc-names-v1
   rbc-revs-v1
@@ -689,6 +690,7 @@ Running hg tags should produce tags2* fi
 
   $ ls tagsclient/.hg/cache
   branch2-served
+  checkisexec
   hgtagsfnodes1
   rbc-names-v1
   rbc-revs-v1


More information about the Mercurial-devel mailing list