Przeglądaj źródła

Fix "tail -f" hang if log rotation occurs while tailing

Mike Naberezny 10 lat temu
rodzic
commit
7595a405cc
3 zmienionych plików z 54 dodań i 15 usunięć
  1. 3 0
      CHANGES.txt
  2. 26 8
      supervisor/http.py
  3. 25 7
      supervisor/tests/test_http.py

+ 3 - 0
CHANGES.txt

@@ -34,6 +34,9 @@
   it.  See https://github.com/Supervisor/supervisor/pull/129.  Patch by Igor
   Sobreira.
 
+- Fixed a bug where ``supervisorctl tail -f name`` output would stop if log
+  rotation occurred while tailing.
+
 3.1.0 (2014-07-29)
 ------------------
 

+ 26 - 8
supervisor/http.py

@@ -636,22 +636,22 @@ class supervisor_af_unix_http_server(supervisor_http_server):
 
 class tail_f_producer:
     def __init__(self, request, filename, head):
-        self.file = open(filename, 'rb')
         self.request = weakref.ref(request)
+        self.filename = filename
         self.delay = 0.1
-        sz = self.fsize()
+
+        self._open()
+        sz = self._fsize()
         if sz >= head:
             self.sz = sz - head
-        else:
-            self.sz = 0
 
     def __del__(self):
-        if self.file:
-            self.file.close()
+        self._close()
 
     def more(self):
+        self._follow()
         try:
-            newsz = self.fsize()
+            newsz = self._fsize()
         except OSError:
             # file descriptor was closed
             return ''
@@ -666,7 +666,25 @@ class tail_f_producer:
             return bytes
         return NOT_DONE_YET
 
-    def fsize(self):
+    def _open(self):
+        self.file = open(self.filename, 'rb')
+        self.ino = os.fstat(self.file.fileno())[stat.ST_INO]
+        self.sz = 0
+
+    def _close(self):
+        self.file.close()
+
+    def _follow(self):
+        try:
+            ino = os.stat(self.filename)[stat.ST_INO]
+        except OSError:
+            return
+
+        if self.ino != ino: # log rotation occurred
+            self._close()
+            self._open()
+
+    def _fsize(self):
         return os.fstat(self.file.fileno())[stat.ST_SIZE]
 
 class logtail_handler:

+ 25 - 7
supervisor/tests/test_http.py

@@ -123,8 +123,7 @@ class TailFProducerTests(unittest.TestCase):
         f = tempfile.NamedTemporaryFile()
         f.write(as_bytes('a' * 80))
         f.flush()
-        t = f.name
-        producer = self._makeOne(request, t, 80)
+        producer = self._makeOne(request, f.name, 80)
         result = producer.more()
         self.assertEqual(result, as_bytes('a' * 80))
         f.write(as_bytes('w' * 100))
@@ -138,6 +137,25 @@ class TailFProducerTests(unittest.TestCase):
         result = producer.more()
         self.assertEqual(result, '==> File truncated <==\n')
 
+    def test_handle_more_follow(self):
+        request = DummyRequest('/logtail/foo', None, None, None)
+        from supervisor import http
+        f = tempfile.NamedTemporaryFile()
+        f.write(as_bytes('a' * 80))
+        f.flush()
+        producer = self._makeOne(request, f.name, 80)
+        result = producer.more()
+        self.assertEqual(result, as_bytes('a' * 80))
+        f.close()
+        f2 = open(f.name, 'w')
+        try:
+            f2.write(as_bytes('b' * 80))
+            f2.close()
+            result = producer.more()
+        finally:
+            os.unlink(f2.name)
+        self.assertEqual(result, as_bytes('b' * 80))
+
 class DeferringChunkedProducerTests(unittest.TestCase):
     def _getTargetClass(self):
         from supervisor.http import deferring_chunked_producer
@@ -174,7 +192,7 @@ class DeferringChunkedProducerTests(unittest.TestCase):
     def test_more_noproducer(self):
         producer = self._makeOne(None)
         self.assertEqual(producer.more(), '')
-        
+
 class DeferringCompositeProducerTests(unittest.TestCase):
     def _getTargetClass(self):
         from supervisor.http import deferring_composite_producer
@@ -291,7 +309,7 @@ class Test_deferring_http_request(unittest.TestCase):
             def push_with_producer(self, producer):
                 self.producer = producer
         return Channel()
-    
+
     def test_done_http_10_nokeepalive(self):
         channel = self._makeChannel()
         inst = self._makeOne(channel=channel, version='1.0')
@@ -305,10 +323,10 @@ class Test_deferring_http_request(unittest.TestCase):
             version='1.0',
             header=['Connection: Keep-Alive'],
             )
-        
+
         inst.done()
         self.assertTrue(channel.closed)
-        
+
     def test_done_http_10_keepalive_and_content_length(self):
         channel = self._makeChannel()
         inst = self._makeOne(
@@ -380,7 +398,7 @@ class Test_deferring_http_request(unittest.TestCase):
             )
         inst.done()
         self.assertTrue(channel.closed)
-        
+
 class EncryptedDictionaryAuthorizedTests(unittest.TestCase):
     def _getTargetClass(self):
         from supervisor.http import encrypted_dictionary_authorizer