В предыдущем посте мы остановились на том что разработали небольшое приложение, которое проводило мониторинг изменений в определенной директории и, в случае обнаружения какого-либо изменения, "сигналило" нам. Для организации мониторинга мы использовали поток (TThread) в котором использовалось три взаимосвязанные функции Windows: FindFirstChangeNotification , FindNextChangeNotification и FindCloseChangeNotification .
Как говорилось ранее, с помощь этих функций нельзя узнать какую-либо специфическую информацию об изменениях. Так, например, при срабатывании события мы не могли узнать изменилось ли имя файла или был добавлен новый файл. Или, если произошла смена имени файла, то мы не можем узнать какое имя было до смены и какое стало после. Все эти нюансы могут натолкнуть неподготовленного разработчика на мысль, что использование приведенных выше функций ограничено - задача мониторинга изменений в директории обычно преследует не абстрактную цель - узнать что что-то поменялось (хотя, иногда и такой информации бывает достаточно), а получить конкретный ответ на вопрос - что изменилось и как (сменилось имя, размер, права доступа и т.д.? Прежде, чем мы перейдем к работе с такой специфической информацией об изменениях, мы немного доработаем наш предыдущий пример и посмотрим как с помощью уже известных нам трех функций можно настроить мониторинг так, чтобы получать максимально конкретизированную (на сколько это возможно) информацию по изменениям.
Итак, сначала вспомним как выглядел Execute нашего потока:
procedure TChangeMonitor.Execute;
var ChangeHandle: THandle;
begin
{получаем хэндл события}
ChangeHandle:=FindFirstChangeNotification(PChar(FDirectory),
FScanSubDirs,
FILE_NOTIFY_CHANGE_FILE_NAME+
FILE_NOTIFY_CHANGE_DIR_NAME+
FILE_NOTIFY_CHANGE_SIZE
);
{Если не удалось получить хэндл - выводим ошибку и прерываем выполнение}
Win32Check(ChangeHandle <> INVALID_HANDLE_VALUE);
try
{выполняем цикл пока}
while not Terminated do
begin
case WaitForSingleObject(ChangeHandle,INFINITE) of
WAIT_FAILED: Terminate; //Ошибка, завершаем поток
WAIT_OBJECT_0: Synchronize(DoChange);
end;
FindNextChangeNotification(ChangeHandle);
end;
finally
FindCloseChangeNotification(ChangeHandle);
end;
end; |
Здесь мы получили всего один хэндл события с помощью которого отслеживали сразу несколько возможных изменений в директории - изменение имени файла, изменение имени директории и изменение размера. Так как хэндл был всего один, то нам вполне достаточно было использовать функцию WaitForSingleObject для ожидания события. Но нам ведь никто не запрещает получить не один, а, например, три хэндла событий каждое из которых будет срабатывать на конкретно заданное изменение? Давайте сделаем это, а заодно и разберемся с работой ещё одной функции для обработки событий. Итак, пишем новый Execute потока:
procedure TChangeMonitor.Execute;
var ChangeHandle: THandle;
WaitHandles: array[0..3] of THandle;{массив хэндлов}
begin
{получаем хэндл события на изменение имени файла}
WaitHandles[0]:=FindFirstChangeNotification(PChar(FDirectory),
FScanSubDirs,
FILE_NOTIFY_CHANGE_FILE_NAME
);
{Если не удалось получить хэндл - выводим ошибку и прерываем выполнение}
Win32Check(WaitHandles[0] <> INVALID_HANDLE_VALUE);
{получаем хэндл события на изменение имени поддиректории}
WaitHandles[1]:=FindFirstChangeNotification(PChar(FDirectory),
FScanSubDirs,
FILE_NOTIFY_CHANGE_DIR_NAME
);
{Если не удалось получить хэндл - выводим ошибку и прерываем выполнение}
Win32Check(WaitHandles[1] <> INVALID_HANDLE_VALUE);
{получаем хэндл события на изменение размера файла}
WaitHandles[2]:=FindFirstChangeNotification(PChar(FDirectory),
FScanSubDirs,
FILE_NOTIFY_CHANGE_SIZE
);
{Если не удалось получить хэндл - выводим ошибку и прерываем выполнение}
Win32Check(WaitHandles[2] <> INVALID_HANDLE_VALUE);
try
{выполняем цикл пока}
while not Terminated do
begin
case WaitForMultipleObjects(3, @WaitHandles,false,INFINITE) of
WAIT_FAILED : Terminate; //Ошибка, завершаем поток
WAIT_OBJECT_0 : begin
FLastChange:='Изменилось название файла';
Synchronize(DoChange);
if not FindNextChangeNotification(WaitHandles[0]) then
break;
end;
WAIT_OBJECT_0 + 1: begin
FLastChange:='Изменилось название поддиректории';
Synchronize(DoChange);
if not FindNextChangeNotification(WaitHandles[1]) then
break;
end;
WAIT_OBJECT_0 + 2: begin
FLastChange:='Изменился размер файла';
Synchronize(DoChange);
if not FindNextChangeNotification(WaitHandles[2]) then
break;
end;
end;
end;
finally
CloseHandle(WaitHandles[0]);
CloseHandle(WaitHandles[1]);
CloseHandle(WaitHandles[2]);
end;
end; |
В приведенном выше Execute мы определили массив на 3 элемента, где каждый элемент - это хэндл события рассчитанного на определенный вид изменений в директории. Таким образом мы получили возможность пусть и не полностью, но конкретизировать изменения. Так как мы планируем ожидать несколько событий, то вместо метода WaitForSingleObject мы воспользовались методом WaitForMultipleObjects , который в MSDN имеет следующее описание:
WaitForMultipleObjects
WaitForMultipleObjects(nCount:DWORD; const lpHandles:PWOHandleArray;bWaitAll:boolean; dwMilliseconds:DWORD): DWORD; |
nCount: DWORD - количество хэндлов событий. Этот параметр не может быть равен нулю. Максимальное количество ожидаемых событий ограничивается значением константы MAXIMUM_WAIT_OBJECTS = 64 ;
lpHandles: PWOHandleArray - указатель на массив дескрипторов. Массив не должен содержать повторяющихся THandle .
bWaitAll: boolean - если значение этого параметра равно True , то функция вернет значение только в том случае, если будет получен сигнал от всех событий хэндлы которых определены в массиве. В случае, если значение флага равно False , то функция будет возвращать значения при срабатывание любого из событий, при этом возвращаемое значение будет указывать на объект, который изменил состояние.
dwMilliseconds:DWORD - время в мс ожидания события. Если значение этого параметра равно INFINITE , то функция будет возвращать значение только при срабатывании одного из событий.
Как видите, работа с несколькими дескрипторами событий оказалось не на много сложнее, чем с одним, но используя метод WaitForMultipleObjects мы смогли боле точно идентифицировать изменения в директории.
И теперь мы вплотную подходим к ситуации, когда помимо получения сигнала об изменениях в определенной директории от нас требуется определить и то, какой именно объект был изменен.
Использование функций CreateFile и ReadDirectoryChangesW
Прежде, чем перейдем к примеру на Delphi, посмотрим на описание этих двух функций.
Функция CreateFile создает или открывает файл или другое устройство ввода/вывода (файл, файловый поток, каталог и т.д.
CreateFile
function CreateFile(lpFileName: PChar; dwDesiredAccess:cardinal; dwShareMode:cardinal; lpSecurityAttributes:PSecurityAttributes; dwCreationDisposition:cardinal; dwFlagsAndAttributes:cardinal; hTemplateFile: THandle): THandle; |
lpFileName: PChar - имя файла или устройства ввода/вывода, которое будет создано или открыто.
dwDesiredAccess: cardinal - флаг, определяющий режим доступа к файлу или устройству (чтение, запись, чтение и запись). Наиболее часто используемые значения GENERIC_READ, GENERIC_WRITE, или сразу оба (GENERIC_READ or GENERIC_WRITE).
dwShareMode:cardinal - один или несколько флагов, определяющих режим совместного доступа к файлу или устройству. Для установки значений могут использоваться следующие константы:
FILE_SHARE_DELETE (0x00000004) - разрешает другим процессам получать доступ к файлу или устройству в том числе и выполнять операции удаления.
FILE_SHARE_READ (0x00000001) - разрешает другим процессам выполнять операции чтения.
FILE_SHARE_WRITE (0x00000002) - разрешает другим процессам выполнять операции записи.
lpSecurityAttributes:PSecurityAttributes - указатель на структуру SECURITY_ATTRIBUTES , содержащую дополнительные параметры безопасности работы с файлом или устройством. Этому параметру можно присваивать значение nil.
dwCreationDisposition:cardinal - флаг, который определяет какие действия следует применить к файлу или устройству. Для всех устройств, кроме файлов, этот параметр обычно устанавливается в значение OPEN_EXISTING . Также может принимать следующие значения:
CREATE_ALWAYS (2) - всегда создавать новый файл. Если файл уже существует и доступен для записи, то код последней ошибки устанавливается в значение ERROR_ALREADY_EXISTS (183) . Если файл не существует и указан правильный к нему путь, то создается новый файл. а код последней ошибки устанавливается в 0.
CREATE_NEW (1) - создать новый файл. Если файл с таким именем уже существует, то вернется код ошибки ERROR_FILE_EXISTS (80) .
OPEN_ALWAYS (4) - всегда открывать файл. Если файл существует, то функция завершится успешно, а код последней ошибки будет установлен в значение ERROR_ALREADY_EXISTS (183) . Если же файл не существует и указан верный путь, то создастся новый файл, а значение код последней ошибки будет установлено в 0.
OPEN_EXISTING (3) - открыть файл или устройство, только если файл или устройство существуют. Если объект не найден, то вернется код ошибки ERROR_FILE_NOT_FOUND (2) .
TRUNCATE_EXISTING (5) - открыть файл и урезать его размер до 0 байт. Если файл не существует, то вернется код ошибки ERROR_FILE_NOT_FOUND (2).
dwFlagsAndAttributes:cardinal - сочетание флагов и атрибутов файла или устройства. Значения обычно начинаются с FILE_ATTRIBUTE_* или FILE_FLAG_*. Наиболее часто используется значение FILE_ATTRIBUTE_NORMAL . При использовании функции CreateFile совместно с ReadDirectoryChangesW необходимо использовать также флаг FILE_FLAG_BACKUP_SEMANTICS .
hTemplateFile: THandle - дескриптор временного файла с режимом доступа GENERIC_READ . Значение этого параметра может быть равно nil.
ReadDirectoryChangesW
function ReadDirectoryChangesW(hDirectory:THandle; lpBuffer:pointer; nBufferLength:cardinal; bWatchSubtree:boolean; dwNotifyFilter:cardinal; lpBytesReturned:cardinal; lpOverlapped:POVERLAPPED; lpCompletionRoutine:POVERLAPPED_COMPLETION_ROUTINE): boolean; |
hDirectory:THandle - дескриптор директории за которой будет проводится наблюдение. Это значение мы получим, выполнив CreateFile .
lpBuffer:pointer - указатель на буфер в который буду записываться обнаруженные изменения. Структура записей в буфере соответствует структуре FILE_NOTIFY_INFORMATION (см. описание ниже). Буфер может записываться как синхронно, так и асинхронно в зависимости от заданных параметров.
nBufferLength:cardinal - размер буфера lpBuffer в байтах.
bWatchSubtree:boolean - True указывает на то, что в результаты мониторинга будут попадать также изменения в подкаталогах.
dwNotifyFilter:cardinal - фильтр событий. Может состоять из одного или нескольких флагов:
FILE_NOTIFY_CHANGE_FILE_NAME (0x00000001) - любое изменение имени файла. Сюда же относятся и операции удаления или добавления файла в каталог или подкаталог.
FILE_NOTIFY_CHANGE_DIR_NAME (0x00000002) - любое изменение имени подкаталога, включая добавление и удаление.
FILE_NOTIFY_CHANGE_ATTRIBUTES (0x00000004) - изменение атрибутов файла или каталога.
FILE_NOTIFY_CHANGE_SIZE (0x00000008) - изменение размера файла. Событие будет вызвано только после того как размер файла изменится и файл будет записан.
FILE_NOTIFY_CHANGE_LAST_WRITE (0x00000010) - изменение времени последней записи в файл или каталог.
FILE_NOTIFY_CHANGE_LAST_ACCESS (0x00000020) - изменение времени последнего доступа к файлу или каталогу.
FILE_NOTIFY_CHANGE_CREATION (0x00000040) - изменение времени создания файла или каталога.
FILE_NOTIFY_CHANGE_SECURITY (0x00000100) - изменение параметров безопасности файла или каталога.
lpBytesReturned:cardinal - в случае синхронного вызова функции этот параметр будет содержать количество байт информации, записанной в буфер. Для асинхронных вызовов значение этого параметра остается неопределенным.
lpOverlapped:POVERLAPPED - указатель на структуру OVERLAPPED , которая поставляет данные, которые будут использоваться во время асинхронной операции. Это значение может быть равным nil .
lpCompletionRoutine:POVERLAPPED_COMPLETION_ROUTINE - указатель на callback-функцию. Этот параметр может устанавливаться в значение nil .
Описания функций есть. Теперь напишем с помощью этих двух функций новую заготовку для Execute нашего потока для мониторинга изменений в директориях и файлах Windows:
procedure TScanLocalThread.Execute;
var
WaitHandles: array [0 .. 1] of THandle;
begin
{хэндл события завершения потока}
FTermEvent := CreateEvent(nil, True, False, nil);
{вызывать ReadDirectory... будем асинхронно, используя структуру Overlapped}
FillChar(FOverlapped, SizeOf(TOverlapped), 0);
{определяем хэндл события на получение данных по изменениям}
FOverlapped.hEvent := CreateEvent(nil, True, False, nil);
{получаем хэндл директории}
HandleChange := CreateFile(PWideChar(FScannedDir), GENERIC_READ,
FILE_SHARE_READ or FILE_SHARE_DELETE or FILE_SHARE_WRITE,
nil,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS or
FILE_FLAG_OVERLAPPED, 0);
{проверяем, что хэндл успешно получен}
Win32Check(HandleChange <> INVALID_HANDLE_VALUE);
{заносим хэндлы в массив для использования в WaitForMultipleObjects}
WaitHandles[0] := FTermEvent;
WaitHandles[1] := FOverlapped.hEvent;
{выполняем мониторинг}
try
while True do
begin
ReadDirectoryChangesW(HandleChange, @Buf, Sizeof(Buf), FScanSubDirs,
FILE_NOTIFY_CHANGE_FILE_NAME or
FILE_NOTIFY_CHANGE_DIR_NAME or
FILE_NOTIFY_CHANGE_SIZE or
FILE_NOTIFY_CHANGE_LAST_WRITE or
FILE_NOTIFY_CHANGE_CREATION,
nil, @FOverlapped, nil);
{ожидаем событие}
if WaitForMultipleObjects(2, @WaitHandles, False, INFINITE)
= WAIT_OBJECT_0 then
Break;
{смотрим, какие были изменения}
FindChanges;
end;
finally
{закрываем все хэндлы}
CloseHandle(HandleChange);
CloseHandle(FTermEvent);
CloseHandle(FOverlapped.hEvent);
FTermEvent := 0;
end;
end; |
В приведенном выше коде присутствуют переменные, которые мы ранее не использовали в работе, поэтому приведу дополнительно их описание:
TScanLocalThread = class(TThread)
private
[...]
Buf: array [0..65535]of byte;
FOverlapped: TOverlapped;
[...] |
Теперь при изменении в директории будет срабатывать событие по которому мы будем получать в буфер информацию по проведенным изменениям. Но получить информацию - это одно, а разобрать е и предоставить в удобном для нас виде - другое. Поэтому я специально вынес весь разбор содержимого буфера в отдельный метод FindChanges. Прежде, чем рассмотрим этот метод немного отвлечемся от Delphi и снова вернемся к MSDN, а именно - посмотрим, что представляет из себя структура FILE_NOTIFY_INFORMATION.
FILE_NOTIFY_INFORMATION
Так как в Delphi описания этой структуры я не обнаружил, но приведу е описание прямо из MSDN:
typedef struct _FILE_NOTIFY_INFORMATION {
DWORD NextEntryOffset;
DWORD Action;
DWORD FileNameLength;
WCHAR FileName[1];
} FILE_NOTIFY_INFORMATION, *PFILE_NOTIFY_INFORMATION; |
NextEntryOffset - сдвиг в байтах, начиная с которого начинается следующая запись. Если это значение равно нулю, то текущая запись является последней.
Action - сдержит информацию по произведенным изменениям и может принимать одно из следующих значений:
FILE_ACTION_ADDED (0x00000001) - новый файл был добавлен в директорию или поддиректорию
FILE_ACTION_REMOVED (0x00000002) - файл или директория были удалены
FILE_ACTION_MODIFIED (0x00000003) - файл был изменен
FILE_ACTION_RENAMED_OLD_NAME (0x00000004) - файл или директория были переименованы и FileName содержит старое имя файла или директории
FILE_ACTION_RENAMED_NEW_NAME (0x00000005) - файл или директория были переименованы и FileName содержит новое имя файла или директории
FileNameLength - длина имени файла.
FileName[1] - указатель на строку, содержащую имя файла.
То, что структура FILE_NOTIFY_INFORMATION не определена ещё не значит, что мы её не способны определить сами. Давайте определим в модуле Monitor.pas следующий тип данных:
type
FILE_NOTIFY_INFORMATION = record
NextEntryOffset: DWORD;
Action: DWORD;
FileNameLength: DWORD;
FileName: array [0 .. 0] of WCHAR;
end; |
Теперь остается только рассмотреть метод FindChanges:
procedure TScanLocalThread.FindChanges;
var
fni: ^FILE_NOTIFY_INFORMATION;
ws: WideString;
begin
fni := @Buf;
while True do
begin
{получаем имя файла}
SetLength(ws, fni^.FileNameLength div SizeOf(WideChar));
Move(fni^.FileName, ws[1], fni^.FileNameLength);
case fni^.Action of
FILE_ACTION_ADDED: {действие на добавление файла};
FILE_ACTION_REMOVED:{действие на удаление файла};
FILE_ACTION_MODIFIED:{действие на изменение файла};
FILE_ACTION_RENAMED_OLD_NAME, FILE_ACTION_RENAMED_NEW_NAME:{действие на переименование файла}
end;
{переходим к следующей записи или выходим из цикла}
if fni^.NextEntryOffset > 0 then
fni := pointer(Cardinal(fni) + fni^.NextEntryOffset)
else
Break;
end;
end; |
В целом состав этого метода будет зависеть от ваших целей, поэтому я не стал перегружать исходник какими-то своими переменными и дополнительными методами, но думаю, что смысл чтения изменений будет Вам понятен.
На этом рассмотрение темы "Мониторинг изменений в директориях и файлах средствами Delphi" можно было бы считать законченным, но есть несколько моментов, о которых следует сказать. В целом можно отметить, что использование этого способа контроля изменений в директориях вместо использования всякого рада таймеров, речь о которых шла в первой части, является более предпочтительным и правильным выбором. Однако иногда можно столкнуться с некоторыми моментами, когда решения проблемы не "лежит на поверхности". К примеру, может возникнуть проблема отслеживания изменений в файлах Microsoft Office. Дело в том, что Word, Excel и PowerPoint, несмотря на то, что относятся к одному пакету программ, сохраняют свои файлы разными способами. Тот же Word при открытии файла создает временный скрытый файл, куда заносятся изменения, а после нажатия кнопки "Сохранить", вначале копирует всё содержимое файла в новый временный файл и только потом переименовывает его. У Excel алгоритм сохранения и работы с файлом ещё более "замороченный". И все эти создания временных файлов, пересохранения и переименования временных файлов каждый раз будут "провоцировать" наш поток выдавать новые и новые записи по изменениям. И тут, по крайней мере пока, я ещё не нашел более-менее универсального способа "отсеивания" лишних событий на этапе их получения, кроме как воспользоаться способом представленным в самом первом примере этого поста, т.е. разбивать фильтр на несколько событий и каждое событие анализировать по отдельности. Так что, если у Вас есть более правильное решение вопроса "Как правильно отлавливать изменение в файле MS Office?", то буду благодарен, если предложите его решение в комментариях к этому посту.