3行
- Windows の OS 名をレジストリから取得すると、Win 11 が Win 10 として表示されてしまう
- 例えば、WMI を使って取得する方法が正攻法っぽい
- Go から WMI を触るサンプルコードもあるよ
Win 10 が Win 11 として Mackerel に登録されている
自分は OSS の動作確認のために Windows を使いたくなったときに、手元の Macbook から別のホストにリモートデスクトップで繋いで使っている*1。また、繋ぐ前に WOL マジックパケットを送ってスタンバイ状態から復帰させるようにしている。
ここで、手元にマシンがない状態だと WOL によりちゃんと起動したかどうかがわからない。そこで、mackerel-agent をインストールしていて、Mackerel 上でメトリックが取れているかどうかでマシンの生存確認をするようにしている。
そんな中、ふと気づいた。ホスト OS は Windows 11 のはずなのに、Mackerel の Web コンソールには Windows 10 として表示されていることに。
Windows なんもわからん……と思いながら、詳しい人が教えてくれたら良いなという楽観的発想でまず issue を立てた。
mackerel-agent の実装はどうなっているのか
mackerel-agent が Windows の OS 名やバージョンを取得している実装は以下の部分である。
osname, _, err := windows.RegGetString(
windows.HKEY_LOCAL_MACHINE, registryKey, `ProductName`)
これらの情報はレジストリから取得していて、例えば OS 名であれば HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\ProductName
から取得した値を利用している。
つまり、先ほどの不具合は、OS が Windows 11 でもこのレジストリの値が依然として "Windows 10"
であることによるものだった。
この問題については Windows の Q&A フォーラムでもスレッドが立っていた。
「Windows 10 は最後の OS だ」と言い切ってしまったが故にこのような混沌が生み出されたのかもしれない。
どう直すか
レジストリを使った取得方法では正しい OS 名を取得できないため、実装を改めなければならない。改修方針案はいくつかあった:
- OS 名は、レジストリ ProductName と CurrentVersion の両者から判定する
- PowerShell のコマンド
Get-Computerinfo
を実行し、パースするConvertTo-Json
コマンドも利用すれば program-friendly な JSON 形式で OS 名を取得可能
- WMI の Win32_OperatingSystem クラスにある Caption というプロパティ値を OS 名とする
レジストリによる取得の延長的な改修に関しては、mackerel-agent の該当実装部分にやたらと分岐が増えて負債となり得るので、避けたいと思った。Windows 11 がこんな状況では、Windows 12 以降どうなるか先が思いやられる。仕様が現実に追いついていない不安定なものに依存したくはない。
PowerShell のコマンドを実行する方式もぱっと見悪くはないと思ったのだが、チーム内で話したところできれば避けたい、という結論になった。これは、PoweShell の動作が重いこと、ConvertTo-Json
を利用できない OS がありそうなことなどによる。
そこで、3つ目に挙げた WMI (Windows Management Instrumentation) を利用する手法に着目した。WMI とは Windows OS を管理・監視するために作られたインタフェースのようだ。WQL という SQL-like な言語を使ってシステム情報を問い合わせることができる。
Win32_OperatingSystem クラスの中に Caption というプロパティがあり、「オペレーティング システムのバージョン」が含まれているようだ。試しに Windows 11 のホストで値を取得すると"Microsoft Windows 11 Pro"
と結果が返ってくる。これこそが求めていた文字列である。
Go のプログラムから WMI に問い合わせる
ここまでできたら、あとは mackerel-agent から WMI というインタフェースをどう触るか、ということを考えれば良い。mackerel-agent は Go 言語で開発されているので、この要件は「Go のプログラムから」と読み替えてもよくなる。
実は mackerel-agent には、すでに go の WMI ライブラリ github.com/StackExchange/wmi が利用されている。しかし、このリポジトリを見るともうメンテされていないようなので、 fork 先の github.com/yusufpapurcu/wmi を使うことにする。
こうして、自分で実験的なアプリケーションを作ってみた。
//go:build windows package windows import "github.com/yusufpapurcu/wmi" // cf.) https://learn.microsoft.com/windows/win32/cimwin32prov/win32-operatingsystem type Win32_OperatingSystem struct { Caption string Version string CSDVersion string } type WinVer struct { OSName string Version string Release string } func GetWinVer() (*WinVer, error) { var dst []Win32_OperatingSystem q := wmi.CreateQuery(&dst, "") if err := wmi.Query(q, &dst); err != nil { return nil, err } winVer := WinVer{} for _, v := range dst { winVer.OSName = v.Caption winVer.Version = v.Version winVer.Release = v.CSDVersion break //nolint } return &winVer, nil }
今回利用した wmi ライブラリで特徴的なのが、結果を格納するために作る構造体の構造からそのまま WQL クエリが作られるところだ。つまり、Win32_OperatingSystem
という構造体の名前やフィールド名を任意に変えることはできない。
GitHub Actions に Windows の runner があるので、これを利用してテストを動かしてみた。OSName が Microsoft Windows Server 2019 Datacenter として取得できていることが確認できる。
mackerel-agent にこれを組み込むにはまだ考えることがあるので未着手だが、近いうちに取り組もうと思う。
(2022-02-03 追記) 上記方針での改修が取り込まれた mackerel-agent v0.75.0 がリリースされました
まとめ
Windows の OS 名を取得したい時には、レジストリを参照するのではなく WMI を利用しよう。もしくは、「もっと良い方法があるよ」という有益情報をお持ちのWindows に詳しい方はぜひ教えていただきたい。