Monitorando Diretórios com FindFirstChangeNotification

 

1. Introdução

Você já teve que fazer um processo de importação de arquivos onde era preciso monitorar uma pasta do sistema operacional? Uma solução possível era colocar um timer que é acionado de X em X segundos e no evento de timer listar os arquivos da pasta em questão para saber se existia algum arquivo com a extensão Y para processar.

A questão é que podemos utilizar a API do Windows para que o Sistema Operacional nos notifique, ou seja, nos avise sobre criação/exclusão/alteração de arquivos ou então saber se um diretório teve seu tamanho modificado, se os atributos de algum arquivo modificou e por aí vai.

Nesta dica iremos trabalhar com duas funções da API do Windows, FindFirstChangeNotification e FindNextChangeNotification. Em próximas dicas iremos verificar outras formas de fazer este processo de monitoramento.

2. Estrutura do exemplo

Iremos construir uma aplicação com um formulário, onde poderemos escolher um diretório a ser monitorado. Teremos uma funcionalidade principal que vai permitir iniciar o processo de monitoramento e parar o mesmo. É o botão que vai controlar uma Thread que teremos dentro do nosso sistema, que vai ser notificada pelo sistema operacional.

Para quem não está familiarizado com o termo Thread pense em um processo extra que é executado junto com nossa aplicação. Desta forma conseguimos ter um trabalho executando "em plano de fundo". É como por exemplo você estar no seu programa de email lendo mensagens enquanto novas estão chegando. O sistema utiliza Threads para permitir este tipo de funcionalidade.

3. O código da Thread

A nossa classe deve ser derivada de TThread. Para criar uma Thread no Delphi, você pode acessar o menu File -> New -> Other e Selecionar na aba "New" a opção "Thread Object". Na tela que se abre, você pode digitar o nome da classe, por exemplo TMonitorThread.

Na Listagem 1 verificamos o código do corpo da Thread:

 

TMonitorThread = class(TThread)

private

   FDiretorio: string;

   FVerificaSubDiretorios: boolean;

   FOpcaoBusca: Cardinal; //semelhante ao DWORD

   FWaitHandle: Integer;

   FAvisaModificacao: TThreadMethod;

protected

   procedure Execute; override;

public

   procedure ConfiguraMonitor(ADiretorio: string; AVerificaSubDiretorios:Boolean;

     AOpcaoBusca: Cardinal; AAvisaModificacao: TThreadMethod);

   destructor Destroy; override;

end;

 

As variáveis existentes são as seguintes:

FDiretorio: indica o diretório a ser monitorado;

FVerificaSubDiretorios: indica se o monitor deve também verificar sub pastas;

FOpcaoBusca: Nesta opção fica configurado qual o tipo de monitor deve ser utilizado. Todas opções se referem a operações ocorridas em uma determinada pasta. Estas opções podem ser agrupadas com o uso de "or" (exemplo: FILE_NOTIFY_CHANGE_FILE_NAME or FILE_NOTIFY_CHANGE_DIR_NAME).

·         FILE_NOTIFY_CHANGE_FILE_NAME: qualquer modificação em nome de arquivos deve ser notificada. As modificações aqui são criar arquivos, excluir arquivos e renomear arquivos.

·         FILE_NOTIFY_CHANGE_DIR_NAME: monitora modificações em nomes de pastas. Notifica criação, exclusão e alteração de nome de pastas (diretórios).

·         FILE_NOTIFY_CHANGE_ATTRIBUTES: monitora modificações nos atributos de arquivos.

·         FILE_NOTIFY_CHANGE_SIZE: monitora modificações em tamanho de arquivos.

·         FILE_NOTIFY_CHANGE_LAST_WRITE: monitora modificações na data de última atualização dos arquivos.

·         FILE_NOTIFY_CHANGE_SECURITY: monitora modificações na segurança da pasta. Exemplo, adicionada permissão de leitura para um usuário na pasta sendo monitorada.

FWaitHandle: É um handle para o processo de espera da notificação por parte do sistema operacional. Com isto sabemos quando devemos iniciar alguma ação.

FAvisaModificacao: Note o tipo de dado TThreadMethod. Um ThreadMethod é um procedimento sem parâmetros! Então qualquer método sem parâmetros pode ser associado a esta propriedade. O objetivo com isto é que o monitor (Thread) notifique a nossa interface gráfica que ocorreu alguma coisa na pasta sendo monitorada.

Como estamos implementando uma Thread, o trabalho todo é responsabilidade do método "execute", chamado quando a Thread inicia o seu trabalho. Veja na listagem 2 o código do método execute:

 

procedure TMonitorThread.Execute;

var

   Obj: DWORD;

begin

   FWaitHandle := FindFirstChangeNotification(PChar(FDiretorio),

     LongBool(FVerificaSubDiretorios), FOpcaoBusca);

   if FWaitHandle = ERROR_INVALID_HANDLE then

     Exit;

   while not Terminated do

   begin

     Obj := WaitForSingleObject(FWaitHandle, INFINITE);

     case Obj of

       WAIT_OBJECT_0:

         begin

           // Se existe método associado e a thread não foi terminada avisa!

           if (Assigned(FAvisaModificacao)) and (not terminated) then

             // chamada sincronizada, necessária quando vai manipular objetos da VCL

             Synchronize(FAvisaModificacao);

           FindNextChangeNotification(FWaitHandle);

         end;

       WAIT_FAILED:

         Exit;

     end;

   end;

