うぃろぅ.log

140字で綴りきれない日々の徒然備忘録

【Windows サービス】 Visual Studio 2017でWindows サービスを作る その3

うぃろぅです。

さくっとその3も公開しようと思っていたのに他の作業に追われてちょっと後回しにしている間に10日経ってしまった。

10日前の記憶なんて容易に忘れてしまうもので、アウトプットしているならなおのこと。
自分の参考のためにも前回のリンクを貼っておく。

【Windows サービス】 Visual Studio 2017でWindows サービスを作る その1
【Windows サービス】 Visual Studio 2017でWindows サービスを作る その2

では引き続き。

ゴール設定

前回はフォルダ/ファイル検知を開始するところまで作成したので、今回は

  • 検知したフォルダ/ファイルのパスをテキストファイルに出力

を目指す。

とりあえず形にする

出力先とファイル名を適当に設定し、単純にテキストファイルに書き込むならこうなる。

/// <summary>
/// 作成検知
/// </summary>
/// <param name="sender">イベント発生元</param>
/// <param name="e">イベントデータ</param>
private void Watcher_Created(object sender, FileSystemEventArgs e)
{
    var logPath = Path.Combine(@"C:\VisualStudio\WatchService\log", "Watcher.log");

    using (StreamWriter sw = new StreamWriter(logPath, true, Encoding.UTF8))
    {
        sw.WriteLine($"作成検知 : {e.FullPath}");
    }
}

/// <summary>
/// 名前変更
/// </summary>
/// <param name="sender">イベント発生元</param>
/// <param name="e">イベントデータ</param>
private void Watcher_Renamed(object sender, RenamedEventArgs e)
{
    var logPath = Path.Combine(@"C:\VisualStudio\WatchService\log", "Watcher.log");

    using (StreamWriter sw = new StreamWriter(logPath, true, Encoding.UTF8))
    {
        sw.WriteLine($"名前変更検知 : {e.FullPath}");
    }
}

わーい頭わるーい
動かしてみる。

f:id:vviilloovv:20181126114014p:plain

サービス開始して

f:id:vviilloovv:20181126114222p:plain

適当にフォルダを置くと

f:id:vviilloovv:20181126114849p:plain

エラーで止まる。
原因はわかっているがエラーログを見てみる。

Windows サービスのエラーはイベントログとして吐き出されるため、イベントビューアーで見ることができる。

f:id:vviilloovv:20181126115103p:plain

例外情報:System.IO.DirectoryNotFoundException
   場所 System.IO.__Error.WinIOError(Int32, System.String)
   (以下略)

ログ出力フォルダを作成していなかった。

ソースに追記する。

private void Watcher_Created(object sender, FileSystemEventArgs e)
{
    var logFolderPath = @"C:\VisualStudio\WatchService\log";
    // ログ出力ディレクトリがない場合のみ作成
    if (!Directory.Exists(logFolderPath))
    {
        Directory.CreateDirectory(logFolderPath);
    }

    var logPath = Path.Combine(logFolderPath, "Watcher.log");

    using (StreamWriter sw = new StreamWriter(logPath, true, Encoding.UTF8))
    {
        sw.WriteLine($"作成検知 : {e.FullPath}");
    }
}

Watcher_Renamedの方にも同様の処理を入れてある。
これでもう一度。

ちなみに、これまで触れていなかったがWindows サービスはデバッグ実行にちょっとした手間が必要で、開始しているサービスにアタッチする必要がある。
ここでは詳しく解説しないが、以下の記事が詳しい。この記事は日本語でもわかりやすい

方法 : Windows サービス アプリケーションをデバッグする | Microsoft Docs

ソースを書き換えた場合、

  1. Windows サービスを停止(今回はエラーで停止済み)
  2. サービス削除
  3. リビルド
  4. サービス登録
  5. サービス開始

の流れで実行することになる。若干面倒だがお試しだしまあ。

f:id:vviilloovv:20181126120900p:plain

サービスを開始して

f:id:vviilloovv:20181126120944p:plain

フォルダを置いてみると

f:id:vviilloovv:20181126121218p:plain

作成検知 : C:\VisualStudio\WatchService\Watch\Test_hoge

テーレッテレー
リネームも試してみる。

f:id:vviilloovv:20181126121456p:plain

f:id:vviilloovv:20181126121518p:plain

作成検知 : C:\VisualStudio\WatchService\Watch\Test_hoge
名前変更検知 : C:\VisualStudio\WatchService\Watch\Sample_fuga  

問題なさそう。

形を整える

さすがにWatcher_CreatedWatcher_Renamedに同じ処理を書き過ぎていて、私はそこまで働き者ではない。
怠惰にいきましょう。

/// <summary>
/// ログ書き込み
/// </summary>
/// <param name="kind">イベント種別</param>
/// <param name="path">検知パス</param>
private void WriteLog (string kind, string path)
{
    var logFolderPath = @"C:\VisualStudio\WatchService\log";
    var logPath = Path.Combine(logFolderPath, "Watcher.log");

    // ログ出力ディレクトリがない場合のみ作成
    if (!Directory.Exists(logFolderPath))
    {
        Directory.CreateDirectory(logFolderPath);
    }

    using (StreamWriter sw = new StreamWriter(logPath, true, Encoding.UTF8))
    {
        sw.WriteLine($"{kind}検知 : {path}");
    }
}

ログ出力クラスを作成。
これに伴って、

