From 775004f934bfe620b68f0e79151ae14ba5c9e9b5 Mon Sep 17 00:00:00 2001 From: Go1c <1c@live.cn> Date: Tue, 14 Apr 2026 17:02:24 +0800 Subject: [PATCH] fix: delay watcher unignore to prevent echo push-back loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the CLI receives a sync event from the server and writes/deletes a file locally, the watchdog inotify event is queued by the kernel and delivered asynchronously. By the time the observer thread processes it, unignore_file had already been called, causing the watcher to treat the server-written file as a local change and push it back — creating an echo loop confirmed in logs (NoteSyncModify received → NoteModify sent back → server pushes again). Fix: add asyncio.sleep(0.6) before unignore_file in all server→client file operation handlers (write, delete, rename, chunked download) in both NoteSync and FileSync. This keeps the file ignored until the watchdog debounce window (0.5s) has expired. --- fns_cli/file_sync.py | 7 ++++++- fns_cli/note_sync.py | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/fns_cli/file_sync.py b/fns_cli/file_sync.py index a7cee47..93e5500 100644 --- a/fns_cli/file_sync.py +++ b/fns_cli/file_sync.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import logging import os import uuid @@ -203,6 +204,7 @@ async def _on_sync_update(self, msg: WSMessage) -> None: except Exception: log.exception("Failed to write file %s", rel_path) finally: + await asyncio.sleep(0.6) self.engine.unignore_file(rel_path) self._received_modify += 1 @@ -232,6 +234,7 @@ async def _on_sync_delete(self, msg: WSMessage) -> None: except Exception: log.exception("Failed to delete file %s", rel_path) finally: + await asyncio.sleep(0.6) self.engine.unignore_file(rel_path) self._received_delete += 1 @@ -256,6 +259,7 @@ async def _on_sync_rename(self, msg: WSMessage) -> None: except Exception: log.exception("Failed to rename file %s → %s", old_path, new_path) finally: + await asyncio.sleep(0.6) self.engine.unignore_file(old_path) self.engine.unignore_file(new_path) @@ -315,8 +319,9 @@ async def _finalize_download(self, session_id: str, session: _DownloadSession) - except Exception: log.exception("Failed to write downloaded file %s", rel_path) finally: - self.engine.unignore_file(rel_path) self._download_sessions.pop(session_id, None) + await asyncio.sleep(0.6) + self.engine.unignore_file(rel_path) async def _on_sync_end(self, msg: WSMessage) -> None: data = _extract_inner(msg.data) diff --git a/fns_cli/note_sync.py b/fns_cli/note_sync.py index 6a63f8a..83cc2d5 100644 --- a/fns_cli/note_sync.py +++ b/fns_cli/note_sync.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import logging import os import uuid @@ -147,6 +148,7 @@ async def _on_sync_modify(self, msg: WSMessage) -> None: except Exception: log.exception("Failed to write %s", rel_path) finally: + await asyncio.sleep(0.6) self.engine.unignore_file(rel_path) self._received_modify += 1 @@ -167,6 +169,7 @@ async def _on_sync_delete(self, msg: WSMessage) -> None: except Exception: log.exception("Failed to delete %s", rel_path) finally: + await asyncio.sleep(0.6) self.engine.unignore_file(rel_path) self._received_delete += 1