ラベル レジストリ の投稿を表示しています。 すべての投稿を表示
ラベル レジストリ の投稿を表示しています。 すべての投稿を表示

2015年11月27日金曜日

ProxySwitcher ver.1.0.0

さて、先日公開しましたSSIDProxy ver.1.1.1ですが、実はこっそりプロキシ設定機能の分離を行っておりました。ProxySwitcher.dllっていう新しいDLLが付属するようになったことが勘の良い方は気づいたかもしれません。
なぜ分離したかと言いますと、まあ、プログラムのモジュール化どうこうみたいな話はとりあえず置いておいて、私以外の他人がこれを直接いじれるようにするためです。 先日、ネットサーフィンと言いますか、いわゆるエゴサをしていたところ、VB.NETでプロキシ設定をいじることに苦労されている方がいるのを見かけました。私もこちらの記事で書いたように、一生懸命調べ、レジストリを解析し、やっとC#で設定できるようにこぎつけた経緯があり、この成果を多くの方に使ってもらえるよう、レジストリのデータ構造だけでなくやっぱりライブラリとして公開しようと思い至りました。

というわけで、SSIDProxyに組み込んでいたプロキシ切り替え機能の部分を抽出し、ライブラリとしました。それがProxySwitcherです。
(ちなみに、先日お話をしましたレジストリの変更通知を吐くライブラリもこれに組み込まれています。レジストリの変更通知だけを使いたい方もこのライブラリをどうぞ)

プロキシ設定の仕組み

復習ですが、Windowsのプロキシ設定にかかわるレジストリの項目は下記のようになっています。

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings
┣Connections
┃┖DefaultConnectionSettings
┠AutoConfigURL
┠ProxyEnable
┠ProxyServer
┖ProxyOverride

システムはこのうちのDefaultConnectionSettingsに格納されているバイナリデータのみを読み込み、それをプロキシ設定として扱うようになっているようです。しかし、バイナリデータだけでは人間には中身がよくわからないからか、Internett Settings直下の各値にも同じデータを格納してくれるので、これを読み込むことで現在の設定を知ることも可能です。しかし、これはあくまでもダミーで、これを書き換えたところでシステムはそれをプロキシ設定として認識されないことに注意してください。
すなわち、プロキシ設定の読み出しにはDefaultConnectionSettingsを解読するのが素直で、書き込みの時にはDefaultConnectionSettingsとProxyEnableなどの値に適切な値を書き込む必要があるといえます。

ProxySwitcherの使い方

SystemLanSettingsクラス

上記のようなプロキシ設定を行うクラスがSystemLanSettingsクラスです。このクラスは以下のプロパティを持っています。
  • AutomaticallyDetectSettings
  • UseScript
  • ScriptAddress
  • UseProxy
  • Proxy
  • ProxyOverride
これらのプロパティを読み込み、書き込みをすることでシステムのプロキシ設定を読み込み、書き込みすることができます。また、このクラスはレジストリの変更を監視し、プロキシ設定が変化したらそれを通知する機能が付いています。その通知はINotifyPropertyChangedインターフェースによるプロパティ変更通知の実装で実現されているので、変更されたプロキシ設定に対応するプロパティの変更通知として飛んできます。また、レジストリ監視はWin32APIを使用しており、終了時に各種ハンドルを解放しなければならないので、IDisposableインターフェースを実装しております。インスタンスが不必要になったときはDisposeメソッドを呼び出す必要があります
多くのプロパティはstring型かbool型なのでわかりやすいかと思います。しかし、ProxyOverrideはプロキシの不使用をするアドレスを列挙したList<string>型になっているので注意してください。ローカルアドレスにもプロキシを使用しない場合は"<local>"というテキストをそのリストに含ませる必要があります。また、ProxyプロパティはProxySettings型になっております。

ProxySettingsクラス

プロキシ設定を保持するためのクラスです。WindowsはHTTP、HTTPS、FTP、Socksの4種類のプロトコルに対して個別にプロキシサーバーを設定することができます(使ったことないですが)。なので、ProxySettingsクラスはそれに合わせて
  • HttpProxy
  • HttpsProxy
  • FtpProxy
  • SocksProxy
