-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
Hello again,
I've revised the patch once again and I'd like to commit it tonight if there
are no complaints. Thanks to Brian for feedback in #gentoo-portage.
Zac
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.2 (GNU/Linux)
iD8DBQFD6k07/ejvha5XGaMRAnnrAJ9nSz524aMURXUvWKdlRqBUR/hA3ACgiidh
7MCV9oDqZWE6bj18SyoSLW8=
=l8EZ
-----END PGP SIGNATURE-----
Index: pym/portage_util.py
===================================================================
--- pym/portage_util.py (revision 2679)
+++ pym/portage_util.py (working copy)
@@ -194,9 +194,8 @@
"""Writes out a dict to a file; writekey=0 mode doesn't write out
the key and assumes all values are strings, not lists."""
myfile = None
- myf2 = "%s.%i" % (myfilename, os.getpid())
try:
- myfile=open(myf2,"w")
+ myfile = atomic_ofstream(myfilename)
if not writekey:
for x in mydict.values():
myfile.write(x+"\n")
@@ -204,11 +203,9 @@
for x in mydict.keys():
myfile.write("%s %s\n" % (x, " ".join(mydict[x])))
myfile.close()
- os.rename(myf2, myfilename)
-
except IOError:
if myfile is not None:
- os.unlink(myf2)
+ myfile.close()
return 0
return 1
@@ -456,4 +453,73 @@
if x not in u:
u.append(x)
return u
-
+
+def apply_permissions(filename, uid=-1, gid=-1, mode=0,
+ stat_cached=None):
+ """Apply user, group, and mode bits to a file
+ if the existing bits do not already match."""
+
+ if stat_cached is None:
+ stat_cached = os.stat(filename)
+
+ if (uid != -1 and uid != stat_cached.st_uid) or \
+ (gid != -1 and gid != stat_cached.st_gid):
+ os.chown(filename, uid, gid)
+
+ if mode & stat_cached.st_mode != mode:
+ os.chmod(filename, mode | stat_cached.st_mode)
+
+def apply_stat_permissions(filename, newstat, stat_cached=None):
+ """wrapper around apply_permissions that gets
+ uid, gid, and mode from a stat object"""
+ apply_permissions(filename, uid=newstat.st_uid, gid=newstat.st_gid,
+ mode=newstat.st_mode, stat_cached=stat_cached)
+
+class atomic_ofstream(file):
+ """Write a file atomically via os.rename(). Atomic replacement prevents
+ interprocess interference and prevents corruption of the target
+ file when the write is interrupted (for example, when an 'out of space'
+ error occurs)."""
+
+ def __init__(self, filename, mode='w', **kargs):
+ """Opens a temporary filename.pid in the same directory as filename."""
+ self._real_name = filename
+ tmp_name = "%s.%i" % (filename, os.getpid())
+ super(atomic_ofstream, self).__init__(tmp_name, mode=mode, **kargs)
+
+ def close(self):
+ """Closes the temporary file, copies permissions (if possible),
+ and performs the atomic replacement via os.rename()."""
+ if not self.closed:
+ try:
+ super(atomic_ofstream, self).close()
+ try:
+ apply_stat_permissions(self.name, os.stat(self._real_name))
+ except OSError, oe:
+ import errno
+ if oe.errno in (errno.ENOENT,errno.EPERM):
+ pass
+ else:
+ raise oe
+ os.rename(self.name, self._real_name)
+ finally:
+ # Make sure we cleanup the temp file
+ # even if an exception is raised.
+ try:
+ os.unlink(self.name)
+ except OSError, oe:
+ pass
+
+ def __del__(self):
+ """Ensure that close() gets called because
+ that's where the rename happens"""
+ self.close()
+ # ensure destructor from the base class is called
+ base_self = super(atomic_ofstream, self)
+ if hasattr(base_self, "__del__"):
+ base_self.__del__()
+
+def write_atomic(file_path, content):
+ f = atomic_ofstream(file_path)
+ f.write(content)
+ f.close()
Index: pym/portage.py
===================================================================
--- pym/portage.py (revision 2679)
+++ pym/portage.py (working copy)
@@ -92,7 +92,7 @@
portage_uid, portage_gid
import portage_util
- from portage_util import grabdict, grabdict_package, grabfile, grabfile_package, \
+ from portage_util import grabdict, grabdict_package, grabfile, grabfile_package, write_atomic, \
map_dictlist_vals, pickle_read, pickle_write, stack_dictlist, stack_dicts, stack_lists, \
unique_array, varexpand, writedict, writemsg, writemsg_stdout, getconfig, dump_traceback
import portage_exception
@@ -5862,10 +5862,7 @@
os.chown(pdir, 0, portage_gid)
os.chmod(pdir, 02770)
- myworld=open(self.myroot+WORLD_FILE,"w")
- for x in newworldlist:
- myworld.write(x+"\n")
- myworld.close()
+ write_atomic(os.path.join(self.myroot,WORLD_FILE),"\n".join(newworldlist))
#do original postrm
if myebuildpath and os.path.exists(myebuildpath):
@@ -6874,10 +6871,7 @@
if processed:
#update our internal mtime since we processed all our directives.
mtimedb["updates"][mykey]=os.stat(mykey)[stat.ST_MTIME]
- myworld=open("/"+WORLD_FILE,"w")
- for x in worldlist:
- myworld.write(x+"\n")
- myworld.close()
+ write_atomic(WORLD_FILE,"\n".join(worldlist))
print ""
def commit_mtimedb():
Index: bin/regenworld
===================================================================
--- bin/regenworld (revision 2679)
+++ bin/regenworld (working copy)
@@ -88,6 +88,4 @@
print "add to world:",myfavkey
worldlist.append(myfavkey)
-myfile=open(portage.WORLD_FILE, "w")
-myfile.write(string.join(worldlist, '\n')+'\n')
-myfile.close()
+portage.write_atomic(portage.WORLD_FILE,"\n".join(worldlist))
Index: bin/emerge
===================================================================
--- bin/emerge (revision 2679)
+++ bin/emerge (working copy)
@@ -1916,7 +1916,7 @@
myfavdict[myfavkey]=myfavkey
print ">>> Recording",myfavkey,"in \"world\" favorites file..."
if not "--fetchonly" in myopts:
- portage.writedict(myfavdict,portage.root+portage.WORLD_FILE,writekey=0)
+ portage.write_atomic(os.path.join(portage.root,portage.WORLD_FILE),"\n".join(myfavdict.values()))
portage.mtimedb["resume"]["mergelist"]=mymergelist[:]
@@ -2087,7 +2087,7 @@
myfavdict[myfavkey]=myfavkey
print ">>> Recording",myfavkey,"in \"world\" favorites file..."
emergelog(" === ("+str(mergecount)+" of "+str(len(mymergelist))+") Updating world file ("+x[pkgindex]+")")
- portage.writedict(myfavdict,myroot+portage.WORLD_FILE,writekey=0)
+ portage.write_atomic(os.path.join(myroot,portage.WORLD_FILE),"\n".join(myfavdict.values()))
if ("noclean" not in portage.features) and (x[0] != "binary"):
short_msg = "emerge: ("+str(mergecount)+" of "+str(len(mymergelist))+") "+x[pkgindex]+" Clean Post"
Index: bin/emaint
===================================================================
--- bin/emaint (revision 2679)
+++ bin/emaint (working copy)
@@ -40,7 +40,7 @@
def fix(self):
errors = []
try:
- open(portage_const.WORLD_FILE, "w").write("\n".join(self.okay))
+ portage.write_atomic(portage_const.WORLD_FILE,"\n".join(self.okay))
except OSError:
errors.append(portage_const.WORLD_FILE + " could not be opened for writing")
return errors