Chapter 04

WSL2時代のアプリケーション実装(Linuxアプリケーション編)

SHIBUKAWA Yoshiki
SHIBUKAWA Yoshiki
2020.10.12に更新

あなたはLinuxアプリケーションです。GoコンパイラでGOOS=linuxというフラグをつけてビルドされました。あなたがアクセスできるのはLinuxのファイルシステムで、呼び出せるのはLinuxのシステムコールです。生まれてこの方、Linux環境しか知りません。しかし、あなたがみている世界は幻の世界かもしれません。Linuxのすぐ外には、Windowsの世界が広がっているかもしれません。

  • 赤いピル
  • 青いピル

赤いピルを飲んだあなたは、Linuxの外の世界を知りました。外の世界にはWindows環境が広がっています。

WSL2のLinuxアプリケーションはLinux環境で動作します。しかし、Windowsのファイルシステム上で、Windowsのアプリケーションから起動されてサービス提供しないといけない可能性もあります。本章ではその複雑なユースケースに対応する方法を紹介します。

環境の判定

まずは自分が動いているのがWSL2環境なのか、ネイティブなLinux環境なのかを知る必要があります。ファイルシステムに次のファイルがあるかどうかで判定できます。

  • /run/WSL

簡単ですね。

ただ、これだけではWindowsに奉仕すべきLinuxアプリなのか、Linux環境のみを相手にすればいいいLinuxアプリなのかがわかりません。これは確実に「これ!」と言える状況はありません。

Linuxアプリの利用のされ方は3通り考えられます。

  • WSL2の環境の中で、Linuxコマンドの一部として実行
  • Windows環境の中から、wsl.exeコマンド経由で実行
  • Windows環境の中から、wsl.exeコマンド経由で実行されるが、直接ではなく、他のプログラム経由で実行

最初の例はLinuxアプリとして振る舞っておけば問題ないと思いますが、それ以降はWindowsのプロセスとして実行すると良い可能性があります。例えば、sshの鍵やらAWSのクレデンシャルやらはWindows側をみた方が良かったりするかもしれません。

環境の判定は2つあります。

一つは実行フォルダです。mnt/c/以下で動いている場合、Windows向けとみなせる可能性が高いです。2章で説明した、「ファイルシステム跨ぎで動かすのは効率が悪い」というのがコンセンサスとして広く広まれば、Windowsの処理系が処理するコードはWindowsのファイルシステムにあるべき、という前提が建てられます。それであれば、カレントフォルダが/mnt/c/であればWindows向けとみなせるかもしれません。

もう一つが親プロセスです。wsl.exeコマンド経由で実行されると親が必ず/initになります。

Goの標準ライブラリでは親のプロセスIDは取得できますが、コマンドはわかりません。github.com/shirou/gopsutil/processパッケージを利用すると親のコマンドがみれます。Pythonもpsutilを使えばわかります。

p, err := process.NewProcess(int32(os.Getppid()))
if err != nil {
	panic("失敗: 親プロセス取得不能")
}
c, err := p.Cmdline()
if err != nil {
	panic("失敗: コマンド名取得不能")
}
if c == "/init" {
	fmt.Println("Windows環境かもしれない")
}

どちらも確実に白黒はつけられません。場合によってはユーザーが明示的に指定することも考慮が必要です。おそらく、次の4ステップで判定となるでしょう。

  1. まずWSL環境下チェック(/run/WSLの存在チェック)
    →何もなければWSL2ではなくネイティブLinux
  2. ユーザーが環境変数などでWindows側を見るかLinux側を見るかを強制するオプション設定
    →指定された方の環境を見る
  3. 親プロセスを見る
    →/initならWindows側環境と判断
  4. 作業フォルダがWindowsならWindows側と判断

子プロセスの起動

WSL2のターミナルからWindowsのプログラムの起動はbashでは何事もなくできますが、Goのos/execパッケージなどの起動する場合は一手間かかります。Windowsの実行ファイルは.comや.exeなど、さまざまな拡張子を取りえます。PATHEXTという環境変数がその拡張子のリストになるため、一つずつ付けてはexec.LookPath()で存在を確認して、見つけたら実行します。

パスの変換

WSL2環境上でWindows用のアプリが起動されたが、設定ファイルに書き出したファイル名をLinuxアプリで活用したいので、Linuxのパス名を出力しておきたいみたいなケースがあるかもしれません。

wslpathというユーティリティコマンドがあります。Linux上ではこれを使います。

  • wslpath -w (Linuxのパス): Windowsのパスに変換
  • wslpath (Windowsのパス): Linuxのパスに変換

環境変数やホームフォルダの取得

Goのosパッケージを使えば、環境変数やホームディレクトリとか設定のフォルダをとってきたりできます。Go以外のPythonでもosパッケージで、Node.jsはruntime経由で環境変数が取得できます。しかし、LinuxアプリとしてビルドしたらLinux環境しか見れませんし、WindowsアプリはWindows環境しか見れません。これらのライブラリはあくまでもビルドターゲットのOSを想定してその環境を得る目的でしか使えません。

LinuxからWindowsの環境変数や特殊なフォルダパス(ホームとか)を取得してくるユーティリティとしてはwslvarというものがありますが、これは起動されるたびに現在のコンソールをフラッシュして消してしまうという余計な副作用つきです。子プロセスとして起動してターミナルには繋がっていないはずの状態でも消してきます。凶悪です。Windowsが入っているドライブやディレクトリやフォルダが改変されているとNGですが、今のところ一番行儀が良いのは次の方法かなと思います。

/mnt/c/Windows/System32/cmd.exe /C set

あとは、Goのosパッケージと同様に環境変数を分析することで、フォルダを決定できます。

  • ホームフォルダはUSERPROFILE環境変数
  • キャッシュフォルダはLocalAppData環境変数
  • 設定のフォルダはAppData環境変数

本来はこれもwslvarで一発だったので、画面フラッシュはなんとかして欲しいです。

Windowsの関連付けでファイルを開く

ブラウザや、生成したファイルのビューアーなどはWindows側のプログラムを利用する方がよいでしょう。Cドライブで決め打ちしてしまっていますが、次のコマンドで対象のファイルを開くことができます。

cmd := exec.Command("/mnt/C/Windows/System32/cmd.exe", "/C", "start", targetFilePath)
cmd.Run()

まとめ

LinuxアプリケーションがWindowsアプリケーションとして振舞うための各種作法を紹介しました。Linuxバイナリなのに、Windows環境を知らなければならないというWSL特有のケースをこなすには必要なことです。

Windowsなので、最終的なUIはWindows側になります。関連付けで開く機能などは積極的に使うようになるのではないでしょうか?