「ゼロからのOS自作入門」を Rust でやる (第8章)

早い物でこのシリーズも4記事目です。 内容もだんだん難しくなってきて、Rust に移植するのも大変になってきましたが、頑張っていきましょう。

第8章

メモリ管理を実装する章です。 前回 の記事 (第6章その1) では USB マウスへ対応させようとしていたのですが、 USB デバイスの制御に必要な MMIO 領域の仮想アドレスが割り当てられていないためにアクセスができず、USB ドライバーが動作しませんでした。 この問題に対処するため、先に8章を実装することにしたのでした。

UEFI メモリマップの表示 (day08a)

ブートローダーから渡されるメモリマップ情報を表示させます。

github.com

bootloader クレートのブートローダーから渡されるメモリマップ情報 (memory_regions) では、連続した領域でもページ毎に(?)バラバラになっており、そのまま println すると出力が大量となってしまいます。 この現象への対処として、 MemoryRegions というイテレータを作成し、連続した領域は一つにまとめるようにしています。

ブートローダーから渡される情報メモリマップ情報では各領域の種別は以下 enum で表されます。

MemoryRegionKind in bootloader::boot_info - Rust より

#[non_exhaustive]
#[repr(C)]
pub enum MemoryRegionKind {
    Usable,
    Bootloader,
    UnknownUefi(u32),
    UnknownBios(u32),
}

Usableカーネルで自由に利用可能な領域です。 UnknownUefi(u32) についても u32 が特定の値の場合は利用可能だと思うのですが、どの数値が何に対応しているのかが分からなかったこと (UEFIのヘッダを見れば分かるのでしょうが)、また、全領域合計でたかだか2MB程度だったので当該領域の再利用はやめ、 Usable の領域だけ利用することとしました。

これにより、UEFI データ領域に割り当てられている領域の移動は不要になりますので、スタック領域の移動、セグメンテーションの設定は省略しました。

ページングの設定

C++版ではページング設定として物理アドレスを同じ仮想アドレスにマッピング (アイデンティティマッピング) していました。 Rust では bootloader クレートが物理メモリのマッピングに対応しているのでそれをそのまま使い楽をしました。

github.com

やったことは簡単で、 Cargo.toml に以下を書き足しただけです。

[package.metadata.bootloader]
map-physical-memory = true

bootloader クレートのマッピング方式は物理アドレスに固定のオフセット値を足した仮想アドレスにマッピングする方式です。 今のところはページテーブルの設定等でこの差異を気にする必要があります。 ひとまず、 Writing an OS in Rust の Paging の章 を参照し、ページテーブルの取得とアドレス変換について実装してみました。

フレームアロケーターの作成 (day08c)

ページフレームを割り当てを管理するフレームアロケーターを作成しました。

github.com

実装は C++ と同じです。 一点引っかかった点として、 "Writing an OS in Rust" のようにフレームアロケーターをローカル変数としてスタック上に割り当てたところ、カーネルが起動しなくなったという点がありました。

これは、恐らくスタックオーバーフローが発生していたためです。 「ゼロからのOS自作入門」のフレームアロケーターは128GiB分のフレームの管理領域を保持するため32MiBの固定長配列を獲得します。 これは bootloader クレートで設定されるスタックサイズ 80KiB より大きいです。

フレームアロケーターを static 変数とすることでスタックオーバーフローを回避しました。

まとめ

メモリ関連の処理を実装しました。 ページテーブルへのアクセスも可能になったため、当初の目的である6章を先に進めることも可能になったことでしょう。 というわけで次回は6章に戻ろうと思います。お楽しみに。

「ゼロからのOS自作入門」を Rust でやる (第6章 その1)

今日も今日とて「ゼロからのOS自作入門」をRustでやっていきます。

6章

MikanOS を USB マウスへ対応させる章です。

マウスカーソルとデスクトップを描画 (day06a)

マウスカーソルとデスクトップを描画する節です。 描画するといっても、ただ表示するだけで操作などはできないものなので簡単です。

github.com

