test(exit): exempt watchdog 7's process-wide inotify reader (#561)

watchdog 7 on Linux reads every inotify watch through one InotifyFD
daemon thread per process that no observer stop ends, by design. The
exit and package tests counted every watchdog BaseThread as the
package's, so the singleton failed them once the cap was lifted. Exclude
it, and require instead that no non-daemon thread is left at exit.
This commit is contained in:
Vito Sansevero
2026-10-31 10:55:38 -07:00
parent c791e2481a
commit b1e2730ee1
2 changed files with 21 additions and 4 deletions
+5 -2
View File
@@ -587,13 +587,16 @@ PACKAGE_THREAD_NAMES = ("PromptManagerPromptCleanup", "PromptManagerImageWorker"
def package_threads():
"""Live threads this design owns: the tracker, the image worker, and the
watchdog observer and emitters (which have no name of their own)."""
watchdog observer and emitters (which have no name of their own).
watchdog 7's InotifyFD, one daemon reader per process that no observer
stop ends, is not the package's."""
from watchdog.utils import BaseThread
return {
thread
for thread in threading.enumerate()
if thread.name in PACKAGE_THREAD_NAMES or isinstance(thread, BaseThread)
if thread.name in PACKAGE_THREAD_NAMES
or (isinstance(thread, BaseThread) and type(thread).__name__ != "InotifyFD")
}
+16 -2
View File
@@ -72,13 +72,24 @@ CHILD = textwrap.dedent("""
package = state.get("package")
result = getattr(package, "_background_work_result", None)
# The image worker, the tracker, and watchdog's observer and emitters.
# watchdog 7's InotifyFD is one daemon reader per process that no
# observer stop ends, by design; it is not the package's.
alive = sorted(
t.name
for t in threading.enumerate()
if t.name in ("PromptManagerImageWorker", "PromptManagerPromptCleanup")
or isinstance(t, BaseThread)
or (isinstance(t, BaseThread) and type(t).__name__ != "InotifyFD")
)
# Threads that would hold the exit before any atexit callback (#561).
holding = sorted(
t.name
for t in threading.enumerate()
if not t.daemon and t is not threading.main_thread()
)
print(
"PM_EXIT_REPORT "
+ json.dumps({"result": result, "alive": alive, "holding": holding})
)
print("PM_EXIT_REPORT " + json.dumps({"result": result, "alive": alive}))
sys.stdout.flush()
# First registered, so it runs last: after the package's teardown and
@@ -168,6 +179,7 @@ class TestPackageExit(unittest.TestCase):
detail,
)
self.assertEqual(report["alive"], [], detail)
self.assertEqual(report["holding"], [], detail)
# The teardown's own warnings. Import-time warnings (the API's
# output-folder search finds no ComfyUI here) are not about exit.
for text in (
@@ -187,5 +199,7 @@ class TestPackageExit(unittest.TestCase):
{"monitor": False, "thumbnails": True, "tracker": True},
detail,
)
# The stuck reader is still alive, but as a daemon it holds nothing.
self.assertEqual(report["holding"], [], detail)
self.assertIn("watchdog observer still running", completed.stderr, detail)
self.assertIn("Background work not stopped in time", completed.stderr, detail)