Share via



Outubro de 2016

Volume 31 – Número 10

Serviço do Windows - Criar um Serviço do Windows FileSystemWatcher Personalizável

Por Diego Ordonez

A classe FileSystemWatcher é uma ferramenta muito poderosa que tem sido parte do Microsoft .NET Framework desde a versão 1.1 e, de acordo com essa definição oficial, (bit.ly/2b8iOvQ), ela “detecta as notificações de alteração do sistema de arquivos e gera eventos quando um diretório ou arquivo em um diretório é alterado.”

Essa classe detecta eventos no sistema de arquivos, como criar, modificar ou excluir arquivos e pastas. Ela é totalmente personalizável. Seu construtor aceita parâmetros como o local da pasta e a extensão de arquivos para detectar e um parâmetro Booliano para especificar se o processo de detecção deve trabalhar recursivamente através da estrutura da pasta. No entanto, incluir esses parâmetros em seu código-fonte não é uma boa ideia porque eles não ajudarão quando o aplicativo precisar incluir novas extensões de arquivos e pastas, o que, além disso, solicitará codificação, compilação e reimplantação. A menos que você tenha certeza de que seu aplicativo dificilmente irá alterar essas configurações, uma ideia melhor é implementar um mecanismo que possa alterar a configuração sem modificar o código-fonte.

Neste artigo, explorei a forma de gravar um aplicativo que utiliza a classe FileSystemWatcher apenas uma vez, mas, depois, através da serialização XML, permite modificar as configurações dos aplicativos, como nomes de pastas, extensões de arquivos e ações a serem executadas após gerar um evento. Desta forma, todas as alterações podem ser facilmente realizadas. Basta atualizar um arquivo XML e reiniciar o serviço do Windows.

Para simplificar, não explicarei os detalhes sobre como executar este aplicativo de console C# como um serviço do Windows, mas há muitos recursos disponíveis online relacionados a esta questão.

A Estrutura das Configurações de Pastas Personalizadas

Como planejo desserializar o arquivo de configurações XML em uma classe C# bem estruturada, o primeiro componente do aplicativo deve ser a definição que os parâmetros FileSystemWatcher solicitam para operar. A Figura 1 mostra o código que define essa classe.

Figura 1 - Definição da Classe CustomFolderSettings

/// <summary>
/// This class defines an individual type of file and its associated
/// folder to be monitored by the File System Watcher
/// </summary>
public class CustomFolderSettings
{
  /// <summary>Unique identifier of the combination File type/folder.
  /// Arbitrary number (for instance 001, 002, and so on)</summary>
  [XmlAttribute]
  public string FolderID { get; set; }
  /// <summary>If TRUE: the file type and folder will be monitored</summary>
  [XmlElement]
  public bool FolderEnabled { get; set; }
  /// <summary>Description of the type of files and folder location –
  /// Just for documentation purpose</summary>
  [XmlElement]
  public string FolderDescription { get; set; }
  /// <summary>Filter to select the type of files to be monitored.
  /// (Examples: *.shp, *.*, Project00*.zip)</summary>
  [XmlElement]
  public string FolderFilter { get; set; }
  /// <summary>Full path to be monitored
  /// (i.e.: D:\files\projects\shapes\ )</summary>
  [XmlElement]
  public string FolderPath { get; set; }
  /// <summary>If TRUE: the folder and its subfolders will be monitored</summary>
  [XmlElement]
  public bool FolderIncludeSub { get; set; }
  /// <summary>Specifies the command or action to be executed
  /// after an event has raised</summary>
  [XmlElement]
  public string ExecutableFile { get; set; }
  /// <summary>List of arguments to be passed to the executable file</summary>
  [XmlElement]
  public string ExecutableArguments { get; set; }
  /// <summary>Default constructor of the class</summary>       
  public CustomFolderSettings()
  {
  }
}

Agora vamos observar como um arquivo XML pode ser traduzido para esta classe C# usando o processo de desserialização. Observe que não haverá uma instância única da classe CustomFolderSettings. Em vez disso, haverá uma lista (List<CustomFolderSettings>) permitindo que o serviço do Windows detecte vários locais de pastas e extensões de arquivos diferentes.

A Figura 2 mostra um exemplo de um arquivo de configurações XML do qual posso fornecer FileSystemWatcher com todos os argumentos que ele necessita para funcionar. Neste ponto, é importante entender que as informações contidas no arquivo XML (Figura 2) agregará a classe C# (Figura 1).

Figura 2 - Estrutura do Arquivo de Configurações XML