デスクトップの描画とマウスカーソルの描画はそれぞれ desktop, mouse というモジュールへと分割しています。 デスクトップの描画処理ではスクリーンのサイズ情報が必要です。 FrameBufferInfo 構造体のメンバが使えるのですが、当該構造体メンバの型は usize であるため、 描画処理で利用している型である i32 への変換が必要です。 スクリーンサイズを必要とする処理は他にも登場することが予想され、それぞれで型変換やそれに伴うエラーハンドリングを実装するのはめんどくさいため、 i32 型でスクリーンサイズ等の情報を格納する ScreenInfo 構造体を用意し、 framebuffer の初期化時に1度だけ初期化するようにしました。

PCIバイスの探索 (day06b)

IO 命令により PCI コンフィグレーション空間にアクセスし、 USB のホストコントローラ (今回は xHC) に対応するデバイスを探索します。

github.com

処理は C++ 版と同じです。工夫した点をいくつか紹介します。

インラインアセンブリを使わない

IO ポートにアクセスするためには通常アセンブリを書く必要があります。 今回は x86_64 クレートx86_64::instructions::port::Port を利用することで、インラインアセンブリを手書きせずに済ませました。

アトミックな IO ポートアクセス

PCI コンフィグレーション空間にアクセスするためには

  1. CONFIG_ADDRESS レジスタへ、アクセスしたい PCI コンフィグレーション空間の位置を設定
  2. CONFIG_DATA レジスタを読み書きする

という2つの手順が必要です。 この 1 と 2 の間に別のタスクが CONFIG_ADDRESS レジスタの操作を行ってしまうと意図せぬ結果となる可能性があるため、両レジスタへアクセスしている間は spin::Mutex のロックを取得するようにしました。

bit_field::BitField の利用

PCI コンフィグレーション空間からデバイスの情報を取得するためには、ビットフィールド操作が必要になります。 u32 のうち n ビット目から m ビット目までを特定の数値を設定する、といった操作です。

この操作を簡単化するため bit_field クレートbit_field::BitField 構造体を使いました。 ビットフィールドの読み書きが以下のように書けます。

// 値の設定
let mut value = 0u32;
value.set_bits(0..8, u32::from(reg_addr));
value.set_bits(8..11, u32::from(function));
value.set_bits(11..16, u32::from(device));
value.set_bits(16..24, u32::from(bus));
value.set_bits(24..31, 0);
value.set_bit(31, true);

// 値の取得
let bus = values.get_bits(16..24) as u8;

range の形式でビット範囲が表せるのは非常に分かり易いですね。

USB マウスへの対応 (day06c)

さて、ここが問題のUSBマウス対応です。

実装方針

C++ 版実装では、「ゼロからのOS自作入門」著者の方が開発された USB ドライバ一式をインポートして使っています。 曰く、「USB関連のドライバは本書で説明するには高度で複雑すぎますので、筆者が開発したドライバを使う方法を説明するだけにします」とのこと。 OS自作が主題であるためこの対応はまったく正しいと思います。

では Rust 版を実装するにあたりどうするかですが、私も「ゼロからのOS自作入門」と同じアプローチを取りたいと思います。 つまり、C++のUSBドライバ実装を流用することとします。

USB ドライバを Rust で実装するのも興味深いですし、実際にそのアプローチで Rust 版 MikanOS を実装しようとされている方も、また、 USB ドライバの実装に成功された方もいらっしゃいます。 しかし、私が同じことをやろうとすると間違いなく時間がかかるでしょうし、その間このブログの更新は止まってしまうでしょうし、なにより作業のモチベーションを保つのが難しそうだと考えたので、 USB ドライバの Rust 移植はまずは諦めることとしました。

USB ドライバの移植は、一通り OS 実装が完了し満足した後で次の課題として取り組めたら取り組もうかなというスタンスで進めていきたいと思います。

というわけで C++ ソースと Rust ソースを結合して一つのバイナリ (カーネル) を作ることになるわけですが、以下のような作戦をとりました。

  1. MikanOS の USB ドライバ一式 (と、Rust から呼び出すためのグルーコード) を含むクレート mikanos_usb を作成
  2. mikanos_usbbuild.rsC++ ソースをコンパイルし、静的ライブラリ libmikanos_usb.a を作成し、クレートに含める
  3. 1, 2 で作成したクレートをカーネルのクレート (sabios) から利用する

この中でもポイントとなる build.rs の中身について説明します。

C++ ソースのコンパイル

build.rs の中で C++ ソースをコンパイルするために、 cc クレート を使いました。 コンパイラgcc/g++ ではなく clang/clang++ を使わせたいため、少し行儀が悪いですが build.rs 内で環境変数 CCCXXclang / clang++ を設定しています。

