Skip to content

Commit 231aaaf

Browse files
committed
ensure edited ini file contents are always complete
Use method 9 from http://www.pixelbeat.org/docs/unix_file_replacement.html to fulfill the AC properties of ACID, useful for asynchronous readers and writers. I.E. that readers will never get a partially written file, and writers will not intersperse their output. Caveats are noted in the source comments. We also better handle any external issues like permissions etc. when writing the ini file, so that only the error is printed without a backtrace, which should also avoid any auto backtrace reporting that may be enabled on the system.
1 parent 83f771a commit 231aaaf

File tree

1 file changed

+86
-15
lines changed

1 file changed

+86
-15
lines changed

crudini

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@
88
# under the terms of the GPLv2, the GNU General Public License version 2, as
99
# published by the Free Software Foundation. http://gnu.org/licenses/gpl.html
1010

11+
import os
1112
import sys
13+
import errno
14+
import contextlib
1215
import ConfigParser
1316
import getopt
1417
import iniparse
1518
import pipes
19+
import shutil
1620
import string
21+
import tempfile
1722
from cStringIO import StringIO
1823

1924
try:
@@ -195,7 +200,7 @@ def _parse_file(filename, add_default=False):
195200
conf = iniparse.RawConfigParser()
196201
conf.readfp(fp)
197202
return conf
198-
except IOError as e:
203+
except EnvironmentError as e:
199204
error(str(e))
200205
sys.exit(1)
201206

@@ -355,18 +360,84 @@ except ConfigParser.NoOptionError:
355360
error('Parameter not found: %s' % param)
356361
sys.exit(1)
357362

363+
def delete_if_exists(path):
364+
"""Delete a file, but ignore file not found error.
365+
"""
366+
try:
367+
os.unlink(path)
368+
except EnvironmentError as e:
369+
if e.errno != errno.ENOENT:
370+
print str(e)
371+
raise
372+
373+
@contextlib.contextmanager
374+
def remove_file_on_error(path):
375+
"""Protect code that wants to operate on PATH atomically.
376+
Any exception will cause PATH to be removed.
377+
"""
378+
try:
379+
yield
380+
except Exception:
381+
t, v, tb = sys.exc_info()
382+
delete_if_exists(path)
383+
raise t, v, tb
384+
385+
def file_replace(name, data):
386+
"""Replace file as atomically as possible,
387+
fulfilling and AC properties of ACID.
388+
This is essentially using method 9 from:
389+
http://www.pixelbeat.org/docs/unix_file_replacement.html
390+
391+
Caveats:
392+
- Changes ownership of the file being edited
393+
by non root users (due to POSIX interface limitations).
394+
- Loses any extended attributes of the original file
395+
(due to the simplicity of this implementation).
396+
- Existing hardlinks will be separated from the
397+
newly replaced file.
398+
- Ignores the write permissions of the original file.
399+
- Requires write permission on the directory as well as the file.
400+
- With python2 on windows we don't fulfill the A ACID property.
401+
402+
To avoid the above caveats see the --inplace option.
403+
"""
404+
(f, tmp) = tempfile.mkstemp(".tmp", prefix=name+".", dir=".")
405+
406+
with remove_file_on_error(tmp):
407+
shutil.copystat(name, tmp)
408+
409+
if hasattr(os, 'fchown') and os.geteuid() == 0:
410+
st = os.stat(name)
411+
os.fchown(f, st.st_uid, st.st_gid)
412+
413+
os.write(f, data)
414+
os.close(f)
415+
416+
if hasattr(os,'replace'): # >= python 3.3
417+
os.replace(tmp, name) # atomic even on windos
418+
elif os.name == 'posix':
419+
os.rename(tmp, name) # atomic on POSIX
420+
else:
421+
backup = tmp+'.backup'
422+
os.rename(name, backup)
423+
os.rename(tmp, name)
424+
delete_if_exists(backup)
425+
358426
if mode != '--get':
359-
with open(cfgfile, 'w') as f:
360-
# XXX: Ideally we should just do conf.write(f) here,
361-
# but to avoid iniparse issues, we massage the data a little here
362-
str_data = str(conf.data)
363-
if len(str_data) and str_data[-1] != '\n':
364-
str_data += '\n'
365-
366-
if (
367-
(added_default_section and not (section_explicit_default and mode in ('--set', '--merge')))
368-
or (mode == '--del' and section == iniparse.DEFAULTSECT and param is None)
369-
):
370-
str_data = str_data.replace('[%s]\n' % iniparse.DEFAULTSECT, '', 1)
371-
372-
f.write(str_data)
427+
# XXX: Ideally we should just do conf.write(f) here,
428+
# but to avoid iniparse issues, we massage the data a little here
429+
str_data = str(conf.data)
430+
if len(str_data) and str_data[-1] != '\n':
431+
str_data += '\n'
432+
433+
if (
434+
(added_default_section and not (section_explicit_default and mode in ('--set', '--merge')))
435+
or (mode == '--del' and section == iniparse.DEFAULTSECT and param is None)
436+
):
437+
str_data = str_data.replace('[%s]\n' % iniparse.DEFAULTSECT, '', 1)
438+
439+
try:
440+
file_replace(cfgfile, str_data)
441+
except EnvironmentError as e:
442+
error(str(e))
443+
sys.exit(1)

0 commit comments

Comments
 (0)