Edgewall Software

Ticket #8623: 8623-atomic-config-updates-r8631.2.patch

File 8623-atomic-config-updates-r8631.2.patch, 8.9 KB (added by rblank, 3 years ago)

Fixed fallback rename when destination doesn't exist.

  • trac/admin/console.py

    diff --git a/trac/admin/console.py b/trac/admin/console.py
    a b  
    3232from trac.env import Environment 
    3333from trac.perm import PermissionSystem 
    3434from trac.ticket.model import * 
    35 from trac.util import getuser 
     35from trac.util import WindowsError, getuser 
    3636from trac.util.datefmt import parse_date, format_date, format_datetime, utc 
    3737from trac.util.html import html 
    3838from trac.util.text import to_unicode, wrap, unicode_quote, unicode_unquote, \ 
  • trac/admin/tests/console.py

    diff --git a/trac/admin/tests/console.py b/trac/admin/tests/console.py
    a b  
    5454    return expected 
    5555 
    5656 
     57class InMemoryConfiguration(Configuration): 
     58    """A subclass of Configuration that doesn't save to disk.""" 
     59    def save(self): 
     60        pass 
     61 
     62 
    5763class InMemoryEnvironment(Environment): 
    5864    """ 
    5965    A subclass of Environment that keeps its' DB in memory. 
     
    7985               cls.__module__.find('.tests.') == -1 
    8086 
    8187    def setup_config(self, load_defaults=None): 
    82         self.config = Configuration(None) 
    83  
    84     def save_config(self): 
    85         pass 
     88        self.config = InMemoryConfiguration(None) 
    8689 
    8790 
    8891class TracadminTestCase(unittest.TestCase): 
  • trac/config.py

    diff --git a/trac/config.py b/trac/config.py
    a b  
    1414 
    1515from ConfigParser import ConfigParser 
    1616from copy import deepcopy 
    17 import os 
     17import os.path 
    1818 
    1919from trac.core import ExtensionPoint, TracError 
     20from trac.util import AtomicFile 
    2021from trac.util.compat import set, sorted 
    2122from trac.util.text import to_unicode, CRLF 
    2223from trac.util.translation import _ 
     
    208209 
    209210        # At this point, all the strings in `sections` are UTF-8 encoded `str` 
    210211        try: 
    211             fileobj = open(self.filename, 'w') 
     212            fileobj = AtomicFile(self.filename, 'w') 
    212213            try: 
    213214                fileobj.write('# -*- coding: utf-8 -*-\n\n') 
    214215                for section, options in sections: 
  • trac/env.py

    diff --git a/trac/env.py b/trac/env.py
    a b  
    2828from trac.core import Component, ComponentManager, implements, Interface, \ 
    2929                      ExtensionPoint, TracError 
    3030from trac.db import DatabaseManager 
    31 from trac.util import get_pkginfo 
     31from trac.util import create_file, get_pkginfo 
    3232from trac.util.text import exception_to_unicode 
    3333from trac.util.translation import _ 
    3434from trac.versioncontrol import RepositoryManager 
     
    299299        If options contains ('inherit', 'file'), default values will not be 
    300300        loaded; they are expected to be provided by that file or other options. 
    301301        """ 
    302         def _create_file(fname, data=None): 
    303             fd = open(fname, 'w') 
    304             if data: 
    305                 fd.write(data) 
    306             fd.close() 
    307  
    308302        # Create the directory structure 
    309303        if not os.path.exists(self.path): 
    310304            os.mkdir(self.path) 
     
    313307        os.mkdir(os.path.join(self.path, 'plugins')) 
    314308 
    315309        # Create a few files 
    316         _create_file(os.path.join(self.path, 'VERSION'), 
    317                      'Trac Environment Version 1\n') 
    318         _create_file(os.path.join(self.path, 'README'), 
    319                      'This directory contains a Trac environment.\n' 
    320                      'Visit http://trac.edgewall.org/ for more information.\n') 
     310        create_file(os.path.join(self.path, 'VERSION'), 
     311                    'Trac Environment Version 1\n') 
     312        create_file(os.path.join(self.path, 'README'), 
     313                    'This directory contains a Trac environment.\n' 
     314                    'Visit http://trac.edgewall.org/ for more information.\n') 
    321315 
    322316        # Setup the default configuration 
    323317        os.mkdir(os.path.join(self.path, 'conf')) 
    324         _create_file(os.path.join(self.path, 'conf', 'trac.ini')) 
     318        create_file(os.path.join(self.path, 'conf', 'trac.ini')) 
     319        create_file(os.path.join(self.path, 'conf', 'trac.ini.sample')) 
    325320        skip_defaults = options and ('inherit', 'file') in [(section, option) \ 
    326321                for (section, option, value) in options] 
    327322        self.setup_config(load_defaults=not skip_defaults) 
     
    526521 
    527522    def _update_sample_config(self): 
    528523        filename = os.path.join(self.env.path, 'conf', 'trac.ini.sample') 
     524        if not os.path.isfile(filename): 
     525            return 
    529526        config = Configuration(filename) 
    530527        for section, default_options in config.defaults().iteritems(): 
    531528            for name, value in default_options.iteritems(): 
  • trac/util/__init__.py

    diff --git a/trac/util/__init__.py b/trac/util/__init__.py
    a b  
    1919 
    2020import errno 
    2121import locale 
    22 import os 
     22import os.path 
    2323import re 
    2424import sys 
    2525import time 
     
    7777 
    7878# -- os utilities 
    7979 
     80try: 
     81    WindowsError = WindowsError 
     82except NameError: 
     83    class WindowsError(OSError): 
     84        """Dummy exception replacing WindowsError on non-Windows platforms""" 
     85 
     86 
     87if os.name == 'nt': 
     88    try: 
     89        import ctypes 
     90        MOVEFILE_REPLACE_EXISTING = 0x1 
     91        MOVEFILE_WRITE_THROUGH = 0x8 
     92         
     93        try: 
     94            MoveFileTransacted = ctypes.kernel32.MoveFileTransactedA 
     95            CreateTransaction = ctypes.windll.ktmw32.CreateTransaction 
     96            CommitTransaction = ctypes.windll.ktmw32.CommitTransaction 
     97            CloseHandle = ctypes.windll.kernel32.CloseHandle 
     98             
     99            def rename(src, dst): 
     100                ta = CreateTransaction(None, 0, 0, 0, 0, 1000, 
     101                                       'Rename "%s" to "%s"' % (src, dst)) 
     102                try: 
     103                    if not MoveFileTransacted(src, dst, None, None, 
     104                                              MOVEFILE_REPLACE_EXISTING 
     105                                              | MOVEFILE_WRITE_THROUGH, ta): 
     106                        raise ctypes.WinError() 
     107                    if not CommitTransaction(ta): 
     108                        raise ctypes.WinError() 
     109                finally: 
     110                    CloseHandle(ta) 
     111        except AttributeError: 
     112            MoveFileEx = ctypes.windll.kernel32.MoveFileExA 
     113             
     114            def rename(src, dst): 
     115                if not MoveFileEx(src, dst, MOVEFILE_REPLACE_EXISTING 
     116                                            | MOVEFILE_WRITE_THROUGH): 
     117                    raise ctypes.WinError() 
     118    except Exception: 
     119        import random 
     120         
     121        def rename(src, dst): 
     122            try: 
     123                os.rename(src, dst) 
     124            except WindowsError, e: 
     125                if e.errno != errno.EEXIST: 
     126                    raise 
     127                old = "%s-%08x" % (dst, random.randint(0, 0xffffffff)) 
     128                os.rename(dst, old) 
     129                os.rename(src, dst) 
     130                try: 
     131                    os.unlink(old) 
     132                except Exception: 
     133                    pass 
     134else: 
     135    rename = os.rename 
     136 
     137 
     138class AtomicFile(object): 
     139    """A file that appears atomically with its full content. 
     140     
     141    This file-like object writes to a temporary file in the same directory 
     142    as the final file. If the file is committed, the temporary file is renamed 
     143    atomically (on Unix, at least) to its final name. If it is rolled back, 
     144    the temporary file is removed. 
     145    """ 
     146    def __init__(self, path, mode='w', bufsize=-1): 
     147        self._path = path 
     148        (dir, name) = os.path.split(path) 
     149        (fd, self._temp) = tempfile.mkstemp(prefix=name + '-', dir=dir) 
     150        self._file = os.fdopen(fd, mode, bufsize) 
     151         
     152        # Try to preserve permissions and group ownership, but failure 
     153        # should not be fatal 
     154        try: 
     155            st = os.stat(path) 
     156            if hasattr(os, 'chmod'): 
     157                os.chmod(self._temp, st.st_mode) 
     158            if hasattr(os, 'chflags') and hasattr(st, 'st_flags'): 
     159                os.chflags(self._temp, st.st_flags) 
     160            if hasattr(os, 'chown'): 
     161                os.chown(self._temp, -1, st.st_gid) 
     162        except OSError: 
     163            pass 
     164     
     165    def __getattr__(self, name): 
     166        return getattr(self._file, name) 
     167     
     168    def commit(self): 
     169        if self._file is None: 
     170            return 
     171        try: 
     172            f, self._file = self._file, None 
     173            f.close() 
     174            rename(self._temp, self._path) 
     175        except Exception: 
     176            os.unlink(self._temp) 
     177            raise 
     178     
     179    def rollback(self): 
     180        if self._file is None: 
     181            return 
     182        try: 
     183            f, self._file = self._file, None 
     184            f.close() 
     185        finally: 
     186            try: 
     187                os.unlink(self._temp) 
     188            except: 
     189                pass 
     190     
     191    close = commit 
     192    __del__ = rollback 
     193 
     194 
     195def create_file(path, data='', mode='w'): 
     196    """Create a new file with the given data.""" 
     197    f = open(path, mode) 
     198    try: 
     199        if data: 
     200            f.write(data) 
     201    finally: 
     202        f.close() 
     203 
     204 
    80205def create_unique_file(path): 
    81206    """Create a new file. An index is added if the path exists""" 
    82207    parts = os.path.splitext(path)