Честно говоря не думал, что на разборки с файловым мониторингом уйдет столько времени. Да, тема попалась не из легких. Во-первых, эффективный мониторинг можно организовать только на уровне системы. Как следствие, необходимость взаимодействовать с API через специфическое расширение, а это - отдельная тема. В общем, пришлось потрудиться как над алгоритмом, так и над способом его запихивания в perl. Но, обо всем по порядку.
У меня есть два файловых сервака (Win2000AS и WinNT4.0), за которыми мне очень неохота следить. На одном из серверов крутятся базы 1С:Предприятия, а другой - просто файл-сервер для пользовательской всяко-всячины. Так вот, как показала практика, в случае чего именно эти сервера доставляют мне больше всего хлопот. Мало того, что машинное время они жрут как черная дыра, так еще и являются самыми слабыми (в плане безопасности) объектами сети. Помнится, пробрался к нам в интранет Клещ - так я эти серваки потом долго вычищал. А вся причина в чем? Правильно, некому предупредить то, что в общих каталогах появилась куча новых файлов.
Историй про полезность файлового мониторинга у меня еще много, но не будем заострять на этом внимание. Пусть применение новой фичи будет вашим домашним заданием.
Сначала я попробовал использовать стандартный модуль AP Win32::ChangeNotify. Неплохо сделано, однако, слишком уж мало информации. Но ведь и мы не лыком шиты и когда-то программировали на сях под WinAPI. Достал я трехметровый справочник по функциям API, и начал свои поиски. Win32::ChangeNotify - это ОО интерфейс к функциям серии Find*ChangeNotification в чистом виде. Как рассказал мне справочник, больше того, что дает нам модуль Win32::ChangeNotify мы посредством этих функций не получим. Я бы еще долго копался в справочнике, а потом полез в DDK, если бы не чистая случайность - обнаружилась функция ReadDirectoryChangesW. Правда, функция эта определена только в системах NT начиная с 4 версии. Ну что ж, для сервера это в самый раз. Да и для Win2000WS, за которыми сидят юзеры в офисе то же пойдет. Так что решение принято - берем!
Дальше речь пойдет, по большей части, о решении на языке С. Не расстраивайтесь, если вы мало что в этом понимаете. Главное в решении - уловить суть.
Функция ReadDirectoryChangesW имеет следующий прототип:
hDirectory - дескриптор каталога за которым мы должны следить. Получить дескриптор можно с помощью WinAPI функции OpenFile (см. ниже).
lpBuffer - указатель на буфер, в который будет помещена информация о произошедших изменениях в каталоге.
nBufferLength - длина буфера. С расчетом этой величины у меня были некоторые проблемы, по этому мы остановимся на буфере чуть позже.
bWatchSubtree - булево значение, указывающее на необходимость слежения за вложенными каталогами.
dwNotifyFilter - набор флагов, который определяет характер изменений, которые нас интересуют.
lpBytesReturned - указатель на переменную, в которую функция ReadDirectoryChangesW() запишет значимую длину данных в буффере lpBuffer.
Аргументы lpOverlapped и lpCompletionRoutine нас, в данном случае, не интересуют.
Как видно из аргументов, прежде чем вызывать эту функцию, необходимо инициализировать дескриптор каталога и выделить достаточный объем памяти под буфер для возврата данных. С первым никаких проблем не возникает. Код инициализации дескриптора каталога выглядит так
Из аргументов нас интересует переменная szDirPath, значение которой представляет собой строку, содержащую путь к интересующей нас директории.
С буфером дело обстоит сложнее. По сути, буффер имеет вид массива из экземпляров структуры типа FILE_NOTIFY_INFORMATION. Каждый экземпляр описывает некое событие, которое имело место в пределах каталога. Дело осложняется еще и за счет того, что одно событие не ограничивается FILE_NOTIFY_INFORMATION, а требует еще и выделения памяти под имя файла. Если кто не догоняет, ниже я привел прототип структуры FILE_NOTIFY_INFORMATION
Как видим, поле FileName представлено всего одним wide-символом (это еще один, хоть и не большой, но все же камень в наш огород). Естественно, что никакое имя файла сюда не поместится. Поле нужно лишь для того, чтобы определить откуда начинается строка, содержащая имя файла (о том, что это ненормальная строка пока можно не вспоминать). С помощью поля FileNameLength мы можем определить, сколько байт в буффере (начиная от FileName[1]) представляют имя файла. С этим ясно, но вот досада - мы ведь не можем определить длину имени заранее. Приходится выкручиваться за счет выделения бОльших объемов памяти. В нашем случае, для одного события используется размер, определяемый выражением
sizeof(FILE_NOTIFY_INFORMATION) + MAX_PATH * 2
Таким образом, мы получаем объем, достаточный для представления самого длинного имени файла (нуль-терминатор мы то же хитрым образом учитываем, так как в FILE_NOTIFY_INFORMATION уже есть место под 1 символ и + MAX_PATH = терминированная строка).
Но не думайте, что все так просто. У нас еще не определено количество таких объемов, необходимое для представления нескольких событий. Вообще, результат работы функции ReadDirectoryChangesW определяется кодом возврата и значением переменной lpBytesReturned (см. прототип функции выше). Если lpBytesReturned после успешного (определяется кодом возврата) выполнения функции равно нулю, то системе не хватило буффера для размещения информации о всех произошедших изменениях. Самое интересное, это ремарка в документации о том, что нужно делать в этом случае - ну, мол, не переживайте? перечитайте каталог и проверьте все сами. Класс! Это при условии вложенности каталогов! Да еще и для сравнения нужно предварительно собрать данные и где-то их сохранить. Таким макаром проще юзать Find*ChangeNotification - алгоритм нисколько не упрощается.
Но, русские не сдаются! Так как ничего полезного о расчете буфера в документации я не нашел и в инете никто не пособил, пришлось идти на таран. Методом научного тыка я пришел к выводу, что более 3-4 событий функция ReadDirectoryChangesW одновременно не обрабатывает. Да и то, 3-4 события это когда функция вызывается с фильтром по максимуму, то есть следит за изменениями атрибутов, сменой имен, созданием и прочими действами. Конечно, тут я могу и ошибаться, но как тут еще протестировать - для объективности необходимо выполнять изменения в каталоге с бешенной скоростью. Для пущей надежности мы будем резервировать место под 32 события (учитывая, что еще под имя файла отводится по максимуму, даже если будут иметь место более 32, то все равно на вряд-ли загнемся от недостатка памяти).
Итак, подведем итог по буферу. Будем выделять столько, сколько необходимо для хранения информации о событии с именем файла по максимуму в 32-х экземплярах. Если при юзании кто нибудь определит, что этого недостаточно, пишите - исправим. И вообще, я буду очень рад, если кто-то знает нормальный способ расчета буффера для этой функции и поделится им.
XSUB тема не из легких. Однако, без этого механизма мы не сможем работать с API-шными функциями. Попробуем ограничиться необходимым минимумом при рассмотрении этого примера, а детальное рассмотрение оставим на потом. Для сборки модуля вам понадобится компилятор С. Я использовал Microsoft Visual C 6.0. В этот пакет входит утилита nmake, которая нам тоже обязательно понадобится. Если у вас другой компилятор, то придется разбираться с настройками самим. Но могу сказать сразу - если через переменную окружения PATH можно найти путь к компилятору, утилите nmake, а переменные окружения LIB и INCLUDE указывают соответственно на путь к каталогам с библиотечными (*.lib) и заголовочными (*.h) файлами С, то все должно работать.
Механизм XSUB подразумевает собой некий стандарт, следуя которому можно соединить программу на C и perl. Большая часть усилий по сопряжению двух языков приходится на С-часть программы. То есть изучая механизм XSUB вы должны всегда понимать, что речь идет о коде на C. По большей части весь механизм заключается в правильной работе со стеком аргументов и приведении типов к типам perl (то есть скаляр, массив, хэш). Что бы не держать в голове все тонкости правил передачи аргументов и уменьшить количество ошибок, связанных с аргументацией, был разработан специальный макроязык описания интерфейсов функций XSUB. Этот язык содержит макросы, которые в процессе обработки преобразуют код программы в соответствии с требованиями XSUB. Часть этих макросов обрабатывается препроцессором С перед компиляцией, а часть специальной программой xsubpp, которая выполняет преобразование файла с кодом XSUB к полноценному С-коду. По сути, xsubpp представляет собой компилятор XSUB.
Для создания макета нового модуля воспользуемся программой h2xs. Перейдите в каталог, в котором будет размещен проект и наберите в командной строке следующую команду
h2xs -A -n WCN
На текущем уровне появится новый каталог с именем WCN. WCN - это имя проекта и модуля perl, который должен получиться в результате компиляции. А сейчас давайте наберем необходимый код и попробуем скомпилировать проект. Как работает механизм XSUB мы разберем позднее.
Итак, файл WCN.xs представляет собой гибрид С-кода и макроязыка XSUB. Именно в этом файле должен быть описан интерфейс модуля, то есть правила интерпретации входных и выходных значений для всех функций, через которые perl будет взаимодействовать с нашим модулем.
Вот такой гибрид С и непонятно чего еще. После ввода этого кода нужно добавить в каталог проекта новый файл typemap. Содержимое этого файла содержит правила приведения типов. В вышеописанном коде имеются 4 типа, о который xsubpp ничего не известно. Это типы BOOL, DWORD, LPCTSTR и HANDLE. На приведении типов тоже не будем сейчас задерживаться. Файл typemap для нашего проекта должен содержать следующее
BOOL T_IV HANDLE T_UV DWORD T_UV LPCTSTR T_PV
Теперь замый ответственный этап - сборка. Сначала с помощью программы Makefile.PL генерируем файл Makefile. Затем собираем проект. Все это выполняется следующими командами
perl Makefile.PL ... nmake ...
Если в ходе сборки не произошло никаких ошибок то все гут. Иначе - разбирайтесь. Ошибка либо в коде WCN.xs, либо в настройках компилятора.
На этом этапе мы отредактируем сам модуль WCN.pm. Давайте создадим ОО интерфейс нашим функциям и объявим константы. После редактирования код модуля должен быть таким
package WCN; require 5.005_62; use strict; use warnings; require Exporter; require DynaLoader; our @ISA = qw(Exporter DynaLoader); # Items to export into callers namespace by default. Note: do not export # names by default without a very good reason. Use EXPORT_OK instead. # Do not simply export all your public functions/methods/constants. our @EXPORT = qw( &WCN_INVALID_HANDLE &WCN_FILE_NAME &WCN_DIR_NAME &WCN_ATTRIBUTES &WCN_SIZE &WCN_LAST_WRITE &WCN_LAST_ACCESS &WCN_SECURITY &WCN_ACTION_ADDED &WCN_ACTION_REMOVED &WCN_ACTION_MODIFIED &WCN_ACTION_RENAMED_OLD_NAME &WCN_ACTION_RENAMED_NEW_NAME ); our $VERSION = '0.01'; bootstrap WCN $VERSION; # Preloaded methods go here. sub WCN_INVALID_HANDLE() {0xFFFFFFFF} sub WCN_FILE_NAME() {0x00000001} sub WCN_DIR_NAME() {0x00000002} sub WCN_ATTRIBUTES() {0x00000004} sub WCN_SIZE() {0x00000008} sub WCN_LAST_WRITE() {0x00000010} sub WCN_LAST_ACCESS() {0x00000020} sub WCN_SECURITY() {0x00000100} sub WCN_ACTION_ADDED() {0x00000001} sub WCN_ACTION_REMOVED() {0x00000002} sub WCN_ACTION_MODIFIED() {0x00000003} sub WCN_ACTION_RENAMED_OLD_NAME(){0x00000004} sub WCN_ACTION_RENAMED_NEW_NAME(){0x00000005} sub new{ my $class = shift; $class= ref($class) || $class; my $self = {}; bless $self,$class; return $self if $self->initialize(@_); return undef; } sub initialize{ my ($self,$path,$tree,$filter) = @_; $path = '.' unless defined($path); $tree = 0 unless defined($tree); $filter = WCN_FILE_NAME unless defined($filter); $self->{path} = $path; $self->{tree} = $tree; $self->{filter} = $filter; Close($self->{handle}) if exists($self->{handle}) && $self->{handle} != WCN_INVALID_HANDLE; $self->{handle} = Open($path); return undef if $self->{handle} == WCN_INVALID_HANDLE; return 1; } sub read_changes{ my $self = shift; return undef if $self->{handle} == WCN_INVALID_HANDLE; return Read($self->{handle},$self->{tree},$self->{filter}); } DESTROY{ my $self = shift; Close($self->{handle}) if $self->{handle} != WCN_INVALID_HANDLE; } 1; __END__
Модуль довольно прост. Все этапы, за исключением деструктора, мы рассматривали выше (деструктор просто закрывает хэндл каталога). В общем, интерфейс стал очень похожим на интерфес Win32::ChangeNotify.
На самом деле, константы, которые определены в модуле константными функциями (WCN_**) можно было импортировать из программы на C. Но после того, как я посмотрел на реализуцию кода С и распухания модуля WCN.pm при использовании стандартного механизма переноса констант, я решил что гораздо эффективнее объявить константы прямо в модуле. Не волнуйтесь, значения этих констант навряд-ли изменяются от версии к версии в Windows NT. Иначе очень многие программы написанные на С просто не работали после переноса.
Теперь давайтеразберемся, как же работает модуль. Прежде всего, конструктор создает экземпляр объекта и инициализирует его. В последствии, объект может быть переориентирован на мониторинг другого каталога. Это достигается путем вызова метода initialize() с соответствующими аргументами.
Вызов метода read_changes() приводит к блокировке программы до возникновения нового события. В качестве результата, метод read_changes возвращает результат работы функции Read() из С-модуля. При обращении к методу read_changes() модуль WCN.pm сам передает ранее сохраненные аргументы в функцию на C. Массив, возвращаемый С-функцией Read() представляет собой последовательность ссылок на анонимные массивы, каждый из которых в свою очередь описывает событие, имевшее место в каталоге. Первый элемент этого анониманого массива содержит код события. Символические константы WCN_ACTION_* облегчают процесс идентификации событий (необходимо заметить, что из модуля экспортируются только имена констант, но не методов и функций). Второй элемент массива представляет собой имя файла. Имя файла представляется путем к файлу, относительно каталога слежения. То есть, если изменения произошли в корне каталога, то имя файла представляет истинное имя файла, подвергшегося изменениям. Если же изменения произошли в одном из подкаталогов, то имя файла включает в себя имена всех каталогов, которые представляют путь к файлу относительно каталога слежения.
Для полного комплекта пакету недостает функции тестирования. Файл test.pl будет вызываться каждый раз, когда в процессе инсталяции модуля будет выполняться команда
nmake test
Из-за специфичности модуля, процесс тестирования представляется не совсем простым, по этому оставим эту затею в качестве домашнего задания. В общем случае, процесс мониторинга может выглядеть так
my $watcher = new WCN('C:\\temp',1,WCN_ATTRIBUTES|WCN_SIZE) or die "Не могу поднять ногУ!\n"; while(1){ my @changes = $watcher->read_changes(); die "Облажались!\n" if ref($changes[0]) ne 'ARRAY'; foreach my $ref (@changes){ print "Событие: ${$ref}[0], Файл/Каталог: ${$ref}[1]\n"; } }