【C#】フォルダ監視ツールを作ってみた

院内ヘルプデスクC#, modx

私の病院では内部の情報共有や必要な資料を公開するために、イントラネットのHPがあるのですが、それぞれの部署や委員会で更新を任せていてどの部署が更新したのかわからない状態でした。そのため各部署のHPを更新しても「あれ?そのことならHPに書いてあるよ」ということが日常茶飯事でした。 各部署を一覧にしたポータルサイトのようなものはあるのですが、更新通知などはなく実際にその部署のページを開いて確認する必要がありました。

今回はこのような状態の各部署のファイルを監視して、更新されたら通知を受けるプログラムを作ってみることにしました。

きっかけ

ある日先輩とのちょっとした雑談で、同じ内容の問い合わせを何度も受けているという話になって、自分もその問い合わせを受けていたので、どうにかしたいねーという話がありました。部署のホームページでよくある質問などを更新してお知らせしようと思ったのですが、更新通知の仕組みがないので見てくれる人が少ないのではないかと思っていました。 そこで「Windows ファイル 監視」などで調べているとファイルを監視する仕組みが色々あったので、院内開発で通知する仕組みが作れるのでないかと思い、開発に取り掛かりました。

目的

院内HPのトップページはmodxというツールで管理されていて、ここで各部署へのリンクやお知らせなどを表示しています。 今回はこのトップページに、各部署が更新したらnewというマークを付けて更新されたことを伝えます。

病院内の各部署・各委員会ごとにフォルダがあり、それぞれのフォルダを各人が管理しています。フォルダ内に「お知らせ.mht」または「osirase.mht」というファイルがあるので、そのファイルを監視して更新されたら画面上に表示するのが目的です。(.mhtファイルはブラウザ上で見ることが出来、HTMLを編集することが難しい非IT職でもWordで編集しやすいファイルの拡張子です。)

環境

今回開発を行って環境は以下の通りです。

  • Windows 10 Pro(64bit)
  • Visual Studio 2013

できたもの

surveillance-tool

とても簡素な表示になっていてボタンは「監視開始」と「監視停止」の2つのみです。監視開始ボタンを押すと、プログラム内部で持っている部署と委員会の監視対象フォルダを読み込みリストに表示します。そして、監視対象フォルダの「お知らせ.mht」または「osirase.mht」が更新されると下部の更新履歴表示エリアに日時と更新された部署・委員会が表示されます。

監視対象部署・委員会は頻繁に変わることがないので、とりあえずアプリ上で部署の追加・削除などはできないようになっていて、部署・委員会の入れ替えにはプログラムを修正する必要があります。

やったこと

今回の実現したい部分の監視処理の方法で、Windowsではbatやpowershellなど様々な方法で実現できるようですが、病院内ではC#での開発に移行していて、私もC#の開発経験が浅いので練習として作ってみることにしてみました。

調査しているとC#ではFileSystemWatcherというクラスが用意されていて、これを使用することによって簡単にフォルダやファイルの監視が実現できます。

準備

和暦変換ツールを作った時と同じようにVisual Studioの画面で部品を置いていき、部品ごとに動きを設定していきます。使っている部品はbutton、label、listViewぐらいです。 そしてメインの監視部分の処理を記述していきます。

プロトタイプ

FileSystemWatcherではChanged、Created、Deleted、Renamedのイベントが用意されていて、理想的な動きとしては「お知らせ.mht」または「osirase.mht」が更新されたときにイベントの通知を受け取りたいので、Changedのイベントで受け取ることを考えました。しかし、Wordで編集した時の.mhtファイルの動きは特殊でChangedのイベントでは更新されたことの通知が受け取れませんでした。

なので、部署ごとのフォルダ内のファイルが更新されれば通知を受けるという動きに変更しました。これだとフォルダ内のファイルが更新されれば随時更新通知が行われました。しかし、どのファイルでも更新された通知が来るので、例えば内容の変更が特になくてもファイルを上書き保存などしてしまったら通知されてしまうなど不具合があります。

mhtへの対応

