[diffoscope] 01/02: diffoscope.diff: Improve FIFO writing robustness. (Closes: #852013)
Chris Lamb
chris at chris-lamb.co.uk
Sat Jan 28 20:52:31 CET 2017
This is an automated email from the git hooks/post-receive script.
lamby pushed a commit to branch master
in repository diffoscope.
commit fa4cab2ab453d5d9b076ec5f3b362c534f065f6d
Author: Brett Smith <brettcsmith at brettcsmith.org>
Date: Fri Jan 27 22:42:23 2017 -0500
diffoscope.diff: Improve FIFO writing robustness. (Closes: #852013)
We used to give input to diff using FIFO objects.
fec9e97c51b3a8ff226a4b3b2b0563a4a680ac68 changed this to avoid relying on
/dev/fd, which is a good change for portability. Unfortunately, the
implementation has a few issues of its own:
* It feeds diff with normal files, rather than FIFOs. This could cause
trouble if diff tries to read farther than the underlying feeder
processes have written.
* It uses threads to write to the pseudo-FIFO, but the file object is
manipulated in multiple threads. I suspect this is the primary cause of
the segfaults observed in #852013.
* fd_from_feeder is decorated as a context manager, but it never yields
anything, which is not how it's expected to be used. I'm not sure this is
causing any problems, but it makes it harder to reason about what's going
on.
This commit introduces a new FIFOFeeder class. It is wholly responsible for
creating and feeding the FIFO, so we don't have to pass file objects across
the thread boundary and risk segfaults. It uses context management to tell
when the FIFO is no longer needed, so it can clean up nicely.
Signed-off-by: Chris Lamb <lamby at debian.org>
---
diffoscope/diff.py | 103 +++++++++++++++++++++++++++--------------------------
1 file changed, 52 insertions(+), 51 deletions(-)
diff --git a/diffoscope/diff.py b/diffoscope/diff.py
index 011916a..0335a26 100644
--- a/diffoscope/diff.py
+++ b/diffoscope/diff.py
@@ -19,6 +19,9 @@
import re
import io
+import os
+import errno
+import fcntl
import hashlib
import logging
import threading
@@ -190,54 +193,55 @@ def run_diff(fifo1, fifo2, end_nl_q1, end_nl_q2):
return parser.diff
-def feed(feeder, f, end_nl_q):
- # work-around unified diff limitation: if there's no newlines in both
- # don't make it a difference
- try:
- end_nl = feeder(f)
- end_nl_q.put(end_nl)
- finally:
- f.close()
-
-class ExThread(threading.Thread):
- """
- Inspired by https://stackoverflow.com/a/6874161
- """
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.__status_queue = Queue()
-
- def run(self, *args, **kwargs):
- try:
- super().run(*args, **kwargs)
- except Exception as ex:
- #except_type, except_class, tb = sys.exc_info()
- self.__status_queue.put(ex)
-
- self.__status_queue.put(None)
+class FIFOFeeder(threading.Thread):
+ def __init__(self, feeder, fifo_path, end_nl_q=None, *, daemon=True):
+ os.mkfifo(fifo_path)
+ super().__init__(daemon=daemon)
+ self.feeder = feeder
+ self.fifo_path = fifo_path
+ self.end_nl_q = Queue() if end_nl_q is None else end_nl_q
+ self._exception = None
+ self._want_join = threading.Event()
- def wait_for_exc_info(self):
- return self.__status_queue.get()
+ def __enter__(self):
+ self.start()
+ return self
- def join(self):
- ex = self.wait_for_exc_info()
- if ex is None:
- return
- raise ex
+ def __exit__(self, exc_type, exc_value, exc_tb):
+ self.join()
- at contextlib.contextmanager
-def fd_from_feeder(feeder, end_nl_q, fifo):
- f = open(fifo, 'wb')
- t = ExThread(target=feed, args=(feeder, f, end_nl_q))
+ def run(self):
+ try:
+ # Try to open the FIFO nonblocking, so we can periodically check
+ # if the main thread wants us to wind down. If it does, there's no
+ # more need for the FIFO, so stop the thread.
+ while True:
+ try:
+ fifo_fd = os.open(self.fifo_path, os.O_WRONLY | os.O_NONBLOCK)
+ except OSError as error:
+ if error.errno != errno.ENXIO:
+ raise
+ elif self._want_join.is_set():
+ return
+ else:
+ break
+
+ # Now clear the fd's nonblocking flag to let writes block normally.
+ fcntl.fcntl(fifo_fd, fcntl.F_SETFL, 0)
+ with open(fifo_fd, 'wb') as fifo:
+ # The queue works around a unified diff limitation: if there's
+ # no newlines in both don't make it a difference
+ end_nl = self.feeder(fifo)
+ self.end_nl_q.put(end_nl)
+ except Exception as error:
+ self._exception = error
- t.daemon = True
- t.start()
+ def join(self):
+ self._want_join.set()
+ super().join()
+ if self._exception is not None:
+ raise self._exception
- try:
- t.join()
- finally:
- f.close()
def empty_file_feeder():
def feeder(f):
@@ -273,15 +277,12 @@ def make_feeder_from_raw_reader(in_file, filter=lambda buf: buf):
return feeder
def diff(feeder1, feeder2):
- end_nl_q1 = Queue()
- end_nl_q2 = Queue()
-
with tempfile.TemporaryDirectory() as tmpdir:
- fifo1 = '{}/f1'.format(tmpdir)
- fifo2 = '{}/f2'.format(tmpdir)
- fd_from_feeder(feeder1, end_nl_q1, fifo1)
- fd_from_feeder(feeder2, end_nl_q2, fifo2)
- return run_diff(fifo1, fifo2, end_nl_q1, end_nl_q2)
+ fifo1_path = os.path.join(tmpdir, 'fifo1')
+ fifo2_path = os.path.join(tmpdir, 'fifo2')
+ with FIFOFeeder(feeder1, fifo1_path) as fifo1, \
+ FIFOFeeder(feeder2, fifo2_path) as fifo2:
+ return run_diff(fifo1_path, fifo2_path, fifo1.end_nl_q, fifo2.end_nl_q)
def reverse_unified_diff(diff):
res = []
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/reproducible/diffoscope.git
More information about the diffoscope
mailing list