Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Следим за изменениями в файловой системе (Часть 1)


Информационный Канал Subscribe.Ru

Программирование для Windows NT на Delphi
Выпуск номер : 7 [ 13 сентября 2002 года]


Здравствуйте, уважаемые подписчики.

Не прошло и месяца с прошлого выпуска, как мы снова с вами :). "По причинчиским технинам" пока нет возможности выпускать рассылку с прежней переодичностью, поэтому теперь она будет выходить 2-4 раза в месяц (вместо 1 раз в неделю). Кол-во выпусков будет зависеть от наличия свободного времени и .. читательской (т.е. вашей) активности. Присылайте ваши вопросы и предложения. Может кто хочет разместить свою (т.е. написаную вами, а не позаимствованную откуда-нибудь) статью - пишите ваши условия на flint@vtc.ru

Сегодня в рассылке:
- Следим за изменениями в файловой системе


Статья
Следим за изменениями в файловой системе


Предположим вы пишите программу, следящую за изменениями, происходящими в файловой системе (например, антивирусы, программы, протоколирующие установку софта, для его полной деинсталляции в дальнейшем и т.д.). Задача очень распространенная, судя по тому, что ответ на этот вопрос есть во многих FAQ. В этой статье мы тоже рассмотрим эту задачу, но уже с учетом Windows NT.

Рассмотрим три варианта решения (варианты использования драйверов и недокументированных функций мы рассматривать не будем :)

1. Использование Find[First/Next/Close]ChangeNotification
Единственный плюс - работоспособность на всех более-менее современных Windows : c NT3.1 и Win95.
Все очень просто - функцией FindFirstChangeNotification создается объект, описатель которого она возвращает. Потом вызывается функция (например,WaitForSingleObject), ждущая пока созданный объект просигнализирует об изменении файловой системы. Вот собственно и все.. что может дать эта функция. Т.е. мы можем узнать, что в заданном каталоге что-то изменилось (типы изменений - создание, изменение и т.д. - можно задать в параметрах FindFirstChangeNotification), но не можем сразу узнать, что именно. Придется пересматривать весь каталог и проверять время последней модификации (при изменении файла) или сравнивать содержимое каталогов с заранее составленным списком (чтобы проверить создание/удаление файлов или каталогов в нем). Если каталог небольшой, то это не сильно большая проблема, но если приглядывать за всем диском, то времени на обход и сравнение будет уходить очень много.

Примеры использования этих функций я приводить не буду, т.к. найти их очень легко можно в yandex.ru или на delphi.mastak.ru была статья на эту тему.
Плавно переходим ко второму варианту

2.Использование ReadDirectoryChangesW
Очень хорошая функция, только "Windows 95/98/Me: Unsupported". Т.е. в Win9x этой функции нет. Но мы же пишем для NT, а там с этой функцией все в порядке (если 3-ий сервис пак для NT3.1 поставили :)))

Кратко пройдемся по описанию функции (взято из windows.pas)

function ReadDirectoryChangesW(
  hDirectory: THandle; // описатель каталога, за которым надо следить
  lpBuffer: Pointer;  // Указатель на буфер, в который будет записана информация
  nBufferLength: DWORD; // Размер буфера
  bWatchSubtree: Bool; // Следить ли за подкаталогами
  dwNotifyFilter: DWORD; // Фильтр действий
  lpBytesReturned: LPDWORD; // Сколько было записано в буфер
  lpOverlapped: POverlapped; // Для асинхронной работы
  lpCompletionRoutine: FARPROC // Функция, которая будет вызвана при окончании операции
  ): BOOL; stdcall;