なお、 C++ の標準ヘッダファイルも一式用意する必要があります。 当初はソースコード中にヘッダファイルを全て含めていたのですが、後に方針変更しています (後述)。

依存ライブラリの同梱

C++ ソースは newliblibc および LLVMlibc++, libc++abi に依存しているため、 libmikanos_usb.aカーネルにリンクする場合は、 libc.a および libc++.a, libc++abi.a もリンクする必要があります。

C++ 版では作者の方がコンパイルしたバイナリを使っています。 Rust 版では本来はこれらのライブラリについてもソースからビルドするのが良いのですが、コンパイル時間が長くなりそうだったので、作者の方がコンパイルしたバイナリを使うことにしました。 具体的には、 build.rs の中で GitHub からライブラリを含んだアーカイブをダウンロードし OUT_DIR 配下に展開、 println!("cargo:rustc-link-lib=static={}", lib); により cargo にライブラリをリンクすることを指示するというやり方をしました。 buiild.rs の中でウェブにアクセスするのは禁じ手ですが、あくまでの学習用のプロジェクトということでご容赦頂きたく...

副作用として、 GitHub からダウンロードしたアーカイブにはヘッダファイル一式も含まれていたため、C++ のビルド時にもこのヘッダファイルを利用することとしました。 リポジトリに大量のヘッダファイルを登録しなくても済むようになったところは嬉しいですね。

さて、後は Rust から C++ コードを呼び出せるように、また、 C++ から Rust を呼び出せるように、グルーコードを書きます。 適当に extern "C" な関数を定義すれば OK です。

github.com

さて、 C++ で定義されたクラスのコンストラクタを呼び出すところまで実装できました。 まだすべては実装できていませんが、できたところから動作確認していくのが良いでしょう。

動作確認

C++ ライブラリをリンク & 呼び出すようにし、さっそく実行してみたところ、 OS が reset され、 QEMU 上で再起動を繰り返すようになってしました。 なんででしょうか。

デバッグのため QEMU の起動オプションに -d int --no-reboot --no-shutdown を追加します。 これにより、割り込み発生時に割り込みの情報が標準出力に吐き出されます。 また、OS リセット時に仮想マシンが再起動せず止まった状態のままになります。

再実行したところ、どうやら例外 0xe が発生しているようです。 OSDev Wiki によると、 0xe は Page Fault とのこと。 どうやら不正なアドレスへのアクセスが起きているようですね。

QEMU の出力から、例外発生時の RIP も分かります。 QEMU のコンソールから x /5i 0x<RIPの値> を実行することで RIP 周辺の実行命令が分かります。 レジスタの値など合わせて判断するに、 xhc_mmio_base のアドレスにアクセスしようとして Page Fault が発生しているようです。 この値は今回呼び出しを追加した C++usb::xhc::Controller クラスのコンストラクタに渡している値でもあり、つじつまが合いますね。

すべての物理メモリをマッピングする

今回利用しているブートローダーの bootloader クレートはページテーブルを更新しているようです。 xhc_mmio_base の値は物理アドレスのはずですが、そのアドレスに向けた仮想アドレスがマッピングされていないのではないかと考えました。 bootloader の設定 を参照したところ、 map-physical-page という設定があり、デフォルト値は false でした。 これを true にすることで物理メモリがマッピングされ、 xhc_mmio_base のアドレスへもアクセスできるようになるのではないかと考え試してみました。

このオプションでは物理アドレス0以降のメモリを physical_memory_offset だけずらした仮想メモリアドレスにマッピングするため、 xhc_mmio_base のアドレスに physical_memory_offset だけ足したものを usb::xhc::Controller のコンストラクタに渡してみます。

github.com

だめでした。

bootloader の map-physical-page 有効時の処理 では、アドレス 0 から物理メモリサイズ分の領域を仮想アドレスにマッピングしています。 今回 xhc_mmio_base の値は 0x8_0000_0000 だったのですが、これは 0x0 から 32GiB 分だけ離れた領域になります。 QEMU 仮想マシンにはのメモリ容量はもっと小さいはずなので、 xhc_mmio_base の領域が仮想アドレスにマッピングされないのは当然です。

どうやら自分で xhc_mmio_base物理アドレスを仮想アドレスにマッピングする必要がありそうですね。

