Diary of a Perpetual Student

Perpetual Student: A person who remains at university far beyond the normal period

Windows の OS 名をレジストリから取得するのはやめよう

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 を立てた。

github.com

mackerel-agent の実装はどうなっているのか

mackerel-agent が Windows の OS 名やバージョンを取得している実装は以下の部分である。

https://github.com/mackerelio/mackerel-agent/blob/7417ef2be05e88c815ddc8a4002ba32a6b74d16c/spec/windows/kernel.go#L25-L44

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 フォーラムでもスレッドが立っていた。

learn.microsoft.com

Windows 10 は最後の OS だ」と言い切ってしまったが故にこのような混沌が生み出されたのかもしれない。

どう直すか

レジストリを使った取得方法では正しい OS 名を取得できないため、実装を改めなければならない。改修方針案はいくつかあった:

  • OS 名は、レジストリ ProductName と CurrentVersion の両者から判定する
    • つまり、ProductName が Windows 10 で CurrentVersion が m.n.o 以上だったら Windows 11 とする、といった感じ
  • 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" と結果が返ってくる。これこそが求めていた文字列である。

learn.microsoft.com

Go のプログラムから WMI に問い合わせる

ここまでできたら、あとは mackerel-agent から WMI というインタフェースをどう触るか、ということを考えれば良い。mackerel-agent は Go 言語で開発されているので、この要件は「Go のプログラムから」と読み替えてもよくなる。

実は mackerel-agent には、すでに go の WMI ライブラリ github.com/StackExchange/wmi が利用されている。しかし、このリポジトリを見るともうメンテされていないようなので、 fork 先の github.com/yusufpapurcu/wmi を使うことにする。

こうして、自分で実験的なアプリケーションを作ってみた。

github.com

//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 に詳しい方はぜひ教えていただきたい。

*1:そのために家のネットワークに VPN で繋げるようにしてあるし、OS も Windows Pro をわざわざ買っている。