Repository: gorakhargosh/watchdog

Issue:
Linux Bad File Descriptor when watched directory is removed
Hello

I tested watchdog on Linux (Ubuntu) and Windows (Win10).

### **Versions**
**Linux::**
```
Python 3.6.9
watchdog-1.0.2
```
**Windows**
```
Python 3.7.9
watchdog-1.0.2
```

### **Test scenario is as below:**
1. Create directory X
2. Watch directory X and handle events using FileSystemEventHandler
3. Remove directory X
4. Unschedule observer (using observer.unschedule_all or observer.unschedule)
5. Stop observer and join

### **Issue:**
**Linux:** 
1. When observer.unschedule_all() is called, OSError(9, "Bad File Descriptor") is raised.
2.  When observer.unschedule(path) is called, KeyError is raised

**Windows**: Everything works fine.

### **Example source code**

```
import os
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

# Linux path
WATCHED_DIR = "/home/eplaszcz/PycharmProjects/project_dir"

# Windows path
# WATCHED_DIR = "C:\\Users\\eplaszcz\\Documents\\watchdog_test\\project_dir"


def run_watchdog():
    observer = Observer()
    watched_dir = WATCHED_DIR
    event_handler = FileSystemEventHandler()

    if not os.path.isdir(watched_dir):
        os.mkdir(WATCHED_DIR)

    observer.start()

    print("Schedule target watcher for %s" % watched_dir)
    observer.schedule(event_handler, path=watched_dir, recursive=True)

    while os.path.isdir(watched_dir):
        time.sleep(2)
        os.rmdir(WATCHED_DIR)

    print("Active watchers: {}".format(observer._watches))

    print("Directory removed, unschedule")
    observer.unschedule_all()

    print("Observer unscheduled")
    print("Active watchers: {}".format(observer._watches))

    print("Directory removed, stop")
    observer.stop()

    print("Directory removed, join")
    observer.join()


if __name__ == '__main__':
    run_watchdog()
```

### **Linux output when using observer.unschedule_all**
```
python3 main.py 
Schedule target watcher for C:\Users\eplaszcz\Documents\watchdog_test\project_dir
Active watchers: {<ObservedWatch: path=C:\Users\eplaszcz\Documents\watchdog_test\project_dir, is_recursive=True>}
Directory removed, unschedule
Traceback (most recent call last):
  File "main.py", line 46, in <module>
    run_watchdog()
  File "main.py", line 33, in run_watchdog
    observer.unschedule_all()
  File "/home/eplaszcz/PycharmProjects/watchdogFileDescriptor/venv/lib/python3.6/site-packages/watchdog/observers/api.py", line 357, in unschedule_all
    self._clear_emitters()
  File "/home/eplaszcz/PycharmProjects/watchdogFileDescriptor/venv/lib/python3.6/site-packages/watchdog/observers/api.py", line 231, in _clear_emitters
    emitter.stop()
  File "/home/eplaszcz/PycharmProjects/watchdogFileDescriptor/venv/lib/python3.6/site-packages/watchdog/utils/__init__.py", line 81, in stop
    self.on_thread_stop()
  File "/home/eplaszcz/PycharmProjects/watchdogFileDescriptor/venv/lib/python3.6/site-packages/watchdog/observers/inotify.py", line 121, in on_thread_stop
    self._inotify.close()
  File "/home/eplaszcz/PycharmProjects/watchdogFileDescriptor/venv/lib/python3.6/site-packages/watchdog/observers/inotify_buffer.py", line 50, in close
    self.stop()
  File "/home/eplaszcz/PycharmProjects/watchdogFileDescriptor/venv/lib/python3.6/site-packages/watchdog/utils/__init__.py", line 81, in stop
    self.on_thread_stop()
  File "/home/eplaszcz/PycharmProjects/watchdogFileDescriptor/venv/lib/python3.6/site-packages/watchdog/observers/inotify_buffer.py", line 46, in on_thread_stop
    self._inotify.close()
  File "/home/eplaszcz/PycharmProjects/watchdogFileDescriptor/venv/lib/python3.6/site-packages/watchdog/observers/inotify_c.py", line 277, in close
    os.close(self._inotify_fd)
OSError: [Errno 9] Bad file descriptor
```

### **Linux output when using observer.unschedule(watched_dir)**
```
python3 main.py 
Schedule target watcher for /home/eplaszcz/PycharmProjects/project_dir
Active watchers: {<ObservedWatch: path=/home/eplaszcz/PycharmProjects/project_dir, is_recursive=True>}
Directory removed, unschedule
Traceback (most recent call last):
  File "main.py", line 46, in <module>
    run_watchdog()
  File "main.py", line 33, in run_watchdog
    observer.unschedule(WATCHED_DIR)
  File "/home/eplaszcz/PycharmProjects/watchdogFileDescriptor/venv/lib/python3.6/site-packages/watchdog/observers/api.py", line 347, in unschedule
    emitter = self._emitter_for_watch[watch]
KeyError: '/home/eplaszcz/PycharmProjects/project_dir'
```