「お知らせ.mht」または「osirase.mht」への完全な対応として、Wordで編集した時の.mhtファイルの動きを分析してみました。そうするとwordで既存の.mhtファイルを編集するときに一度.tmp(テンポラリー)ファイルという異常終了などをしたときにデータの損失を防ぐための一時ファイルが作られます。このファイルが正常に上書き保存した際に元のファイル名で保存されていました。なのでFileSystemWatcherでこの動きを検出するためのイベントとしてはRenamedでイベント通知が受け取る必要がありました。 監視対象のフォルダの.tmpファイルを監視して、「お知らせ.mht」または「osirase.mht」にRenamedされたときにイベント通知を受け取るように変更したところ、無事に狙っていた理想の動きを実現することができました。

modxへの通知

ファイルの更新通知を受け取った後に、院内HPのトップにある各部署をまとめたポータルサイトに更新の表示を付けるようにしました。ポータルサイトはmodxというWordpressと同じようなCMS(Contents Management System)で管理されています。 普段modxはブラウザ上のGUIで管理していますが、そのデータの実態はDBで管理されているため、プログラムでそのDBへ更新されたときに書き換える処理を追加して、更新通知の表示を実現させました。DBに書き込む値としてscriptのテキストを書き込み、ポータルサイトの元となるページにjavascriptの本文を書き込んで置き、何日間だけ通知の表示を行うという処理にしています。

プログラム本体

今回作成したプログラムを下記に載せています。下記の文字をクリックすることでソースコードが開きます。(何気に折り畳み表示に苦労しました…)