の4つのプロパティを持っており、それぞれの値を設定できます。
このクラスは初期化時以外では値を書き換えることができない(immutableな)設計がされています。プロキシを設定した場合は適当に値を書き換えた上で新しいインスタンスを作ってあげてください。
ちなみに、コンストラクタのオーバーロードがいくつかありますが、そのうちでもstring型のテキストを受け取るものは、レジストリのProxyServerの値をそのまま受け取ってインスタンス化できるものになっています。また、ProxySettingsのインスタンスをToStringするとそのProxyServerのレジストリ値に書き込めるタイプのフォーマットになります。
なお、このクラスで扱われるそれぞれのプロキシ設定はProxy型になっています。

Proxyクラス

これはサーバーのアドレスとポートの2つの値を保持するためだけのクラスです。ProxySettingsクラスと同様にimmutableな設計をしています。string1つを受け取るコンストラクタは「URL:ポート番号」の書式のテキストを受け付けます。

LanSettingsクラス

今まではシステムのLAN設定を読み書きするためのクラスと、そのクラスに付随したクラスの説明をしましたが、単にLAN設定を保持するためのLanSettingsクラスもあります。このクラスのプロパティを読み書きしてもシステムのLAN設定が変化することはありませんが、SystemLanSettings.ToData()やSystemLanSettings.FromData()というstaticメソッドを介してシステムのLAN設定とLanSettingsインスタンスを相互変換できます。この場合、SystemLanSettingsクラスのインスタンスを作るのに比べてレジストリ監視に伴うオーバーヘッドを減らせますし、Dispose()メソッドを呼ぶ必要も無いので扱いやすいかと思います。

サンプルコード

static void Main(string[] args)
{
    LanSettings stg = SystemLanSettings.ToData();
    Console.WriteLine(stg);

    using(var sysstg = new SystemLanSettings()) {
        sysstg.PropertyChanged += (s, e) => Console.WriteLine($"\"{e.PropertyName}\" has changed.");

        Console.WriteLine("Watching LAN Settings...");
        Console.ReadLine();
    }

    Console.WriteLine("Stop Watching.");
    Console.WriteLine("Press Enter to exit...");
    Console.ReadLine();
}

このプログラムを実行するとこんな感じになります。


最初に現在の設定を表示しております。そのあとにインターネットオプションからいくらかプロキシ設定をいじってみたところ、ちゃんとレジストリ変更を認識して画面に表示してくれています。最後にEnterを押せば監視を終了してくれます。

おまけ:レジストリ監視

このライブラリはレジストリ監視機能を含んでいます。ProxySwitcher.Registry.Watcher名前空間内のRegistryWatcherがそれです。面倒くさいんでサンプルプログラムで使い方は察してください。

