Skip to content

Commit 0e55882

Browse files
committed
Merge branch 'pr-17-file-watcher-fix'
2 parents 7bb0ae5 + 0f3341c commit 0e55882

File tree

1 file changed

+89
-66
lines changed

1 file changed

+89
-66
lines changed

src/code_index_mcp/services/file_watcher_service.py

Lines changed: 89 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
trigger index rebuilds when relevant files are modified, created, or deleted.
66
It uses the watchdog library for cross-platform file system event monitoring.
77
"""
8+
# pylint: disable=missing-function-docstring # Fallback stub methods don't need docstrings
89

910
import logging
11+
import os
12+
import traceback
1013
from threading import Timer
1114
from typing import Optional, Callable
1215
from pathlib import Path
@@ -18,17 +21,27 @@
1821
except ImportError:
1922
# Fallback classes for when watchdog is not available
2023
class Observer:
21-
def __init__(self): pass
22-
def schedule(self, *args, **kwargs): pass
23-
def start(self): pass
24-
def stop(self): pass
25-
def join(self, *args, **kwargs): pass
26-
def is_alive(self): return False
24+
"""Fallback Observer class when watchdog library is not available."""
25+
def __init__(self):
26+
pass
27+
def schedule(self, *args, **kwargs):
28+
pass
29+
def start(self):
30+
pass
31+
def stop(self):
32+
pass
33+
def join(self, *args, **kwargs):
34+
pass
35+
def is_alive(self):
36+
return False
2737

2838
class FileSystemEventHandler:
29-
def __init__(self): pass
39+
"""Fallback FileSystemEventHandler class when watchdog library is not available."""
40+
def __init__(self):
41+
pass
3042

3143
class FileSystemEvent:
44+
"""Fallback FileSystemEvent class when watchdog library is not available."""
3245
def __init__(self):
3346
self.is_directory = False
3447
self.src_path = ""
@@ -111,8 +124,8 @@ def start_monitoring(self, rebuild_callback: Callable) -> bool:
111124

112125
# Log detailed Observer setup
113126
watch_path = str(self.base_path)
114-
self.logger.debug(f"Scheduling Observer for path: {watch_path}")
115-
127+
self.logger.debug("Scheduling Observer for path: %s", watch_path)
128+
116129
self.observer.schedule(
117130
self.event_handler,
118131
watch_path,
@@ -124,11 +137,10 @@ def start_monitoring(self, rebuild_callback: Callable) -> bool:
124137
self.observer.start()
125138
self.is_monitoring = True
126139
self.restart_attempts = 0
127-
140+
128141
# Log Observer thread info
129142
if hasattr(self.observer, '_thread'):
130-
thread_info = f"Observer thread: {self.observer._thread}"
131-
self.logger.debug(thread_info)
143+
self.logger.debug("Observer thread: %s", self.observer._thread)
132144

133145
# Verify observer is actually running
134146
if self.observer.is_alive():
@@ -140,18 +152,17 @@ def start_monitoring(self, rebuild_callback: Callable) -> bool:
140152
"supported_extensions": len(SUPPORTED_EXTENSIONS)
141153
}
142154
)
143-
155+
144156
# Add diagnostic test - create a test event to verify Observer works
145-
import os
146-
self.logger.debug(f"Observer thread is alive: {self.observer.is_alive()}")
147-
self.logger.debug(f"Monitored path exists: {os.path.exists(str(self.base_path))}")
148-
self.logger.debug(f"Event handler is set: {self.event_handler is not None}")
149-
157+
self.logger.debug("Observer thread is alive: %s", self.observer.is_alive())
158+
self.logger.debug("Monitored path exists: %s", os.path.exists(str(self.base_path)))
159+
self.logger.debug("Event handler is set: %s", self.event_handler is not None)
160+
150161
# Log current directory for comparison
151162
current_dir = os.getcwd()
152-
self.logger.debug(f"Current working directory: {current_dir}")
153-
self.logger.debug(f"Are paths same: {os.path.normpath(current_dir) == os.path.normpath(str(self.base_path))}")
154-
163+
self.logger.debug("Current working directory: %s", current_dir)
164+
self.logger.debug("Are paths same: %s", os.path.normpath(current_dir) == os.path.normpath(str(self.base_path)))
165+
155166
return True
156167
else:
157168
self.logger.error("File watcher failed to start - Observer not alive")
@@ -165,7 +176,7 @@ def start_monitoring(self, rebuild_callback: Callable) -> bool:
165176
def stop_monitoring(self) -> None:
166177
"""
167178
Stop file system monitoring and cleanup all resources.
168-
179+
169180
This method ensures complete cleanup of:
170181
- Observer thread
171182
- Event handler
@@ -175,43 +186,43 @@ def stop_monitoring(self) -> None:
175186
if not self.observer and not self.is_monitoring:
176187
# Already stopped or never started
177188
return
178-
189+
179190
self.logger.info("Stopping file watcher monitoring...")
180-
191+
181192
try:
182193
# Step 1: Stop the observer first
183194
if self.observer:
184195
self.logger.debug("Stopping observer...")
185196
self.observer.stop()
186-
197+
187198
# Step 2: Cancel any active debounce timer
188199
if self.event_handler and self.event_handler.debounce_timer:
189200
self.logger.debug("Cancelling debounce timer...")
190201
self.event_handler.debounce_timer.cancel()
191-
202+
192203
# Step 3: Wait for observer thread to finish (with timeout)
193204
self.logger.debug("Waiting for observer thread to finish...")
194205
self.observer.join(timeout=5.0)
195-
206+
196207
# Step 4: Check if thread actually finished
197208
if self.observer.is_alive():
198209
self.logger.warning("Observer thread did not stop within timeout")
199210
else:
200211
self.logger.debug("Observer thread stopped successfully")
201-
212+
202213
# Step 5: Clear all references
203214
self.observer = None
204215
self.event_handler = None
205216
self.rebuild_callback = None
206217
self.is_monitoring = False
207-
218+
208219
self.logger.info("File watcher stopped and cleaned up successfully")
209220
print("STOPPED: File watcher stopped")
210-
221+
211222
except Exception as e:
212223
self.logger.error("Error stopping file watcher: %s", e)
213224
print(f"WARNING: Error stopping file watcher: {e}")
214-
225+
215226
# Force cleanup even if there were errors
216227
self.observer = None
217228
self.event_handler = None
@@ -340,21 +351,17 @@ def on_any_event(self, event: FileSystemEvent) -> None:
340351
event: The file system event
341352
"""
342353
# Always log events for debugging
343-
event_info = f"Raw event: {event.event_type} - {event.src_path} (is_directory: {event.is_directory})"
344-
self.logger.debug(event_info)
345-
354+
self.logger.debug("Raw event: %s - %s (is_directory: %s)", event.event_type, event.src_path, event.is_directory)
355+
346356
# Test event processing
347357
should_process = self.should_process_event(event)
348-
process_info = f"Should process: {should_process}"
349-
self.logger.debug(process_info)
350-
358+
self.logger.debug("Should process: %s", should_process)
359+
351360
if should_process:
352-
process_msg = f"Processing file system event: {event.event_type} - {event.src_path}"
353-
self.logger.debug(process_msg)
361+
self.logger.debug("Processing file system event: %s - %s", event.event_type, event.src_path)
354362
self.reset_debounce_timer()
355363
else:
356-
filter_msg = f"Event filtered out: {event.event_type} - {event.src_path}"
357-
self.logger.debug(filter_msg)
364+
self.logger.debug("Event filtered out: %s - %s", event.event_type, event.src_path)
358365

359366
def should_process_event(self, event: FileSystemEvent) -> bool:
360367
"""
@@ -368,38 +375,60 @@ def should_process_event(self, event: FileSystemEvent) -> bool:
368375
"""
369376
# Skip directory events
370377
if event.is_directory:
371-
self.logger.debug(f"Skipping directory event: {event.src_path}")
378+
self.logger.debug("Skipping directory event: %s", event.src_path)
372379
return False
373380

381+
# Select path to check: dest_path for moves, src_path for others
382+
if event.event_type == 'moved':
383+
if not hasattr(event, 'dest_path'):
384+
self.logger.debug("Move event missing dest_path")
385+
return False
386+
target_path = event.dest_path
387+
self.logger.debug("Move event: checking destination path %s", target_path)
388+
else:
389+
target_path = event.src_path
390+
self.logger.debug("%s event: checking source path %s", event.event_type, target_path)
391+
392+
# Unified path checking
374393
try:
375-
path = Path(event.src_path)
394+
path = Path(target_path)
395+
return self._should_process_path(path)
376396
except Exception as e:
377-
# Handle any path conversion issues
378-
self.logger.debug(f"Path conversion failed for {event.src_path}: {e}")
397+
self.logger.debug("Path conversion failed for %s: %s", target_path, e)
379398
return False
380399

400+
def _should_process_path(self, path: Path) -> bool:
401+
"""
402+
Check if a specific path should trigger index rebuild.
403+
404+
Args:
405+
path: The file path to check
406+
407+
Returns:
408+
True if path should trigger rebuild, False otherwise
409+
"""
381410
# Log detailed filtering steps
382-
self.logger.debug(f"Checking path: {path}")
383-
411+
self.logger.debug("Checking path: %s", path)
412+
384413
# Skip excluded paths
385414
is_excluded = self.is_excluded_path(path)
386-
self.logger.debug(f"Is excluded path: {is_excluded}")
415+
self.logger.debug("Is excluded path: %s", is_excluded)
387416
if is_excluded:
388417
return False
389418

390419
# Only process supported file types
391420
is_supported = self.is_supported_file_type(path)
392-
self.logger.debug(f"Is supported file type: {is_supported} (extension: {path.suffix})")
421+
self.logger.debug("Is supported file type: %s (extension: %s)", is_supported, path.suffix)
393422
if not is_supported:
394423
return False
395424

396425
# Skip temporary files
397426
is_temp = self.is_temporary_file(path)
398-
self.logger.debug(f"Is temporary file: {is_temp}")
427+
self.logger.debug("Is temporary file: %s", is_temp)
399428
if is_temp:
400429
return False
401430

402-
self.logger.debug(f"Event will be processed: {event.src_path}")
431+
self.logger.debug("Event will be processed: %s", path)
403432
return True
404433

405434
def is_excluded_path(self, path: Path) -> bool:
@@ -472,15 +501,14 @@ def reset_debounce_timer(self) -> None:
472501
self.debounce_timer.cancel()
473502
self.logger.debug("Previous debounce timer cancelled")
474503

475-
timer_msg = f"Starting debounce timer for {self.debounce_seconds} seconds"
476-
self.logger.debug(timer_msg)
477-
504+
self.logger.debug("Starting debounce timer for %s seconds", self.debounce_seconds)
505+
478506
self.debounce_timer = Timer(
479507
self.debounce_seconds,
480508
self.trigger_rebuild
481509
)
482510
self.debounce_timer.start()
483-
511+
484512
self.logger.debug("Debounce timer started successfully")
485513

486514
def trigger_rebuild(self) -> None:
@@ -490,19 +518,14 @@ def trigger_rebuild(self) -> None:
490518

491519
if self.rebuild_callback:
492520
try:
493-
callback_msg = "Calling rebuild callback..."
494-
self.logger.debug(callback_msg)
495-
521+
self.logger.debug("Calling rebuild callback...")
522+
496523
result = self.rebuild_callback()
497-
498-
result_msg = f"Rebuild callback completed with result: {result}"
499-
self.logger.debug(result_msg)
524+
525+
self.logger.debug("Rebuild callback completed with result: %s", result)
500526
except Exception as e:
501-
error_msg = f"Rebuild callback failed: {e}"
502-
self.logger.error(error_msg)
503-
import traceback
527+
self.logger.error("Rebuild callback failed: %s", e)
504528
traceback_msg = traceback.format_exc()
505529
self.logger.error("Traceback: %s", traceback_msg)
506530
else:
507-
no_callback_msg = "No rebuild callback configured"
508-
self.logger.warning(no_callback_msg)
531+
self.logger.warning("No rebuild callback configured")

0 commit comments

Comments
 (0)