2020年12月22日火曜日

C#でコードを書く前にPowerShellで試行錯誤する

 どんなプラットフォームでもAPIの数は膨大で、全部を知っているという人はあまり多くはないと思う。となると、開発時には、APIを動かして挙動を確認すると必要がある。こういうときにはテスト用のコードを作るが、開発中のアプリケーションがある程度の大きさになってくると、テストコードを組み込んで動かすのも大変なので、独立したテストプログラムを作らねばならない。しかし、これは意外に面倒だ。というのも、世の中には込み入ったパラメーターを要求するAPIがある。また、どのAPIを使えばいいかははっきりしているが、与えるパラメーターが膨大で、正しい組合せがなかなかわからないといったこともある。サンプルコードがあれば、このあたりもかなり解決するのだが、世の中そうそう、うまくはいかない。また、APIの返すデータやオブジェクトが大量で、そのうちどれを選べば良いのか、そもそも、正しいものを選ぶための条件は何かというのがまったく分からないことがある。

 C#での開発となると、相手は、.NET Frameworkになるのだが、こいつも結構面倒なAPIが多い。こういうときには、APIをいろいろと叩いて、どれをどう使えばいいのかを探る必要がある。もちろん、インターネット検索で答えが見つかればいいのだが、いつでも見つかるとは限らない。

 で、PowerShellである。PowerShellは、.NET FrameworkのAPIそのままのコマンドがあり、定義されたコマンドがなくても、.NET Frameworkの呼び出しは簡単だ。たとえば、CMIをC#から扱うには、

ManagementClass myCIMClass = new ManagementClass("Win32_PerfFormattedData_Tcpip_NetworkInterface");
foreach (ManagementObject item inmyCIMClass.GetInstances()) {
Debug.writeline  ((String)item["Name"]) + "\t"
    +((ulong)item["CurrentBandwidth"]).toString() + "\t"
    +((ulong)item["BytesSentPersec"]).toString();}

という感じにしなければならないが、PowerShellなら

PS C:> Get-CimInstance -Class Win32_PerfFormattedData_Tcpip_NetworkInterface | Format-Table
Name,CurrentBandwidth,BytesReceivedPersec,BytesSentPersec
Name CurrentBandwidth BytesReceivedPersec BytesSentPersec
---- ---------------- ------------------- ---------------
Killer E2200 Gigabit Ethernet Controller 1000000000 880 0
Teredo Tunneling Pseudo-Interface 100000 0 0

とするだけで結果を見ることができる。わざわざテストコードを書くまでもない。

 しかし、問題はこれでは終わらない。前記のPowerShellの実行例で、インターネット接続の速度を測定するにはどっちのネットワークインターフェースの値を取るべきか。もちろん、人間なら最初の「killer E2200」のほうだとわかる。しかし、これをコンピューターに判断させるには、どうしたらいいのか? Win32_PerfFormattedData_Tcpip_NetworkInterfaceは、稼働中の状態を返してくれるが、インターフェース自体の情報は名前しかわからない。また、Teredo Tunneling Pseudo-Interfaceのほうは、読み書きの速度はずっとゼロのままだが、これを確認するには、一定時間速度を測定し続ける必要があるし、また、ある期間ゼロが続いたとしても、そのあともゼロという保証もない。
 そのためには、正しいネットワークアダプタを見付ける必要があるが、「正しいネットワークアダプタ」ってなんだろ? だいたいシステムには、仮想マシンで使う仮想アダプタ、有線LANに無線LAN、Bluetoothとかあって、ネットワークアダプタ(ネットワークインターフェース)がいくつもある。PowerShellでWin32_NetworkAdapterConfigurationを使ってネットワークアダプターを列挙させたら、16個もあった。

 この中から正しいものを見付ける必要がある。ではどうするか? それも、PowerShellで試行できる(錯誤もあるが) 。まずは、いったいどんな情報が含まれているのか? これは、結果オブジェクトをget-memberに喰わせてみる。

PS> Get-CimInstance -Class Win32_NetworkAdapterConfiguration | get-member
 TypeName: Microsoft.Management.Infrastructure.CimInstance#root/cimv2/Win32_NetworkAdapterConfiguration
Name                         MemberType  Definition
----                         ----------  ----------
Dispose                      Method      void Dispose(), void
  IDisposable.Dispose()
Equals                       Method      bool Equals(System.Object obj)
GetCimSessionComputerName    Method      string GetCimSessionComputerName()
GetCimSessionInstanceId      Method      guid GetCimSessionInstanceId()
GetHashCode                  Method      int GetHashCode()
GetType                      Method      type GetType()
      :
      :
    以下省略

ざっと眺めた感じ、IPアドレスを持ってる(IPEnabled)で、デフォルトゲートウェイ(DefaultIPGateway)がnullでないとすると、インターネット接続に使っていそうなネットワークインターフェースを取り出せそう。