### ** Windows output **
```
py main.py
Schedule target watcher for C:\Users\eplaszcz\Documents\watchdog_test\project_dir
Active watchers: {<ObservedWatch: path=C:\Users\eplaszcz\Documents\watchdog_test\project_dir, is_recursive=True>}
Directory removed, unschedule
Observer unscheduled
Active watchers: set()
Directory removed, stop
Directory removed, join
```

Discussion:

Patch:
diff --git a/changelog.rst b/changelog.rst
index 61e2b48..b47c5d8 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -10,6 +10,7 @@ Changelog
 
 - Avoid deprecated ``PyEval_InitThreads`` on Python 3.7+ (`#746 <https://github.com/gorakhargosh/watchdog/pull/746>`_)
 - [inotify] Add support for ``IN_CLOSE_WRITE`` events. A ``FileCloseEvent`` event will be fired. Note that ``IN_CLOSE_NOWRITE`` events are not handled to prevent much noise. (`#184 <https://github.com/gorakhargosh/watchdog/pull/184>`_, `#245 <https://github.com/gorakhargosh/watchdog/pull/245>`_, `#280 <https://github.com/gorakhargosh/watchdog/pull/280>`_, `#313 <https://github.com/gorakhargosh/watchdog/pull/313>`_, `#690 <https://github.com/gorakhargosh/watchdog/pull/690>`_)
+- [inotify] Allow to stop the emitter multiple times (`#760 <https://github.com/gorakhargosh/watchdog/pull/760>`_)
 - [mac] Support coalesced filesystem events (`#734 <https://github.com/gorakhargosh/watchdog/pull/734>`_)
 - [mac] Drop support for OSX 10.12 and earlier (`#750 <https://github.com/gorakhargosh/watchdog/pull/750>`_)
 - [mac] Fix an issue when renaming an item changes only the casing (`#750 <https://github.com/gorakhargosh/watchdog/pull/750>`_)
diff --git a/src/watchdog/observers/inotify.py b/src/watchdog/observers/inotify.py
index 7c5d9c6..ff01938 100644
--- a/src/watchdog/observers/inotify.py
+++ b/src/watchdog/observers/inotify.py
@@ -120,6 +120,7 @@ def on_thread_start(self):
     def on_thread_stop(self):
         if self._inotify:
             self._inotify.close()
+            self._inotify = None
 
     def queue_events(self, timeout, full_events=False):
         # If "full_events" is true, then the method will report unmatched move events as separate events
@@ -179,7 +180,8 @@ def queue_events(self, timeout, full_events=False):
             #     cls = FileClosedEvent
             #     self.queue_event(cls(src_path))
             elif event.is_delete_self and src_path == self.watch.path:
-                self.queue_event(DirDeletedEvent(src_path))
+                cls = DirDeletedEvent if event.is_directory else FileDeletedEvent
+                self.queue_event(cls(src_path))
                 self.stop()
 
     def _decode_path(self, path):
diff --git a/src/watchdog/observers/inotify_c.py b/src/watchdog/observers/inotify_c.py
index 9beff2b..c07960c 100644
--- a/src/watchdog/observers/inotify_c.py
+++ b/src/watchdog/observers/inotify_c.py
@@ -275,7 +275,12 @@ def close(self):
             if self._path in self._wd_for_path:
                 wd = self._wd_for_path[self._path]
                 inotify_rm_watch(self._inotify_fd, wd)
-            os.close(self._inotify_fd)
+
+            try:
+                os.close(self._inotify_fd)
+            except OSError:
+                # descriptor may be invalid because file was deleted
+                pass
 
     def read_events(self, event_buffer_size=DEFAULT_EVENT_BUFFER_SIZE):
         """


Test patch:
diff --git a/tests/test_emitter.py b/tests/test_emitter.py
index a1140b9..62b9500 100644
--- a/tests/test_emitter.py
+++ b/tests/test_emitter.py
@@ -70,11 +70,7 @@ def setup_teardown(tmpdir):
 
     yield
 
-    try:
-        emitter.stop()
-    except OSError:
-        # watch was already stopped, e.g., in `test_delete_self`
-        pass
+    emitter.stop()
     emitter.join(5)
     assert not emitter.is_alive()
 
diff --git a/tests/test_inotify_c.py b/tests/test_inotify_c.py
index a18f140..81f6586 100644
--- a/tests/test_inotify_c.py
+++ b/tests/test_inotify_c.py
@@ -40,11 +40,7 @@ def watching(path=None, use_full_emitter=False):
     emitter = Emitter(event_queue, ObservedWatch(path, recursive=True))
     emitter.start()
     yield
-    try:
-        emitter.stop()
-    except OSError:
-        # watch was already stopped, e.g., because root was deleted
-        pass
+    emitter.stop()
     emitter.join(5)