Ceci est une ancienne révision du document !


Surveiller modification Fichier/Répertoire

Pistes de recherches

FindFirstChangeNotification

Cette méthode est documentée ici → Obtention de notifications de modification de répertoire

Apparemment cette méthode permet de savoir si un dossier a été modifier, mais pas de connaitre les modification apportées. Quel fichier modifié ? Quel fichier ajouté ou supprimé ?

Comment ça marche ?

Pour faire simple :

On récupere les Handle des répértoires a surveiller avec FindFirstChangeNotification

function FindFirstChangeNotification(lpPathName:LPCSTR; bWatchSubtree:WINBOOL; dwNotifyFilter:DWORD):HANDLE;
  • lpPathname = PChar('C:\MonRepertoire\Etc\')
  • bWatchSubtree = boolean → on surveille juste le répertoire (False) ou le répertoire et ses enfants ? (True)
  • dwNotifyFilter = dWord → Changement que l'on veut capturer (Filename, Dirname, Attributes, Size…) plus d'infos ici

On met ces Handle dans un array de ce type : (par exemple)

THandleList = array [0..MAXIMUM_WAIT_OBJECTS - 1] of THandle;

Ensuite on attends une modification avec WaitForMultipleObjects:

function WaitForMultipleObjects( nCount:DWORD; lpHandles : PWOHandleArray; bWaitAll:WINBOOL; dwMilliseconds:DWORD):DWORD
  • nCount = nombre de handle dans la liste
  • lpHandles = pointer vers le premier enregistrement de la liste de type THandleList (voir ci-dessus) (dans notre cas=@HandleList[0])
  • bWaitAll = Boolean → On attend toutes les modifs ??? (False dans notre cas)
  • dwMilliseconds = Temps d'attente en Millisecondes

La fonction WaitForMultipleObjects retourne un DWord qui peut être interprété de la façon suivante:

  • $102 (258) = TIMEOUT, le temps d'attente (dwMilliseconds) s'est écoulé.
  • $FFFFFFFF (-1) = FAILED !
  • $0..$40 = Index (+WAIT_OBJECT_0) du Handle dans la liste correspondant au répertoire qui a été modifié
  • $80..$C0 = Index (+WAIT_ABANDONED_0) du Handle qui… je sais pas trop

Plus d'infos → Valeur de retour de la fonction WaitForMultipleObjects

On traite la valeur retournée par la fonction WaitForMultipleObjects.
Puis on retourne a la surveillance par un FindNextChangeNotification et rebouclage sur WaitForMultipleObjects:

function FindNextChangeNotification(hChangeHandle:HANDLE):WINBOOL;

ou le parametre hChangeHandle est le Handle du répertoire modifié precedement.
Ce Handle est récuperable dans la liste des Handles THandleList passé a la fonction WaitForMultipleObjects a l'index retourné par cette méme fonction… (suis-je bien claire ???)

Exemple de code (Dans un Thread, car WaitForMultipleObjects est une fonction bloquante…) :

   // vIndex: integer;
   // vHandleList : THandleList (voir ci-dessus)
   // FCount = nombre de handle dans la liste vHandleList
   // FWaitMs = nombre de millisecondes a attendre
   // FDirChangedHandle = Handle du Find Change Notifier du répertoire qui a été modifié
   // Le Handle du Find Change Notifier est retourné par la function FindFirstChangeNotification (voir ci-dessus)
         while not Terminated do
           begin
            vIndex := WaitForMultipleObjects(FCount, vHandleList[0], False, FWaitMs);
            if (vIndex >= WAIT_OBJECT_0) and
               (vIndex < WAIT_OBJECT_0 + FNHandleList.Count)  then
              begin
                  FDirChangedHandle :=FHandleList[vIndex - WAIT_OBJECT_0];
                  ...
                  FindNextChangeNotification( FDirChangedHandle);
              end;
           end;

ReadDirectoryChangesW

Voici un exemple de code pour l'utilisation de ReadDirectoryChangesW:

DirMonDOS.lpr
program DirMonDOS;
 
{$mode objfpc}{$H+}
 
uses
 {$IFDEF UNIX}
 {$IFDEF UseCThreads}
   cthreads,
 {$ENDIF}
 {$ENDIF}
   Classes,
   crt,
   JwaWindows,
   SysUtils;
 
var
   FPathName: string;
   FDirHandle: THandle;
   FBuffer:  array[0..9999] of byte;
   FBytesReturned: dword;
   FOverLapped: TOverlapped;
   FOverResult: boolean;
   FFileNotifyInformation: PFileNotifyInformation;
   FOffset:  dword;
   FAction:  dword;
   FActionStr: string;
   FFilename: string;
   FLen:     dword;
   FTerminate: boolean;
   FLastError: dword;
   FNotifyFilter: DWord;
   FLooping: boolean;
 
begin
   { Répertoire à surveiller }
   FPathName     := 'J:\';
 
   { Initialisation des variables }
   FTerminate := False;
   FLooping   := False;
   ZeroMemory(@FBuffer, sizeof(FBuffer));
   FNotifyFilter := FILE_NOTIFY_CHANGE_FILE_NAME or FILE_NOTIFY_CHANGE_DIR_NAME or
      FILE_NOTIFY_CHANGE_LAST_WRITE;
   { FILE_NOTIFY_CHANGE_FILE_NAME = 1;
     FILE_NOTIFY_CHANGE_DIR_NAME = 2;
     FILE_NOTIFY_CHANGE_ATTRIBUTES = 4;
     FILE_NOTIFY_CHANGE_SIZE = 8;
     FILE_NOTIFY_CHANGE_LAST_WRITE = 16;
     FILE_NOTIFY_CHANGE_LAST_ACCESS = 32;
     FILE_NOTIFY_CHANGE_CREATION = 64;
     FILE_NOTIFY_CHANGE_SECURITY = 256; }
 
   writeln('Press [s] to exit');
   WriteLn('Surveillance du dossier [', FPathName, ']');
 
   { Récuperation du Handle du Répertoire a surveiller }
   FDirHandle := CreateFile(PChar(FPathName), FILE_LIST_DIRECTORY,
      FILE_SHARE_READ or FILE_SHARE_DELETE or FILE_SHARE_WRITE, nil,
      OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS or FILE_FLAG_OVERLAPPED, 0);
   WriteLn('Handle du dossier [', FDirHandle, ']');
 
 
   if FDirHandle <> INVALID_HANDLE_VALUE then
     begin
        try
          { Bouclage jusqu'a ce que Terminate=True }
          repeat
             { Mise en place de la surveillance }
             if ReadDirectoryChangesW(FDirHandle, @FBuffer, sizeOf(FBuffer),
               True, FNotifyFilter, @FBytesReturned, @FOverLapped, nil) then
              begin
 
               { Bouclage jusqu'a ce que FOverResult=True }
               repeat
                  { Si FOverResult = True, cela signifie qu'il y a eu des modifications }
                  FOverResult :=
                     GetOverlappedResult(FDirHandle, FOverLapped,
                     FBytesReturned, False);
                  { Je capture GetLastError ci-dessous, car il peut etre modifié
                    par d'autres instructions, comme un WriteLn par exemple }
                  FLastError  := GetLastError;
 
                  { Affichage des informations pour faire beau }
                  if not FOverResult and not FLooping then
                    begin
                     Writeln('Looping...');
                     FLooping := True;
                    end;
                  sleep(100);
                  { Verification si on a pressé la touche [S] pour arreter le processus }
                  if KeyPressed and (ReadKey = 's') then
                     FTerminate := True;
 
               until FOverResult or (FLastError <> ERROR_IO_INCOMPLETE) or FTerminate;
               FLooping := False;
 
               if FOverResult then
                 begin
                  Writeln('--- begin modification ---');
 
                  { Positionnement sur le debut du Buffer }
                  pointer(FFileNotifyInformation) := @FBuffer[0];
                  repeat
                     FOffset := FFileNotifyInformation^.NextEntryOffset;
                     FAction := FFileNotifyInformation^.Action;
                     { FLen = FileNameLength/2
                       car FileName est codé en WideString
                       et WideString est 2x plus long que AnsiString
                       ->https://wiki.freepascal.org/Character_and_string_types/fr }
                     FLen    := FFileNotifyInformation^.FileNameLength div 2;
                     WideCharLenToStrVar(@(FFileNotifyInformation^.FileName),
                        FLen, FFilename);
 
                     case FAction of
                        FILE_ACTION_ADDED: FActionStr    := 'Created';
                        FILE_ACTION_REMOVED: FActionStr  := 'Deleted';
                        FILE_ACTION_MODIFIED: FActionStr := 'Modified';
                        FILE_ACTION_RENAMED_OLD_NAME: FActionStr := 'RenamedFrom';
                        FILE_ACTION_RENAMED_NEW_NAME: FActionStr := 'RenamedTo';
                        else
                           FActionStr := '???';
                       end;
 
                     { Affichage du résultat }
                     WriteLn('Action [', FAction, '|', FActionStr, '] len[',
                        FLen, '] sur fichier [', FFilename, ']');
 
                     { Positionnement sur le prochain FFileNotifyInformation }
                     pointer(FFileNotifyInformation) :=
                        pointer(FFileNotifyInformation) + FOffset;
 
                  until FOffset = 0;
                  { Nettoyage du Buffer pour eviter d'accumuler des parasites
                    surtout dans Filename }
                  ZeroMemory(@FBuffer, sizeof(FBuffer));
                  Writeln('--- end modification ---');
                 end;
              end;
         until FTerminate;
        finally
         CloseHandle(FDirHandle);
        end;
     end
   else
      Writeln('Dir Handle invalid');
   Writeln('Press to exit');
   Readln;
 
end.

A la Procmon

Procmon est un excellent utilitaire qui montre en temps réel les accès aux fichiers par les processus.
Reste a savoir comment il fait ?

Apparemment cette solution impliquerai l'utilisation d'un drivers annexe, ce qui reste relativement fastidieux…

Piste OpenFilesView de Nirsoft

Une piste peut etre sur la page→https://www.nirsoft.net/utils/opened_files_view.html.
Un programme de Nirsoft apparement similaire a Procmon qui fonctionne de la facon suivante:

OpenedFilesView utilise l'API NtQuerySystemInformation pour énumérer tous les handles dans le système.
Après avoir filtré les handles non-fichiers, il utilise un driver temporaire - NirSoftOpenedFilesDriver.sys pour lire les informations sur chaque handle depuis la mémoire du noyau.
Ce driver est automatiquement déchargé du système lorsque vous quittez l'utilitaire OpenedFilesView.

Projet de ShareVB

En vrac à étudier

Sources & Ressources

Vous pourriez laisser un commentaire si vous étiez connecté.