Repository: gorakhargosh/watchdog
Pull request #895 (2022-05-30 23:29:57)

avoid bad file descriptor shutdown
https://github.com/gorakhargosh/watchdog/issues/805

altendky: To be blunt, I'm not the biggest fan of this reactive solution.  I could also use a bit better understanding of the mechanics around this, but maybe those with authority to review here can bring that.  For example, I think that this `os.read()` is blocking and waits so what happens if the file descriptor is closed while waiting instead of before initiating the read?  Also, the `os.read()` may return fewer bytes than expected so there should probably be a loop until we either get as many as we wanted or otherwise give up.  In digging further into related details a better solution may surface.
BoboTiG: Thanks @altendky !

The solution works for me. We could move forward with this, and see later if a deeper chante is required.

Would you mind rebasing from `master`, and add a line + your GitHub nickname in the changelog 🙏 ?
BoboTiG: Thanks @altendky !

The solution works for me. We could move forward with this, and see later if a deeper chante is required.

Would you mind rebasing from `master`, and add a line + your GitHub nickname in the changelog 🙏 ?

Patch:
diff --git a/changelog.rst b/changelog.rst
index 7549fec..977cad8 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -11,7 +11,8 @@ Unreleased
 - [fsevents] Fix flakey test to assert that there are no errors when stopping the emitter.
 - [watchmedo] Make ``auto-restart`` restart the sub-process if it terminates. (`#896 <https://github.com/gorakhargosh/watchdog/pull/896>`__)
 - [watchmedo] Avoid zombie sub-processes when running ``shell-command`` without ``--wait``. (`#405 <https://github.com/gorakhargosh/watchdog/issues/405>`__)
-- Thanks to our beloved contributors: @samschott, @taleinat
+- [inotify] Suppress occasional ``OSError: [Errno 9] Bad file descriptor`` at shutdown (`#805 <https://github.com/gorakhargosh/watchdog/issues/805>`__)
+- Thanks to our beloved contributors: @samschott, @taleinat, @altendky
 
 2.1.8
 ~~~~~
diff --git a/src/watchdog/observers/inotify_c.py b/src/watchdog/observers/inotify_c.py
index 6161f4f..d942d2a 100644
--- a/src/watchdog/observers/inotify_c.py
+++ b/src/watchdog/observers/inotify_c.py
@@ -286,6 +286,8 @@ def _recursive_simulate(src_path):
             except OSError as e:
                 if e.errno == errno.EINTR:
                     continue
+                elif e.errno == errno.EBADF:
+                    return []
                 else:
                     raise
             break


Test patch:
diff --git a/tests/test_inotify_buffer.py b/tests/test_inotify_buffer.py
index da4a210..40e85cb 100644
--- a/tests/test_inotify_buffer.py
+++ b/tests/test_inotify_buffer.py
@@ -22,6 +22,7 @@
 
 import os
 import random
+import time
 
 from watchdog.observers.inotify_buffer import InotifyBuffer
 
@@ -132,8 +133,28 @@ def test_unmount_watched_directory_filesystem(p):
     assert not inotify.is_alive()
 
 
-def test_close_should_terminate_thread(p):
-    inotify = InotifyBuffer(p('').encode(), recursive=True)
+def delay_call(function, seconds):
+    def delayed(*args, **kwargs):
+        time.sleep(seconds)
+
+        return function(*args, **kwargs)
+
+    return delayed
+
+
+class InotifyBufferDelayedRead(InotifyBuffer):
+    def run(self, *args, **kwargs):
+        # Introduce a delay to trigger the race condition where the file descriptor is
+        # closed prior to a read being triggered.
+        self._inotify.read_events = delay_call(function=self._inotify.read_events, seconds=1)
+
+        return super().run(*args, **kwargs)
+
+
+@pytest.mark.parametrize(argnames="cls", argvalues=[InotifyBuffer, InotifyBufferDelayedRead])
+def test_close_should_terminate_thread(p, cls):
+    inotify = cls(p('').encode(), recursive=True)
+
     assert inotify.is_alive()
     inotify.close()
     assert not inotify.is_alive()