PS> Get-CimInstance -Class Win32_NetworkAdapterConfiguration | select-object index,Description,IPEnabled,DefaultIPGateway
 index Description                              IPEnabled DefaultIPGateway
 ----- -----------                              --------- ----------------
     0 Microsoft Kernel Debug Network Adapter       False
     1 VirtualBox Host-Only Ethernet Adapter         True
     2 Hyper-V Virtual Switch Extension Adapter     False
     3 Hyper-V Virtual Ethernet Adapter              True
     4 Bluetooth Device (Personal Area Network)     False
     5 Killer E2200 Gigabit Ethernet Controller      True {192.168……
     6 WAN Miniport (SSTP)                          False
     7 WAN Miniport (IKEv2)                         False
     8 WAN Miniport (L2TP)                          False
     9 WAN Miniport (PPTP)                          False
    10 WAN Miniport (PPPOE)                         False
    11 WAN Miniport (IP)                            False
    12 WAN Miniport (IPv6)                          False
    13 WAN Miniport (Network Monitor)               False
    14 Hyper-V Virtual Switch Extension Adapter     False
    15 RAS Async Adapter                            False
    16 Hyper-V Virtual Switch Extension Adapter     False
    17 Hyper-V Virtual Ethernet Adapter #2           True

 方針としては、まずWin32_PerfFormattedData_Tcpip_NetworkInterfaceを見る。ここのオブジェクトは実際にネットワークとして動作しているネットワークインターフェースだけが列挙される。ただし、「CurrentBandwidth」がゼロになっているものは、無視して良い。Windows10では、無線LANがタスクバーのネットワーク接続フライアウトでオフになっているようなものがこれに相当する。

 これで残り1つなら、それがインターネット接続に使っているアダプターの測定結果なので、これを選べば良い。しかし、それでは決定できないとき、Win32_NetworkAdapterConfiguration の結果を利用し、IPアドレスを持ち、かつデフォルトルート(デフォルトゲートウェイ)が設定されているネットワークアダプターを探し、 そのDescriptionとWin32_PerfFormattedData_Tcpip_NetworkInterface のNameの一致をみる。このとき、Description中の一部記号がName内では変換されているときがある。いままで見た感じでは、「#」は「_」(アンダーバー)に、カッコ「( )」は、角カッコ「[ ]」になるようだ。これでもまだ複数残るような場合、たとえば、有線LANと無線LANがともに有効で接続しているような場合がありえる。まあ、こういうときには、最初に見付けたほうか、CurrentBandwidth の大きい方ぐらいしか手がかりがない。もし、ユーザーに選択させるなら、この段階まで絞り込んだほうがいいだろう。それで作ったコードがこれ。

//あとで使うので変数に入れとく
// 断片だからこのままじゃ動かないからね。
var myCIMClass = 
 new ManagementClass("Win32_PerfFormattedData_Tcpip_NetworkInterface");
var MyNetIfList = GetNetIFList();
var NetIF = FindNetIF(MyNetIfList);

private List<string> GetNetIFList() {
    List<string> Result = new List<string>();
    foreach (ManagementObject item in myCIMClass.GetInstances()) {
        if ((ulong)item["CurrentBandwidth"] > 0) {
            Result.Add((string)item["Name"]);    }  }//End foreach
    return Result;
}

private string FindNetIF(List<string> netlist) {
    // netlistに要素が1個しかないならこれを戻す
    if (netlist.Count == 1) { return netlist.First();                
        } else if (netlist.Count == 0) {
            return null;
        }
    // netlistに要素が複数ある
    // NetworkAdapterの情報を取得
    var tempcim = new ManagementClass(
        "Win32_NetworkAdapterConfiguration" );
    string Desc="";
    // NetworkAdapterConfigurationのコレクションをスキャン
    foreach (ManagementObject item in tempcim.GetInstances()) {
        if ((bool)item["IPEnabled"] 
            &amp;&amp; (null != item["DefaultIPGateway"])) {
            // IPアドレスを持ち、かつ、デフォルトゲートウェイが存在
            // DescriptionはTcpip_NetworkInterfaceのNameに相当
            Desc = ((string)item["Description"])
                .Replace("#", "_")
                .Replace("(", "[")
                .Replace(")", "]" );
      // ↑ 記号の置き換え
            break; } } //End IF
        if (Desc != "") {
            // 名前があるやつだけチェック
            // ループ回すのは相手がコレクションだから
            foreach (string item in netlist) {
                // 最初にみつけたやつでいいので戻る
                if (item == Desc) { return Desc; } } } //End foreach
    // みつからなかったのでnullを戻す
    return null;
}

これでも、有線LANと無線LANが同時接続している場合には、どっちを取るべきかは判断できない。とりあえず、最初に見付けたほうを使うが、正解になるかどうかはわからない。なので、別のところで、GetNetIFList()の結果をコンテキストメニューにして選択できるようにしてある。これが先週の土日の成果である。時間がかかったわりにはコード量的にはたいしたことがない。結局、複数のマシンでデバッグできる環境設定とかに時間を取られた。存在しないけど無線LANとか有線LANのダミーのデバイスを作ることができる仮想環境が欲しいところ。

0 件のコメント:

コメントを投稿