しかし、6章時点ではまだページング関連処理は実装されていません。 6章を進めるのは一旦ここでやめ、先にページングを実装することとしました。

まとめ

USB ドライバを利用しようとしたところ、C++プログラムのコンパイルとリンクはうまくいきましたが、 bootloader クレートの仕様により xhc_mmio_base へのアクセスができませんでした。 対処のためにはページテーブルを更新して当該アドレスへアクセスできるようにする必要があります。

一旦6章は中断し、先に8章に進むことにしました。次回もお楽しみに。

「ゼロからのOS自作入門」を Rust でやる (第5章)

引き続き「ゼロからのOS自作入門」を Rust でやっていきます。

完走できるといいなぁ。

第5章に入る前に

前回作った部分でいくつか気になった箇所があったので修正しました。代表的なものを紹介します。

unwrap / expect を減らす

main 以外の処理では unwrap / expect を使わず Result で呼び出し元にエラーを通知するようにしました。

github.com

unwrap / expect の利用箇所を見つけるためには clippy の以下 lint が便利です。

#![warn(clippy::unwrap_used)]
#![warn(clippy::expect_used)]

今回独自の Error 型を定義した訳ですが、 thiserror などが使えないのはやはりめんどくさい。 thiserror の no_std 対応は Pull Request はあるのですが、マージされないまま1年以上経っており状況がよくわからないです。。。

描画関連処理を trait へ括りだす

framebuffer::Drawer の描画関連処理を graphics::Draw トレイトへ括りだし、 framebuffer::Drawer が当該トレイトを実装するようにました。

github.com

今後 &dyn Draw により描画のバックエンドを切り替えられるようにすることが必要になってくるので、このような対応をしました。 Draw が object safe であることを保証するため、 static_assertions::assert_obj_safe を使っています。

最初からこのような形にしなかったのは、トレイトの関数の引数では型パラメータや impl Trait を使いたかったためです。 例えば、以下のような関数を持つトレイトは object safe ではありません。

impl Draw {
  fn draw(&mut self, point: impl Into<Point<i32>>);
}

上記のように引数を impl Into<T> で受けられると便利なのですが、今回は諦めました。

もう一点、細かい話ですが画面上の座標や長さを表す型は Point<i32>, Size<i32> としています。 どちらも Vector<i32> の別名で同じ型なのですが、コードを見やすくするために別名を与えています。 数値として i32 を利用しているのは、座標が負値になることがあったり、座標の引き算などを簡単にできるようにしたかったためです。

配列のインデックスの usize 型との変換がめんどくさい (そして asunwrap が登場しがち) という問題はありますが、 細かな工夫をすることで乗り越えることにします (例えば、 iter.enumerate() の代わりに (0..).zip(iter) を使うなど)。

第5章

文字列の出力をする章です。

文字 'A' の出力 (day05a)

github.com

特に難しいことはやっていません。 スクリーン範囲外に描画しようとした場合に発生したエラーを無視するための手段として Result<(), DrawError>::ignore_out_of_range() なんて関数を定義しているあたりが少し面白いくらいでしょうか (この後のコミットでスクリーン範囲外に描画した場合もエラーにしないよう変更しており、結局この関数は削除されていますが)。

分割コンパイル (day05b)

C++ のファイルを複数に分割する話です。 最初からモジュール分割してプログラムを作成していたため、スキップしました。

フォントを増やす & 文字列の出力 (day05c)

'A' 以外の文字を出力するために、テキストファイルに定義された字形 (フォント) をプログラム中から利用するという節でした。 C++ 実装ではテキストからオブジェクトファイルを生成していました。 Rust 実装では build.rs でコード生成しモジュールとしてインポートするようにしました。 Rust ではよくあるパターンですね。

github.com

C++ で写経していたときは objcopy 実行ディレクトリと入力ファイルの (hankaku.bin) のパスが変わると生成されるシンボル名が変わる現象に悩まされたりしましたが、 Rust のソースコード生成ではこのあたりの悩みとは無縁なので良いですね。

write / writeln で文字列を出力できるようにする (day05d)

github.com

C++ では sprintf を使えるようにしていましたが、 Rust では write!writeln! を使えるようにします。 まだヒープアロケーションはないので format! は使えません。

