diff --git a/trac/admin/console.py b/trac/admin/console.py
--- a/trac/admin/console.py
+++ b/trac/admin/console.py
@@ -32,7 +32,7 @@
 from trac.env import Environment
 from trac.perm import PermissionSystem
 from trac.ticket.model import *
-from trac.util import getuser
+from trac.util import WindowsError, getuser
 from trac.util.datefmt import parse_date, format_date, format_datetime, utc
 from trac.util.html import html
 from trac.util.text import to_unicode, wrap, unicode_quote, unicode_unquote, \
diff --git a/trac/admin/tests/console.py b/trac/admin/tests/console.py
--- a/trac/admin/tests/console.py
+++ b/trac/admin/tests/console.py
@@ -54,6 +54,12 @@
     return expected
 
 
+class InMemoryConfiguration(Configuration):
+    """A subclass of Configuration that doesn't save to disk."""
+    def save(self):
+        pass
+
+
 class InMemoryEnvironment(Environment):
     """
     A subclass of Environment that keeps its' DB in memory.
@@ -79,10 +85,7 @@
                cls.__module__.find('.tests.') == -1
 
     def setup_config(self, load_defaults=None):
-        self.config = Configuration(None)
-
-    def save_config(self):
-        pass
+        self.config = InMemoryConfiguration(None)
 
 
 class TracadminTestCase(unittest.TestCase):
diff --git a/trac/config.py b/trac/config.py
--- a/trac/config.py
+++ b/trac/config.py
@@ -14,9 +14,10 @@
 
 from ConfigParser import ConfigParser
 from copy import deepcopy
-import os
+import os.path
 
 from trac.core import ExtensionPoint, TracError
+from trac.util import AtomicFile
 from trac.util.compat import set, sorted
 from trac.util.text import to_unicode, CRLF
 from trac.util.translation import _
@@ -208,7 +209,7 @@
 
         # At this point, all the strings in `sections` are UTF-8 encoded `str`
         try:
-            fileobj = open(self.filename, 'w')
+            fileobj = AtomicFile(self.filename, 'w')
             try:
                 fileobj.write('# -*- coding: utf-8 -*-\n\n')
                 for section, options in sections:
diff --git a/trac/env.py b/trac/env.py
--- a/trac/env.py
+++ b/trac/env.py
@@ -28,7 +28,7 @@
 from trac.core import Component, ComponentManager, implements, Interface, \
                       ExtensionPoint, TracError
 from trac.db import DatabaseManager
-from trac.util import get_pkginfo
+from trac.util import create_file, get_pkginfo
 from trac.util.text import exception_to_unicode
 from trac.util.translation import _
 from trac.versioncontrol import RepositoryManager
@@ -299,12 +299,6 @@
         If options contains ('inherit', 'file'), default values will not be
         loaded; they are expected to be provided by that file or other options.
         """
-        def _create_file(fname, data=None):
-            fd = open(fname, 'w')
-            if data:
-                fd.write(data)
-            fd.close()
-
         # Create the directory structure
         if not os.path.exists(self.path):
             os.mkdir(self.path)
@@ -313,15 +307,16 @@
         os.mkdir(os.path.join(self.path, 'plugins'))
 
         # Create a few files
-        _create_file(os.path.join(self.path, 'VERSION'),
-                     'Trac Environment Version 1\n')
-        _create_file(os.path.join(self.path, 'README'),
-                     'This directory contains a Trac environment.\n'
-                     'Visit http://trac.edgewall.org/ for more information.\n')
+        create_file(os.path.join(self.path, 'VERSION'),
+                    'Trac Environment Version 1\n')
+        create_file(os.path.join(self.path, 'README'),
+                    'This directory contains a Trac environment.\n'
+                    'Visit http://trac.edgewall.org/ for more information.\n')
 
         # Setup the default configuration
         os.mkdir(os.path.join(self.path, 'conf'))
-        _create_file(os.path.join(self.path, 'conf', 'trac.ini'))
+        create_file(os.path.join(self.path, 'conf', 'trac.ini'))
+        create_file(os.path.join(self.path, 'conf', 'trac.ini.sample'))
         skip_defaults = options and ('inherit', 'file') in [(section, option) \
                 for (section, option, value) in options]
         self.setup_config(load_defaults=not skip_defaults)
@@ -526,6 +521,8 @@
 
     def _update_sample_config(self):
         filename = os.path.join(self.env.path, 'conf', 'trac.ini.sample')
+        if not os.path.isfile(filename):
+            return
         config = Configuration(filename)
         for section, default_options in config.defaults().iteritems():
             for name, value in default_options.iteritems():
