# HG changeset patch # User Greg Ward # Date 2011-04-24 23:25:10 # Node ID d764463b433e05b660904f26041b9cf7ad97960a # Parent a395575691a6d2a9b84c46e2c6c4dd09e406317f 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 @@ -726,31 +726,31 @@ class atomictempfile(object): name, making the changes visible. """ 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