mirror of https://github.com/python/cpython
Issue #14632: Updated WatchedFileHandler to deal with race condition. Thanks to John Mulligan for the problem report and patch.
This commit is contained in:
parent
231d474a7b
commit
885f6fd472
|
@ -23,7 +23,7 @@ Copyright (C) 2001-2012 Vinay Sajip. All Rights Reserved.
|
|||
To use, simply 'import logging.handlers' and log away!
|
||||
"""
|
||||
|
||||
import logging, socket, os, cPickle, struct, time, re
|
||||
import errno, logging, socket, os, cPickle, struct, time, re
|
||||
from stat import ST_DEV, ST_INO, ST_MTIME
|
||||
|
||||
try:
|
||||
|
@ -392,11 +392,13 @@ class WatchedFileHandler(logging.FileHandler):
|
|||
"""
|
||||
def __init__(self, filename, mode='a', encoding=None, delay=0):
|
||||
logging.FileHandler.__init__(self, filename, mode, encoding, delay)
|
||||
if not os.path.exists(self.baseFilename):
|
||||
self.dev, self.ino = -1, -1
|
||||
else:
|
||||
stat = os.stat(self.baseFilename)
|
||||
self.dev, self.ino = stat[ST_DEV], stat[ST_INO]
|
||||
self._statstream()
|
||||
|
||||
def _statstream(self):
|
||||
if self.stream:
|
||||
sres = os.fstat(self.stream.fileno())
|
||||
self.dev, self.ino = sres[ST_DEV], sres[ST_INO]
|
||||
|
||||
def emit(self, record):
|
||||
"""
|
||||
|
@ -406,19 +408,27 @@ class WatchedFileHandler(logging.FileHandler):
|
|||
has, close the old stream and reopen the file to get the
|
||||
current stream.
|
||||
"""
|
||||
if not os.path.exists(self.baseFilename):
|
||||
stat = None
|
||||
changed = 1
|
||||
# Reduce the chance of race conditions by stat'ing by path only
|
||||
# once and then fstat'ing our new fd if we opened a new log stream.
|
||||
# See issue #14632: Thanks to John Mulligan for the problem report
|
||||
# and patch.
|
||||
try:
|
||||
# stat the file by path, checking for existence
|
||||
sres = os.stat(self.baseFilename)
|
||||
except OSError as err:
|
||||
if err.errno == errno.ENOENT:
|
||||
sres = None
|
||||
else:
|
||||
stat = os.stat(self.baseFilename)
|
||||
changed = (stat[ST_DEV] != self.dev) or (stat[ST_INO] != self.ino)
|
||||
if changed and self.stream is not None:
|
||||
raise
|
||||
# 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()
|
||||
if stat is None:
|
||||
stat = os.stat(self.baseFilename)
|
||||
self.dev, self.ino = stat[ST_DEV], stat[ST_INO]
|
||||
self._statstream()
|
||||
logging.FileHandler.emit(self, record)
|
||||
|
||||
class SocketHandler(logging.Handler):
|
||||
|
|
|
@ -31,6 +31,7 @@ import cStringIO
|
|||
import gc
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import select
|
||||
import socket
|
||||
|
@ -40,6 +41,7 @@ import sys
|
|||
import tempfile
|
||||
from test.test_support import captured_stdout, run_with_locale, run_unittest
|
||||
import textwrap
|
||||
import time
|
||||
import unittest
|
||||
import warnings
|
||||
import weakref
|
||||
|
@ -1873,6 +1875,43 @@ class ChildLoggerTest(BaseTest):
|
|||
self.assertTrue(c2 is c3)
|
||||
|
||||
|
||||
class HandlerTest(BaseTest):
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
# Set the locale to the platform-dependent default. I have no idea
|
||||
# why the test does this, but in any case we save the current locale
|
||||
# first and restore it at the end.
|
||||
|
@ -1882,7 +1921,7 @@ def test_main():
|
|||
CustomLevelsAndFiltersTest, MemoryHandlerTest,
|
||||
ConfigFileTest, SocketHandlerTest, MemoryTest,
|
||||
EncodingTest, WarningsTest, ConfigDictTest, ManagerTest,
|
||||
ChildLoggerTest)
|
||||
ChildLoggerTest, HandlerTest)
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_main()
|
||||
|
|
Loading…
Reference in New Issue