やったことは簡単で、 fmt::Write を実装した font::StringDrawer を用意しました。 write! 呼び出し1回につき複数回 fmt::Write::write_fmt が呼び出されることがあるので、 font::StringDrawer では次に描画する位置を覚えておき、1回出力することに更新する必要があります。

最初この処理を忘れてしまったため、文字が同じ場所に重ねて出力されて大変な事になっていました。 書いたプログラムが滅茶苦茶な動作をするのを見るのも自作の楽しさかなという感じです。

コンソールを追加する (day05e)

最終的に C++ 版をベタ移植したようなコードに落ち着きました。

github.com

Console::buffer の持ち方をどうするか悩んだのですが、どうせ ASCII の範囲しか表示されないだろうということで割り切って u8 の配列としました。 ASCII の範囲で表せない文字を表示しようとした場合、 font::char_to_byte により b'?' に変換されます。 また、前の章で &str を受け取る描画関数 font::draw_str を用意したのですがこれは使わず、 u8 の配列を受け取る font::draw_byte_str を使うようになっています。

このあたりの文字列処理は後々ちゃんとしたいですね。

printf! / println! を追加する (day05f)

先ほど作った console::Consoleグローバル変数にし、カーネル内の各所から printf! / println! により文字列を書き込めるようにしました。

github.com

グローバル変数の排他の取り方に苦労しています。 元々の Console の実装ではフレームバッファへの可変参照 &mut framebuffer::FrameBuffer を保持していましたが、これをそのままにして console::Consoleグローバル変数にするのは論外です。 console::Console 以外から画面への出力ができなくなってしまうためです。

ちょっと悩んだのですが、以下のようにしています。

  • console::Console には framebuffer::Framebuffer を含めず初期化
  • コンソールへの出力処理 (_print) でフレームバッファとコンソールのロックを取得し、 Console::writer() により fmt::Write を実装した ConsoleWriter を取得する
  • ConsoleWrite に対して write! する

framebuffer のロックは様々なタスクで奪い合いになりそうなのでなんだか不安な感じがしますが、ひとまずはこれでいこうかと。 そのうちちゃんとします。

おまけ

今回作成した print!, println! を使い、 panic 時にメッセージを出力するようにしました。

github.com

panic 時に行っていた処理で framebuffer やコンソールのロックを取得していた場合デッドロックしますが、まあ何もしないよりはマシかなと。

unsafe なコードを書き始めたら、割り込み発生時の情報などの出力できるようにしたいですね。

まとめ

文字列の描画関連を用意しました。 ロック周りや文字/文字列の扱いで少し悩んだところはありましたが、まだまだ全然簡単ですね。

次章は Rust 実装最大の (?) 鬼門であるUSBデバイス対応です。 この難題にどう立ち向かうのか。お楽しみに。

「ゼロからのOS自作入門」を Rust でやる (第1章~第4章)

ゼロからのOS自作入門一通り写経 したところ、 Rust に移植したくなったのでやっていきます。

github.com Rust で OS なので名前は "錆OS" です。安直です。 "sabios" でざっとググったところ、スペイン語で "賢い" って意味があるようです。 良いですね。 クレバーな実装を目指したいところです。

方針

「ゼロからのOS自作入門」の章立てに沿って1章から順番に実装していきます。 C++ で一通り写経は完了しているので完成形の OS と関係ない節はスキップしていきます。

せっかく Rust で実装するので安全性や抽象化という点で MikanOS との差異を出せたら良いなと思っています。

それでは早速やっていきましょう。

第1章~3章

UEFIブートローダーを作成し自作カーネルを起動する章です。 uefi-rs を利用してブートローダーを自作している方も多かったですが、 今回は楽をして Writing an OS in Rust の作者が提供している bootloader を利用することにしました。 bootloader は 2021/5/5 に公開された v0.10 より UEFI でのブートに対応しています。

bootloader を使ったブートローダーの作成は特殊な手順を踏む必要があるのですが、 Writing an OS in Rust の 3rd edition のドラフトpost-02 で詳しく説明されているのでこれに従って実装します。当該記事では cargo の alias を利用してビルドコマンド実行を簡易化していますが、あまり好みではなかったので sabios では Makefile を採用しました。

記事に従って実装していくとあら不思議、3章完了時点の MikanOS とほとんど同じ画面出力が得られました。 これにて3章まで完了とします。 (2章のメモリマップファイルの作成は UEFI 側の処理なので省略しました。)

