[PATCH] atomictempfile: avoid infinite recursion in __del__()

Greg Ward greg-hg at gerg.ca
Sun Apr 24 18:29:30 CDT 2011


# HG changeset patch
# User Greg Ward <greg at gerg.ca>
# Date 1303687510 14400
# Node ID 1ade059f83dd780ab48ac27cd05009746d70ce13
# Parent  81010c598d28d10cb8946e5877b7a9f5a51adb1e
atomictempfile: avoid infinite recursion in __del__().

The problem is that a programmer using atomictempfile directly can
make an innocent everyday mistake -- not enough args to the
constructor -- which escalates badly.  You would expect a simple
TypeError crash in that case, but you actually get an infinite
recursion that is surprisingly difficult to kill: it happens between
__del__() and __getattr__(), and Python does not handle infinite
recursion from __del__() well.

The fix is to not implement __getattr__(), but instead assign instance
attributes for the methods we wish to delegate to the builtin file
type: write() and fileno(). I've audited mercurial.* and hgext.* and
found no users of atomictempfile using methods other than write() and
rename(). I audited third-party extensions and found one (snap)
passing an atomictempfile to util.fstat(), so I also threw in
fileno().

The last time I submitted a similar patch, Matt proposed that we make
atomictempfile a subclass of file instead of wrapping it. Rejected on
grounds of unnecessary complexity: for one thing, it would make the
Windows implementation of posixfile quite a bit more complex. It would
have to become a subclass of file rather than a simple function -- but
since it's written in C, this is non-obvious and non-trivial.
Furthermore, there's nothing wrong with wrapping objects and
delegating methods: it's a well-established pattern that works just
fine in many cases. Subclassing is not the answer to all of life's
problems.

diff --git a/mercurial/util.py b/mercurial/util.py
--- a/mercurial/util.py
+++ b/mercurial/util.py
@@ -729,31 +729,31 @@
     simply deleting the temporary file.
     '''
     def __init__(self, name, mode='w+b', createmode=None):
-        self.__name = name
-        self._fp = None
-        self.temp = mktempcopy(name, emptyok=('w' in mode),
-                               createmode=createmode)
-        self._fp = posixfile(self.temp, mode)
+        self.__name = name      # permanent name
+        self._tempname = mktempcopy(name, emptyok=('w' in mode),
+                                    createmode=createmode)
+        self._fp = posixfile(self._tempname, mode)
 
-    def __getattr__(self, name):
-        return getattr(self._fp, name)
+        # delegated methods
+        self.write = self._fp.write
+        self.fileno = self._fp.fileno
 
     def rename(self):
         if not self._fp.closed:
             self._fp.close()
-            rename(self.temp, localpath(self.__name))
+            rename(self._tempname, localpath(self.__name))
 
     def close(self):
-        if not self._fp:
-            return
         if not self._fp.closed:
             try:
-                os.unlink(self.temp)
-            except: pass
+                os.unlink(self._tempname)
+            except OSError:
+                pass
             self._fp.close()
 
     def __del__(self):
-        self.close()
+        if hasattr(self, '_fp'): # constructor actually did something
+            self.close()
 
 def makedirs(name, mode=None):
     """recursive directory creation with parent mode inheritance"""
diff --git a/tests/test-atomictempfile.py b/tests/test-atomictempfile.py
new file mode 100644
--- /dev/null
+++ b/tests/test-atomictempfile.py
@@ -0,0 +1,49 @@
+import os
+import glob
+from mercurial.util import atomictempfile
+
+# basic usage
+def test1_simple():
+    if os.path.exists('foo'):
+        os.remove('foo')
+    file = atomictempfile('foo')
+    (dir, basename) = os.path.split(file._tempname)
+    assert not os.path.isfile('foo')
+    assert basename in glob.glob('.foo-*')
+
+    file.write('argh\n')
+    file.rename()
+
+    assert os.path.isfile('foo')
+    assert basename not in glob.glob('.foo-*')
+    print 'OK'
+
+# close() removes the temp file but does not make the write
+# permanent -- essentially discards your work (WTF?!)
+def test2_close():
+    if os.path.exists('foo'):
+        os.remove('foo')
+    file = atomictempfile('foo')
+    (dir, basename) = os.path.split(file._tempname)
+
+    file.write('yo\n')
+    file.close()
+
+    assert not os.path.isfile('foo')
+    assert basename not in os.listdir('.')
+    print 'OK'
+
+# if a programmer screws up and passes bad args to atomictempfile, they
+# get a plain ordinary TypeError, not infinite recursion
+def test3_oops():
+    try:
+        file = atomictempfile()
+    except TypeError:
+        print "OK"
+    else:
+        print "expected TypeError"
+
+if __name__ == '__main__':
+    test1_simple()
+    test2_close()
+    test3_oops()
diff --git a/tests/test-atomictempfile.py.out b/tests/test-atomictempfile.py.out
new file mode 100644
--- /dev/null
+++ b/tests/test-atomictempfile.py.out
@@ -0,0 +1,3 @@
+OK
+OK
+OK


More information about the Mercurial-devel mailing list