この記事は最終更新日から1年以上が経過しています。情報が古くなっている可能性があります。
かれこれ 2 年くらい WSL1 → WSL2 を使っていたが、適当に使っていた事もあって環境が汚れてきてしまったのと、Ubuntu 20.04 LTS にアップグレードしたいがそのままアップグレードすると酷いことになることが大方予想ついたので、データをバックアップした上で再インストールすることにした。そのときにやったことをメモしておく。
自分用のメモなので、他の環境で同じようにやって上手くいくかは微妙。
とにかくやった事をメモ書きしていったら 41000 字にまで到達してしまい、記事としてもかなり雑多なものになってしまった。より良さそうなソリューションも見つけたので、将来的に別記事として書き直すかもしれない。
- バックアップ
- やったこと
- Genie で systemd を動かし、サービスを自動起動できるようにする
- WSL に外部から接続できるようにする
- 最終的な .profile の内容
- 振り返って
バックアップ
WSL 本体のバックアップ
WSL は複数インストール可能なので旧 WSL と並行で徐々にデータを移植することも可能だが、元々 SSD の余裕がない。さらに既に「Ubuntu」でインストールしてしまっており、並行にインストールする場合「Ubuntu 20.04 LTS」の方をインストールすることになるが、フォルダ諸々「Ubuntu」の名前で扱いたかった(WSL のディストリビューションはリネームが基本できず、やるとしてもエクスポート→インポートになる)ため、敢えて再インストールとした。
WSL は公式でエクスポートができる。エクスポートは上の記事を参考にした。
私の場合はローカルに置くほど容量がなかったので、.tar の解凍先は NAS に設定した。
私の場合 9GB ほどあり、NAS に置いたこともあってバックアップに数十分程度かかった。
あらかじめ上の記事を参考にディスクイメージを圧縮し、apt 等のキャッシュ( /var/cache/apt/
・~/.cache/
など)を消しておくと良いかもしれない。
こうしておけば、最悪セットアップに失敗しても前の WSL をインポートして復活させることができる。
MySQL のバックアップ
MySQL を使っていたので、こちらは mysqldump でバックアップをとる。
mysqldump でのバックアップは上の記事を参考にした。全てのデータベースとユーザー情報をバックアップした。
.dump ファイルはバックアップできたらかならず WSL 外に移動しておくこと(そうしないと本末転倒)。
apt のインストール情報
ぐちゃぐちゃになった環境のリフレッシュも兼ねているので、そのまま今まで入れていたパッケージを全てインストールするわけではない。ただ「以前の WSL で何入れてたっけ…?」となることがあるかもしれないなーと思い、一応保存しておく。
$ sudo apt list --installed > apt-list.txt
インストール情報は上記のコマンドで書き出せる。上と同じく、書き出した後は WSL 外に移動しておくこと。
やったこと
再インストール
Windows の設定 → [アプリ] から、Ubuntu を一旦アンインストールする。
リセットも可能だが、その場合また Ubuntu 18.04 LTS になってしまう可能性が大きそうだったのでやめた。
アンインストール後、Microsoft Store から Ubuntu をもう一度インストールする。
インストールする前に、wsl --set-default-version 2
を実行し、WSL2 をデフォルトにしておくこと。
インストール後、本来であれば一般ユーザーを作ることになる。が、root ログインできないのは色々めんどくさいなーと感じたので、とりあえず root でログインするようにする。この記事 を参考に、起動後出てくるプロンプトで root と入力した後、既に存在するユーザーですとか言われるけどそのままウインドウをそっ閉じすると、root でログインできるようになる。
めんどくさかったので再インストールしてなんとかした。
\\wsl$\Ubuntu
と入力すると開ける。\\wsl$ の権限の問題
一般ユーザーにしたはいいが、一つだけ「 Windows 側から /etc などのシステムファイルを操作できない」という致命的な問題が。
Windows 側から WSL のファイルシステムへは \\wsl$
という仮想的なネットワークドライブとしてアクセスする。だが、MS のドキュメントいわく、
\\wsl$
経由で Linux ファイルにアクセスする場合は、WSL ディストリビューションの既定のユーザーが使用されます。 したがって、Linux ファイルにアクセスするすべての Windows アプリに、既定のユーザーと同じアクセス許可が付与されます。だそうで、つまり \\wsl$
から見た WSL のファイルシステムは(既定ユーザーを一般ユーザーにしていれば)一般ユーザーの権限しか与えられていないため、システムディレクトリ以下に Windows 側から書き込みを行うことができない。
私の場合以前は root ユーザーを既定のユーザーとして使っていたこともあって、頻繁にシステムファイルを Windows 側から編集していた(だって楽なんだもん…)。
9P かなんかの設定で Windows 側の共有するユーザーとかが変更できればよかったんだけど、調べる限りなさそう。
散々 root と同じ権限を一般ユーザーに付与するだとか色々調べるも、UID を 0 にする必要があるそうでそれじゃ root で使うのと大して変わらないじゃん… という。/etc/sudoers
はあくまで sudo
をつけてコマンドを実行する場合の話だけで、そのユーザー自体を sudoers で指定した権限にするわけではないらしい(👈ハマリポイント)
一般ユーザーを root グループに入れて /etc/
以下のファイルに root グループの書き込み権限を与えれば一般ユーザーでも /etc/ 以下が操作できるようになると思いきや、WSL のバグ で効いてくれない。死んでくれ。
下記の方法で運用していたが、やっぱり面倒でやらなくなってしまった。他にもいくつか試したけど、結局普通に nano で編集するか、VSCode の Remote-WSL で WSL に入り、そこから /etc/ 以下のファイルを編集する正攻法に落ち着いた。
上記の通り私はユーザー権限でも /etc/ 以下のファイルをいじれるようにしているので、特にエラーが起きることもなく普通に書き込めている。
色々な方法を試行錯誤した結果、
- システムファイルを編集する機会は頻繁にあるわけではないので、システムファイルを編集するときだけ WSL の既定ユーザーを root にして、終わったら一般ユーザーの方に戻す
\\wsl$
へは WSL を再起動しないと反映されないので、既定ユーザーを変更したら WSL を再起動する
という結論に落ち着いた。最悪すぎる。
> ubuntu config --default-user root && wsl -t Ubuntu && wsl exit
既定ユーザーを root に変更する場合は上のコマンドを PowerShell 上で実行、
> ubuntu config --default-user user && wsl -t Ubuntu && wsl exit
既定ユーザーを一般ユーザー(ここでは user とする)に変更する場合は上のコマンドを PowerShell 上で実行する。
こうして設定を変えたいときだけ ubuntu config --default-user root && wsl -t Ubuntu && wsl exit
を実行して Windows 側からファイルをいじれるようにし、終わったら戻す運用をすることにした。wsl -t Ubuntu && wsl exit
の部分で同時に WSL を再起動している。
# WSL のデフォルトを root に設定
function wsl-default-root {
ubuntu config --default-user root && wsl -t Ubuntu && wsl exit;
}
# WSL のデフォルトを一般ユーザーに設定
function wsl-default-user {
$user = '(一般ユーザー名)';
ubuntu config --default-user $user && wsl -t Ubuntu && wsl exit;
}
長ったらしいコマンド叩くのが嫌だったので、PowerShell のプロファイルにエイリアス的な関数を書いて wsl-default-root
で root をデフォルトに、wsl-default-user
で指定した一般ユーザーに変えられるようにした。
PowerShell のプロファイルのパスは PowerShell 上で echo $profile
と実行すると取得できる。
本当はやっぱり root で実行できるのが一番楽なんだけど、それだと Homebrew が動かなかったりするし、つらい(つらい)。
アップデート
$ sudo apt update -y && sudo apt upgrade -y
Ubuntu ユーザーならおなじみのアップデートをしておく。
結構大量に降ってくるので注意。
日本語化
そのままだと英語なので、上の記事を参考に日本語化した。コマンド丸コピで普通に日本語になった。
.wslconfig の設定
[wsl2]
# WSL の軽量 VM に割り当てる最大メモリサイズを指定する
memory=3GB
# WSL のネットワークポート待ち受けを、ホストマシンにフォワーディングする
localhostForwarding=true
C:\Users\(ユーザー名)\.wslconfig
に新しいファイルを作り、中に上記の内容を記述する(もちろんエンコードは UTF-8・LF )。
memory=3GB
のところはスペックに合わせて各自調整すること。
最大メモリを制限しておかないと WSL がメモリを食いつぶすことがある。Hyper-V 側の問題で、暫定的な対処としてこの方法が推奨されているらしい(https://github.com/microsoft/WSL/issues/4166)。
私の PC はメモリが 8GB しかなくうち 4GB くらいが Windows システム側に使われてしまっているので、試行錯誤を経て結局 1GB にした。
localhostForwarding=true
は設定すると WSL 側に localhost でアクセスできるようになる。たとえば WSL 側で Apache をポート 8000 で立ち上げた場合、localhost:8000 で WSL で起動している Apache にアクセスできる。
Windows 側の .wslconfig
と WSL 側の /etc/wsl.conf
は対になっているらしい。設定項目は上の記事を参照。
ちなみに /etc/wsl.conf
の /mnt/
以下をパーミッションを有効にしてマウントにするオプションはいつのまにか標準で有効になっていた。
Windows Terminal でログインした時の初期ディレクトリを ~ にする
この記事の通り、Windows Terminal の settings.json の WSL の項目に "startingDirectory" : "//wsl$/Ubuntu/home/(ユーザー名)"
みたいに追記すれば OK 。
Docker Desktop をインストール
今回のメイン。Windows 10 Home なので今まで Docker が入れられず、WSL2 を使い Docker が使えるようになった後も Ubuntu 18.04 の環境に Docker を入れるのは抵抗があったので、結局 Docker を入れるために WSL2 環境を再構築したようなもの。
上の記事を参考に行った。インストーラーをダウンロードして実行するだけなので簡単。
インストーラー起動後、Install required Windows component for WSL 2
にチェックが入っていることを確認してインストール。
Close and logout とか言われるのでクリックするとサインアウトされる。ログインし直すと勝手に Docker が立ち上がってくる。
チュートリアルどうすかとか言われるけど時間ないのでパス(どうせ後でもできそう)。
起動するとこんな感じ。ウインドウを一番小さくしてこれなので、ちょっとウインドウ大きくないか?って気はする。
> wsl -l -v
NAME STATE VERSION
* Ubuntu Running 2
docker-desktop Running 2
docker-desktop-data Running 2
wsl -l -v
って実行すると現在存在する WSL インスタンスの一覧が出てくる。
docker-desktop と docker-desktop-data っていうインスタンスが新たに作成されるようになっているらしい(Ubuntu の方も使われてるのか…?)
設定の Resource → WSL Integration を覗くとこんな感じ。
現在のデフォルトのディストリビューションを Docker のバックエンドに使うか、それとも追加のディストリビューションを使うか選べるらしい(?) 今回は Ubuntu しか入れてないのでこのままでいいはず。
Ubuntu 側から docker
コマンドが叩けるようになってれば多分 OK だと思う。勝手に docker-compose
も入っていた。
Enable integration with additional distros:
以下にチェックを入れるといけることがあるみたい。あと、docker を終了した状態で WSL 側で docker を使おうとしても
The command 'docker' could not be found in this WSL 2 distro.
のエラーがでる。仕様っぽい?これ以外にも Hyper-V 上に Linux を作って使う方法もあるらしいんだけど、開発マシンの癖に Windows 10 Home なので Hyper-V が使えず…。 WSL2 をバックエンドにする場合は Home でも使えるらしい。
Homebrew をインストール
これ以降の内容はかなり 「Windows10 WSL2にLinux居城を爆誕させる」 の記事を参考にした。
Homebrew っててっきり Mac だけだと思っていたんだけど、いつのまにか Linux でも使えるようになっていたらしい。いろいろ brew でインストールしろとか言われること多いので、brew を使えるようにしてみる。
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
上記のコマンドを実行して Homebrew をインストールする。
インストール先はユーザーごと (~/.linuxbrew)
かグローバルごと (/home/linuxbrew/.linuxbrew)
かで選べるが、基本的にグローバルでいいと思う。
$ test -d ~/.linuxbrew && eval $(~/.linuxbrew/bin/brew shellenv)
$ test -d /home/linuxbrew/.linuxbrew && eval $(/home/linuxbrew/.linuxbrew/bin/brew shellenv)
$ test -r ~/.bash_profile && echo "eval \$($(brew --prefix)/bin/brew shellenv)" >>~/.bash_profile
$ echo "eval \$($(brew --prefix)/bin/brew shellenv)" >>~/.profile
インストールが終わったら、Homebrew からインストールしたバイナリが入っているディレクトリにパスを通す。https://docs.brew.sh/Homebrew-on-Linux いわく上記のコマンドを実行すればいいらしい。
brew
と実行していろいろ出てくれば OK 。
Homebrew の場合、システムをあまり汚染しないらしいのと、apt よりもわかりやすくできてるのがメリット。
あと一般ユーザーなのに root がいらない。
apt は公式リポジトリの更新が結構遅かったりする(これでも CentOS 系よりかは全然早い)ので、最新のバージョンをインストールしたい場合は brew の方がよさそうなかんじ。
Git を最新化する
$ brew install git
apt 側の git ではなく、brew 側の git を使うようにする。
apt でインストールする git よりも優先されるので、敢えて apt の方の git を消す必要はない。
Homebrew 側で依存関係を全て管理する関係でDLにかなり時間がかかる。なぜか勝手に python もインストールされる。
pyenv で Python をインストール
$ brew uninstall --ignore-dependencies [email protected]
まずさっき勝手にインストールされてしまった python が邪魔なので(依存関係になってるけどなんで必要なんだろう?)強制的に消す。
ただ今後パッケージ入れてる中で勝手に入っちゃったりもする( pyenv 側の Python は認識してくれないらしい)。
$ sudo apt install build-essential zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev
$ brew install pyenv
Python は人権なので、pyenv をインストールする。pyenv は MacBook 側で使ってるけどバージョン管理できて便利。
事前にビルドツール群がいるみたいなので、それもインストールする。
$ pyenv install 3.8
python-build: definition not found: 3.8
The following versions contain `3.8' in the name:
3.8.0
3.8-dev
3.8.1
3.8.2
3.8.3
3.8.4
3.8.5
3.8.6
3.8.7
miniconda-3.8.3
miniconda3-3.8.3
See all available versions with `pyenv install --list'.
If the version you need is missing, try upgrading pyenv:
brew update && brew upgrade pyenv
Python 3.8 系でインストールする。今は 3.8.7 が最新らしい。
一応 Python 3.7 系もインストールしておく。
$ sudo apt install libffi-dev
…その前に、libffi-dev をインストールしておく。
これをインストールしておかないと _ctypes という Python の組み込みモジュール?が使えなくなり(pyenv は Python をソースコードからビルドするため)PyInstaller がインストールできなくなる(👈ハマりポイント)。
$ pyenv install 3.7.9
$ pyenv install 3.8.7
$ pyenv global 3.8.7
Python 3.8.7 と Python 3.7.9 をインストールし、このうち 3.8.7 をグローバルに有効化する。
ただ pyenv のインストールフォルダが PATH に入っていないので、.bash_profile または .profile を編集して、最後の行に eval "$(pyenv init -)"
と追記しておく。これで Python 関係は一旦完了。
pyenv は shell・local・global で切り替えられるらしい。
rbenv で Ruby をインストール
Ruby 全然書けないので開発自体はしないけど、割と Ruby で書かれたスクリプトがあってないと困る事が多々あるのでインストールしておく。Python は pyenv でインストールしたので Ruby は rbenv で。
$ brew install rbenv
rbenv のインストールはこれだけ。
$ rbenv install --list
2.5.8
2.6.6
2.7.2
3.0.0
jruby-9.2.14.0
mruby-2.1.2
rbx-5.0
truffleruby-21.0.0
truffleruby+graalvm-21.0.0
Only latest stable releases for each Ruby implementation are shown.
Use 'rbenv install --list-all / -L' to show all local versions.
インストールするバージョン一覧を表示。各系統の最新バージョンが表示される。
3.0 系は新しすぎるので、今回は 2.7 系をインストールする。
$ rbenv install 2.7.2
$ rbenv global 2.7.2
Ruby 2.7.2 をインストールし、グローバルに有効化する。この辺も pyenv と同じ。
Python と違ってビルドにかなりスペックを要求するようで、Vmmem(WSL2 の VM プロセス)が CPU を 80% くらい食うこともあった。ビルド時間も長め。
pyenv 同様、rbenv のインストールフォルダが PATH に入っていないので、.bash_profile または .profile を編集して、最後の行に eval "$(rbenv init -)"
と追記しておく。
これで Ruby のインストールは完了。
nvm で Node.js をインストール
pyenv や rbenv と使い方が似てる nodenv もある。導入方法は pyenv などとほとんど同じで、〇〇env 系で開発環境を統一できるし、今から導入するのであれば nvm よりも nodenv の方をおすすめしておく。機能的な差異もほとんどないと思う。
こちらの記事を参考にした。
$ brew install nvm
node.js を入れるには nodebrew と nvm とあといくつかあるらしいんだけど、nvm の方が有名?らしいのでそっちでインストールしてみる。
.profile に source $(brew --prefix nvm)/nvm.sh
の記述が別途必要だが、私の場合は勝手に記述されていた。WSL を開き直すと nvm
コマンドが叩けるようになってるはず。
ゴリゴリ node 使ってるわけではないので手っ取り早く Homebrew で入れる手もあったが、どうも Homebrew は現時点での最新版しか保持しない方針らしく、さらに node.js の奇数バージョンは安定しないらしいとどこかで聞いたのもあって LTS バージョンを選べる nvm を使うことにした。
$ nvm install --lts --latest-npm
$ nvm alias default lts/*
このコマンドで LTS 版をインストールする。node.js は奇数番号のバージョンが最新の機能を搭載したもの、偶数バージョンが LTS で安定版だそうで、奇数バージョンはあまり推奨されないらしい(アプデ速度早すぎだろ…)。
勝手に npm も入ってる。
sudo したときに通常ユーザーの PATH を引き継ぐ
ここまで色々 .profile にパスを追加してきたが、あれはあくまで通常ユーザーに適用されるパスなので、デフォルトでは sudo
をつけて実行したときには通常ユーザーのパスは引き継がれないようになっているらしい。つまり、sudo をつけると pyenv とかでインストールした python が叩けないだとか、色々問題が生じてくる。
ただし、visudo
で /etc/sudoers
をゴニョゴニョすれば引き継げるようにできた。
詳しくは上の記事が詳しいので割愛。
Apache + PHP をインストール
以前の環境でも Apache + PHP の Web サーバーを利用していたので、今回もその構成で構築する。
Homebrew でもインストールできるが、Apache は Debian/Ubuntu とそれ以外で設定ファイルの構成が大幅に異なる。
PHP もモジュール版で Apache と協調して動く関係上、Homebrew でインストールしてしまうと Mac 同様に Apache を httpd.conf で設定することになり設定の移行など諸々で面倒そうだったので、敢えて Apache と PHP は apt でインストールすることにした。
まずは手元の環境を復元したかったので…
$ sudo apt install php7.4
Ubuntu 20.04 LTS の場合、デフォルトの PHP は 7.4 に設定されているので、これをそのまま使う。
php7.4 と指定した場合、勝手に Apache と Apache のモジュールもインストールしてくれるようで、上のコマンドを実行しただけで Apache + PHP の環境が一応できた。コマンドラインだけ使いたい場合は php7.4-cli と指定すべきなのだろう。
あとは設定を行う。私の場合は以前バックアップしたものを復元した。
また、前述の通り root 権限云々があるので開発フォルダは全て一般ユーザーのユーザー・グループに設定してあるのだが、それだと Apache が www-data というユーザーで書き込みを行おうとするために何か PHP 側からファイルに書き込もうとしてもエラーになってしまう。
これを防ぐため、/etc/apache2/envvars
ファイルを編集し、APACHE_RUN_USER
と APACHE_RUN_GROUP
変数を既定に設定している一般ユーザーの名前に変更しておく。
こうしておけば、パーミッション云々でコケることはなくなる。
あと、デフォルトの Apache では .htaccess が使えなくなっているが、これは /etc/apache2/apache2.conf
内の AllowOverRide none
を AllowOverRide none
に変更すると使えるようになる。
.htaccess を使う場合によく利用する mod_rewrite も初期状態では確か有効になっていないので、sudo a2enmod rewrite
で有効化しておく。
$ sudo apt install php7.4-bcmath php7.4-curl php7.4-mbstring php7.4-mysql php7.4-xml php7.4-zip
このほか PHP で Laravel を動かす場合に必要になる PHP の拡張機能がいくつかあったので、それらもインストールしておいた。
php7.4-mysql は MySQL を使う場合に必要。PDO とは別個で要るらしい。
$ brew install composer
ついでに composer もインストールしておく。composer は Homebrew で。
composer は 2.0.x が降ってくるけど、互換性は担保されてるっぽいので問題が出るまではこれで。
$ sudo service apache2 start
あとはサービスが起動すれば OK。
サービスを Genie (systemd) 経由で起動すると落ちやすくなる問題の対策
こちらの記事がドンピシャだった。
Genie はプロセス ID を特殊な手法で制御しているからか、サービスの PID が 1000 以上になってしまうことがある。Apache も例外ではなく、また Apache は PID が 1000 以上になってしまうと不安定になりやすいらしい(その状態で SSH にログインしたりログアウトしたりするとセマフォとやらが消えてしまうとかなんとか…)。
この記事同様、/etc/apache2/apache2.conf
の #Mutex file:${APACHE_LOCK_DIR} default
の一行をコメントアウトするだけで直った。安定して稼働できているように見える。
MySQL をインストール
$ sudo apt install mysql-server
MySQL もサービス型ソフトなので、これも apt の方でインストールする。Ubuntu 18.04 LTS とは異なり MySQL のバージョンは 8.0 が既定になっているので、すんなりインストールできる。
$ sudo usermod -d /var/lib/mysql mysql
サービスを起動する前に、MySQL ユーザーが使うフォルダを設定しておく。
これをやらないと su: 警告: ディレクトリを /nonexistent に変更できません: そのようなファイルやディレクトリはありません
とか言われてしまう。
$ sudo service mysql start
その後サービスが無事立ち上がれば OK 。
$ sudo mysql -u root < mysql-db.dump
$ sudo mysql -u root mysql < mysql-user.dump
以前 MySQL のデータをバックアップしておいたので、それをインポートする。
最近の MySQL の root にはパスワードが設定されておらず、root 権限で MySQL を実行したときしかログインできないらしい(逆を言うと、root 権限で実行すればパスワードなしで入れる)。
[mysqld]
table_definition_cache = 400
performance_schema = 0
ただメモリ使用量がちょっと気になったので、私の場合は /etc/mysql/my.cnf
に上記の項目を追記した。メモリが潤沢にある人は気にしなくていいと思う。
こうすることで、メモリ使用量が 1/3 くらいまで減った。環境によると思うけど。
SSH サーバーをインストール
$ sudo apt install ssh
$ sudo ssh-keygen -A
$ sudo chmod -R 700 ~/.ssh/
$ sudo service ssh start
最後に SSH サーバーをインストールする。SSH サーバーがあれば、VSCode の Remote-SSH 拡張機能で(同じネットワークに属してさえいれば)リモートでファイルの操作が行えるようになる。
鍵諸々は前のバックアップから持ってきた。鍵をつくるかに関わらず ssh-keygen は必要らしい。
あと ~/.ssh/
以下のファイルは 700 とかにしておく必要がある。
SSH でログイン中に Windows のコマンドを叩くとフリーズする問題
Windows から ssh コマンドで接続する場合は、Windows 側のコマンドを打つとフリーズするという OpenSSH の不具合がある(ここまでたどり着くのに苦労した)。
すでに OpenSSH 自体は修正されているが、Windows 側に組み込まれているので未だに更新されていないとかそういう感じらしい。修正版が配布されているので、適当に DL して配置する。System32 以下に上書きすることもできなくはなさそうだけど、権限周りがめんどくさかったのでやめた。
DL した OpenSSH フォルダをシステム環境変数に登録するが、このとき system32 よりも上に配置する(重要)。一旦 Windows Terminal を閉じてもう一度起動させ、ssh -V
の結果が OpenSSH_for_Windows_8.1p1(執筆時点)になっていたら OK。
SSH でログインすると Windows 側の環境変数 $PATH が WSL 側に追加されていない問題
SSH でログインした場合に共通の問題として、Windows 側の環境変数 $PATH が WSL 側に追加されてなかったりする。WSL の Issues にも上がっていた。
WSL に wsl.exe・bash.exe・ubuntu.exe からログインする場合は独自の処理が走るらしく、その中で Windows 側の環境変数 $PATH が WSL 側に追加され、Windows 側のコマンドが cmd.exe
のようにフルパスでなくても実行できるようになるらしい(ただし .exe は付ける必要がある)。
SSH でログインすると bash.exe や ubuntu.exe がやっている(?)そういった処理をバイパスし通常の Ubuntu のようにログインしてしまうため、$PATH が WSL 側に追加されなくなってしまう。
上の Issue には .bash_profile に追記する方法が書かれていたが、私の環境ではうまく動かなかった。少し編集すれば一応動いたものの、WSLENV という特殊な環境変数もインポートするようなコードになっているのにそれが機能していなかった。
結局、自分で Windows 側から $PATH を取得し、wslpath
コマンドで WSL 用のパスに変換した上で WSL 側の $PATH に追加するコードを書いた。
systemctl show-environment
を使うことで簡単に環境変数をサルベージできるので、この手順はスキップして OK。PATH 以外もサルベージしてくれるのでこの方法より良い。
# SSH からのログイン時に Windows 側の $PATH を追加する
if [[ -v SSH_CLIENT ]]; then
# Windows 側の $PATH を取得( cd /mnt/c/・cd ~ は UNC パス云々のエラーを抑制するため)
cd /mnt/c/
PATHS_WINDOWS=$(/mnt/c/Windows/System32/cmd.exe /c echo %PATH% | tr '\\' '/' | tr ';' '\n')
cd ~
# wslpath で WSL 用のパスに変換した上で WSL 側の $PATH に追加
while read PATH_WINDOWS; do
PATH_UNIX=$(wslpath "$PATH_WINDOWS")
export PATH="$PATH:$PATH_UNIX"
done < <(echo "$PATHS_WINDOWS")
fi
以上のコードを .bash_profile か .profile の一番上に貼り付ければ、SSH からログインしても Windows 側のコマンドが普通に叩けるようになっているはず。
VS Code Remote Development 拡張機能を使う場合に .bash_profile や .profile が読み込まれない問題を解決する
私は VS Code (Visual Studio Code) を使っているので、Remote Development 拡張機能をインストールして Remote-WSL や Remote-SSH で WSL や SSH でアクセスした PC 内のプロジェクトで開発することがままある。
ところが、先程 .bash_profile か .profile に書いた内容が VS Code 上のターミナルには全く反応されていない事に気づく。なぜか PATH だけは効いているが…。
ググってみるとすぐ原因が分かった。どうも VS Code のターミナルが内部的にログインシェルではなくインタラクティブシェルで実行していたため、プロファイル類を読み込まず .bashrc があればそれだけ読み込んでいたって感じらしい。
つまりは .bashrc に書けば効くって事らしいんだけど、VS Code のターミナルをログインシェルとして起動する方法があったのでそれを試したら行けた。
さっきの記事にもあるように、VS Code の設定から変更できる。settings.json をいじるのもいいが、ここでは GUI からやる。
VS Code の設定を開き、タブがユーザーになっていることを確認する。
あとは検索窓に terminal.integrated.shellArgs.linux
と入れて、出てきた項目に -l
と追加する。
こんな感じに設定できていれば OK。
macOS 向けは最初から -l
オプションが付与されていた。
通常ログイン時に motd を表示する
実機の Ubuntu もあるのでそれでもやってみたけど、同じ結果だった。SSH で接続することの方が多い(なぜか motd は SSH でのログイン時は毎回表示される)ので完全に勘違いしていた。もともとうざったくなってきていたのもあり、結局私はプロファイルの記述を消して表示しないようにした。
パッケージのアップデート情報とかが見たいのであれば
landscape-sysinfo
を実行すれば見れるし、表示されなくても問題はない(実機の Ubuntu では元からその挙動だし、macOS もそう)。Welcome to Ubuntu 20.04.1 LTS (GNU/Linux 4.19.128-microsoft-standard x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of 2021年 1月 22日 金曜日 05:00:00 JST
System load: 0.0 Processes: 6
Usage of /: 2.8% of 250.98GB Users logged in: 0
Memory usage: 4% IPv4 address for eth0: 172.27.202.92
Swap usage: 0%
0 updates can be installed immediately.
0 of these updates are security updates.
WSL に SSH でログインする場合、WSL は通常の Ubuntu のように振る舞う(WSL 特有の処理が行われない?)ため、上のような motd (Message Of The Day) と呼ばれる画面が表示される。普通の Ubuntu を使ってる人は知ってるはず。ただし、WSL に通常ログインする場合は用途的にうざいと判断されたのか、motd が表示されずさびしい感じのコンソールになる。ほかの Ubuntu で motd の表示に見慣れてることもあって、できれば表示させておきたい。
# 通常ログイン時に motd を表示する
# 表示したくない motd はスクリプトの実行権限を外すことで表示されなくなる
if [[ ! -v SSH_CLIENT ]]; then
# echo "Show motd..."
while read file; do
# まとめて出力するため、一旦一時ファイルに書き出す
"/etc/update-motd.d/$file" 1>> .motd.tmp 2>/dev/null
done < <(ls -A -1 /etc/update-motd.d/)
# 一時ファイルの内容を出力
cat .motd.tmp && rm .motd.tmp
fi
以上のコードを .bash_profile または .profile の一番上に追記する。
motd に表示するシェルスクリプトは /etc/update-motd.d/
に入っているので、連番の番号が小さいものから順に実行し、一旦処理結果を一時ファイルに書き出す。その後 cat
コマンドで一時ファイルの内容を表示し、一時ファイルを削除している。
…とやってみたはいいものの、毎回ログイン時に表示するには正直行数が多すぎる。/etc/update-motd.d/
の中から不要な motd を選び、sudo chmod a-x /etc/update-motd.d/50-motd-news
みたいに実行して実行権限を外す。そうすることで、指定した motd を表示させないように無効化できる。
私は明らかになくても問題なさそうな /etc/update-motd.d/10-help-text
と /etc/update-motd.d/50-motd-news
の実行権限を外して無効化した。
"/etc/update-motd.d/$file" 1>> .motd.tmp 2>/dev/null
の 2>/dev/null
がポイント。こうしておけば実行権限がないスクリプトが実行されず、エラーも出力されなくなる。
外部から SSH 接続できるようにする
Genie で systemd を動かし、サービスを自動起動できるようにする
Linux では PID 1 としてシステムの起動を担当する、/init と呼ばれるマスタープロセス(?)が起動する。
多くの Linux ではこの /init に systemd が使われている。しかし、WSL では systemd ではなく、WSL 専用の /init が使われている。
この WSL 専用の /init がかなりの曲者で、簡易的なものらしく systemd の機能の一部しか実装していない。
このため systemd に依存するソフトが WSL 上では使えないほか、systemctl も使えないのでサービスが自動起動されない(らしい)。これが WSL 上だと一部のソフトが動作しなかったりする事がある理由。たとえば snap は systemd に依存するので使えない。
そこで、WSL でも systemd を使えるようにできるらしい Genie というソフトを入れてみる。WSL2 でしか動かないが、既に日本語の記事もいくつかあった。
.NET ランタイムのインストール
Genie は .NET で書かれているらしく(なぜ .NET で書いたし…)、使うにあたってはまず .NET のランタイムのインストールが必要。この関係でちょっとインストールが面倒。
$ cd ~
$ wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
$ sudo dpkg -i packages-microsoft-prod.deb
$ sudo apt install apt-transport-https
以上のコマンドを実行し(参考)、.NET のリポジトリを追加する。
apt は実は https に対応していないらしくこのままではリポジトリを参照できないので、apt-transport-https というパッケージもインストールする。
.NET 本体は Genie のインストールと同時にインストールされるので(後述)、手動でインストールする必要はない。
Genie のインストール
Genie はまだできてさほど時間が経っていないこともあり、古い情報と新しい情報が入り乱れてかなり訳のわからないことになっている。
README を読む限り PackageCloud というところのリポジトリを登録すればインストールできそうに見えるが、利用者があまりにも多かったからなのか 429 Too Many Requests エラーが出てしまい(どうも PackageCloud 側の帯域制限を使い果たすとこうなるらしい)、インストールができないし apt update でエラーが出まくる。
運良くインストールできたとしても古いバージョンが降ってきたりするのでおすすめしない。
結局、作者さんが公開している GitHub Pages 上の apt リポジトリ を使う方法に落ち着いた。
$ wget -O - https://arkane-systems.github.io/wsl-transdebian/apt/wsl-transdebian.gpg 2>/dev/null | sudo apt-key add -
まずは GPG 鍵をインポート。
$ sudo nano /etc/apt/sources.list.d/wsl-transdebian.list
------------------------------------------------------------------------
deb https://arkane-systems.github.io/wsl-transdebian/apt/ focal main
deb-src https://arkane-systems.github.io/wsl-transdebian/apt/ focal main
その後、/etc/apt/sources.list.d/ に wsl-transdebian のリポジトリを登録する。
nano かお好きなエディタを開いて、以上の内容を貼り付けて保存する。
focal
の部分は各 OS のバージョンのコードネームに合わせる必要がある。Ubuntu 20.04 LTS なら focal
、Ubuntu 18.04 LTS なら bionic
。作者さんの説明では bullseye
になっているが、これは Debian 11 のコードネーム。$ sudo apt upgrade
$ sudo apt install systemd-genie
あとは apt update してリポジトリを登録し、普段どおりインストールする。
このとき、依存関係の .NET ランタイムもいっしょにインストールされる。
これで genie のインストールは完了。
$ genie -s
あとはシェルで genie -s
と実行すれば Genie による「裏Linux」に入れる。
Genie の動作原理
これは私もよく分かっていないので正しいかはかなり微妙だけど、どうも Genie は PID 名前空間という Linux の機能を使っているらしい。
systemd はマスタープロセス?みたいなもので、PID(プロセス ID )が 1 でないと動作しない。
WSL の場合、通常の Linux では systemd が担っている PID 1 のプロセスが WSL によるカスタムされた /init(簡易的な systemd のようなもの)になっているため、systemd は起動できない。
apache2 や mysql のようなバックグラウンドサービスも通常 systemd が動かしているが、WSL ではこれも WSL 用 /init が動かしている。そして、WSL 用 /init にはサービスの自動起動機能が存在しないため、サービスは WSL の起動毎に毎回起動させなければならない。
/init は全てのプロセス中一番最初に起動されるもののため、WSL 用 /init を systemd に置き換えて PID 1 を systemd にする、ということは今のところできない。仮にできたとしても、WSL 用の /init が WSL と Windows の統合機能など WSL を WSL たらしめる機能の根幹を担っているだろうから、まともに動作しないだろう。
そこで Genie では、PID 名前空間を使って、同じ Linux 内に別の「裏Linux」のようなものを作る。コンテナといった方がいいかもしれない(作者さんは「ボトル」と呼んでいる)。
PID 名前空間が別れていれば、名前空間の名の通り PID 1 を重複させることができる。
Genie を起動すると、PID 1 に systemd を割り当てた「裏Linux」が立ち上がり、「裏Linux」の systemd は「表Linux」に登録されたサービスを自動起動する。
「裏Linux」上であれば、「裏Linux」上で立ち上がっているサービスプロセスの起動・再起動・終了なり、systemd を必要とするコマンドの実行だったりができる、という具合らしい。
Waiting for systemd….!!!!!! が延々と続く問題
genie -s
で Genie を実行したはいいものの、Waiting for systemd....!!!!!!
が延々と続いて終わらない。README の記述 いわく、バージョン 1.31 から全ての systemd サービス/ユニットが起動し、systemd が「running」の状態になるのを待ってから「裏Linux」に入るように変更されたのが原因とのこと。
タイムアウトはデフォルトでは 180 秒で、これは /etc/genie.ini
にて変更できる。
つまり、Waiting for systemd....!!!!!!
がタイムアウトになるまで続くということは、1 つ以上のサービスが正常に起動しなかったことを意味しているらしい。「裏Linux」に入った状態で systemctl status
を実行すると、確かに State が degraded(劣化)状態になっている。
systemctl
と実行することで、systemd が起動したサービス/ユニットの一覧を確認できる。起動に失敗していれば赤色で表示されるので、それを見つける。
私の場合は multipathd.service・multipathd.socket・systemd-remount-fs.service の 3 サービス/ユニットが起動に失敗していた。名前からして WSL だと使えなさそうなものばかりという印象。
これらのサービス/ユニットの自動起動を無効化すれば、systemd がタイムアウトすることもなくなるはず。
$ sudo systemctl disable multipathd.service
$ sudo systemctl disable multipathd.socket
$ sudo systemctl mask systemd-remount-fs.service
systemctl disable
で、指定したサービス/ユニットの自動起動を無効化する。
systemd-remount-fs.service
だけ systemctl mask
になっているかというと、自動起動を無効化したはずなのに無効化できていなかったようで、また systemd の起動がタイムアウトしてしまったから。systemctl disable
はあくまで自動起動を無効化するもので手動で起動することはできる。これに対し、
systemctl mask
はユニットの起動そのものを無効化する。つまり、手動ですら起動できなくなる(参考記事)。私の環境では
systemctl mask
で systemd-remount-fs.service
を完全に封じ込めることでタイムアウトせずに Genie を起動できた。$ sudo systemctl disable [email protected]
README で作者さんが「systemctl を介して getty@tty1
サービスを無効にすることをお勧めします」と言っているので、(理由はよくわからないけど)これもついでに自動起動しないようにしておく。
$ sudo systemctl mask systemd-udevd.service
$ sudo systemctl mask snapd.service
$ sudo systemctl disable snap.lxd.activate.service
snap.lxd.activate.service
がいつのまにか追加されていたので、これも無効化する。このサービスだけなぜか mask が使えないので、disable のみ。あとは負荷対策でこれら 3 つのサービスを無効化する。
一部動かなくなるものが出てくるかもしれないので、CPU 使用率が気になる場合のみ。
snapd は snap アプリを動かしたい人は無効化しない方がいいだろうけど、基本 CLI しか触らないので使わないし、単に CPU とメモリを食うだけだったので私は無効化した。使いたくなったら systemd unmask
で有効化するだけだし。
> wsl -t Ubuntu
あとは Windows 側から WSL を終了させてから、もう一度 WSL に入って genie -s
を実行する。
この状態で systemctl status
を実行して、State が running
になっていれば OK 。systemd の起動も圧倒的に早くなっているはず。
Windows のパスが $PATH に追加されていない問題
説明を見てもよく理解できなかったが、Genie の Wiki によると、systemd が構築したユーザーセッションを壊さないために環境変数の引き継ぎは最小限にしているらしい。このため、環境変数 $PATH
には Windows のパスが追加されておらず、Windows の exe を実行するには絶対パスでの指定が必要。めんどい。
$ sudo nano /etc/genie.ini
-------------------------------------------------------------------------------------
[genie]
secure-path=/lib/systemd:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
unshare=/usr/bin/unshare
update-hostname=true
clone-path=true
clone-env=WSL_DISTRO_NAME,WSL_INTEROP,WSLENV
systemd-timeout=30
Genie 1.30 以降では /etc/genie.ini
の clone-path
が false
になっているところを true
に変更することで、$PATH に Windows のパスを追加することができる。
終わったら先程同様に WSL をシャットダウン→起動し、$PATH
に Windows のパスが含まれていれば OK。
SSH でログインすると exe ファイルが実行できなくなる
SSH でログインした場合はうまく動かなさそうだな…と当初から予想していたが、Genie を起動した状態で SSH でログインすると、「裏Linux」に入る。
そして、その状態だと exe ファイルが実行できない。というより、何も出力されずに終わる。
「裏Linux」の中から Genie を終了することはできないらしく、exit を実行すると SSH のセッションが閉じてしまうので、如何ともし難い。
Issue を出してみたところ回答をもらえた。作者さんが言うには「これは Genie の問題ではなく、SSH で接続すると WSL 統合用などの環境変数が失われてしまうため」らしい(後日 Wiki にも追記された)。
# Windows 側の $PATH を追加する
# 参考: https://github.com/arkane-systems/genie/wiki/No-Windows-interop-when-connecting-to-WSL-genie-via-ssh
if [[ -v SSH_CLIENT ]]; then
TMPFILE=$(mktemp)
systemctl show-environment | awk '{print "export "$0}' >> $TMPFILE
source $TMPFILE
rm $TMPFILE
fi
Genie が起動した状態であれば、以上のコマンドを .bash_profile か .profile に記載することでそれらの環境変数をサルベージできる。動作も確認した。
Genie がまだ起動していなかったら起動する
Genie はすでに起動している状態の場合自動的に「裏Linux」にログインしてくれるが、起動していない場合は「表Linux」の方にログインされる。
# Genie がまだ起動していなかったら起動する
# 参考: https://qiita.com/I_Tatsuya/items/9f85cdb9adc3adcf1abd
if [ "`ps -eo pid,cmd | grep systemd | grep -v grep | sort -n -k 1 | awk 'NR==1 { print $1 }'`" != "1" ]; then
echo "Starting genie..."
[ -t 0 ] && genie -c bash --login
fi
.bash_profile または .profile の一番上に以上の内容を追記する。echo "Starting genie..."
はなくても OK。
[ -t 0 ] &&
は こちらの記事 の通り、シェルが端末を介して実行されているか(端末モード)、直接実行されているか(バッチモード)を判定するコード。Genie はバッチモードでは起動に失敗するようで、
[ -t 0 ] &&
を外すと VS Code の Remote-WSL 拡張機能のターミナル機能と Git ソース管理機能の初期化に支障するのか使えなくなってしまう(原因を突き止めるまでにかなり時間がかかった)。こちらの記事に書いてある内容を参考にした。
systemd の PID
かなり紛らわしいが、WSL を起動してから一度でも Genie を起動した事がある場合、ホスト名には -wsl
がついて systemd も一応起動しているので、勝手に「裏Linux」の中に入れているように見える。
Genie の Wiki で紹介されている INSIDE_GENIE
変数も true になっている。
ところが実際は systemd の PID は 1 ではなく、そのため systemctl や service コマンドは使用できない。
実行中のサービスは起動こそしているが、停止や再起動ができない面倒な状態になってしまう。
INSIDE_GENIE 環境変数は不完全な状態でも true になってしまうようなので、これも実質使えない。
実はこの状態で genie -s
を実行し「裏Linux」に入ると、再び systemd の PID が 1 になっている空間に入ることができる。つまり、systemd が起動していないか、PID が 1 以外になっているときに genie -s
を実行できれば、どんな時でも systemctl や service を操作できるようになる。
echo "Systemd PID: $(ps -eo pid,cmd | grep systemd | grep -v grep | sort -n -k 1 | awk 'NR==1 { print $1 }')"
systemd の PID は以上のコマンドで確認できるので、それの実行結果が 1 以外であれば Genie を起動するようになっている。SSH ログイン時は既に PID が 1 になっているので、Genie は起動しない。
ただ wsl コマンドで実行するのはせいぜい
ls
とかでサービスを再起動するとかはあまりやらないだろうし、wsl genie -c (コマンド)
のようにすれば一応「裏Linux」内でコマンドを実行できる。後者に関しては Genie の Wiki に対処法が載っているが、そもそもその右クリックメニューを使っていなかったので問題なかった。
exec genie -c bash –login
2021/02/16 追記:
前述のコードで exec genie -s
ではなく exec genie -c bash --login
としたのは、PowerShell などの他のシェルから WSL 内の bash に入ったときに、カレントディレクトリ (cwd) を保持するため。上の Issue が参考になる(執筆時点ではこのページを開いた数分後に返信されてたりなかなかタイムリーでびっくり…)。
exec
は同じプロセスで他のコマンドを実行するコマンド。今回は genie に入ることでシェルが 2 重になって 2 回 exit
しないと bash から出られなくなるのを防ぐために使っている。
Genie の README にあるように、genie -s
は通常の Linux システムでのログイン動作をエミュレートするらしく、カレントディレクトリを保持しない。exec
をつけなくても同様。
これで何が起きるかというと、PowerShell 等の現在のカレントディレクトリから bash (Genie) に入ると、カレントディレクトリが WSL 上のホームディレクトリになってしまい、カレントディレクトリが引き継がれない。
また、VSCode の WSL 上のプロジェクトをターミナルで開いても、プロジェクトのフォルダに移動せずこれもホームディレクトリになってしまうなどの問題が起きる。支障はないっちゃないが、面倒くさいのはたしか。
そこで、カレントディレクトリを保持する genie -c
内で bash を実行することで、genie -s
を代替する。
作者さんいわく、機能上はカレントディレクトリが保持される以外の違いはないようなので、特に問題はないと思う。
PowerShell から bash に移ってもきちんと Genie の中に入れた上でカレントディレクトリを保持できている事が確認できた。我ながら素晴らしい。exec
コマンドで実行しているので exit も一回でできる。
--login
オプションをつけているのは、これも作者さんが述べているようにログインシェルで起動するため。
非ログインシェル(バッチモード)では /etc/bash.bashrc
と .bashrc
しか読み込まないため、.bash_profile
や .profile
に書いたスクリプトは実行されない。ログインシェル(端末モード)であればそれらもちゃんと実行されるというわけ。
環境変数を追加する
ただし、このように不完全に「裏Linux」に入っている状態から「裏Linux」に入り直す場合、追加する設定にしたはずの Windows 側のパスが環境変数 PATH に追加されていない状態になってしまう。当然絶対パスで指定すれば実行できるが、結構面倒。WSLENV など他の環境変数は問題ないので謎。
# Genie が既に起動していたら Windows 側の $PATH を追加する
# 参考: https://github.com/arkane-systems/genie/issues/101
if [ "`ps -eo pid,cmd | grep systemd | grep -v grep | sort -n -k 1 | awk 'NR==1 { print $1 }'`" == "1" ]; then
TMPFILE=$(mktemp)
systemctl show-environment | awk '{print "export "$0}' >> $TMPFILE
source $TMPFILE
rm $TMPFILE
fi
結局、このようなコードになった。
前の項で説明したものと同じ処理を、systemd の PID が 1 のときだけ実行する。
SSH でログインするときは常に PID が 1 になるっぽいので、前の項で追加したコードは削除して構わない。
Windows の起動時に Genie を起動し、WSL 内のサービスを自動起動する
> schtasks /create /tn "Genie" /tr "wsl.exe -d Ubuntu -- genie -i" /sc onlogon
schtasks
コマンドで、ログオン時に wsl.exe -d Ubuntu -- genie -i
が実行されるようタスクスケジューラに登録する。wsl -d Ubuntu
の部分は使っている WSL の OS に合わせて Ubuntu-18.04
などへ変更すること。
タスクを管理者権限で実行する関係上、管理者権限で実行した Windows Terminal から実行する。
genie -i
は Genie をバックグラウンドで起動するだけのコマンドらしい。README いわくタスクスケジューラを使って Windows の起動時に実行するような用途を想定しているようなので、自動起動にはうってつけ。
こうすることで、Windows を再起動した後でも WSL 側のサービスを自動起動することができる。
以前は genie -i
で事前に Genie を起動してしまうと起動したサービスの停止や再起動ができなくなると思い込んでいたが、前述の通り自動で Genie に入り直すようにしておけば問題ないし、systemctl や service は使えるしで中々快適な環境にできたんじゃないかと思う。
WSL に外部から接続できるようにする
WSL2 では Windows と WSL2 でネットワークインターフェイスが分離され、Windows は仮想ネットワーク内の WSL2 を見に行くような構成になっている。localhost で見れるのはポートを自動で転送しているため( localhostForwarding=true
はこのための設定)。
しかし、ポートを localhost に転送しているとはいっても、WSL2 をインストールした Windows 内の仮想ネットワーク (vEthernet) 内で擬似的に転送しているだけなので、WSL2 をインストールした PC 以外からは WSL 内でリッスンしているポートに接続することができない。
そこで、WSL から Windows 側にポートフォワーディングを設定し、さらにポートを開放することで、同じローカルネットワーク内の他の端末からでも接続できるようにしてみる。
ところがその仮想ネットワークが中々の癖物で、WSL をシャットダウンしたり、PC を再起動すると仮想ネットワークの IP アドレス帯ごと変わってしまう(変わらない事もあり、どういうタイミングかは謎)。
WSL の Issue で議論されているがどうも Hyper-V 側の仕様らしく、「毎回 WSL2 の IP アドレス取得するとかして頑張ってね」とかいう無慈悲な回答がなされている。ググっても大体は「Windows の起動時に WSL2 の IP アドレスを取得して、netsh で毎回ポートフォワーディングの設定をやり直す」というもので、お世辞にもスマートな実装方法ではない。
この記事から引用すると、WSL のネットワークは、
- Windows [vEthernet (WSL)]
- WSL2 起動時に作成され、Windows をシャットダウンすると削除される
- IP はあらゆるプライベート IP からランダムで、クラス B やクラス C などのサブネットすら定まらず、割り当て方があまりにダイナミック
- Ubuntu [eth0]
- vEthernet (WSL) のネットワーク内から DHCP で割り当てられ、WSL をシャットダウンし、再び起動すると IP が変更される
ip addr change
で無理矢理 IP を割り当てることは可能だが、vEthernet (WSL) の IP が変わってしまうので意味を成さない
という構成になっている(どうしてこうなった…)。
go-wsl2-host をインストール
何かいい回避策はないかと GitHub を漁っていると、go-wsl2-host というツールを発見。
このツールは Windows サービスとしてインストールされ、WSL2 の IP 変更を常に監視する。
WSL2 の IP が変更されたら、Windows の hosts (C:\Windows\System32\drivers\etc\hosts
) ファイルを変更して、取得した WSL2 の IP に ubuntu.wsl(Ubuntu-18.04 なら ubuntu1804.wsl 、Debian なら debian.wsl のようにディストリビューションによって変わる)というローカルドメインを割り当てる、という仕組み。
つまり、WSL2 の実体の IP の代わりに(go-wsl2-host によって割り当てられた)ubuntu.wsl というローカルドメインを使うようにすれば、わざわざ WSL2 を再起動する毎にポートフォワーディングをやり直す必要がなくなる。
IP Helper サービス (iphlpsvc) について
後述する WSL2 へのポートフォワーディング設定では、go-wsl2-host を利用し、ころころ変わる WSL2 の IP に対して設定した ubuntu.wsl というローカルドメインに向けて転送するようにしている。
さて、IP Helper サービス (iphlpsvc) は netsh interface portproxy
で設定した Windows から WSL2 へのポートフォワーディングを司る Windows サービスだが、何時間もかけて検証した結果、IP Helper サービスは再起動しないと ubuntu.wsl に紐付いた WSL2 の IP アドレスの変更を適用してくれないらしい。
つまり、WSL2 の IP アドレスが変わった場合、そのタイミングで IP Helper サービスを手動で再起動しないとポートフォワーディングが効かなくなり、外部 PC からアクセスできなくなる(これじゃ大して意味ないじゃん…)。
- Windows の再起動
- Windows が接続しているネットワークの切断・再接続
wsl --shutdown
の実行wsl -t Ubuntu
では IP アドレスはリセットされないらしい
WSL2 の IP アドレスは以上のような原因でころころ変わってしまうので、頻繁とは言わないまでも、気づかないうちに WSL2 の IP がリセットされていてもおかしくはない。
そこで、go-wsl2-host がサービスを起動している間、WSL2 の IP アドレスの変更を常に監視していることに着目した。先程の go-wsl2-host を改造して、WSL2 の IP アドレスの変更を察知し hosts を書き換えたタイミングで、IP Helper サービスを WSL2 Host サービス側から再起動するようにした(当該コミット)。
改造版の go-wsl2-host は ここ のリポジトリに置いてある。
net
コマンドを叩くだけのコードだが、一度 cmd.exe を挟まないと動かなかったりだとかでだいぶ詰まってしまった。net
コマンドは本来管理者権限で実行する必要のあるが、go-wsl2-host は元々管理者権限で実行されるようになっているので問題なく実行できた。これで、先程説明した問題がほぼ完全に解決された。
何度も再起動や wsl –shutdown をして試してみたが、go-wsl2-host((「サービス」アプリ上では WSL2 Host)) を Windows サービスとして自動起動するようにしておけば、WSL2 の IP アドレスが変わると ubuntu.wsl に紐付いた IP アドレスも変更され、IP Helper サービスも自動で再起動されるので、ubuntu.wsl 宛に転送する設定であればポートフォワーディングが常に効くようになる。
netsh interface portproxy
を使わずにサードパーティのポートフォワーディングソフトを使っている場合は当然ながら適用されない。ただ、この記事や GitHub の Issue 、Reddit や StackOverflow などの海外サイト含めほとんどが前者を使う(もしくは、VPN を使ったりブリッジ接続にするなどの別の手法)もものばかりだったので、よほどこだわりがなければ問題ないと思う。ubuntu.wsl をころころ変わってしまう WSL2 の IP アドレスの代わりに使っておけば、実質的に WSL2 の IP アドレス問題も解決したことになる。
次の項で説明するが、セットアップはダウンロードしてインストールするだけ(自動起動とか権限は wsl2host install
したときに自動で設定される)なので、WSL2 ユーザーならインストールして損はないと思う。
セットアップ
インストールとセットアップは簡単。私のフォークのリリースページ から最新の go-wsl2-host.exe をダウンロードして、Program Files 以外の適当な場所に配置する。私は C:\Applications\wsl2host\wsl2host.exe
に配置した。ドキュメント以下とかでもいいらしい。
あとはシステム環境変数の PATH に wsl2host.exe
のあるパスを通す。パスは別に通さなくてもいいけど、通しておいた方が楽だと思う(以下はパスを通した前提で記述)。
> wsl2host install
Windows Username: (Windows のユーザー名を入力)
Windows Password: (Username で指定したユーザーのパスワード入力)
あとは wsl2host install
を管理者権限で実行した Windows Terminal 上で実行するだけ。
Windows サービスの登録には管理者権限が必要なので、普通に実行しようとすると登録に失敗する。
Windows のユーザー名とパスワードを訊かれるので、間違わないように入力する。もし間違った場合はサービスの起動に失敗するので、wsl2host remove
でサービスを削除してからもう一度登録し直すと良いだろう。
サービスアプリを開き、WSL2 Host
が実行中になっていれば正しくサービスを起動できている。
> ping ubuntu.wsl
ubuntu.wsl [192.168.235.202]に ping を送信しています 32 バイトのデータ:
192.168.235.202 からの応答: バイト数 =32 時間 =2ms TTL=64
192.168.235.202 からの応答: バイト数 =32 時間 =1ms TTL=64
192.168.235.202 からの応答: バイト数 =32 時間 <1ms TTL=64
192.168.235.202 からの応答: バイト数 =32 時間 <1ms TTL=64
192.168.235.202 の ping 統計:
パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
最小 = 0ms、最大 = 2ms、平均 = 0ms
あとは ubuntu.wsl ドメインに ping を送ってみて、無事返ってきたら成功。
Apache をインストールしている場合は、ブラウザに http://ubuntu.wsl/ と入れてみると Apache2 Ubuntu Default Page が表示されるはず。
Windows から WSL へポートフォワーディングを設定する
無事 ubuntu.wsl で WSL にアクセスできるようになったので、最後に Windows 側の特定ポートに飛んできたアクセスを、WSL 側に転送(ポートフォワーディング)する設定を行う。
手動でやることもできるが、折角なので WSL 側で使用中のポートを取得して、それらのポート開放とポートフォワーディングを自動で設定する PowerShell スクリプトを作ってみた。いちいち実行するの大変だし…
PowerShell 7 の方が色々新技術が導入されて使いやすいので、PowerShell コマンドレット自体を大して使わずデフォルトのシェルとしてしか使わない私のような人にもおすすめ。コマンドプロンプトは論外(シングルクオート使えなかったり諸々しんどすぎる)。
# Windows から WSL へポートフォワーディングを設定
function Set-WslPortForwarding {
# WSL のディストリビューション
$wsl_distribution = 'Ubuntu';
# WSL に割り当てられたローカルドメイン
$wsl_domain = 'ubuntu.wsl';
# 管理者権限で実行するために一時的に作成する PowerShell スクリプトのパス
$temp_ps1 = "${env:LOCALAPPDATA}\WslPortForwarding.ps1";
echo '' > $temp_ps1; # ファイルを作成
# WSL が以前リッスンしていたポート番号を取得
$wsl_ports_txt = "${env:LOCALAPPDATA}\WslPortForwarding.txt"; # ポート情報を保存するファイルのパス
if (Test-Path $wsl_ports_txt) {
$wsl_ports = cat $wsl_ports_txt;
} else {
$wsl_ports = @();
}
# WSL が現在リッスンしているポート番号を取得
$wsl_listen_ports = wsl -d $wsl_distribution -- ss -nltu `| grep -Eo ":[0-9]+" `| sed "s/://g" `| sort -V `| uniq;
$wsl_listen_ports[[Array]::IndexOf($wsl_listen_ports, 53)] = $null; # ポート 53 は DNS 関連でバッティングしそうなので削除
$wsl_listen_ports[[Array]::IndexOf($wsl_listen_ports, 3306)] = $null; # ポート 3306 は Windows 側の MySQL クライアントからエラーで接続できなくなるので削除
$wsl_listen_ports[[Array]::IndexOf($wsl_listen_ports, 33060)] = $null; # ポート 33060 は Windows 側の MySQL クライアントからエラーで接続できなくなるので削除
$wsl_listen_ports = $wsl_listen_ports -ne $null; # 配列のインデックスを詰める
echo $wsl_listen_ports > $wsl_ports_txt; # ポート情報を保存
# 古いファイアウォール・ポートフォワーディングの設定を削除
foreach ($wsl_port in $wsl_ports) {
$register_name = "WSL PortForwarding ${wsl_port}"; # 登録する名前
# ファイアウォールの設定を削除
echo "echo `"Remove Port: ${wsl_port}`"" >> $temp_ps1;
echo "netsh advfirewall firewall delete rule name=`"${register_name}`" protocol=TCP" >> $temp_ps1;
echo "netsh advfirewall firewall delete rule name=`"${register_name}`" protocol=UDP" >> $temp_ps1;
# ポートフォワーディングの設定を削除
echo "netsh interface portproxy delete v4tov4 listenport=${wsl_port}" >> $temp_ps1;
echo "" >> $temp_ps1; # 空白行
}
# 新しいファイアウォール・ポートフォワーディングの設定を追加
foreach ($wsl_listen_port in $wsl_listen_ports) {
$register_name = "WSL PortForwarding ${wsl_listen_port}"; # 登録する名前
$register_profile = 'private,public'; # 登録するプロファイルの種類
# ファイアウォールの設定を削除・追加
echo "echo `"Set Port: ${wsl_listen_port}`"" >> $temp_ps1;
echo "netsh advfirewall firewall delete rule name=`"${register_name}`" protocol=TCP" >> $temp_ps1;
echo "netsh advfirewall firewall delete rule name=`"${register_name}`" protocol=UDP" >> $temp_ps1;
echo "netsh advfirewall firewall add rule name=`"${register_name}`" dir=in action=allow profile=${register_profile} protocol=TCP localport=${wsl_listen_port}" >> $temp_ps1;
echo "netsh advfirewall firewall add rule name=`"${register_name}`" dir=in action=allow profile=${register_profile} protocol=UDP localport=${wsl_listen_port}" >> $temp_ps1;
# ポートフォワーディングの設定を削除・追加
echo "netsh interface portproxy delete v4tov4 listenport=${wsl_listen_port}" >> $temp_ps1;
echo "netsh interface portproxy add v4tov4 listenport=${wsl_listen_port} connectaddress=${wsl_domain}" >> $temp_ps1;
echo "" >> $temp_ps1; # 空白行
}
# ポートフォワーディングサービスの有効化
echo "echo `"Enable PortForwarding...`"" >> $temp_ps1;
echo 'sc config iphlpsvc start=auto' >> $temp_ps1;
echo 'net stop iphlpsvc' >> $temp_ps1;
echo 'net start iphlpsvc' >> $temp_ps1;
# 作成した PowerShell スクリプトを管理者として実行
Start-Process pwsh.exe -ArgumentList "-NoProfile -ExecutionPolicy unrestricted ${temp_ps1}" -Wait -Verb runas
rm $temp_ps1; # 作成した PowerShell スクリプトを削除
# ポートフォワーディング設定を表示して終了
netsh interface portproxy show v4tov4
}
以上のコードを、PowerShell のプロファイルの末尾に追記する。$wsl_distribution
と $wsl_domain
の部分は各自使っているディストリビューション名に変更すること。
PowerShell のプロファイルのパスは PowerShell 上で echo $profile
と実行すると取得できる。
動作原理
ss -nltu | grep -Eo ":[0-9]+" | sed "s/://g" | sort -V | uniq
で、WSL 側で現在リッスンしているポート番号を取得する。- 取得したポート情報はテキストファイルに保存し、WSL 側でリッスンしているポートが増減したときにポート開放とポートフォワーディングの設定を反映できるようにする。
netsh
をフル活用してファイアウォールのポート開放とポートフォワーディングの設定を行うコマンドを生成する。- 生成したコマンドは後で一括で管理者権限で実行するため、一時ファイルに書き込む。
- コマンド生成が終わったら、
Start-Process
で作成した一時ファイル内の PowerShell スクリプトを管理者権限で実行し、ポート開放とポートフォワーディングを一気に設定する。 - 実行が終わったら一時ファイルを削除し、ポートフォワーディング設定を表示して終了する。
使い方
> Set-WslPortForwarding
ipv4 をリッスンする: ipv4 に接続する:
Address Port Address Port
--------------- ---------- --------------- ----------
* 80 ubuntu.wsl 80
* 5000 ubuntu.wsl 5000
* 5100 ubuntu.wsl 5100
* 5200 ubuntu.wsl 5200
* 10022 ubuntu.wsl 10022
PowerShell 内で Set-WslPortForwarding
と実行するだけ。
UAC の昇格プロンプトが表示されるので [はい] を選択すると、現在 WSL 側でリッスンされているポートを元に、ポート開放・ポートフォワーディングの設定が全自動で行われる。
実行後、上記のようにポートフォワーディングの状態が表示されていれば OK。
右側の Address が ubuntu.wsl になっているのが確認できる。
この状態で他の PC からブラウザでポートを開いてみたり、SSH してみたりを試して、表示できたり接続できたりすれば完了。再起動しても接続できるはず。
ポートフォワーディングは ubuntu.wsl ドメイン宛に行われるので、WSL や PC が再起動されたからといって Set-WslPortForwarding
を実行し直す必要はない。
新しいソフトをインストールしてリッスンポートが増えた(または減った)時や、Apache のリッスンポートを増やした・減らした時に実行すると、現在の WSL のリッスンポートに合わせてポート開放・ポートフォワーディングを設定し直すことができる。
lost connection to mysql server at 'reading initial communication packet', system error: 0
というエラーが出て別 PC はおろか、Windows 側から WSL2 内の MySQL サーバーに接続できなくなる(おま環かも…?)。そのため、これらのポートもポートフォワーディングしないようにしている。HTTP や SSH と違って別 PC からリモートで MySQL に接続するというシチュエーションがあまりないだろうし、そもそもポートフォワーディングが何故か正常に機能しない上にローカル上にも支障が出るので何れにせよそうするほかない。
この問題に関してはもう少しトライすれば解決できるかも、とは思うけど、他の PC からは SSH 経由で接続すればいいのでやる気がなく放置中。
C:\Users\(ユーザー名)\AppData\Local\WslPortForwarding.txt
に以前実行した際の WSL のリッスンポートが記録されている。このファイルを削除してしまうと、リッスンポートが減ったときにポート開放・ポートフォワーディングの設定を自動で削除することができない(以前設定したポートの設定を全部削除してから現在のリッスンポートの設定を全部追加するような処理になっているため)。Bash でカーソル操作時のベル(エラー音)を消す
だいぶ理想の環境を実現できた訳だけど、奇妙なことにカーソルを左端や右端に持っていくと Windows の致命的エラーの音がなったりしてだいぶうざい。
色々調べていたらこれは WSL 特有のものではなく、Bash に元からある機能らしい。
今までは鳴らなかったのになーと思ったら、これはどうも Windows Terminal 側でベルを鳴らすための特殊文字(?)がサポートされたことで鳴るようになったということみたい(参考)。
もしベル音をオフにしたいのなら、Windows Terminal のプロファイルに "bellStyle": "none",
と追記すればよい。
全てのプロファイルに適用させたいのであれば "profiles"
内の "defaults"
以下に記述する。
{
"guid": "{574e775e-4f2a-5b96-ac1e-a2962a402336}",
"name": "PowerShell",
"source": "Windows.Terminal.PowershellCore",
"bellStyle": "none",
"hidden": false
},
プロファイルごとにやりたい場合は、上記のように source
または commandline
の下にあたりに追記すると良いだろう。
最終的な .profile の内容
いろいろ試行錯誤してた事もあって .profile に結局何を追記すればいいのかよくわからない記事になってしまったので、最後に私の .profile (.bash_profile でも OK ) の内容を公開しておく。
.profile の前に追記
# Genie がまだ起動していなかったら起動する
# 参考: https://qiita.com/I_Tatsuya/items/9f85cdb9adc3adcf1abd
if [ "`ps -eo pid,cmd | grep systemd | grep -v grep | sort -n -k 1 | awk 'NR==1 { print $1 }'`" != "1" ]; then
# echo "Starting genie..."
[ -t 0 ] && exec genie -c bash --login
fi
# Genie が既に起動していたら Windows 側の $PATH を追加する
# 参考: https://github.com/arkane-systems/genie/wiki/No-Windows-interop-when-connecting-to-WSL-genie-via-ssh
if [ "`ps -eo pid,cmd | grep systemd | grep -v grep | sort -n -k 1 | awk 'NR==1 { print $1 }'`" == "1" ]; then
# echo "Add environ..."
TMPFILE=$(mktemp)
systemctl show-environment | awk '{print "export "$0}' >> $TMPFILE
source $TMPFILE
rm $TMPFILE
fi
.profile の後に追記
この記事では解説しなかったが、oh-my-posh (oh-my-posh 3) は Powerline の実装の一つで、Windows 含め様々な OS で同じ設定で Powerline が使えるため愛用している。powerline-custom.omp.json は Gist からどうぞ。
私は oh-my-posh を Homebrew でインストールしたため、念のため Homebrew のパスを読み込んでから実行するようにした。Homebrew の管理ディレクトリ下に置くとアップデートした時消えるので注意。
カスタムテーマを使うなら Homebrew に含まれているテーマではなく公式の unix の手順の通り別途 ~/.poshthemes
にテーマ DL してそこから参照させた方が良い気がする(この辺は好みで)。
履歴をリアルタイムで保存する
以下は bash の履歴を実行直後に保存するためのコマンド。
bash の履歴は ~/.bash_history
に保存されているが、そこへの記録は bash が終了したときに一括で記録されるため、たとえば何かの拍子に bash が強制終了されたりすると、履歴が一切記録されなくて泣く事がある。なぜかはわからないが、Genie 環境だと特になりやすい気がする。
PROMPT_COMMAND
はプロンプトを表示する前に実行されるコマンドを入れる環境変数。プロンプト表示前に履歴を保存しておき、さらに最新の履歴を読み込むというわけ。こうしておけば、複数ターミナルを同時に開いていても履歴が共有される。コマンドはシェルスクリプト同様に ; で区切れるらしい。
ただ oh-my-posh でも PROMPT_COMMAND
を使っているようで、上書きしてしまうと oh-my-posh が効かなくなってしまった。$PATH
のように既存の環境変数の内容に追記する形にすることで、oh-my-posh とも共存することができた。
# Homebrew のパス
eval $(/home/linuxbrew/.linuxbrew/bin/brew shellenv)
# oh-my-posh を読み込む
eval "$(oh-my-posh --init --shell bash --config $HOME/.poshthemes/powerline-custom.omp.json)"
# pyenv のパス
eval "$(pyenv init -)"
# rbenv のパス
eval "$(rbenv init -)"
# nvm のパス
source $(brew --prefix nvm)/nvm.sh
# 履歴をリアルタイムで保存/読込する
export PROMPT_COMMAND="$PROMPT_COMMAND history -a; history -r"
振り返って
もともとだーいぶイレギュラーな事やろうとしてるからというのがかなり大きいのだけど、WSL 環境構築大変だな…って感想。もちろん WSL 環境自体を作るのは Microsoft Store からインストールするだけなので簡単なんだけど。
結局丸々ぶっ通しでやってもセットアップで 4 日 + Genie 関連やポートフォワーディングで 3 日くらいかかってしまった。特に WSL 特有の問題に関しての調査でだいぶ時間を取られた。
WSL は凝った事やろうとすると systemd しかり WSL 特有の問題でコケやすいような気がする。Docker 使うようなモダンな開発なら問題にならなさそうだが。
Genie の話とポートフォワーディングの話は完全に執念で、結果的にうまくいったから良いとはいえ、相当な時間を使ってしまった。元々ややこしいので、ある程度前提知識がないと理解できないと思う。
いくら Ubuntu がそのまま動くとはいえ VM 上になったことによる諸問題もあるし、Genie とか使わない限りサービス自動起動してくんないし、Mac のデフォで Bash が入ってる UNIX 環境に比べるとセットアップめんどくさいだろうなー…と思った。ある程度割り切って諦めれば楽なんだろう。
エンジニアは Mac ユーザーが多いのでいまだ情報が少ないのもあるとは思う(WSL2 はまだ正式リリースされてから 1 年も経ってないくらい)。この備忘録は私がやったことを公開しておくことで情報を増やしたい意味合いもある(本当に正しい内容かはちょっと微妙だけど)。
でも Mac はあくまで UNIX 系 OS であって Linux 用のソフトがそのまま動くわけではないので、その点でいうと本物の Linux が動く WSL の方が軍配が上がるような気も。
コメント