3章完了時点のソースは以下。

github.com

第4章

本格的なOS実装の始まりです。 最初の一歩はフレームバッファを利用した画面描画用モジュールの作成です。

最初の実装

まずは細かい事を気にせずに、 Rust のスタイルでベタに書いてみました。 フレームバッファへの描画を担当する framebuffer::Drawer 構造体を作成しました。 この構造体を main 関数内で初期化し、描画処理を呼び出しています。

github.com

特に難しいことはないですね。 おまけで MikanOS 本では対応していなかった PixelFormat::U8フレームバッファにも対応しています。

Rust では同一領域に対する &mut の参照が複数同時に存在すると Undefined Behavior となるため、 ブートローダーから渡される FrameBuffer からバッファのスライス &mut [u8] を取り出して利用するのではなく、 FrameBuffer 構造体そのものを framebuffer::Drawer のフィールドとして保持するようにしています。

フレームバッファをどこからでも使えるようにする

これで描画はできるようになったのですが、このままではフレームバッファに描画するために framebuffer::Drawer への参照をあちこちに取り回す必要があります。 それではあまりにも不便なので framebuffer::Drawer 型のグローバル変数 (static 変数) を作成し、カーネル内のどこからでもアクセスできるようにします。

github.com

FrameBufferカーネルのエントリポイント (kernel_main) の引数として渡される情報であるため、static 変数の初期化のタイミングでは利用することができません。 static 変数の初期化のタイミングでは未初期化状態のままにしておき、 kernel_main から framebuffer::init を呼び出すタイミングで初期化します。 このような初期化動作を実現するために conquer_once::OnceCell を利用しています。 また、 static 変数への mutable アクセスを実現するために spin::Mutex を利用しています。 spin::Mutexno_std でも利用できる Mutex です。名前の通りスピンロックが使われています。 どちらの crate も Writing an OS in Rust で知りました (post-03post-12)。本当に良い記事です。

フレームバッファの性能改善

ここまででフレームバッファを使うための基本的な機能は一通り揃いました。 続いては性能改善です。 MikanOS では C++ の仮想関数を使うことで override により条件分岐の回数を大幅に減らすことで性能改善していました。 sabios では dyn Trait を使った動的ディスパッチにより同様の性能改善を実現しました。 画面を見ながらストップウォッチで描画速度を計測したところ、だいたい2倍強高速になったようです。やったね!

github.com

framebuffer::Drawer 全体を dyn Trait 化するのはややこしくなりそうな気がしたので、分岐のある部分のみ dyn Trait として抽出しています。

性能改善の度合いを測定するため、カーネルを release ビルドできるように Makefile も改造しています (コミットメッセージが typo してて意味不明になってますが)。

github.com

まとめ

「ゼロからのOS自作入門」の第4章までを Rust で実装しました。 ここまでは大きな引っかかりもなくスムーズに実装できました。 次の記事では第5章以降を実装していきます。

PlantUML サーバーの Docker コンテナをオンデマンドで起動する

tl;dr

  • VSCode の PlantUML 拡張は便利だがデフォルトでは遅い (JVM 起動や各種 class ファイルのロードに時間がかかる?)
  • PlantUML サーバーをローカルで起動することで高速化可能
  • PlantUML のようなたまにしか使わないサービスを常時起動させておくのはなんとなく嫌
  • 本記事では特定のポートにアクセスがあった時点で PlantUML を起動するための設定方法を記す

使うもの

  • systemd
  • docker

手順1: PlantUML Docker サービスを systemd サービス化する

systemctl コマンドで PlantUML サーバーを起動できるようにします。 以下では TCP 51000番ポートを PlantUML サーバーへ割り当てます。

# unit ファイルの作成
$ sudo -E systemctl edit --force --full docker-plantuml.service

エディタが開くので以下を入力。

[Unit]
Description=PlantUML Server Container
After=docker.service
Requires=docker.service

[Service]
TimeoutStartSec=0
Restart=always
ExecStartPre=-/usr/bin/docker stop %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStartPre=/usr/bin/docker pull plantuml/plantuml-server
ExecStart=/usr/bin/docker run --rm --name %n -p 51000:8080 plantuml/plantuml-server
ExecStartPost=/usr/bin/wget --quiet --method HEAD --waitretry 0.1 --tries 600 --retry-connrefused http://localhost:51000