Ну а теперь пример работы этой функции (исходник этого примера (пока без комментариев!! и на Delphi6) можно скачать здесь - http://www.delphi.xonix.ru/download/filemon1/readdirchanges.zip )

Чтобы программа могла нормально работать во время ожидания очередного изменения, мы функции мониторинга выделим отдельный поток. Поток "сделан" на WinAPI (функция WorkThread). При нажатии на одну кнопку он будет создаваться, а на другую - жестоко уничтожаться.
Вся полезная информация будет выводиться в TListView.

Функция потока будет описана так :
procedure WorkThread(LV : TListView);stdcall;
LV - это то, во что мы будем выводить инфу. И не забывайте stdcall;

А вот ее текст
procedure WorkThread(LV : TListView);stdcall;
var
 hDir : THandle;

 lpBuf : Pointer;
 Ptr   : Pointer;

 cbReturn : Cardinal;
 FileName : PWideChar;

 Item : TListItem;
 sTime : _SYSTEMTIME;
begin
 // Сначала нам надо получить описатель каталога, за которым мы будем следить
 // В данном примере это будет весь диск C:
 hDir := CreateFile ('C:\',GENERIC_READ,FILE_SHARE_READ or FILE_SHARE_WRITE
   or FILE_SHARE_DELETE,nil,OPEN_EXISTING,FILE_FLAG_BACKUP_SEMANTICS,0);
 // Если ошиблись...
 if hDir = INVALID_HANDLE_VALUE
  then begin ShowMessage(SysErrorMessage(GetLastError)); exit; end;

 // Выделяем память под буфер
 // const BUF_SIZE = 2048 - думаю вполне достаточно
 GetMem(lpBuf,BUF_SIZE);

 repeat
  // очищаем память перед записью в нее (на всякий случай)
  ZeroMemory(lpBuf,BUF_SIZE);

  // Теперь мы будем ждать пока чего-нибудь в интересующем нас каталоге
  // изменится или произойдет ошибка (и мы выйдем из цикла)
  // FILE_NOTIFY_CHANGE - это список флагов - о них ниже.
  if not ReadDirectoryChangesW(hDir,lpBuf,BUF_SIZE,true,
                   FILE_NOTIFY_CHANGE,@cbReturn,nil,nil)
   then Break;

  // Сюда мы попадаем, если функция выполнилась успешно
  // и lpBuf указывает на одну или несколько структур FILE_NOTIFY_INFORMATION
  Ptr:=lpBuf;
Отойдем пока от исходного кода и рассмотрим, что у нас появится в буфере.
В данный момент lpBuf и Ptr указывают на первую структуру FILE_NOTIFY_INFORMATION.
Вторым полем этой структуры является - Action -тип действия, которое было совершено. Четвертым - FileName - первый символ имени файла. Имя файла не заканчивается нулем #0 и для определения его длины используется 3 параметр - FileNameLength. При этом надо учесть, что имя файла в формате Unicode т.е. каждый символ занимает 2 байта, а FileNameLength дается в байтах. Придется эту длину делить на 2, чтобы узнать кол-во символов.

Но возникает вопрос - как узнать, сколько таких структур было записано в буфер. Для этого используется 1 параметр структуры - NextEntryOffset. Если он не равен нулю, то в нем будет кол-во байт, через которые находится следующая запись и нам надо сдвинуть указатель на это кол-во байт, чтобы "получить" следующую структуру. И так далее, пока NextEntryOffset не будет равен 0 (т.е. эта запись была последней).

  repeat
   // Добавляем новый элемент в TListView (Viewstyle="vsReport" )
   Item := LV.Items.Add;

   // Выделяем память под имя файла
   GetMem(FileName,PFileNotifyInformation(Ptr).FileNameLength+2);
   // Очищаем память - чтобы последним символом после копирования
   // был бы #0 нуль
   ZeroMemory(FileName,PFileNotifyInformation(Ptr).FileNameLength+2);
   // WinAPI функция для копирования Unicode строки
   lstrcpynW(FileName,PFileNotifyInformation(Ptr).FileName,
             PFileNotifyInformation(Ptr).FileNameLength div 2+1);
   // Имя файла у нас дается относительно папки
   // т.е.если изменится файл C:\File\test.dat, то FileName
   // будет равно File\test.dat
   Item.Caption:='C:\'+FileName;
   // Имя файла нам больше не нужно - очищаем память
   FreeMem(FileName);

   // Определяем тип произошедшего действия
   case PFileNotifyInformation(Ptr).Action of
    FILE_ACTION_ADDED    : Item.SubItems.Add('Файл был создан');
    FILE_ACTION_REMOVED  : Item.SubItems.Add('Файл был удален');
    FILE_ACTION_MODIFIED : Item.SubItems.Add('Файл был изменен');
    FILE_ACTION_RENAMED_OLD_NAME :
   Item.SubItems.Add('Файл был переименован и в имени файла - предыдущее имя');
    FILE_ACTION_RENAMED_NEW_NAME :
  Item.SubItems.Add('новое имя после переименования');
    else Item.SubItems.Add('Произошло что-то странное');
   end;

   // Время, когда произошло событие
   GetLocalTime(sTime);
   with sTime do
    Item.SubItems.Add(Format('%.2d:%.2d:%.2d',[wHour,wMinute,wSecond])); // 13:54:20

   // Если эта запись не последняя (NextEntryOffset <> 0), то...
   if PFileNotifyInformation(Ptr).NextEntryOffset=0
    then Break
     else begin
   // ... добавляем строку в примечания (если интересно посмотреть смещение)
      Item.SubItems.Add('Offset : '+
       IntToStr(PFileNotifyInformation(Ptr).NextEntryOffset));
   //Передвигаем указатель на NextEntryOffset байт вперед
      Inc(Cardinal(Ptr),PFileNotifyInformation(Ptr).NextEntryOffset);
   // Теперь Ptr указывает на следующую запись
     end;

  // Передвигать надо именно Ptr, а не lpBuf
  until false;

 until false;

 // Очищаем память
 FreeMem(lpBuf);
end; 


Параметр функции dwNotifyFilter - действия, информацию о которых мы хотим получать.

FILE_NOTIFY_CHANGE_FILE_NAME - создание, удаление, переименование файла.
FILE_NOTIFY_CHANGE_DIR_NAME - созданием и удаление каталога.
FILE_NOTIFY_CHANGE_ATTRIBUTES - изменение атрибутов файла.
FILE_NOTIFY_CHANGE_SIZE - изменение размера файла.
FILE_NOTIFY_CHANGE_LAST_WRITE - изменение файла (это и предыдущее уведомление будет получено только в момент реальной записи файла на диск).
FILE_NOTIFY_CHANGE_LAST_ACCESS - изменение времени последнего доступа.
FILE_NOTIFY_CHANGE_CREATION - изменение времени создания файла.
FILE_NOTIFY_CHANGE_SECURITY - изменение параметров безопасности (прав доступа и т.д.)

У меня в примере используются FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_DIR_NAME и FILE_NOTIFY_CHANGE_LAST_WRITE.

А теперь надо только запустить поток.

procedure TForm1.Button1Click(Sender: TObject);
var
 ThID : Cardinal;
begin
 // hThread - THandle - глобальная переменная
 // Создаем поток
 // LV - TListView, WorkThread - функция выше
 hThread:=CreateThread(nil,0,@WorkThread,LV,0,ThID);
 // В случае неудачи выводим сообщение
 if hThread=0 then ShowMessage(SysErrorMessage(GetLastError));
end;

У меня в исходниках поток останавливается функцией TerminateThread(hThread,Cardinal(-1)). Но при таком завершении не будут освобождены все ресурсы, занятые потоком (а это как минимум BUF_SIZE байт памяти. Вместо этой функции было бы лучше использовать SuspendThread(hThread), а при запуске проверять на существование потока WaitForSingleObject(hThread,0)= WAIT_TIMEOUT и если он существует - делать ResumeThread(hThread)... но в исходниках этого пока нет :)

А третий вариант будет описан в следующем выпуске (планируется во вторник), а то большие выпуски, имхо, слабо воспринимаются.

С уважением, ведущий рассылки FliNT





http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу

В избранное