<?xml version="1.0" encoding="utf-8"?>
<ArrayOfCustomFolderSettings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <CustomFolderSettings FolderID="ExampleKML_files">
    <FolderEnabled>true</FolderEnabled>   
    <FolderDescription>Files in format KML corresponding to the example project
      </FolderDescription>
    <FolderFilter>*.KML</FolderFilter>
    <FolderPath>C:\Temp\testKML\</FolderPath>
    <FolderIncludeSub>false</FolderIncludeSub>
    <ExecutableFile>CMD.EXE</ExecutableFile>
    <!-- The block {0} will be automatically replaced with the
      corresponding file name -->
    <ExecutableArguments>/C echo It works properly for .KML extension-- File {0}
      &gt; c:\temp\it_works_KML.txt</ExecutableArguments>
  </CustomFolderSettings>
  <CustomFolderSettings FolderID="ExampleZIP_files">
    <FolderEnabled>false</FolderEnabled>
    <FolderDescription>Files in format ZIP corresponding to the example project
      </FolderDescription>
    <FolderFilter>*.ZIP</FolderFilter>
    <FolderPath>C:\Temp\testZIP\</FolderPath>
    <FolderIncludeSub>false</FolderIncludeSub>
    <ExecutableFile>CMD.EXE</ExecutableFile>
    <!-- The block {0} will be automatically replaced with the
      corresponding file name -->
    <ExecutableArguments>/C echo It works properly for .ZIP extension -- File {0}
      &gt; c:\temp\it_works_ZIP.txt</ExecutableArguments>
  </CustomFolderSettings>
</ArrayOfCustomFolderSettings>

Agora vamos observar detalhadamente os parâmetros contidos no arquivo XML. Primeiro, observe que o elemento raiz XML é <ArrayOfCustomFolderSettings> e isso permite vários elementos <CustomFolderSettings> conforme necessário. Esta é a chave para poder monitorar simultaneamente vários locais de pastas e extensões de arquivos.

Em segundo lugar, observe que o parâmetro <FolderEnabled> é verdadeiro para a primeira pasta, mas falso para a segunda. Esta é uma maneira fácil de desabilitar um dos FileSystemWatchers sem ter de excluí-lo do arquivo XML, o que significa que, mesmo que a configuração esteja presente, a classe o omitirá quando estiver em execução.

Por fim, é importante entender como especificar qual ação será disparada após a detecção de um arquivo que foi criado, excluído ou modificado, que é o objetivo final da classe FileSystemWatcher.

O parâmetro <ExecutableFile> contém o aplicativo que será iniciado, neste exemplo a linha de comando DOS (CMD.EXE).

O parâmetro <ExecutableArguments> contém as opções que serão passadas para o executável como argumentos. Veja um exemplo na Figura 2:

>/C echo It works properly for .ZIP extension -- File {0} &gt;
  c:\temp\it_ZIP_works.txt

Ele será traduzido para o tempo de execução a seguir:

CMD.EXE /C echo it works properly for .ZIP extension –– File
  d:\tests\file_modified_detected.doc > c:\temp\it_works_ZIP.txt

Ele gravará a cadeia de caracteres no arquivo c: \temp\it_works_ZIP.txt, e o valor {0} no XML será substituído pelo nome real em que o arquivo FileSystemWatcher foi detectado. Se você estiver familiarizado com o método C# string.Format, não terá problemas para descobrir.

Neste ponto, tenho um arquivo de configuração XML e uma classe C# com atributos correspondentes, então, a próxima etapa é desserializar a informação XML para uma lista de classes (List<CustomFolderSettings>). A Figura 3 mostra o método que realiza essa etapa fundamental.

Figura 3 - Desserialização do Arquivo de Configurações XML

/// <summary>Reads an XML file and populates a list of <CustomFolderSettings> </summary>
private void PopulateListFileSystemWatchers()
{
  // Get the XML file name from the App.config file
  fileNameXML = ConfigurationManager.AppSettings["XMLFileFolderSettings"];
  // Create an instance of XMLSerializer
  XmlSerializer deserializer =
    new XmlSerializer(typeof(List<CustomFolderSettings>));
  TextReader reader = new StreamReader(fileNameXML);
  object obj = deserializer.Deserialize(reader);
  // Close the TextReader object
  reader.Close();
  // Obtain a list of CustomFolderSettings from XML Input data
  listFolders = obj as List<CustomFolderSettings>;
}

Quando este método for executado, uma lista contendo todas as instâncias necessárias de FileSystemWatcher ficará disponível. Em seguida, a próxima etapa será iniciar a classe FileSystemWatcher que começa o processo de escuta.