HPUpdateCheckのソースコード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
using System.Text.RegularExpressions;
using MySql.Data.MySqlClient;
namespace HPUpdateCheck
{
    public partial class Form1 : Form
    {
        private List watchers = new List();
        private List> checkGroups = new List>();
        public Form1()
        {
            InitializeComponent();
            this.Text = "院内HP更新Watcher";
        }
        private void Form1_Load(object sender, EventArgs e)
        {
            button1.Text = "監視開始";
            button2.Text = "監視停止";
            label1.Text = "監視中のフォルダ";
            label2.Text = "直近で更新のあったお知らせファイル";
            label3.Text = "監視停止";
            addGroups();
            InitListView();
        }
        private void InitListView()
        {
            this.listView1.Columns.Clear();
            this.listView1.Columns.Add("部署・委員会", 100, HorizontalAlignment.Left);
            this.listView1.Columns.Add("フォルダ", 300, HorizontalAlignment.Left);
            this.listView2.Columns.Clear();
            this.listView2.Columns.Add("日時", 120, HorizontalAlignment.Right);
            this.listView2.Columns.Add("処理内容", 220, HorizontalAlignment.Left);
        }
        private void addGroups()
        {
            //listの中身は{oshiraseファイル格納フォルダ,部署名,modxで振られているID}
            //部署
            checkGroups.Add(new List(new string[] {@"\\部署フォルダ","放射線科","194"}));
            ...
            //委員会
            checkGroups.Add(new List(new string[] {@"\\委員会フォルダ", "環境委員会" ,"171"}));
            ...
        }
        private void button1_Click_1(object sender, EventArgs e)
        {
            if (watchers.Count != 0) return;
            for (int i = 0; i < checkGroups.Count; i++)
            {
                //監視インスタンスを作成
                watchers.Add(new FileSystemWatcher());
                //監視するディレクトリを指定
                watchers[i].Path = checkGroups[i][0];
                //最終アクセス日時、最終更新日時、ファイル、フォルダ名の変更を監視する
                watchers[i].NotifyFilter =
                (System.IO.NotifyFilters.LastAccess
                | System.IO.NotifyFilters.LastWrite
                | System.IO.NotifyFilters.FileName
                | System.IO.NotifyFilters.DirectoryName);
                //お知らせのファイルを監視
                //mhtファイルをwordで編集する際にtmpファイルが出来るのでそれを監視する。
                watchers[i].Filter = "*.tmp";
                watchers[i].SynchronizingObject = this;
                //イベントハンドラの追加
                watchers[i].Changed += new System.IO.FileSystemEventHandler(watcher_Changed);
                watchers[i].Renamed += new System.IO.RenamedEventHandler(watcher_Renamed);
                //監視を開始する
                watchers[i].EnableRaisingEvents = true;
                string filePath = watchers[i].Path;
                string fileName = Path.GetFileName(filePath);
                listView1.Items.Add(new ListViewItem(new string[] { checkGroups[i][1], filePath }));
            }
            label3.Text = "監視中";
            Console.WriteLine("監視を開始しました。");
            button1.Enabled = false;
        }
        private void button2_Click_1(object sender, EventArgs e)
        {
            if (watchers.Count <= 0) return;
            //監視を終了
            for (int i = 0; i < checkGroups.Count; i++)
            {
                watchers[i].EnableRaisingEvents = false;
                watchers[i].Dispose();
            }
            watchers = new List();
            listView1.Items.Clear();
            label3.Text = "監視停止";
            Console.WriteLine("監視を停止しました。");
            button1.Enabled = true;
        }
        private void watcher_Changed(System.Object source,
            System.IO.FileSystemEventArgs e)
        {
            switch (e.ChangeType)
            {
                case System.IO.WatcherChangeTypes.Changed:
                    Console.WriteLine(
                        "ファイル 「" + e.FullPath + "」が変更されました。");
                    break;
                case System.IO.WatcherChangeTypes.Created:
                    Console.WriteLine(
                        "ファイル 「" + e.FullPath + "」が作成されました。");
                    break;
                case System.IO.WatcherChangeTypes.Deleted:
                    Console.WriteLine(
                        "ファイル 「" + e.FullPath + "」が削除されました。");
                    break;
            }
        }
        private void watcher_Renamed(System.Object source,
            System.IO.RenamedEventArgs e)
        {
            //ファイル名取得
            string strFileName = e.FullPath;
            //.tmpファイルがosirase or お知らせ .mhtにリネームされる時(保存したとき)に呼び出される。
            if (Regex.IsMatch(strFileName, @"(osirase|お知らせ)\.mht", RegexOptions.IgnoreCase))
            {
                Console.WriteLine(
                "ファイル 「" + e.FullPath + "」の名前が変更されました。");
                for (int i = 0; i < checkGroups.Count; i++)
                {
                    if (e.FullPath.Contains(checkGroups[i][0]))
                    {
                        DateTime dNow = System.DateTime.Now;
                        listView2.Items.Add(new ListViewItem(new string[] { dNow.ToString(), checkGroups[i][1] + "のページが更新されました。" }));
                        add_new_mark(dNow, checkGroups[i][2]);
                        break;
                    }
                }
            }
        }
        private void add_new_mark(DateTime dNow, string id)
        {
            int year = dNow.Year;
            int month = dNow.Month;
            int day = dNow.Day;
            //MySqlへの接続情報
            string server = "サーバのIPアドレス";
            string user = "ユーザー名";
            string pass = "パスワード";
            string database = "DB名";
            string connectionString = string.Format("Server={0};Database={1};Uid={2};Pwd={3}", server, database, user, pass);
            //MySQLへの接続
            try
            {
                MySqlConnection connection = new MySqlConnection(connectionString);
                connection.Open();
                Console.WriteLine("MySQLに接続しました!");
                //SQL実行
                MySqlCommand cmd = new MySqlCommand("UPDATE `modxのテーブル名` SET `menutitle` = CONCAT(`pagetitle`, '') WHERE `id` ='" + id + "';", connection);
                MySqlDataReader reader = cmd.ExecuteReader();
                connection.Close();
            }
            catch (MySqlException me)
            {
                Console.WriteLine("ERROR: " + me.Message);
                listView2.Items.Add(new ListViewItem("DB書き込みでエラーが発生しました:" + me.Message));
            }
        }
    }
}

フォルダ監視ツールのまとめ

私の職場ではちょっと前まではVB6でプログラムを作成していましたが、新しいプログラムを作る場合はC#で作成することになっていて、今回作成したプログラムが業務で使う初めてのC#のプログラムでしたが、個人的には満足いく出来栄えでした。アプリ上で部署の追加・削除できないとかの問題はありますので、今後の課題とします。

ただ、ポータルサイトの管理がMySQLで管理していてC#ではMySQLにデータを書き込むためにライブラリが必要でそのインストールの手間と、長期運用して問題ないか確証がなく、上司から常時起動端末で動かす許可がもらえなかったので、個人の業務用PCで勤務時間だけ動かしていました。

実際に業務PCで動かしてみて動作は安定していて、メモリ使用量も2~5MBほどしか使わないのでリソースもあまり消費していなかったです。その後実績が認められ、常時起動PCで運用していいと許可をもらったので、現在は個人のPCを離れ、無事に別PCで運用を続けられています。