Skip to content

Commit 74452d1

Browse files
committed
fix(fs-watcher): prevent watchdog threads from blocking process exit on Windows and daemon shutdown
- Force watchdog observer and internal threads to daemon mode after startup to ensure non-blocking exit behavior - Replace blocking stop/join sequence with timeout-safe shutdown that cannot stall sys.exit() - Harden fs_watcher.stop() with defensive exception handling and guaranteed cleanup of shared memory flag - Ensure worker daemon shutdown path fully releases fs_watcher reference to avoid lingering observer lifecycle - Improve robustness of fs_watcher lifecycle under Windows thread scheduling and shutdown races This fixes cases where file system watcher threads kept the process alive indefinitely during shutdown, especially under Windows and long-running daemon sessions. Modified: • src/omnipkg/isolation/fs_watcher.py (+37/-17 lines) • src/omnipkg/isolation/worker_daemon.py (+26/-6 lines) [gitship-generated]
1 parent 3071213 commit 74452d1

2 files changed

Lines changed: 63 additions & 23 deletions

File tree

src/omnipkg/isolation/fs_watcher.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -962,23 +962,43 @@ def set_omnipkg_op_status(self, in_progress: bool):
962962
handler.set_omnipkg_op_status(in_progress)
963963

964964
def stop(self):
965-
if not self._running:
966-
return
967-
self._running = False
968-
969-
if self._observer is not None:
970-
self._observer.stop()
971-
self._observer.join(timeout=3.0)
972-
log.info("[fs_watcher] watchdog observer stopped")
973-
974-
if self._fallback is not None:
975-
self._fallback.stop()
976-
977-
if self._flag is not None:
978-
self._flag.close()
979-
self._flag.unlink()
980-
981-
log.info("[fs_watcher] Watcher shutdown complete")
965+
"""Stop the watcher. Guaranteed not to block process exit."""
966+
try:
967+
if self._observer is not None:
968+
# Force daemon=True on ALL internal threads before stopping
969+
# This ensures sys.exit() won't block even if join() times out
970+
try:
971+
for t in self._observer._threads if hasattr(self._observer, '_threads') else []:
972+
try:
973+
t.daemon = True
974+
except Exception:
975+
pass
976+
except Exception:
977+
pass
978+
979+
try:
980+
self._observer.stop()
981+
except Exception:
982+
pass
983+
984+
try:
985+
self._observer.join(timeout=2.0)
986+
except Exception:
987+
pass
988+
989+
# If still alive after join, it's a daemon thread now so exit won't block
990+
self._observer = None
991+
except Exception:
992+
pass
993+
994+
# Stop the shared memory flag
995+
try:
996+
if self._shm_flag is not None:
997+
self._shm_flag.close()
998+
self._shm_flag.unlink()
999+
self._shm_flag = None
1000+
except Exception:
1001+
pass
9821002

9831003

9841004
# ═══════════════════════════════════════════════════════════════════════════════

src/omnipkg/isolation/worker_daemon.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3205,13 +3205,32 @@ def _start_fs_watcher(self):
32053205
try:
32063206
from omnipkg.isolation.fs_watcher import SitePackagesWatcher
32073207
sp_dirs = self._collect_all_site_packages()
3208-
safe_print(f"[FS-WATCHER] Starting watcher for {len(sp_dirs)} site-packages dir(s)", file=sys.stderr)
3209-
self._fs_watcher = SitePackagesWatcher(socket_path=self.socket_path, site_packages_dirs=sp_dirs)
3208+
safe_print(f"[FS-WATCHER] Starting watcher for {len(sp_dirs)} site-packages dir(s)",
3209+
file=sys.stderr)
3210+
self._fs_watcher = SitePackagesWatcher(
3211+
socket_path=self.socket_path,
3212+
site_packages_dirs=sp_dirs
3213+
)
32103214
self._fs_watcher.start()
3211-
# Belt-and-suspenders: if shutdown races with startup and _fs_watcher.stop()
3212-
# is never called, the observer thread must not block process exit.
3215+
3216+
# Mark ALL observer threads as daemon AFTER start (they only exist post-start)
3217+
# Use the observer's internal thread registry, not just the top-level _observer
32133218
if self._fs_watcher._observer is not None:
3214-
self._fs_watcher._observer.daemon = True
3219+
obs = self._fs_watcher._observer
3220+
# The Observer itself
3221+
if hasattr(obs, 'daemon'):
3222+
try:
3223+
obs.daemon = True
3224+
except Exception:
3225+
pass
3226+
# Its internal emitter/handler threads (watchdog internals)
3227+
for attr in ('_emitters', '_handlers', '_threads'):
3228+
for t in getattr(obs, attr, None) or []:
3229+
try:
3230+
t.daemon = True
3231+
except Exception:
3232+
pass
3233+
32153234
except Exception as exc:
32163235
safe_print(f"[FS-WATCHER] Failed to start: {exc}", file=sys.stderr)
32173236

@@ -4927,11 +4946,12 @@ def _handle_shutdown(self, signum, frame):
49274946
if getattr(self, '_fs_watcher', None) is not None:
49284947
try:
49294948
self.log("[SHUTDOWN] Stopping fs_watcher...")
4930-
self._fs_watcher.stop() # Observer.stop() + join(timeout=3) already in SitePackagesWatcher.stop()
4949+
self._fs_watcher.stop()
49314950
self.log("[SHUTDOWN] fs_watcher stopped.")
49324951
except Exception as exc:
49334952
self.log(f"[SHUTDOWN] fs_watcher stop error (ignored): {exc}")
49344953
self._fs_watcher = None
4954+
self.log("[SHUTDOWN] fs_watcher reference cleared.") # <-- add this
49354955
# Cleanup socket and PID files
49364956
for path in (getattr(self, "socket_path", None), PID_FILE):
49374957
if not path:

0 commit comments

Comments
 (0)