O método precisa saber onde está o arquivo de configurações XML e eu uso o arquivo App.config para definir o local do arquivo XML. Veja o conteúdo de App.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="XMLFileFolderSettings" value=
      "C:\Work\CSharp_FileSystemW\CustomSettings.xml" />
  </appSettings>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
</configuration>

É importante lembrar que quaisquer alterações no arquivo de configurações XML ou no arquivo App.config solicitará a reinicialização do serviço do Windows para que essas alterações sejam aplicadas.

Iniciando o Processo FileSystemWatcher (Escuta das Alterações)

Neste ponto, todas as configurações solicitadas para as várias (ou pelo menos uma) instâncias de FileSystemWatcher estarão disponíveis na lista criada na Figura 3.

Agora é hora de iniciar o processo de escuta. Para isso, preciso executar um loop na lista e iniciar as instâncias uma a uma. O código na Figura 4 mostra como executar o processo de inicialização e como atribuir todos os parâmetros recuperados do arquivo XML.

Figura 4 - Inicialização das Instâncias FileSystemWatcher

/// <summary>Start the file system watcher for each of the file
/// specification and folders found on the List<>/// </summary>
private void StartFileSystemWatcher()
{
  // Creates a new instance of the list
  this.listFileSystemWatcher = new List<FileSystemWatcher>();
  // Loop the list to process each of the folder specifications found
  foreach (CustomFolderSettings customFolder in listFolders)
  {
    DirectoryInfo dir = new DirectoryInfo(customFolder.FolderPath);
    // Checks whether the folder is enabled and
    // also the directory is a valid location
    if (customFolder.FolderEnabled && dir.Exists)
    {
      // Creates a new instance of FileSystemWatcher
      FileSystemWatcher fileSWatch = new FileSystemWatcher();
      // Sets the filter
      fileSWatch.Filter = customFolder.FolderFilter;
      // Sets the folder location
      fileSWatch.Path = customFolder.FolderPath;
      // Sets the action to be executed
      StringBuilder actionToExecute = new StringBuilder(
        customFolder.ExecutableFile);
      // List of arguments
      StringBuilder actionArguments = new StringBuilder(
        customFolder.ExecutableArguments);
      // Subscribe to notify filters
      fileSWatch.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName |
        NotifyFilters.DirectoryName;
      // Associate the event that will be triggered when a new file
      // is added to the monitored folder, using a lambda expression                   
      fileSWatch.Created += (senderObj, fileSysArgs) =>
        fileSWatch_Created(senderObj, fileSysArgs,
         actionToExecute.ToString(), actionArguments.ToString());
      // Begin watching
      fileSWatch.EnableRaisingEvents = true;
      // Add the systemWatcher to the list
      listFileSystemWatcher.Add(fileSWatch);
      // Record a log entry into Windows Event Log
      CustomLogEvent(String.Format(
        "Starting to monitor files with extension ({0}) in the folder ({1})",
        fileSWatch.Filter, fileSWatch.Path));
    }
  }
}

Neste código, FileSystemWatcher está escutando apenas um evento de criação. No entanto, outros eventos estão disponíveis, como Excluídos e Renomeados.

Quero apontar especialmente para a linha onde a função se inscreve ao evento FileSystemWatcher criado. Aqui, uso uma expressão lambda por um motivo importante: Como tenho uma lista de instâncias da classe FileSystemWatcher, preciso associar um executável específico para cada instância. Se eu tratar isso de forma diferente (ou seja, sem usar uma expressão lambda, mas atribuindo diretamente a função), somente o último executável será mantido e todas as instâncias de FileSystemWatcher realizarão a mesma ação.

A Figura 5 mostra o código para a função que realiza a ação com base no critério individual para cada instância única de FileSystemWatcher.

Figura 5 - Realizando uma Ação com Base no Critério de Cada Instância

/// <summary>This event is triggered when a file with the specified
/// extension is created on the monitored folder</summary>
/// <param name="sender">Object raising the event</param>
/// <param name="e">List of arguments - FileSystemEventArgs</param>
/// <param name="action_Exec">The action to be executed upon detecting a change in the File system</param>
/// <param name="action_Args">arguments to be passed to the executable (action)</param>
void fileSWatch_Created(object sender, FileSystemEventArgs e,
  string action_Exec, string action_Args)
{
  string fileName = e.FullPath;
  // Adds the file name to the arguments. The filename will be placed in lieu of {0}
  string newStr = string.Format(action_Args, fileName);
  // Executes the command from the DOS window
  ExecuteCommandLineProcess(action_Exec, newStr);
}

E, por fim, a Figura 6 mostra a função ExecuteCommandLineProcess, que é uma maneira muito padrão de executar instruções da linha de comando (um console DOS).

