🦔

【winax】node.js で Windows に接続されている USB メモリのデバイスインスタンスパスを取得するまで

2023/06/26に公開

動機

node.js から Windows に接続されている USB メモリのデバイスインスタンスパスを取得したいが、child_process の結果を正規表現で解析したくない。どうしたらよいだろう?

結論

  1. winax を利用する
  2. WMI オブジェクトの実態は SWbemServices オブジェクトのため、シグネチャはここを参照する
    • ただし、Visual Basic 向けなので適宜読み替える必要がある😭
  3. WMI クラス名 (Win32_USBHub など) の一覧は CIMWin32 WMI プロバイダーから閲覧できる
    • 必要そうなものは大体あるが、一部はこのページのツリーの外にも存在すると思う😭
  4. WQL (WMI 用の SQL 方言) の仕様は WQL を使用したクエリから参照できるが、自分は Select * from ... where ... 程度しか利用していないので特に方言だとは意識しなかった
import * as ActiveX from 'winax'

const wmi = new ActiveX.Object('WinMgmts:', { getobject: true })
const collection = wmi.InstancesOf('Win32_USBHub')
for (let index = 0; index < collection.Count; ++index) {
  console.log(collection.ItemIndex(index).DeviceID)
}

実装に至るまでの過程

  • 実装のみ知りたい方は読み飛ばして構わない
  • 結構な長旅だったため、今後難しい課題にぶつかったときに、同様に解決できるとよいなと思う

libusb アプローチ

  1. python 向けではあるものの、pyusb を発見した
    • C 言語で書かれた USB ライブラリである libusbctypes を利用して接続してある
  2. node.js にも C/C++ API は存在するので、libusb の node.js 向けバインディングが実装されているはず
  3. pyusb の方を $ pip install して少し実装してみるが、デバイスインスタンスパスを取得できず
    • libusb の Wiki にたどり着く
    • Windows がデバイスマネージャにおける「USB 大容量記憶装置」向けにデフォルトでインストールするデバイスドライバは USBSTOR のようである
    • このドライバは libusb と互換性がなく、デバイスインスタンスパスの取得を含むほとんどの機能が利用できない
    • デバイスドライバを WinUSB に置き換えれば確かに取得できるが、今度はエクスプローラから USB メモリにアクセスできなくなる
      • USB メモリへの GUI ベースアクセスは USBSTOR に依存しているのであろう
  4. この方法は諦めて、別の方法を探すことにした

WMI アプローチ

  1. デバイスインスタンスパスはデバイスマネージャに表示される以上、Windows API により取得できるはず
    • ChatGPT が pywin32 を利用するサンプルコードを提示する
    • とはいえ、Windows API を直接利用するのはだいぶ面倒
  2. Windows API よりもう少し楽な方法を探したい
    • pywin32 の中でも、win32com.client を利用しているページを発見
    wmi = win32com.client.GetObject("WinMgmts:")
    wmi.InstancesOf("Win32_USBHub")
    
    • どうやら WMI (Windows Management Instrumentation) というサービスを利用しているらしい
    • WMI は COM オブジェクトの一種であり、また、Microsoft は COM 技術の呼び名を頻繁に変えた歴史があるようだ
      • OLE, COM, (悪名高き?) ActiveX など…
    • この辺の単語と node.js で検索して、winax を発見
      • インターフェースは new ActiveX.Object 1つだけのようだ
  3. WMI サービスオブジェクトをどうにか取得したい
    • win32com.client.GetObject 相当をどのように呼べばよいか
    • GitHub の右上の検索窓に repo:durs/node-activex GetObject と入れてみると、以下がヒットする
    global.GetObject = function GetObject(strMoniker) {
      return new g_winax.Object(strMoniker, {getobject:true});
    }
    
    • 第2引数の { getobject: true } が肝のようだ
  4. WMI サービスオブジェクトを使う方法を知りたい
    • スタブがないので型がわからない
    • ソースも C++ なのであまり読み解くことができない
    • WMI GetObject で検索すると、Microsoft の VBScript のサンプルコードが出てくる
      • インターフェースが全く同じであることから、これを JavaScript に変換すれば動作しそう
    • new ActiveX.Object('WinMgmts:', { getobject: true }) で返ってくるのは SWbemServices オブジェクト のようである
      • この通りにスタブファイルを書いてみることで、(ある程度は) 型安全になった
      • 以下は型定義ファイル(一部抜粋)
      type SWbemClassName = 'Win32_USBHub'
      type SWbemServices = {
        InstancesOf<T extends SWbemClassName>(className: T): SWbemObjectSet<T>
      }
      
  5. 高速にデバッグをしたい
    • プロジェクトに書き込んでいると、TypeScript + webpack (esbuild に移行したい) という構成のため、それなりに1イテレーションが長め
    • このため、JScript を記述する
      • 実は Windows にはデフォルトで使用できるスクリプトランタイムがある
      • そのフロントエンドは JavaScript 風の JScript と、Visual Basic 風の VBScript が存在する
      • Visual Basic は残念ながらよくわからないので JScript を記述してデバッグする
        • どこかで JScript は ES3 程度だという記述を見たことがあるので、そこには気を付ける
        • constletfor ... of ループは存在しない
        • ブラウザとは環境がだいぶ違って、console は存在しないので WScript.Echo する
        • 一応 Enumerator という便利なクラスも存在するようだが、TypeScript に移植すると動作しないので使用しないでおく
      var wmi = GetObject('WinMgmts:')
      var collection = wmi.InstancesOf('Win32_USBHub')
      for (var index = 0; index < collection.Count; ++index) {
        console.log(collection.ItemIndex(index).DeviceID)
      }
      
    • このように printf デバッグをしながら型定義ファイルを詰めていき完成

Discussion