static void Main(string[] args)
{
    using(var watcher = new RegistryWatcher(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings")) {
        watcher.ValueChanged += (s, e) => {
            foreach(var info in e.ChangeInfo)
                Console.WriteLine(info);
        };
        watcher.SubKeyChanged += (s, e) => {
            foreach(var key in e.CreatedKeyNames)
                Console.WriteLine($"\"{key}\" has created.");
            foreach(var key in e.DeletedKeyNames)
                Console.WriteLine($"\"{key}\" has deleted.");
        };

        Console.WriteLine("Watching LAN Settings...");
        Console.ReadLine();
    }

    Console.WriteLine("Stop Watching.");
    Console.WriteLine("Press Enter to exit...");
    Console.ReadLine();
}

実行結果はこんな感じになります。


このクラスでは指定したキーの直下にある値の変更、指定したキーの直下へのキーの作成、削除を認識できます。これをうまく使って上記のSystemLanSettingsクラスはプロキシ設定の変更通知を行っています。ちゃんとそのキー下の値やキー名を記憶しておいて、変更があった時に素の差分を通知してくれるようになっています。

免責事項

このソフトウェアを使用することによって発生したいかなるトラブル(データ喪失、パソコン故障、データ流出等)についても、作成者は責任を負わないものといたします。特に、このソフトウェアは独自研究によるレジストリ編集を行っているため、その方法が完全ではなかったり、Windowsのバージョン・システム構成等の環境によって正常に動作しない可能性があります。場合によっては重大なトラブルを引き起こす可能性がありますので、そのようなリスクをご承知の上、ご利用ください。
なお、このソフトウェアを起動した時点で、免責事項を読んだかどうかにかかわらず、これらの免責事項に同意したものとみなします。
テンプレですが、どうぞご注意、ご理解くださいませ。

ライセンス

さて、このライブラリのライセンスをどうしようか考えましたが、SSIDProxyの開発に伴って作ったものということもあり、基本的にはSSIDProxyに準じ、それ以外に下記の条項を付け加えています。
  • このライブラリを改変してはならない。
  • 有償アプリケーションには使用してはならない。
  • 使用方法やバグに関するサポートをする責任を作者は負わない。
  • 完成したソフトウェアのどこか(ヘルプ、バージョン情報など)と、ReadMeなどのドキュメンテーションに私のライブラリを使用したことを明記すること。ただし、作者がこのライブラリを使用するときはその限りではない。
  • このライブラリを利用したアプリケーションをユーザーが配布するときに限って、作者に無断で再配布できるものとする。ただし、必要最小限のファイルに留めること。
今時クローズドソースも流行らないかもしれませんが、まあ、とりあえず現状はこのような形で。ニーズが出てきたら適宜ライセンスを変更してオープンソースにするかもしれません。

ダウンロード

SSIDProxyをダウンロードしてください。
SSIDProxy ver.1.1.1
そのうちの 
  • bin\ProxySwitcher.dll
  • bin\ja-JP\ProxySwitcher.resources.dll
の2つがProxySwitcherで必要なファイルです。プロジェクトからProxySwitcher.dllを参照をして使用してください。

2015年11月26日木曜日

SSIDProxy ver.1.1.1

さて、前回に引き続きSSIDProxyを更新いたしました。いちいち概要や使い方を何度も書くのもアレなので、初めてこのソフトについて知った方は前回の記事を読んでください。今回の更新は主にディテールのリファインやリファクタリングですが、表に出てくるような変更点としては下記のとおりです。

今回の変更

【新機能】
    • 他のアプリケーションによるプロキシ設定の変更を検知できるようになった。
    【機能変更】
    • 翻訳の改善
    • ポップアップ、タスクトレイアイコンのツールチップのメッセージを微妙に変更
    【バグ修正】
    • 一部環境でLAN設定の読み込み時に不正終了するバグの修正
    • 一部レジストリ設定の挙動模倣が不十分だったので修正
    • 自動設定用スクリプトの設定機能が不十分だったのを修正
    • ManagedNativeWifiはManaged Wifi APIの派生プロジェクトではありませんでした。バージョン情報等の謝辞を一部変更しています。
    まず、今回のバージョンからレジストリを監視し、他のアプリケーション(もちろんインターネットオプションからの変更を含む)によるプロキシの変更を検知し、ポップアップでお知らせできるようにしました。
    他のアプリケーションによってルールに違反した設定がされたとき、すぐさま元に戻すような機能を実装しよかと一瞬考えましたが、そのプロキシを設定した側のソフトで意図しない動作が起こる可能性や、もしくはSSIDProxyと同様のソフトが同時に動作していた場合、プロキシの設定の変更合戦になってしまうことを考えて、通知に留めました。SSIDProxyの保持しているルールに則った設定に戻したい場合は、アイコンを右クリックして今すぐルールを適用すればおkです。

    なお、上の箇条書きのところには書いておりませんが、内部的にルールの保存データのフォーマットが若干変更になっています。ver.1.1.0からのコンバーターは内蔵しているので、上書きインストールをすれば特に気にすることなく新データに移行してくれますが、多分次のバージョンでそのコンバーターが削除されるので、ver.1.1.1を介さずにver.1.1.0のデータを引き継ぐことはできなくなるかと思います。

    免責事項

    このソフトウェアを使用することによって発生したいかなるトラブル(データ喪失、パソコン故障、データ流出等)についても、作成者は責任を負わないものといたします。特に、このソフトウェアは独自研究によるレジストリ編集を行っているため、その方法が完全ではなかったり、Windowsのバージョン・システム構成等の環境によって正常に動作しない可能性があります。場合によっては重大なトラブルを引き起こす可能性がありますので、そのようなリスクをご承知の上、ご利用ください。
    なお、このソフトウェアを起動した時点で、免責事項を読んだかどうかにかかわらず、これらの免責事項に同意したものとみなします。
    テンプレですが、どうぞご注意、ご理解くださいませ。

    ダウンロード

    SSIDProxy ver.1.1.1

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