Skip to content

Commit

Permalink
ensure edited ini file contents are always complete
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pixelb committed Jun 5, 2014
1 parent 83f771a commit 231aaaf
Showing 1 changed file with 86 additions and 15 deletions.
101 changes: 86 additions & 15 deletions crudini
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@
# under the terms of the GPLv2, the GNU General Public License version 2, as
# published by the Free Software Foundation. http://gnu.org/licenses/gpl.html

import os
import sys
import errno
import contextlib
import ConfigParser
import getopt
import iniparse
import pipes
import shutil
import string
import tempfile
from cStringIO import StringIO

try:
Expand Down Expand Up @@ -195,7 +200,7 @@ def _parse_file(filename, add_default=False):
conf = iniparse.RawConfigParser()
conf.readfp(fp)
return conf
except IOError as e:
except EnvironmentError as e:
error(str(e))
sys.exit(1)

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

def delete_if_exists(path):
"""Delete a file, but ignore file not found error.
"""
try:
os.unlink(path)
except EnvironmentError as e:
if e.errno != errno.ENOENT:
print str(e)
raise

@contextlib.contextmanager
def remove_file_on_error(path):
"""Protect code that wants to operate on PATH atomically.
Any exception will cause PATH to be removed.
"""
try:
yield
except Exception:
t, v, tb = sys.exc_info()
delete_if_exists(path)
raise t, v, tb

def file_replace(name, data):
"""Replace file as atomically as possible,
fulfilling and AC properties of ACID.
This is essentially using method 9 from:
http://www.pixelbeat.org/docs/unix_file_replacement.html
Caveats:
- Changes ownership of the file being edited
by non root users (due to POSIX interface limitations).
- Loses any extended attributes of the original file
(due to the simplicity of this implementation).
- Existing hardlinks will be separated from the
newly replaced file.
- Ignores the write permissions of the original file.
- Requires write permission on the directory as well as the file.
- With python2 on windows we don't fulfill the A ACID property.
To avoid the above caveats see the --inplace option.
"""
(f, tmp) = tempfile.mkstemp(".tmp", prefix=name+".", dir=".")

with remove_file_on_error(tmp):
shutil.copystat(name, tmp)

if hasattr(os, 'fchown') and os.geteuid() == 0:
st = os.stat(name)
os.fchown(f, st.st_uid, st.st_gid)

os.write(f, data)
os.close(f)

if hasattr(os,'replace'): # >= python 3.3
os.replace(tmp, name) # atomic even on windos
elif os.name == 'posix':
os.rename(tmp, name) # atomic on POSIX
else:
backup = tmp+'.backup'
os.rename(name, backup)
os.rename(tmp, name)
delete_if_exists(backup)

if mode != '--get':
with open(cfgfile, 'w') as f:
# XXX: Ideally we should just do conf.write(f) here,
# but to avoid iniparse issues, we massage the data a little here
str_data = str(conf.data)
if len(str_data) and str_data[-1] != '\n':
str_data += '\n'

if (
(added_default_section and not (section_explicit_default and mode in ('--set', '--merge')))
or (mode == '--del' and section == iniparse.DEFAULTSECT and param is None)
):
str_data = str_data.replace('[%s]\n' % iniparse.DEFAULTSECT, '', 1)

f.write(str_data)
# XXX: Ideally we should just do conf.write(f) here,
# but to avoid iniparse issues, we massage the data a little here
str_data = str(conf.data)
if len(str_data) and str_data[-1] != '\n':
str_data += '\n'

if (
(added_default_section and not (section_explicit_default and mode in ('--set', '--merge')))
or (mode == '--del' and section == iniparse.DEFAULTSECT and param is None)
):
str_data = str_data.replace('[%s]\n' % iniparse.DEFAULTSECT, '', 1)

try:
file_replace(cfgfile, str_data)
except EnvironmentError as e:
error(str(e))
sys.exit(1)

0 comments on commit 231aaaf

Please sign in to comment.