end;

 

O comando mais importante do método execute é a chamada para FindFirstChangeNotification. O primeiro parâmetro indica o diretório a ser monitorado, o segundo parâmetro indica se devemos observar sub diretórios também e o terceiro parâmetro indica o tipo de monitoramento que será realizado. Exemplo, FILE_NOTIFY_CHANGE_FILE_NAME.

O retorno desta função é um Handle, que utilizamos em seguida na função WaitForSingleObject. A nossa função fica "parada" neste local esperando que alguma ação ocorra com o nosso Handle FWaitHandle. O retorno da chamada para WaitForSingleObject é avaliada e se o retorno for "WAIT_OBJECT_0", ocorreu o evento desejado. Existe outra assinatura possível de utilizar que é a chamada para WaitForMultipleObjects, que não entrarei em detalhes aqui, mas ela permite enviar vários Handles para avaliação.

Dentro desta ação do WAIT_OBJECT_0, avaliamos se a nossa thread está em execução e também avaliamos se existe um ThreadMethod associado. Isto ok, chamamos o ThreadMethod e desta forma a nossa interface gráfica poderá executar alguma ação dentro da pasta. Note que a chamada foi realizada utilizando "Synchronize", o que faz com que o acesso a esta função seja "enfileirado". Precisamos disto pois os componentes Windows não são multi-threaded e não saberiam resolver várias chamadas simultâneas para modificar o estado de um componente da interface.

4. Código de controle da Thread

Agora vamos ver como a nossa interface gráfica controla a execução da Thread. O jogo todo ocorre no pressionamento do botão "Iniciar verificação de mudanças na pasta". Quando o usuário pressiona este botão, a Thread de monitoramento é iniciada e o estado do botão troca o caption para "Parar verificação de mudanças na pasta".

O botão "Criar arquivo .bak" fica sempre disponível. Este botão simplesmente cria um arquivo que tem no seu nome a data e hora atual. O objetivo com isto é fazer com que a nossa interface receba uma notificação quando um arquivo for criado.

Outra forma de fazer o teste, é (1) habilitar a verificação de mudanças, (2) ir até a pasta que está marcada para ser monitorada e executar alguma operação como renomear um arquivo, criar um arquivo ou excluir algum arquivo. Você vai notar que a interface gráfica está sendo notificada que algo ocorreu na pasta. Na listagem 3 temos o código do método SetAtivo, que recebe um parâmetro indicando o estado de monitoramento de ser verdadeiro ou falso. Temos uma propriedade chamada Ativo na interface gráfica que é modificada no pressionamento do botão (trocamos o seu valor de true->false, false->true).

 

procedure TFormPrincipal.SetAtivo(const Value: boolean);

begin

   FAtivo := Value;

   case FAtivo of

     true:

       begin

         if (DirectoryExists(edNomePasta.Text)) then

         begin

           FThread := TMonitorThread.Create(true);

           btnMonitor.Caption := 'Parar verificação de mudanças na pasta';

           FThread.ConfiguraMonitor(edNomePasta.Text, true,

             FILE_NOTIFY_CHANGE_FILE_NAME, AvisaModificacao);

           FThread.Resume; // inicia a thread

         end;

       end;

     false:

       begin

         if (FThread <> nil) then

         begin

           FThread.Terminate; // termina a thread

           FreeAndNil(FThread);

         end;

         btnMonitor.Caption := 'Iniciar verificação de mudanças na pasta';

       end;

   end;

end;

 

Note que sempre que se pede para parar o monitoramento a Thread está sendo destruída. Além disto, existe uma Action configurada em um componente ActionList que faz um controle para desabilitar os botões de seleção de pasta quando a Thread está ativa. Isto para que o usuário não consiga modificar a pasta selecionada durante a execução da Thread. O ideal seria ter um controle na Thread para que ela verificasse as chamadas ao método ConfiguraMonitor. Seria alguma notificação para que o método execute saiba que tem que reconfigurar o processo de Find que verificamos no código da Thread.

Estas são mudanças que você pode trabalhar para melhorar o código e adaptar a dica as suas necessidades.

5. Conclusões

A grande questão no uso das funções FindFirstChangeNotification e FindNextChangeNotification é que nossa aplicação é notificada de mudanças, mas se estivermos monitorando mais de uma informação, como por exemplo "FILE_NOTIFY_CHANGE_SIZE or FILE_NOTIFY_CHANGE_SECURITY or FILE_NOTIFY_CHANGE_DIR_NAME", não saberemos qual das três operações ocorreu.

Neste caso pode ser interessante ter três instâncias do nosso monitor, para que cada um fique responsável por um tipo de operação.

Nas próximas dicas veremos uma outra forma de trabalhar com monitoramento de pastas, onde poderemos saber qual operação ocorreu.

Por hoje era isto pessoal. Até a próxima!