October 2016

Volume 31 Number 10

Windows サービス - カスタマイズ可能な FileSystemWatcher Windows サービスの作成

Diego Ordonez

FileSystemWatcher クラスは非常に強力なツールで、Microsoft .NET Framework のバージョン 1.1 から含まれています。公式の定義 (bit.ly/2b8iOvQ、英語) では「ファイル システムの変更通知をリッスンし、ディレクトリまたはディレクトリ内のファイルが変更されたときにイベントを発生します」とされています。

このクラスにより、ファイルやフォルダーの作成、変更、削除など、ファイル システムでのイベントを検出できます。このクラスは完全にカスタマイズできます。クラスのコンストラクターは、リッスン対象のフォルダーの場所やファイル拡張子のようなパラメーターと、フォルダー構造全体を再帰的にリッスンするがあるかどうかを指定するブール値パラメーターを受け取ります。ただし、このようなパラメーターをソース コードに含めるのは適切なアプローチではありません。アプリケーションに新しいフォルダーやファイル拡張子を含める必要があり、そのうえコーディング、ビルド、再配置が必要になるため役に立ちません。このような設定をほとんど変更することがないアプリケーションを除いて、ソース コードを変更しないで構成を変更できるメカニズムを実装するのが適切な考え方です。

今回は、FileSystemWatcher クラスを 1 回しか使わないアプリケーションの作成方法を取り上げます。ただし、その後 XML シリアル化を利用して、フォルダー名、ファイル拡張子、イベントの発生時に実行する操作など、アプリケーションの設定への追加の変更を行うことができるようにします。つまり、XML ファイルを更新して Windows サービスを再開するだけで、すべての変更が簡単に反映されます。

説明を簡単にするために、ここでは C# コンソール アプリケーションを Windows サービスとして実行する方法については詳しく取り上げません。この方法については、多くのリソースがオンラインで公開されています。

カスタマイズ後のフォルダー設定の構造

今回は設定用の XML ファイルを適切な構造の C# クラスにシリアル化解除することを考えています。この C# クラスがアプリケーションの 1 つ目のコンポーネントで、FileSystemWatcher クラスから操作する必要があるパラメーターの定義になります。このクラスを定義するコードを図 1 に示します。

図 1 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()
  {
  }
}

ここからは、シリアル化解除のプロセスを使用して、XML ファイルをこの C# クラスに変換する方法について見ていきます。CustomFolderSettings クラスのインスタンスが 1 つだけということはありません。代わりに、Windows サービスがフォルダーのさまざまな場所やファイル拡張子をリッスンできるリスト (List<CustomFolderSettings>) になります。

図 2 は XML 設定ファイルの例です。このファイルを使って、操作する必要のあるすべての引数を FileSystemWatcher クラスに提供します。ここで理解すべき重要な点は、XML ファイル (図 2) に含まれる情報を C# クラス (図 1) にフィードすることです。

図 2 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>

ここで、XML ファイルに含まれるパラメーターについて詳しく見ていくことにします。まず、XML のルート要素は <ArrayOfCustomFolderSettings> です。ここに <CustomFolderSettings> 要素を必要なだけいくつでも指定できます。これは、フォルダーの場所やファイル拡張子を複数同時に監視できるようにするうえで重要な要素です。

次に、<FolderEnabled> パラメーターが 1 つ目のフォルダーでは true、2 つ目のフォルダーでは false になっているのがわかります。これにより、XML ファイルから削除することなく、FileSystemWatcher クラスの 1 つを簡単に無効にすることができます。つまり、構成は存在していても、クラスは実行時にこの指定を省略します。

最後に、作成、削除、または変更されているファイルの検出時にトリガーされるアクションを指定する方法を理解しておくことが重要です。これが、FileSystemWatcher クラスの最終目標です。

<ExecutableFile> パラメーターには起動するアプリケーションを含めます。この例では、DOS コマンド ライン (CMD.EXE) を指定しています。

<ExecutableArguments> パラメーターには、実行可能ファイルに引数として渡すオプションを含めます。以下は図 2 から抜粋した例です。

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

これは実行時に以下のように変換されます。

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

これにより、c:\temp\it_works_ZIP.txt ファイルに文字列が書き込まれ、XML の {0} の値が FileSystemWatcher クラスによって検出されたファイルの実際の名前に置き換えられます。C# メソッドの string.Format を使い慣れている方は、問題なく理解できるでしょう。

これで、1 つの XML 構成ファイルと、対応する属性を持つ 1 つの C# クラスを作成しました。次は、XML の情報をクラスのリスト (List<CustomFolderSettings>) にシリアル化解除します。図 3 はこの重要な手順を実行するメソッドを示しています。

図 3 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>;
}