[Install]
WantedBy=multi-user.target

参考にしたサイト では nc -z $host $port でサービス起動を待ち合わせていましたが、 PlantUML サーバーの場合は TCPポートへ connect(2) できるようになってから (コンテナが起動してから?) HTTP サーバーが応答を返せるようになるまで10秒弱程度のタイムラグがあるようなので、 wget(1) でポーリングするようにしています。

手順2: ソケット接続時に docker-plantuml サービスを起動する

systemd には socket activation という特定のポートへの接続があった時にサービスを起動するような仕組みがあります。 この仕組みを使うためにはは、起動されるサービス側が socket activateion に対応している必要があり、 残念ながら Docker は対応していません。

socket activation に対応したプロキシである systemd-socket-proxyd を利用し、 特定のポートへの接続と Docker の listen(2) しているポートへの接続を仲介することで、 特定の TCP ポートに接続があった時に Docker コンテナを起動させることができます。

以下では、 TCP 50000 番ポートへ connect(2) された場合に、 PlantUML のサービス (TCP 510000番) へ転送するように設定しています。

$ sudo -E systemctl edit --force --full docker-plantuml-proxy.socket

エディタが開くので以下を入力。

[Socket]
ListenStream=50000

[Install]
WantedBy=sockets.target
$ sudo -E systemctl edit --force --full docker-plantuml-proxy.service

エディタが開くので以下を入力。

[Unit]
Requires=docker-plantuml.service
After=docker-plantuml.service

[Service]
ExecStart=/usr/lib/systemd/systemd-socket-proxyd 127.0.0.1:51000

最後に、 docker-plantuml-proxy.socket を有効化 & 起動してください。

$ sudo systemctl enable docker-plantuml-proxy.socket
$ sudo systemctl start docker-plantuml-proxy.socket

手順3: VSCode の設定

以下のように設定すればOK!

{
    "plantuml.render": "PlantUMLServer",
    "plantuml.server": "http://localhost:50000/"
}

手順4: 動作確認

VSCode で PlantUML ファイルを開きプレビューすると、 PlantUML サーバーが起動してレンダリングされるはずです。

参考にしたサイト:

Arch Linux on WSL2 で systemd を動かす

昨日のエントリは systemd はいらないなどと言っていましたが、 podman コンテナ上で systemd を動かすためにはホスト側でも systemd が動いていないといけないようだったので、観念して systemd をセットアップしました。

gifnksm.hatenablog.jp

tl;dr

genie のインストール

github.com

上記リポジトリに含まれている PKGBUILD は古いバージョン用のものだったので、自分で新たに PKGBUILD を作成しました。

2021/1/18 追記: genie 1.31 へアップデートしました。 2021/1/18 追記2: GitHub へ移動しました。

github.com

Arch パッケージガイドライン - ArchWiki によると /usr/libexec は避けろとのことだったので、 /usr/lib/genie にファイルを配置しても動くよう、ソースコードを編集するような PKGBUILD になっています。 手元環境で試した限りでは問題なく動作しています。

dependsmakedepends が AUR にあり makepkg -s が使えないため、以下のようにインストール。また、 community リポジトリdotnet はバージョンが古いため、 AUR の dotnet-sdk-bin をインストールします。

$ paru -S --needed --asdeps daemonize inetutils dotnet-sdk-bin
$ makepkg -si

グループポリシーの設定

以下のスクリプトを適当なパス (今回は %userprofile%\scripts\logoff.bat) に配置します。

wsl -d Arch -- genie -u
wsl -t Arch

wsl -t Arch を実行しているのは、 genie -u 実行後、WSL を再起動することなく再度 genie -i などを実行すると何かおかしなことになるためです。 (ログオフ時には WSL も終了されると思うので明示的に行う必要はないと思いますが、念のため。)

以下記事に従い設定します。

www.nextofwindows.com

以下方法で動作確認しました。

  1. 上記を設定
  2. genie -i を実行し systemd を立ち上げる
  3. Windows をログアウト & 再ログイン
  4. genie -s でシェル立ち上げ後、 journalctl でログを確認する
  5. systemd-shutdown 等のログが出ていればOK (たぶん)

Windows Terminal の設定