private void Watcher_Created(object sender, FileSystemEventArgs e)
{
    WriteLog("作成", e.FullPath);
}

/// コメント省略
private void Watcher_Renamed(object sender, RenamedEventArgs e)
{
    WriteLog("名前変更", e.FullPath);
}

検知時処理もすっきり。
同期処理なのでイベントをとりあうこともなくこれで問題なさそう。

非同期にしてみる

今回はログ書き込みだけなので同期処理だろうが非同期処理だろうが問題なく動くが、これが

private void Watcher_Created(object sender, FileSystemEventArgs e)
{
    WriteLog("作成", e.FullPath);
    (何か時間のかかる処理)
}

だったりすると100件検知したいのにInternalBufferSizeのオーバーフローで実際には30件くらいしか取れませんでした、なんてよくある話。
というか実際あった。

取りこぼしを少しでも減らすためにはInternalBufferSizeを適当に大きくするのもいいが、それとは別に

  • Directory.EnumerateFilesかなにかを使って監視対象フォルダを定期的に走査
  • イベントハンドラを非同期実行にして処理を早く手放す

あたりの対策を打つとよさそう。
非同期処理については以下を読めば大体わかるので参考にしつつ実装してみる。

qiita.com

private async void Watcher_Created(object sender, FileSystemEventArgs e)
{
    await Task.Run(() =>
    {
        WriteLog("作成", e.FullPath);
    });
}

private async void Watcher_Renamed(object sender, RenamedEventArgs e)
{
    await Task.Run(() =>
    {
        WriteLog("名前変更", e.FullPath);
    });
}

これでOK。
結果としてソース全文は以下のようになった。

Watcher.cs(長いので折りたたみ)

using System.IO;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;

namespace WatchService
{
    public partial class Watcher : ServiceBase
    {
        public Watcher()
        {
            InitializeComponent();
        }

        protected override void OnStart(string[] args)
        {
            StartFileWatch();
        }

        protected override void OnStop()
        {
        }

        /// <summary>
        /// フォルダ監視処理
        /// </summary>
        private void StartFileWatch()
        {
            // 命名規則を設定
            // 複数条件はスペースを入れず「|」で区切る
            var folderNamingRule = "Test_*|Sample_*";

            var filters = folderNamingRule.Split('|');
            foreach(var filter in filters)
            {
                // インスタンスの設定
                var watcher = new FileSystemWatcher
                {
                    Path = @"C:\VisualStudio\WatchService\Watch",
                    Filter = filter,
                    IncludeSubdirectories = false,
                    NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.FileName
                };

                // 監視バッファサイズの設定
                watcher.InternalBufferSize = 16 * 1024;

                // 監視イベントの設定
                watcher.Created += Watcher_Created;
                watcher.Renamed += Watcher_Renamed;

                // 監視開始
                watcher.EnableRaisingEvents = true;
            }
        }

        /// <summary>
        /// フォルダ作成
        /// </summary>
        /// <param name="sender">イベント発生元</param>
        /// <param name="e">イベントデータ</param>
        private async void Watcher_Created(object sender, FileSystemEventArgs e)
        {
            await Task.Run(() =>
            {
                WriteLog("作成", e.FullPath);
            });
        }

        /// <summary>
        /// フォルダ名変更
        /// </summary>
        /// <param name="sender">イベント発生元</param>
        /// <param name="e">イベントデータ</param>
        private async void Watcher_Renamed(object sender, RenamedEventArgs e)
        {
            await Task.Run(() =>
            {
                WriteLog("名前変更", e.FullPath);
            });
        }

        /// <summary>
        /// ログ書き込み
        /// </summary>
        /// <param name="kind">イベント種別</param>
        /// <param name="path">検知パス</param>
        private void WriteLog (string kind, string path)
        {
            var logFolderPath = @"C:\VisualStudio\WatchService\log";
            var logPath = Path.Combine(logFolderPath, "Watcher.log");

            // ログ出力ディレクトリがない場合のみ作成
            if (!Directory.Exists(logFolderPath))
            {
                Directory.CreateDirectory(logFolderPath);
            }

            using (StreamWriter sw = new StreamWriter(logPath, true, Encoding.UTF8))
            {
                sw.WriteLine($"{kind}検知 : {path}");
            }
        }
    }
}

サービス停止時はwatcher.EnableRaisingEvents = falseにしておいた方がいいような気がするが、まあお試しだし…。

お疲れ様でした

ざっくり書いたけれどそんなに難しいことではない。難しい処理してないし。

エラー処理も全然作りこんでいないしあくまでお試し。
起こりそうなエラーとしては、

  • 非同期処理の内外で同じログに書き込むケースがある場合のIOエラー
  • 監視対象フォルダが存在しない(サービス開始後のパス設定段階でエラーを吐く)
  • ログを排他で開きっぱなし
  • 権限エラー

あたりだろうか。

無事ひと段落!!宣言どおり終わったよ!!!!

黒い画面張ってなかった。いや別に義務というわけではないけれど。

f:id:vviilloovv:20181126151917p:plain

蛇足

業務で使うってんでC#覚えてきたはいいけれど次はKotlinやるかもって気軽に宣言されてなんだその軽いノリでのしかかる学習コスト。
なのでそうと決まった場合Kotlinの記事も書く…かも。

ちなみに個人的にはRubyが好き。これもうわかんねえな

ではまた。