# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """Watcher to detect filesystem changes in the project's directory.""" # Standard lib imports import logging # Third-party imports from qtpy.QtCore import QObject, Signal from qtpy.QtWidgets import QMessageBox import watchdog from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler # Local imports from spyder.config.base import _ from spyder.py3compat import to_text_string logger = logging.getLogger(__name__) class BaseThreadWrapper(watchdog.utils.BaseThread): """ Wrapper around watchdog BaseThread class. This is necessary for issue spyder-ide/spyder#11370 """ queue = None def __init__(self): super(BaseThreadWrapper, self).__init__() self._original_run = self.run self.run = self.run_wrapper def run_wrapper(self): try: self._original_run() except OSError as e: logger.exception('Watchdog thread exited with error %s', e.strerror) self.queue.put(e) # Monkeypatching BaseThread to prevent the error reported in # spyder-ide/spyder#11370 watchdog.utils.BaseThread = BaseThreadWrapper class WorkspaceEventHandler(QObject, FileSystemEventHandler): """ Event handler for watchdog notifications. This class receives notifications about file/folder moving, modification, creation and deletion and emits a corresponding signal about it. """ sig_file_moved = Signal(str, str, bool) sig_file_created = Signal(str, bool) sig_file_deleted = Signal(str, bool) sig_file_modified = Signal(str, bool) def __init__(self, parent=None): super(QObject, self).__init__(parent) super(FileSystemEventHandler, self).__init__() def fmt_is_dir(self, is_dir): return 'directory' if is_dir else 'file' def on_moved(self, event): src_path = event.src_path dest_path = event.dest_path is_dir = event.is_directory logger.info("Moved {0}: {1} to {2}".format( self.fmt_is_dir(is_dir), src_path, dest_path)) self.sig_file_moved.emit(src_path, dest_path, is_dir) def on_created(self, event): src_path = event.src_path is_dir = event.is_directory logger.info("Created {0}: {1}".format( self.fmt_is_dir(is_dir), src_path)) self.sig_file_created.emit(src_path, is_dir) def on_deleted(self, event): src_path = event.src_path is_dir = event.is_directory logger.info("Deleted {0}: {1}".format( self.fmt_is_dir(is_dir), src_path)) self.sig_file_deleted.emit(src_path, is_dir) def on_modified(self, event): src_path = event.src_path is_dir = event.is_directory logger.info("Modified {0}: {1}".format( self.fmt_is_dir(is_dir), src_path)) self.sig_file_modified.emit(src_path, is_dir) class WorkspaceWatcher(QObject): """ Wrapper class around watchdog observer and notifier. It provides methods to start and stop watching folders. """ def __init__(self, parent=None): super(QObject, self).__init__(parent) self.observer = None self.event_handler = WorkspaceEventHandler(self) def connect_signals(self, project): self.event_handler.sig_file_created.connect(project.file_created) self.event_handler.sig_file_moved.connect(project.file_moved) self.event_handler.sig_file_deleted.connect(project.file_deleted) self.event_handler.sig_file_modified.connect(project.file_modified) def start(self, workspace_folder): # Needed to handle an error caused by the inotify limit reached. # See spyder-ide/spyder#10478 try: self.observer = Observer() self.observer.schedule( self.event_handler, workspace_folder, recursive=True) try: self.observer.start() except OSError: # This error happens frequently on Linux logger.debug("Watcher could not be started.") except OSError as e: if u'inotify' in to_text_string(e): QMessageBox.warning( self.parent(), "Spyder", _("File system changes for this project can't be tracked " "because it contains too many files. To fix this you " "need to increase the inotify limit in your system, " "with the following command:" "

" "" "sudo sysctl -n -w fs.inotify.max_user_watches=524288" "" "

For a permanent solution you need to add to" "/etc/sysctl.conf" "the following line:

" "" "fs.inotify.max_user_watches=524288" "" "

" "After doing that, you need to close and start Spyder " "again so those changes can take effect.")) self.observer = None else: raise e def stop(self): if self.observer is not None: # This is required to avoid showing an error when closing # projects. # Fixes spyder-ide/spyder#14107 try: self.observer.stop() self.observer.join() del self.observer except RuntimeError: pass