自分は Windows Terminal を利用しているため、設定変更し端末を開いた時点で systemd 管理下にログインするよう設定しました。

            {
                "guid": "{a5a97cb8-8961-5535-816d-772efe0c6a3f}",
                "hidden": false,
                "name": "Arch",
                "source": "Windows.Terminal.Wsl",
                "startingDirectory": "//wsl$/Arch/home/nksm",
                "icon": "ms-appdata:///Local/archlinux.png",
                "commandline": "wsl -d Arch -- genie -s"
            },

重要なのは commandline です。

以上で systemd の設定が完了します。 また、無事 podman コンテナ内で systemd を立ち上げることもできるようになりました。

追記

ログイン時にも wsl -d Arch -- genie -i を実行する方が良いかもしれません。 初回のWindows Terminal 起動時に wsl -d Arch -- genie -s を実行すると、 systemd の起動が間に合わないからか failed to connect bus といったエラーが出ることがあるためです。

github.com

2021/1/18 追記: 上記 issue は genie 1.31 で修正されました。

追記2

systemd インストール後、全パッケージを再インストールした方が良いかもしれません。 非 systemd 環境ではパッケージインストール時に dbus 関連でエラーが出るパッケージがあり、正しく設定されていないかもしれないためです。

$ pacman -Qqn | sudo pacman -S -

追記3

PKGBUILD に間違いがあった (10-genie-envvar.sh のインストール先がおかしかった) ため、修正しました。

Arch Linux on WSL2 で podman でコンテナを動かす

自分用メモ。

tl;dr

  • GitHub - yuk7/ArchWSL: ArchLinux based WSL Distribution. Supports multiple install.
    • Arch Linux on WSL2 を入れる。 scoop 使うと楽。
  • Podman - ArchWiki
    • /etc/sub{u,g}id の設定が必要。設定する値は <user>:100000:65536 などとすると分かりやすい (コンテナの uid=1000 がホストの uid=101000 になり対応関係が一目瞭然)
    • 上記設定前に podman 叩いちゃった場合は podman system migratepodman system reset を実行する必要があるみたい
  • How to run Podman on Windows with WSL2 | Enable Sysadmin
    • Red Hat の記事。最後の方の /etc/containers の設定例だけ見れば良い。
  • pacman -S shadow
    • ArchWSL インストール直後だと /usr/bin/new{u,g}idmap に capability (or setuid bit) が設定されていないため、これらのバイナリが含まれる shadow パッケージを再インストールすることで設定させる
    • 以下になっていればOK。capabilityがセットされていれば setuid bit の設定は不要。

      $ getcap /usr/bin/new{u,g}idmap
      /usr/bin/newgidmap cap_setgid=ep
      /usr/bin/newuidmap cap_setuid=ep
      $ ls -l /usr/bin/new{u,g}idmap
      -rwxr-xr-x 1 root root 41K  9月  7 22:42 /usr/bin/newgidmap
      -rwxr-xr-x 1 root root 37K  9月  7 22:42 /usr/bin/newuidmap
      

当時のツイート

飲酒正月気分で殴り書きしてるから今読むと日本語がおかしい。

その他

  • podman in podman も余裕やろと思ってやってみたら overlayfs on overlayfs (?) はできないみたいなエラーが出て挫折してしまった。 ググった感じ /var/lib/containers あたりを volume にすれば良さそうっぽいが試してない。
  • podman をググると docker と違って英語記事ばかり出てくるのは良いことなのかどうなのか。 Red Hatサブスクリプションがないと読めない記事ばっか出てくるのは辛い。 Red Hat Developerに登録 (無料) すれば読めるとのこと。Shion Tanaka さん教えて頂きありがとうございます!
  • WSL2 で systemd を動かすかどうかは悩ましいところ。 以下を使えば簡単に systemd を使えるようになるらしいが、今のところ導入していない。 github.com 導入していない理由は以下。
    • ホストマシンのシャットダウン時に systemd に対するシャットダウン操作が必要になるなど、WSL2の気軽さが損なわれてしまいそうな気がする。
    • systemd の提供する仕組みと Windows の提供する init の仕組みがコンフリクトしたりしないかが心配。ネットワーク周りとか。
    • 今のところ systemd なしでも困っていない。動かしたいサービスがあったら podman コンテナで代用できそう。 まあそのうち気が変わるかもしれない。