diff --git a/trac/util/__init__.py b/trac/util/__init__.py
--- a/trac/util/__init__.py
+++ b/trac/util/__init__.py
@@ -19,7 +19,7 @@
 
 import errno
 import locale
-import os
+import os.path
 import re
 import sys
 import time
@@ -77,6 +77,131 @@
 
 # -- os utilities
 
+try:
+    WindowsError = WindowsError
+except NameError:
+    class WindowsError(OSError):
+        """Dummy exception replacing WindowsError on non-Windows platforms"""
+
+
+if os.name == 'nt':
+    try:
+        import ctypes
+        MOVEFILE_REPLACE_EXISTING = 0x1
+        MOVEFILE_WRITE_THROUGH = 0x8
+        
+        try:
+            MoveFileTransacted = ctypes.kernel32.MoveFileTransactedA
+            CreateTransaction = ctypes.windll.ktmw32.CreateTransaction
+            CommitTransaction = ctypes.windll.ktmw32.CommitTransaction
+            CloseHandle = ctypes.windll.kernel32.CloseHandle
+            
+            def rename(src, dst):
+                ta = CreateTransaction(None, 0, 0, 0, 0, 1000,
+                                       'Rename "%s" to "%s"' % (src, dst))
+                try:
+                    if not MoveFileTransacted(src, dst, None, None,
+                                              MOVEFILE_REPLACE_EXISTING
+                                              | MOVEFILE_WRITE_THROUGH, ta):
+                        raise ctypes.WinError()
+                    if not CommitTransaction(ta):
+                        raise ctypes.WinError()
+                finally:
+                    CloseHandle(ta)
+        except AttributeError:
+            MoveFileEx = ctypes.windll.kernel32.MoveFileExA
+            
+            def rename(src, dst):
+                if not MoveFileEx(src, dst, MOVEFILE_REPLACE_EXISTING
+                                            | MOVEFILE_WRITE_THROUGH):
+                    raise ctypes.WinError()
+    except Exception:
+        import random
+        
+        def rename(src, dst):
+            try:
+                os.rename(src, dst)
+            except WindowsError, e:
+                if e.errno != errno.EEXIST:
+                    raise
+                old = "%s-%08x" % (dst, random.randint(0, 0xffffffff))
+                os.rename(dst, old)
+                os.rename(src, dst)
+                try:
+                    os.unlink(old)
+                except Exception:
+                    pass
+else:
+    rename = os.rename
+
+
+class AtomicFile(object):
+    """A file that appears atomically with its full content.
+    
+    This file-like object writes to a temporary file in the same directory
+    as the final file. If the file is committed, the temporary file is renamed
+    atomically (on Unix, at least) to its final name. If it is rolled back,
+    the temporary file is removed.
+    """
+    def __init__(self, path, mode='w', bufsize=-1):
+        self._path = path
+        (dir, name) = os.path.split(path)
+        (fd, self._temp) = tempfile.mkstemp(prefix=name + '-', dir=dir)
+        self._file = os.fdopen(fd, mode, bufsize)
+        
+        # Try to preserve permissions and group ownership, but failure
+        # should not be fatal
+        try:
+            st = os.stat(path)
+            if hasattr(os, 'chmod'):
+                os.chmod(self._temp, st.st_mode)
+            if hasattr(os, 'chflags') and hasattr(st, 'st_flags'):
+                os.chflags(self._temp, st.st_flags)
+            if hasattr(os, 'chown'):
+                os.chown(self._temp, -1, st.st_gid)
+        except OSError:
+            pass
+    
+    def __getattr__(self, name):
+        return getattr(self._file, name)
+    
+    def commit(self):
+        if self._file is None:
+            return
+        try:
+            f, self._file = self._file, None
+            f.close()
+            rename(self._temp, self._path)
+        except Exception:
+            os.unlink(self._temp)
+            raise
+    
+    def rollback(self):
+        if self._file is None:
+            return
+        try:
+            f, self._file = self._file, None
+            f.close()
+        finally:
+            try:
+                os.unlink(self._temp)
+            except:
+                pass
+    
+    close = commit
+    __del__ = rollback
+
+
+def create_file(path, data='', mode='w'):
+    """Create a new file with the given data."""
+    f = open(path, mode)
+    try:
+        if data:
+            f.write(data)
+    finally:
+        f.close()
+
+
 def create_unique_file(path):
     """Create a new file. An index is added if the path exists"""
     parts = os.path.splitext(path)

