fix(watcher): auto-polling sur mounts reseau (NFS/SMB) pour maj temps reel des recents
CI / lint (push) Failing after 25s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Successful in 28s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
CI / lint (push) Failing after 25s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Successful in 28s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
inotify ne voit pas les ecritures faites depuis d'autres clients NFS (Obsidian sur Windows), donc index_updated n'etait jamais emis et la section Recent du sidebar ne se rafraichissait pas. Detection du fstype par vault via /proc/mounts -> PollingObserver (stat-based) sur mounts reseau, Observer natif conserve sur les mounts locaux.
This commit is contained in:
+67
-5
@@ -1,7 +1,8 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Iterable
|
||||
from pathlib import Path
|
||||
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
@@ -12,6 +13,55 @@ from backend.indexer import IGNORED_DIRS, SUPPORTED_EXTENSIONS
|
||||
|
||||
logger = logging.getLogger("obsigate.watcher")
|
||||
|
||||
# Filesystem types where inotify does NOT fire for changes made by other
|
||||
# clients (the edit happens on another machine — e.g. Obsidian on Windows
|
||||
# writing to an NFS/SMB export). For those mounts watchdog's native
|
||||
# Observer silently misses everything and polling is the only option.
|
||||
NETWORK_FSTYPES = {
|
||||
"nfs", "nfs4", "cifs", "smbfs", "smb2", "smb3",
|
||||
"fuse", "fuse.sshfs", "glusterfs", "9p", "virtiofs", "lustre",
|
||||
}
|
||||
|
||||
|
||||
def find_mount_fstype(resolved_path: str, mount_lines: Iterable[str]) -> str:
|
||||
"""Return the fstype of the longest /proc/mounts entry covering the path.
|
||||
|
||||
Pure function (mount_lines are text lines) so it can be unit-tested
|
||||
without /proc. Returns "" when no mount matches.
|
||||
"""
|
||||
best_point = ""
|
||||
best_fstype = ""
|
||||
for line in mount_lines:
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
mount_point, fstype = parts[1], parts[2]
|
||||
# /proc/mounts escapes special chars in octal
|
||||
mount_point = mount_point.replace("\\040", " ").replace("\\011", "\t")
|
||||
root = mount_point.rstrip("/") or "/"
|
||||
if resolved_path == root or resolved_path.startswith(root + "/"):
|
||||
if len(mount_point) > len(best_point):
|
||||
best_point = mount_point
|
||||
best_fstype = fstype
|
||||
return best_fstype
|
||||
|
||||
|
||||
def is_network_mount(path: str) -> bool:
|
||||
"""True if *path* sits on a network/fuse filesystem (Linux only).
|
||||
|
||||
inotify events are local to the client that made the write, so vaults
|
||||
shared over NFS/SMB never see Obsidian's edits. On those mounts we
|
||||
must fall back to PollingObserver, which detects changes by stat().
|
||||
"""
|
||||
if os.name != "posix":
|
||||
return False
|
||||
try:
|
||||
with open("/proc/mounts", "r", encoding="utf-8") as fh:
|
||||
fstype = find_mount_fstype(os.path.realpath(path), fh)
|
||||
except OSError:
|
||||
return False
|
||||
return fstype in NETWORK_FSTYPES
|
||||
|
||||
|
||||
class VaultEventHandler(FileSystemEventHandler):
|
||||
"""Gestionnaire d'événements filesystem pour une vault Obsidian.
|
||||
@@ -114,7 +164,12 @@ class VaultWatcher:
|
||||
vault_path: str,
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
):
|
||||
"""Créer et démarrer un observer pour une vault."""
|
||||
"""Créer et démarrer un observer pour une vault.
|
||||
|
||||
Le mode (natif inotify vs polling) est choisi par vault :
|
||||
les mounts réseau (NFS/SMB/fuse) sont surveillés en polling car
|
||||
inotify ne voit pas les écritures faites depuis d'autres clients.
|
||||
"""
|
||||
path = Path(vault_path)
|
||||
if not path.exists():
|
||||
logger.warning(f"Vault '{vault_name}' path not found: {vault_path}")
|
||||
@@ -122,16 +177,23 @@ class VaultWatcher:
|
||||
|
||||
handler = VaultEventHandler(vault_name, self.event_queue, loop)
|
||||
|
||||
ObserverClass = PollingObserver if self.use_polling else Observer
|
||||
polling = self.use_polling or is_network_mount(vault_path)
|
||||
if polling and not self.use_polling:
|
||||
logger.info(
|
||||
f"Vault '{vault_name}' is on a network mount — "
|
||||
"using polling watcher (inotify cannot see remote edits)"
|
||||
)
|
||||
|
||||
ObserverClass = PollingObserver if polling else Observer
|
||||
try:
|
||||
observer = ObserverClass(
|
||||
timeout=self.polling_interval if self.use_polling else 1
|
||||
timeout=self.polling_interval if polling else 1
|
||||
)
|
||||
observer.schedule(handler, str(path), recursive=True)
|
||||
observer.daemon = True
|
||||
observer.start()
|
||||
self.observers[vault_name] = observer
|
||||
mode = "polling" if self.use_polling else "native"
|
||||
mode = "polling" if polling else "native"
|
||||
logger.info(f"Watching ({mode}): {vault_name} -> {vault_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start watcher for '{vault_name}': {e}")
|
||||
|
||||
Reference in New Issue
Block a user