25 March 2012

Мониторинг за изменениями файловой системы

Python
В поисках готового велосипеда для решения задачи мониторинга за изменениями в ФС с поддержкой linux+freebsd наткнулся на приятную python либу watchdog (github, packages.python.org). Которая помимо интересных мне ОС поддерживает также MacOS (есть своя специфика) и Windows.
Тем, кому данный вопрос интересен и кого не отпугнет индийское происхождение автора, прошу .

Установка


Можно взять готовую версию из PIP:
$ pip install watchdog
Сам PIP ставится как пакет python-pip, порт devel/py-pip, etc.
Либо собрать из исходников через setup.py.

Достаточно подробно все расписано в оригинальном руководстве. Правда там описание версии 0.5.4, а сейчас актуальна 0.6.0. Однако, вся разница в правке копирайтов и замене отступа в 4 пробела на отступ в 2. «Google code style» :)

Вообще, там довольно много особенностей сборки по версиям самого python так и по целевой платформе. Они все описаны по ссылке выше, но если будет нужно, допишу в статью вкратце на русском.

Кроме того, собрать модуль можно на несовместимой ОС, но тогда в дело вступится fallback-реализация, делающая «слепки» структуры ФС с последующими сравнениями. Возможно, так кто-то и делал у себя при решении подобной задачи :)

Сам же я пробовал собрать под ubuntu 11.4 и freebsd-8.2 RELEASE, каких-либо проблем при сборке и работе не возникло.

Базовый пример


Предположим, что нас интересуют изменения по некоему пути /path/to/smth, связанные с созданием, удалением и переименованием файлов и директорий.

Подключаем:
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

Класс Observer выбирается в /observers/__init__.py исходя из возможностей вашей ОС, так что нет необходимости самостоятельно решать, что же выбрать.
Класс FileSystemEventHandler является базовым классом обработчика событий изменения. Он мало что умеет, но мы научим его потомка:
class Handler(FileSystemEventHandler):
    def on_created(self, event):
        print event

    def on_deleted(self, event):
        print event

    def on_moved(self, event):
        print event

Полный список методов можно увидеть в самом FileSystemEventHandler.dispatch: on_modified, on_moved, on_created, on_deleted.

Запускаем это все:
observer = Observer()
observer.schedule(Handler(), path='/path/to/smth', recursive=True)
observer.start()


Observer является относительно далеким потомком threading.Thread, соотвественно после вызова start() мы получаем фоновый поток, следящий за изменениями. Так что если скрипт сразу завершится, то ничего толкового мы не получим. Реалиация ожидания зависит в первую очередь от применения модуля в реальном проекте, сейчас же можно просто сделать костыль:
try:
    while True:
        time.sleep(0.1)
except KeyboardInterrupt:
    observer.stop()
observer.join()

Ждем событий изменений ФС до прихода Ctrl+C (SIGINT), после чего говорим нашему потоку завершиться и ждем, пока он это выполнит.

Запускаем скрипт, идем по нашему пути и:
# mkdir foo
# touch bar
# mv bar baz
# cd foo/
# mkdir foz
# mv ../baz ./quz
# cp ./quz ../hw
# cd ..
# rm -r ./foo
# rm -f ./*


На выходе скрипта имеем:
<DirCreatedEvent: src_path=/path/to/smth/foo>
<FileCreatedEvent: src_path=/path/to/smth/bar>
<FileMovedEvent: src_path=/path/to/smth/bar, dest_path=/path/to/smth/baz>
<DirCreatedEvent: src_path=/path/to/smth/foo/foz>
<FileMovedEvent: src_path=/path/to/smth/baz, dest_path=/path/to/smth/foo/quz>
<FileCreatedEvent: src_path=/path/to/smth/hw>
<FileDeletedEvent: src_path=/path/to/smth/foo/quz>
<DirDeletedEvent: src_path=/path/to/smth/foo/foz>
<DirDeletedEvent: src_path=/path/to/smth/foo>
<FileDeletedEvent: src_path=/path/to/smth/hw>

В методы нашего класса Handler в поле event приходят потомки FileSystemEvent, перечисленные в watchdog/events.py.
У всех есть свойства src_path, is_directory, event_type («created», «deleted», и т.п.). Для события moved добавляется свойство dest_path.

Ну если вы больше ничего не хотите… А разве ещё что-нибудь есть?


На закуску у нас остаются потомки FileSystemEventHandler:


PatternMatchingEventHandler можно использовать для получения событий только о тех узлах ФС, имена которых подходят по маске с правилами:
  • * любые символы
  • ? любой единичный символ
  • [seq] любой единичный символ из указанных
  • [!seq] любой единичный символ НЕ из указанных

Задание правил выполняется при создании:
class Handler(PatternMatchingEventHandler): pass
event_handler = Handler(
    patterns = ['*.py*'],
    ignore_patterns = ['cache/*'],
    ignore_directories = True,
    case_sensitive = False
)

observer = Observer()
observer.schedule(event_handler, path='/home/LOGS/', recursive=True)


RegexMatchingEventHandler делает тоже самое, но с явным указанием regexp-выражений в конструкторе:
class Handler(RegexMatchingEventHandler): pass
event_handler = Handler(
    regexes = ['\.py.?'],
    ignore_regexes = ['cache/.*'],
    ignore_directories = True,
    case_sensitive = False
)


PatternMatchingEventHandler внутри себя в итоге транслирует шаблоны в регулярки, так что должен работать медленнее из-за наличия такого оверхеда.

Наконец, LoggingEventHandler выводит все в лог через logging.info().

— Вот и все. Может кому пригодится.

P.S.
При слежении за директорией, в которой (и в ее дочерних) содержатся папки/файлы не с ascii именованием, возникнет исключение exceptions.UnicodeEncodeError в глубинах watchdog'а. В Linux (inotify) он возникает в watchdog.observers.inotify.Inotify._add_watch.
Причина — чтение содержимого в ascii кодировке.
Для исправления ситуации можно пропатчить метод:
from watchdog.observers.inotify import Inotify
_save = Inotify._add_watch
Inotify._add_watch = lambda self, path, mask: _save(self, path.encode('utf-8'), mask)


Вот пример исходной строки, и ее repr() до и после обработки encode():
/home/atercattus/.wine/drive_c/users/Public/Рабочий стол
u'/home/atercattus/.wine/drive_c/users/Public/\u0420\u0430\u0431\u043e\u0447\u0438\u0439
        \u0441\u0442\u043e\u043b'
'/home/atercattus/.wine/drive_c/users/Public/\xd0\xa0\xd0\xb0\xd0\xb1\xd0\xbe\xd1\x87\xd0\xb8\xd0\xb9
        \xd1\x81\xd1\x82\xd0\xbe\xd0\xbb'
Tags:pythoninotifykqueuefseventsReadDirectoryChanges
Hubs: Python
+37
25.8k 204
Comments 34