このメソッドが実行されると、必要な FileSystemWatcher インスタンスをすべて含むリストが使用可能になります。次に、FileSystemWatcher クラスを開始します。これにより、リッスン処理が始まります。

当然、メソッドは XML 設定ファイルの場所を把握している必要があります。ここでは、App.config ファイルを使用して、XML ファイルの場所を定義します。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>

XML 設定ファイルまたは App.config ファイルに変更を加えた場合は、変更を適用するため、Windows サービスを再開する必要があります。

FileSystemWatcher 処理の開始 (変更のリッスン)

この時点では、FileSystemWatcher クラスの複数 (または少なくとも 1 つ) のインスタンスに必要な設定はすべて図 3 で作成したリストから使用できます。

ここで、リッスン処理を開始します。そのためには、リスト全体をループ処理し、インスタンスを 1 つずつ開始する必要があります。図 4 のコードは、この初期化プロセスを実行する方法と、XML ファイルから取得したすべてのパラメーターを割り当てる方法を示しています。

図 4 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));
    }
  }
}

このコードの FileSystemWatcher クラスは作成イベントのみをリッスンしていますが、削除や名前の変更など、他のイベントも利用できます。

関数で FileSystemWatcher クラスの Created イベントをサブスクライブしている行に注目します。ここではラムダ式を使用しています。これには重要な理由があります。 FileSystemWatcher クラスのインスタンスのリストを使用しているため、特定の実行可能ファイルを各インスタンスに関連付ける必要があります。これを別の方法で処理すると (つまり、ラムダ式を使用せず関数を直接割り当てると)、最後の実行可能ファイルのみ保持され、すべての FileSystemWatcher インスタンスで同じ処理が実行されることになります。

図 5 は、FileSystemWatcher クラスのインスタンスごとに、個別の条件に基づいて実際に処理を実行する関数のコードを示しています。

図 5 インスタンスごとに条件に応じたアクションの実行

/// <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);
}

最後に、図 6 は ExecuteCommandLineProcess 関数を示しています。これは、コマンド ラインの命令 (DOS コンソール) を実行するごく標準的な方法です。

図 6 コマンド ラインの命令の実行

/// <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
  }
}

Windows サービス内での FileSystemWatcher クラスの開始と停止

最初に示したように、このアプリケーションは Windows サービスとして実行するよう設計しています。そのため、Windows サービスを開始、停止、または再開するときに、FileSystemWatcher のインスタンスを自動的に開始または停止する方法が必要です。ここでは Windows サービスの定義については詳しく説明しませんが、Windows サービスを実装するうえで重要な OnStart メソッドと OnStop メソッドの 2 つのメソッドについて説明しておきます。まず、Windows サービスを開始するたびに 2 つのアクションを実行する必要があります。 1 つは XML ファイルから FileSystemWatcher インスタンスのリストを設定するアクション (図 3)、もう 1 つはインスタンスを開始するアクション (図 4) です。

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();
}

最後に、図 7 のメソッドは FileSystemWatcher クラスを停止するロジックを実装します。これを行うには、Windows サービスを停止または再開する必要があります。

図 7 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();
  }
}

まとめ

FileSystemWatcher クラスは、ファイルやフォルダーの作成、削除、変更、名前の変更など、ファイル システム内で発生する変更を監視 (リッスン) できるようにする強力なクラスです。今回のアプリケーションは、Windows サービスとして実行することを目的としており、ファイル拡張子を含め、監視対象のファイルやフォルダーを簡単に変更できるように設計しています。ここで説明した方法は、シリアル化とシリアル化解除という、.NET Framework で使用可能な非常に手軽な考え方を使用しています。これにより、ソース コードに変更を加える必要なく、XML ファイルから FileSystemWatcher クラスに情報をフィードできます。XML 設定ファイルに変更を加えた後、Windows サービスを再開するだけで変更が適用されるようになります。


Diego Ordonez は、IT 業界で 15 年以上のキャリアがある民間のエンジニアです。アナリスト、開発者、アーキテクトとして、主に GIS や CAD テクノロジに携わってきました。C#、ASP.NET、ADO.NET、SQL Server のマイクロソフト認定プロフェッショナル デベロッパーであり、心から楽しんで .NET Framework 関連のテクノロジを学び活用しています。現在、夫人と 2 人のまな娘とカナダ アルバータ州カルガリーで暮らし、Altus Geomatics で GIS チーム リーダーとして勤務しています (bit.ly/2aWfi34、英語)。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの James McCaffrey に心より感謝いたします。
Dr.James McCaffrey は、ワシントン州レドモンドの Microsoft Research に勤務しています。これまでに、Internet Explorer、Bing などの複数のマイクロソフト製品にも携わってきました。Dr.McCaffrey の連絡先は jammc@microsoft.com (英語のみ) です。