Issue #14632: Updated WatchedFileHandler to deal with race condition. Thanks to John Mulligan for the problem report and patch.

This commit is contained in:
Vinay Sajip 2012-04-24 23:33:33 +01:00
commit a5798ded26
2 changed files with 70 additions and 24 deletions

View File

@ -23,7 +23,7 @@ Copyright (C) 2001-2012 Vinay Sajip. All Rights Reserved.
To use, simply 'import logging.handlers' and log away! To use, simply 'import logging.handlers' and log away!
""" """
import logging, socket, os, pickle, struct, time, re import errno, logging, socket, os, pickle, struct, time, re
from codecs import BOM_UTF8 from codecs import BOM_UTF8
from stat import ST_DEV, ST_INO, ST_MTIME from stat import ST_DEV, ST_INO, ST_MTIME
import queue import queue
@ -419,11 +419,13 @@ class WatchedFileHandler(logging.FileHandler):
""" """
def __init__(self, filename, mode='a', encoding=None, delay=False): def __init__(self, filename, mode='a', encoding=None, delay=False):
logging.FileHandler.__init__(self, filename, mode, encoding, delay) logging.FileHandler.__init__(self, filename, mode, encoding, delay)
if not os.path.exists(self.baseFilename): self.dev, self.ino = -1, -1
self.dev, self.ino = -1, -1 self._statstream()
else:
stat = os.stat(self.baseFilename) def _statstream(self):
self.dev, self.ino = stat[ST_DEV], stat[ST_INO] if self.stream:
sres = os.fstat(self.stream.fileno())
self.dev, self.ino = sres[ST_DEV], sres[ST_INO]
def emit(self, record): def emit(self, record):
""" """
@ -433,21 +435,30 @@ class WatchedFileHandler(logging.FileHandler):
has, close the old stream and reopen the file to get the has, close the old stream and reopen the file to get the
current stream. current stream.
""" """
if not os.path.exists(self.baseFilename): # Reduce the chance of race conditions by stat'ing by path only
stat = None # once and then fstat'ing our new fd if we opened a new log stream.
changed = True # See issue #14632: Thanks to John Mulligan for the problem report
else: # and patch.
stat = os.stat(self.baseFilename) try:
changed = (stat[ST_DEV] != self.dev) or (stat[ST_INO] != self.ino) # stat the file by path, checking for existence
if changed and self.stream is not None: sres = os.stat(self.baseFilename)
self.stream.flush() except OSError as err:
self.stream.close() if err.errno == errno.ENOENT:
self.stream = self._open() sres = None
if stat is None: else:
stat = os.stat(self.baseFilename) raise
self.dev, self.ino = stat[ST_DEV], stat[ST_INO] # compare file system stat with that of our stream file handle
if not sres or sres[ST_DEV] != self.dev or sres[ST_INO] != self.ino:
if self.stream is not None:
# we have an open file handle, clean it up
self.stream.flush()
self.stream.close()
# open a new file handle and get new stat info from that fd
self.stream = self._open()
self._statstream()
logging.FileHandler.emit(self, record) logging.FileHandler.emit(self, record)
class SocketHandler(logging.Handler): class SocketHandler(logging.Handler):
""" """
A handler class which writes logging records, in pickle format, to A handler class which writes logging records, in pickle format, to

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# #
# Copyright 2001-2011 by Vinay Sajip. All Rights Reserved. # Copyright 2001-2012 by Vinay Sajip. All Rights Reserved.
# #
# Permission to use, copy, modify, and distribute this software and its # Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose and without fee is hereby granted, # documentation for any purpose and without fee is hereby granted,
@ -18,7 +18,7 @@
"""Test harness for the logging module. Run all tests. """Test harness for the logging module. Run all tests.
Copyright (C) 2001-2011 Vinay Sajip. All Rights Reserved. Copyright (C) 2001-2012 Vinay Sajip. All Rights Reserved.
""" """
import logging import logging
@ -33,6 +33,7 @@ import gc
import json import json
import os import os
import queue import queue
import random
import re import re
import select import select
import socket import socket
@ -539,8 +540,8 @@ class HandlerTest(BaseTest):
h = logging.handlers.WatchedFileHandler(fn, delay=True) h = logging.handlers.WatchedFileHandler(fn, delay=True)
if existing: if existing:
dev, ino = h.dev, h.ino dev, ino = h.dev, h.ino
self.assertNotEqual(dev, -1) self.assertEqual(dev, -1)
self.assertNotEqual(ino, -1) self.assertEqual(ino, -1)
r = logging.makeLogRecord({'msg': 'Test'}) r = logging.makeLogRecord({'msg': 'Test'})
h.handle(r) h.handle(r)
# Now remove the file. # Now remove the file.
@ -581,6 +582,41 @@ class HandlerTest(BaseTest):
self.assertFalse(h.shouldFlush(r)) self.assertFalse(h.shouldFlush(r))
h.close() h.close()
@unittest.skipUnless(threading, 'Threading required for this test.')
def test_race(self):
# Issue #14632 refers.
def remove_loop(fname, tries):
for _ in range(tries):
try:
os.unlink(fname)
except OSError:
pass
time.sleep(0.004 * random.randint(0, 4))
def cleanup(remover, fn, handler):
handler.close()
remover.join()
if os.path.exists(fn):
os.unlink(fn)
fd, fn = tempfile.mkstemp('.log', 'test_logging-3-')
os.close(fd)
del_count = 1000
log_count = 1000
remover = threading.Thread(target=remove_loop, args=(fn, del_count))
remover.daemon = True
remover.start()
for delay in (False, True):
h = logging.handlers.WatchedFileHandler(fn, delay=delay)
self.addCleanup(cleanup, remover, fn, h)
f = logging.Formatter('%(asctime)s: %(levelname)s: %(message)s')
h.setFormatter(f)
for _ in range(log_count):
time.sleep(0.005)
r = logging.makeLogRecord({'msg': 'testing' })
h.handle(r)
class BadStream(object): class BadStream(object):
def write(self, data): def write(self, data):
raise RuntimeError('deliberate mistake') raise RuntimeError('deliberate mistake')
@ -3724,7 +3760,6 @@ for when, exp in (('S', 1),
# Failures occur on some systems for MIDNIGHT and W0. # Failures occur on some systems for MIDNIGHT and W0.
# Print detailed calculation for MIDNIGHT so we can try to see # Print detailed calculation for MIDNIGHT so we can try to see
# what's going on # what's going on
import time
if when == 'MIDNIGHT': if when == 'MIDNIGHT':
try: try:
if rh.utc: if rh.utc: