2015年11月16日月曜日

C#でレジストリの変化を通知するイベントを作る

レジストリの変化の通知を受け取りたいことがあると思います。C#にはRegistryKeyクラスなど、レジストリの読み書きや列挙等をするのに十分なクラスがありますが、変更通知だけは対応していません。これを行うにはWin32APIを直接叩く必要があります。

基本的な実装 - Win32APIのラッピング

さて、それっぽい関数をWin32APIから探すと、RegNotifyChangeKeyValue関数が見つかります。そして、その下のほうにC言語のサンプルコードが書いてあります。
インデントが崩れていたり、コマンドライン引数のパース作業とかがあって微妙に読みにくかったりしますが、おおむね次の手順です。
  1. RegOpenKeyEx関数を使って変更通知許可を付加してレジストリキーをオープン
  2. CreateEvent関数を使ってイベントを作成
  3. RegNotifyChangeKeyValue関数を使ってレジストリの監視を開始
  4. WaitForSingleObject関数を使ってレジストリが変更されるまで待つ
  5. 変更監視が終わったらRegCloseKey関数CloseHandle関数を使ってリソースを解放する
さて、レジストリを開いて、最終的に閉じる、イベントを作って最終的に閉じる、というのはいいかと思います。Win32APIでは、Create***()などの名前の関数はインスタンスをヒープ領域に確保しておりますので、後々にそのハンドルを使ってインスタンスを解放してやる必要があります。もちろんガベージコレクタなんてないですからね。その原則にのっとって、レジストリキーのハンドルと、イベントハンドルを最後に解放しております。

ここで問題になるのが「イベント」と呼ばれるものです。これは何なのでしょう。C#のイベント機能とは違う感じですよね。この辺を詳しく説明しようとするとWin32APIのマルチスレッドの話になってすごく泥沼になってしまいますが、まあ、簡単に言うと、シグナル状態と非シグナル状態を手動で切り替えられる「オブジェクト」の一種ですね。オブジェクトと言うとなんかとても表現があいまいになってしましますが、ここで言うオブジェクトとは、スレッドやプロセス、ミューテックスやイベントなどを包含する概念です(すなわち、イベントとはスレッドやプロセスなどと並列する概念と言えます)。このオブジェクトにはシグナル状態と非シグナル状態というものがあり、例えばスレッドはスレッドが動作している状態が非シグナル状態、スレッドが終了したらシグナル状態に切り替わります。プロセスも動作中はシグナル状態で終了したら非シグナル状態になります。この非シグナル状態からシグナル状態に切り替わるのを待つことでで、複数のスレッドの同期を取ることができるようになるわけですね。そして、そのようにオブジェクトがシグナル状態から非シグナル状態に変化するのを待機するための関数がWaitForSingleObject関数なわけです。
ここまで来るとだいたい予想が付くと思いますが、RegNotifyChangeKeyValue関数は、レジストリの変化が起きたらイベントを非シグナル状態に切り替えてくれる関数というわけですね。そして、オブジェクトが非シグナル状態になるまで制御を返さない関数がWaitForSingleObject関数というわけです。

ここまで読むとわかるかと思いますが、レジストリが変化をするまで制御を返してくれなくなるので、このあたりの処理は別スレッドでやる必要があります。そして、監視を終了したいときにWaitForSingleObjectから制御を強制的に戻してやらなければならないという問題も生まれますね。
これに関しては、もう1つ別のイベントを作ることで解決できます。WaitForMultipleObjects関数というものがあり、これを使えば複数のオブジェクトを同時に監視できます。この関数は第3引数に待機オプションを指定することができ、TRUEを指定するとすべてのオブジェクトがシグナル状態になったときに制御を返し、FALSEを指定するといずれか1つのオブジェクトがシグナル状態になったときに制御を返します。スレッドの同期などには前者が使えそうですが、今回は後者を使えば、レジストリの変化が発生していないときでも手動でもう一方のイベントをシグナル状態にすることで関数から制御を返してもらい監視を終了することができます。
ちなみに、イベントを手動でシグナル状態にするにはSetEvent関数を使えばおkです。

さて、ここで大体方針が定まりました。 このようにやっていこうと思います。
  1. RegOpenKeyEx関数を使って変更通知許可を付加してレジストリキーをオープン
  2. CreateEvent関数を使ってレジストリ監視用と終了用のイベントを作成
  3. RegNotifyChangeKeyValue関数を使ってレジストリの監視を開始
  4. 別スレッドからWaitForMultipleObjects関数を使ってレジストリが変更されるまで待つ
  5. レジストリが変更されたら適当に(C#の)イベントを発生させ、3.に戻って監視を再び続ける
  6. 変更監視を終えるときは、終了用のイベントを発生させ、スレッドが終了したのを確認したらRegCloseKey関数やCloseHandle関数を使ってリソースを解放する
という流れになります。

ここからが地獄のプログラミングのスタートです。C#でWin32APIを叩きまくるのは、何度やってもどうにも好きになれませんが、やらないわけにはいかないのでやっていきます…。

void StartWatching(IntPtr RootKey, string subkey)
{
    // キーを開く。
    int ErrorCode = Win32.RegOpenKeyEx(RootKey, subkey, 0, Win32.REGSAM.KEY_NOTIFY, out SubKey);
    if(ErrorCode != Win32.ERROR_SUCCESS)
        throw new Win32Exception(ErrorCode, nameof(Win32.RegOpenKeyEx));

    // イベントを作成する。
    RegEvent = Win32.CreateEvent(IntPtr.Zero, true, false, null);
    if(RegEvent == IntPtr.Zero)
        throw new Win32Exception(nameof(Win32.CreateEvent));

    // イベントを作成する。
    TerminateEvent = Win32.CreateEvent(IntPtr.Zero, true, false, null);
    if(TerminateEvent == IntPtr.Zero)
        throw new Win32Exception(nameof(Win32.CreateEvent));

    Win32.REG_NOTIFY_CHANGE Filter =
        Win32.REG_NOTIFY_CHANGE.NAME |
        Win32.REG_NOTIFY_CHANGE.ATTRIBUTES |
        Win32.REG_NOTIFY_CHANGE.LAST_SET |
        Win32.REG_NOTIFY_CHANGE.SECURITY;

    var events = new List<IntPtr>(new[] { RegEvent, TerminateEvent });
    var terminateIndex = events.IndexOf(TerminateEvent);

    WatchingTask = Task.Run(() => {
        while(true) {
            // レジストリエントリが変更されるかどうか、レジストリキーを監視する。
            ErrorCode = Win32.RegNotifyChangeKeyValue(SubKey, true, Filter, RegEvent, true);
            if(ErrorCode != Win32.ERROR_SUCCESS)
                throw new Win32Exception(ErrorCode, nameof(Win32.RegNotifyChangeKeyValue));

            var waitret = Win32.WaitForMultipleObjects(events, false, Win32.INFINITE);
            if(waitret == Win32.WAIT_FAILED)
                throw new Win32Exception(nameof(Win32.WaitForMultipleObjects));

            //イベントの発生が終了イベント起因だったらループ終了
            if(waitret == (Win32.WAIT_OBJECT_0 + terminateIndex))
                break;

            RaiseRegistryChangedEvent();
        }
    });
}

このような形でどうでしょうか。DllImportした関数や、その他大勢の値などはWin32というクラスを作ってまとめてあります。

internal static class Win32
{
    [DllImport("advapi32.dll")]
    internal static extern int RegOpenKeyEx(IntPtr hKey, string lpSubKey, uint ulOptions, REGSAM samDesired, out IntPtr phkResult);

    [DllImport("advapi32.dll")]
    internal static extern int RegNotifyChangeKeyValue(IntPtr hKey, bool bWatchSubtree, REG_NOTIFY_CHANGE dwNotifyFilter, IntPtr hEvent, bool fAsynchronous);

    [DllImport("advapi32.dll")]
    internal static extern int RegCloseKey(IntPtr hKey);

    [DllImport("kernel32.dll")]
    internal static extern IntPtr CreateEvent(IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName);

    [DllImport("kernel32.dll")]
    internal static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);

    [DllImport("kernel32.dll", EntryPoint = "WaitForMultipleObjects")]
    private static extern uint _WaitForMultipleObjects(uint nCount, IntPtr lpHandles, bool fWaitAll, uint dwMilliseconds);

    [DllImport("kernel32.dll")]
    internal static extern bool SetEvent(IntPtr hEvent);

    [DllImport("kernel32.dll")]
    internal static extern bool ResetEvent(IntPtr hEvent);

    [DllImport("kernel32.dll")]
    internal static extern bool CloseHandle(IntPtr hObject);

    internal static uint WaitForMultipleObjects(IEnumerable<IntPtr> lpHandles, bool fWaitAll, uint dwMilliseconds)
    {
        IntPtr[] array = lpHandles.ToArray();

        // アンマネージ配列のメモリを確保
        IntPtr ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(IntPtr)) * array.Length);
        try {
            Marshal.Copy(array, 0, ptr, array.Length);

            return _WaitForMultipleObjects((uint)array.Length, ptr, fWaitAll, dwMilliseconds);
        }
        finally {
            // アンマネージ配列のメモリを解放
            Marshal.FreeCoTaskMem(ptr);
        }
    }

    [Flags]
    internal enum REGSAM : uint
    {
        KEY_ALL_ACCESS = 0xF003F,
        KEY_CREATE_LINK = 0x0020,
        KEY_CREATE_SUB_KEY = 0x0004,
        KEY_ENUMERATE_SUB_KEYS = 0x0008,
        KEY_EXECUTE = 0x20019,
        KEY_NOTIFY = 0x0010,
        KEY_QUERY_VALUE = 0x0001,
        KEY_READ = 0x20019,
        KEY_SET_VALUE = 0x0002,
        KEY_WRITE = 0x20006,
    }

    [Flags]
    internal enum REG_NOTIFY_CHANGE : uint
    {
        NAME = 1,
        ATTRIBUTES = 2,
        LAST_SET = 4,
        SECURITY = 8,
    }

    internal static readonly IntPtr HKEY_CLASSES_ROOT = new IntPtr(unchecked((int)0x80000000));
    internal static readonly IntPtr HKEY_CURRENT_USER = new IntPtr(unchecked((int)0x80000001));
    internal static readonly IntPtr HKEY_LOCAL_MACHINE = new IntPtr(unchecked((int)0x80000002));
    internal static readonly IntPtr HKEY_USERS = new IntPtr(unchecked((int)0x80000003));
    internal static readonly IntPtr HKEY_PERFORMANCE_DATA = new IntPtr(unchecked((int)0x80000004));
    internal static readonly IntPtr HKEY_PERFORMANCE_TEXT = new IntPtr(unchecked((int)0x80000050));
    internal static readonly IntPtr HKEY_PERFORMANCE_NLSTEXT = new IntPtr(unchecked((int)0x80000060));
    internal static readonly IntPtr HKEY_CURRENT_CONFIG = new IntPtr(unchecked((int)0x80000005));
    internal static readonly IntPtr HKEY_DYN_DATA = new IntPtr(unchecked((int)0x80000006));
    internal static readonly IntPtr HKEY_CURRENT_USER_LOCAL_SETTINGS = new IntPtr(unchecked((int)0x80000007));

    internal const int ERROR_SUCCESS = 0;

    internal const uint INFINITE = 0xFFFFFFFF;  // Infinite timeout
    
    internal const uint WAIT_TIMEOUT = 0x00000102;
    internal const uint WAIT_FAILED = 0xFFFFFFFF;
    internal const uint WAIT_OBJECT_0 = 0;
    internal const uint WAIT_ABANDONED_0 = 128;
}

WaitForMultipleObjects関数はオブジェクトのハンドルを並べた配列へのポインタを受け取りますが、C#などのマネージドな環境では仮想マシンによってメモリ上の配置が任意に変えられてしまいますので、一旦アンマネージ配列を確保してそっちにコピーしてから渡すようラッピングしています。それ以外の関数は基本的にそのままにしています(一部、フラグを組み合わせる引数はenumにしています)。

終了処理はこのような形になります。

bool disposed = false;

/// <summary>
/// リソースを破棄する
/// </summary>
public void Dispose()
{
    Dispose(true);
    GC.SuppressFinalize(this);
}

/// <summary>
/// リソースを破棄する
/// </summary>
/// <param name="disposing">Dispose()メソッド中か</param>
protected virtual void Dispose(bool disposing)
{
    if(!disposed) {
        if(disposing) {
            // Free other state (managed objects).
        }

        // Free other state (unmanaged objects).

        //終了イベントを立てる
        if(!Win32.SetEvent(TerminateEvent))
            throw new Win32Exception(nameof(Win32.SetEvent));

        //終了待ち
        WatchingTask.Wait();

        // キーを閉じる。
        int ErrorCode = Win32.RegCloseKey(SubKey);
        if(ErrorCode != Win32.ERROR_SUCCESS)
            throw new Win32Exception(ErrorCode, nameof(Win32.RegCloseKey));

        // ハンドルを閉じる。
        if(!Win32.CloseHandle(RegEvent))
            throw new Win32Exception(nameof(Win32.CloseHandle));

        // ハンドルを閉じる。
        if(!Win32.CloseHandle(TerminateEvent))
            throw new Win32Exception(nameof(Win32.CloseHandle));

        disposed = true;
    }
}

/// <summary>
/// デストラクタ
/// </summary>
~RegistryWatcher()
{
    Dispose(false);
}

いたって普通のDisposeパターンです。Disposeメソッドを呼び出してリソースを解放するのが基本ですが、やり忘れたときはデストラクタが呼ばれたときにアンマネージドリソースの解放を行うことでとりあえずしっかりと解放するようにしています。

レジストリの変化の種類を調べる

さて、RegNotifyChangeKeyValue関数ですが、上記で説明した通り、「変化が発生したらイベントをシグナル状態にする」という動作のみをします。そのため、どのキーや値がどう変化したかなどの情報は一切よこしてくれません。それではいささか不便なので、レジストリがどう変化したのかを調べ、それをEventArgsに含めようと思います。
それをするためには、事前にレジストリを読み込んで保存しておき、変更通知が発生したときにもう一度読み込んで比較し、通知すれば良いという話になりますね。これはC#で実装できそうです。

string[] SubKeyNames;
Dictionary<string, object> Values;

void LoadNowRegistryState()
{
    SubKeyNames = TargetKey.GetSubKeyNames().OrderBy(p => p).ToArray();
    Values = new Dictionary<string, object>();
    foreach(var name in TargetKey.GetValueNames().OrderBy(p => p))
        Values[name] = TargetKey.GetValue(name);
}

private void RaiseRegistryChangedEvent()
{
    string[] OldSubKeyNames = SubKeyNames;
    Dictionary<string, object> OldValues = Values;

    LoadNowRegistryState();
    
    //サブキーの変化を見る
    var CreatedKeys = SubKeyNames.Except(OldSubKeyNames);
    var DeletedKeys = OldSubKeyNames.Except(SubKeyNames);

    if(SubKeyChanged != null && (CreatedKeys.Any() || DeletedKeys.Any()))
        SubKeyChanged(this, new RegistrySubKeyChangedEventArgs(CreatedKeys, DeletedKeys));

    //値の変化を見る
    var CreatedValues = Values.Keys.Except(OldValues.Keys)
        .Select(p => new RegistryValueChangedInfo(p, RegistryValueChangeType.Created, null, Values[p]));
    var DeletedValues = OldValues.Keys.Except(Values.Keys)
        .Select(p => new RegistryValueChangedInfo(p, RegistryValueChangeType.Deleted, OldValues[p], null));
    var ChangedValues = OldValues.Keys.Intersect(Values.Keys)
        .Select(p => new { ValueName = p, Old = OldValues[p], New = Values[p] })
        .Where(p => p.Old is System.Collections.IEnumerable && p.New is System.Collections.IEnumerable ?  //列挙可能?
                !((System.Collections.IEnumerable)p.Old).Cast<object>().SequenceEqual(((System.Collections.IEnumerable)p.New).Cast<object>()) :
                !object.Equals(p.Old, p.New))
        .Select(p => new RegistryValueChangedInfo(p.ValueName, RegistryValueChangeType.Changed, p.Old, p.New));

    var MergedValues = CreatedValues.Concat(DeletedValues).Concat(ChangedValues).OrderBy(p => p.ValueName).ToArray();

    if(ValueChanged != null && MergedValues.Any())
        ValueChanged(this, new RegistryValueChangedEventArgs(MergedValues));
}

/// <summary>
/// サブキーが変化した
/// </summary>
public event EventHandler<RegistrySubKeyChangedEventArgs> SubKeyChanged;

/// <summary>
/// 値が変化した
/// </summary>
public event EventHandler<RegistryValueChangedEventArgs> ValueChanged;

このように、LoadNowRegistryStateメソッドで現在のレジストリのサブキー名および値を保存しておき、変化通知イベントがあるたびにそれを確認してどこが変わったかを見ています。このときの「値の変化があったかどうか」ということに関しては多くのトラップがあって、処理がとても面倒になります。
1つ目のトラップは配列のEquals()メソッドは参照の等価性しか見ていないということでした。新旧2つの値はどのような型の値でもobject型の状態で保管してあるので、各クラスがオーバーライドしているはずのObject.Equals()メソッドを呼び出して値が等しいかを判定しようとしました。配列以外はそれでもいいのですが、値が配列だった場合(レジストリの値が「バイナリ値」だった場合byte[]型になります)、配列のEquals()メソッドは中身が同じでもインスタンスが違えばfalseを返してくるため意図した動作はしません。なので、IEnumerableを継承していたらLINQのSequenceEqualメソッドを使って配列の等価性を調べるという条件分けが必要になってきます(string型もIEnumerableを継承しているのでSequenceEqualを呼び出すことになりますが、まあ大きな問題では無いでしょう)
そこで現れるのが2つ目のトラップで、.NETは値型の共変性、反変性が無いということでした。 非ジェネリックスのIEnumerable時代遅れなのでもう使いたくないんですがSequenceEqualメソッドでは使えない(定義されていない)のですが、byte[]は値型の配列なのでIEnumerableにキャストできてもIEnumerable<object>にはキャストできません。この辺りで一度はまってしまいました。結局、いったんIEnumerableにキャストしてからCast<object>()メソッドを呼び出してIEnumerable<object>に変換することで解決しています。冒頭にusing System.Collectionsを書かないようにするために名前空間付きでをIEnumerableを書いたので結構横長になってしまいました。

まあ、そんな感じで以前に保存していた値との比較をし、値が変化すればその内容を報告するようなイベントを作り、呼び出すようにしました。


このような形で、C#からのレジストリ監視が可能になりました。めでたしめでたし。
えっ、このライブラリは公開してくれないのかって?うーん、まあそのうちね( ◠‿◠ )

0 件のコメント:

コメントを投稿