# HG changeset patch # Parent be1e015a84051595ee41f8d788ba0fa243294039 Rewrite single-file mailboxes by copying, add some warnings about locking. diff --git a/Lib/mailbox.py b/Lib/mailbox.py --- a/Lib/mailbox.py +++ b/Lib/mailbox.py @@ -17,6 +17,8 @@ import email import email.message import email.generator import StringIO +import shutil +import warnings try: if sys.platform == 'os2emx': # OS/2 EMX fcntl() not adequate @@ -630,6 +632,10 @@ class _singlefileMailbox(Mailbox): if not self._locked: _lock_file(self._file) self._locked = True + if self._pending: + warnings.warn("mailbox .lock() method called with pending changes; " + "should have been locked before making changes", + stacklevel=2) def unlock(self): """Unlock the mailbox if it is locked.""" @@ -661,6 +667,15 @@ class _singlefileMailbox(Mailbox): '(expected %i, found %i)' % (self._file_length, cur_len)) + if fcntl and self._locked and not hasattr(self._file, 'truncate'): + warnings.warn('as file.truncate() is unavailable, flush() may ' + 'momentarily release the fcntl lock; if you depend ' + 'on fcntl locking, you should regard flush() as ' + 'invalidating the message keys', RuntimeWarning, + stacklevel=2) + + orig_file = self._file + remove_temp_file = True new_file = _create_temporary(self._path) try: new_toc = {} @@ -678,32 +693,50 @@ class _singlefileMailbox(Mailbox): new_file.write(buffer) new_toc[key] = (new_start, new_file.tell()) self._post_message_hook(new_file) - self._file_length = new_file.tell() - except: - new_file.close() - os.remove(new_file.name) - raise - _sync_close(new_file) - # self._file is about to get replaced, so no need to sync. - self._file.close() - # Make sure the new file's mode is the same as the old file's - mode = os.stat(self._path).st_mode - os.chmod(new_file.name, mode) - try: - os.rename(new_file.name, self._path) - except OSError, e: - if e.errno == errno.EEXIST or \ - (os.name == 'os2' and e.errno == errno.EACCES): - os.remove(self._path) - os.rename(new_file.name, self._path) - else: - raise - self._file = open(self._path, 'rb+') - self._toc = new_toc - self._pending = False - self._pending_sync = False - if self._locked: - _lock_file(self._file, dotlock=False) + new_len = new_file.tell() + _sync_flush(new_file) + new_file.seek(0) + self._file.seek(0) + if new_len < cur_len and not hasattr(self._file, 'truncate'): + # Ensure that orig_file won't write any more data when + # closed (we can't close it here as that would release + # any fcntl lock on the mailbox). + orig_file.flush() + try: + if not os.path.samestat(os.fstat(self._file.fileno()), + os.stat(self._path)): + raise ExternalClashError("Mailbox has been replaced: " + "%s" % self._path) + except OSError as e: + if e.errno == errno.ENOENT: + raise NoSuchMailboxError(self._path) + raise + except AttributeError: + # No stat(), etc. + pass + # *** race condition *** + remove_temp_file = False + self._file = open(self._path, 'wb+') + remove_temp_file = False + shutil.copyfileobj(new_file, self._file) + if hasattr(self._file, 'truncate'): + self._file.truncate(new_len) + self._file_length = new_len + self._toc = new_toc + _sync_flush(self._file) + remove_temp_file = True + self._pending = False + self._pending_sync = False + finally: + try: + new_file.close() + if remove_temp_file: + os.remove(new_file.name) + finally: + if self._file is not orig_file: + orig_file.close() + if self._locked: + _lock_file(self._file, dotlock=False) def _pre_mailbox_hook(self, f): """Called before writing the mailbox to file f."""