Figura 6 - Executando Instruções da Linha de Comando

/// <summary>Executes a set of instructions through the command window</summary>
/// <param name="executableFile">Name of the executable file or program</param>
/// <param name="argumentList">List of arguments</param>
private void ExecuteCommandLineProcess(string executableFile, string argumentList)
{
  // Use ProcessStartInfo class
  ProcessStartInfo startInfo = new ProcessStartInfo();
  startInfo.CreateNoWindow = true;
  startInfo.UseShellExecute = false;
  startInfo.FileName = executableFile;
  startInfo.WindowStyle = ProcessWindowStyle.Hidden;
  startInfo.Arguments = argumentList;
try
  {
    // Start the process with the info specified
    // Call WaitForExit and then the using-statement will close
    using (Process exeProcess = Process.Start(startInfo))
    {
      exeProcess.WaitForExit();
      // Register a log of the successful operation
      CustomLogEvent(string.Format(
        "Succesful operation --> Executable: {0} --> Arguments: {1}",
        executableFile, argumentList));
    }
  }
  catch (Exception exc)
  {
    // Register a Log of the Exception
  }
}

Iniciando e Interrompendo FileSystemWatcher em um Serviço do Windows

Conforme definido inicialmente, este aplicativo foi projetado para ser executado como um serviço do Windows, portanto, preciso de uma forma de iniciar ou parar as instâncias de FileSystemWatcher automaticamente quando o serviço do Windows iniciar, parar ou reiniciar. Mesmo que eu não vá falar muito da definição do Serviço do Windows aqui, vale a pena mencionar os dois métodos principais da implementação do serviço do Windows: OnStart e OnStop. Inicialmente, sempre que o serviço do Windows inicia, ele deve realizar duas ações: Preencha a lista de instâncias de FileSystemWatcher no arquivo XML (Figura 3) e inicie as instâncias (Figura 4).

Veja o código solicitado para iniciar o processo no serviço do Windows:

/// <summary>Event automatically fired when the service is started by Windows</summary>
/// <param name="args">array of arguments</param>
protected override void OnStart(string[] args)
{
  // Initialize the list of FileSystemWatchers based on the XML configuration file
  PopulateListFileSystemWatchers();
  // Start the file system watcher for each of the file specification
  // and folders found on the List<>
  StartFileSystemWatcher();
}

E, finalmente, o método na Figura 7implementa a lógica para interromper FileSystemWatcher. Ele solicita parar ou reiniciar o serviço do Windows.

Figura 7 - Interrompendo o FileSystemWatcher

/// <summary>Event automatically fired when the service is stopped by Windows</summary>
protected override void OnStop()
{
  if (listFileSystemWatcher != null)
  {
    foreach (FileSystemWatcher fsw in listFileSystemWatcher)
    {
      // Stop listening
      fsw.EnableRaisingEvents = false;
      // Dispose the Object
      fsw.Dispose();
    }
    // Clean the list
    listFileSystemWatcher.Clear();
  }
}

Conclusão

FileSystemWatcher é uma classe avançada que permite monitorar (ouvir) as alterações que ocorrem no sistema de arquivos, como criar, excluir e renomear os arquivos e pastas e, também, modificá-las. Este aplicativo, direcionado a executar como um serviço do Windows, foi projetado para permitir uma modificação fácil dos arquivos e pastas a serem monitorados, incluindo as extensões de arquivos. A abordagem que eu segui utiliza um conceito muito útil disponível no .NET Framework, serialização e desserialização, tornando possível sustentar a classe FileSystemWatcher a partir de um arquivo XML sem solicitar alterações no código-fonte. Após qualquer modificação no arquivo de configurações XML, basta reiniciar o serviço do Windows e as alterações estarão aplicadas.


Diego Ordonezé um engenheiro civil com mais de 15 anos de experiência em TI, trabalhando principalmente com as tecnologias GIS e CAD como analista, desenvolvedor e arquiteto. Ele é um Desenvolvedor profissional certificado da Microsoft em C#, ASP.NET, ADO.NET, SQL Server e realmente gosta de aprender e aplicar tecnologias no .NET Framework. Ele mora em Calgary, Alberta, Canadá, com sua esposa e duas lindas filhas e trabalha para a Altus Geomatics como líder de equipe de GIS (bit.ly/2aWfi34).

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: James McCaffrey
Entre em contato com o Dr. James McCaffrey trabalha para a Microsoft Research em Redmond, Washington. Ele trabalhou em vários produtos da Microsoft, incluindo Internet Explorer e Bing. Entre em contato com o Dr. McCaffrey pelo email jammc@microsoft.com.