Commit f1c71a92 authored by Vinay Sajip's avatar Vinay Sajip

Issue #14632: Updated WatchedFileHandler to deal with race condition. Thanks...

Issue #14632: Updated WatchedFileHandler to deal with race condition. Thanks to John Mulligan for the problem report and patch.
parent 7e0d0e02
...@@ -23,7 +23,7 @@ Copyright (C) 2001-2012 Vinay Sajip. All Rights Reserved. ...@@ -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, cPickle, struct, time, re import errno, logging, socket, os, cPickle, struct, time, re
from stat import ST_DEV, ST_INO, ST_MTIME from stat import ST_DEV, ST_INO, ST_MTIME
try: try:
...@@ -392,11 +392,13 @@ class WatchedFileHandler(logging.FileHandler): ...@@ -392,11 +392,13 @@ class WatchedFileHandler(logging.FileHandler):
""" """
def __init__(self, filename, mode='a', encoding=None, delay=0): def __init__(self, filename, mode='a', encoding=None, delay=0):
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
else: self._statstream()
stat = os.stat(self.baseFilename)
self.dev, self.ino = stat[ST_DEV], stat[ST_INO] 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): def emit(self, record):
""" """
...@@ -406,19 +408,27 @@ class WatchedFileHandler(logging.FileHandler): ...@@ -406,19 +408,27 @@ 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 = 1 # 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: else:
stat = os.stat(self.baseFilename) raise
changed = (stat[ST_DEV] != self.dev) or (stat[ST_INO] != self.ino) # compare file system stat with that of our stream file handle
if changed and self.stream is not None: 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.flush()
self.stream.close() self.stream.close()
# open a new file handle and get new stat info from that fd
self.stream = self._open() self.stream = self._open()
if stat is None: self._statstream()
stat = os.stat(self.baseFilename)
self.dev, self.ino = stat[ST_DEV], stat[ST_INO]
logging.FileHandler.emit(self, record) logging.FileHandler.emit(self, record)
class SocketHandler(logging.Handler): class SocketHandler(logging.Handler):
......
...@@ -31,6 +31,7 @@ import cStringIO ...@@ -31,6 +31,7 @@ import cStringIO
import gc import gc
import json import json
import os import os
import random
import re import re
import select import select
import socket import socket
...@@ -40,6 +41,7 @@ import sys ...@@ -40,6 +41,7 @@ import sys
import tempfile import tempfile
from test.test_support import captured_stdout, run_with_locale, run_unittest from test.test_support import captured_stdout, run_with_locale, run_unittest
import textwrap import textwrap
import time
import unittest import unittest
import warnings import warnings
import weakref import weakref
...@@ -1873,6 +1875,43 @@ class ChildLoggerTest(BaseTest): ...@@ -1873,6 +1875,43 @@ class ChildLoggerTest(BaseTest):
self.assertTrue(c2 is c3) 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 # 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 # why the test does this, but in any case we save the current locale
# first and restore it at the end. # first and restore it at the end.
...@@ -1882,7 +1921,7 @@ def test_main(): ...@@ -1882,7 +1921,7 @@ def test_main():
CustomLevelsAndFiltersTest, MemoryHandlerTest, CustomLevelsAndFiltersTest, MemoryHandlerTest,
ConfigFileTest, SocketHandlerTest, MemoryTest, ConfigFileTest, SocketHandlerTest, MemoryTest,
EncodingTest, WarningsTest, ConfigDictTest, ManagerTest, EncodingTest, WarningsTest, ConfigDictTest, ManagerTest,
ChildLoggerTest) ChildLoggerTest, HandlerTest)
if __name__ == "__main__": if __name__ == "__main__":
test_main() test_main()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment