使わなくなったAndroid端末にLinux開発環境を構築した。TermuxとPRoot-Distroを使えばroot化なしでUbuntuが動く。動くのだが、いくつか盛大にハマったポイントがあった。
PRootで動かないもの一覧
PRootはカーネルレベルの仮想化ではなく、システムコールをユーザー空間で変換する仕組みだ。このため、カーネルに直接アクセスする機能は使えない。
動かないものを先に整理しておく。
Dockerは動かない。Dockerはcgroupsやnamespacesといったカーネル機能を直接使うため、PRootでは原理的に不可能。Podmanも同様だ。コンテナ技術は全滅だと思っておいた方がいい。
systemdも動かない。PRootではPID 1がsystemdにならないため、systemctlコマンドは使えない。サービスの自動起動はbashスクリプトで代替することになる。
一部のシステムコールが失敗する。これが一番厄介で、どのシステムコールが使えないかは実行してみないと分からない場合がある。
os.networkInterfaces() の罠
Node.jsのアプリケーションを動かしていて、特定の条件下でクラッシュする問題に遭遇した。エラーメッセージは以下の通り。
Error: uv_interface_addresses: permission denied
原因はNode.jsのos.networkInterfaces()。ネットワークインターフェースの情報を取得するこの関数が、PRootでは権限エラーで失敗する。
厄介なのは、手動でTermuxを開いてPRoot環境にログインした場合は問題なく動くこと。自動起動スクリプトから非対話的に呼び出した時だけ失敗する。対話的なログインではTermuxの擬似TTYが一部のシステムコールを通すらしく、この違いに気づくまで原因の特定に時間がかかった。
対処法は、該当する関数呼び出しをtry-catchで囲むこと。
// Before
const interfaces = os.networkInterfaces();
// After
let interfaces;
try {
interfaces = os.networkInterfaces();
} catch (e) {
interfaces = {};
}
自分が書いたコードなら簡単に修正できるが、依存ライブラリの内部で呼ばれている場合はnode_modulesの中を直接編集する必要がある。grepで該当箇所を探して、ひとつずつパッチを当てていった。
OOMキラー対策
Android端末でNode.jsとHeadless Chromeを同時に動かすと、メモリが逼迫する場面がある。AndroidのOOMキラーは容赦なく、メモリが足りなくなるとTermuxプロセスごと殺してくる。signal 9で即死。ログも残らない。
完全に防ぐ方法は見つからなかった。代わりに、死んでも自動復帰する仕組みを作ることにした。
#!/data/data/com.termux/files/usr/bin/bash
# watchdog.sh
termux-wake-lock # スリープ防止
while true; do
proot-distro login ubuntu -- bash -c \
"source ~/.env && node app.js" \
>> watchdog.log 2>&1
echo "[$(date)] Process exited. Restarting in 15s..." >> watchdog.log
sleep 15
done
termux-wake-lockは必須で、これがないと画面オフ時にAndroidがTermuxをサスペンドする。バックグラウンド実行が止まってしまう。
安全弁として、短時間に連続で失敗した場合は自動停止するロジックも入れた。設定ミスなどで起動直後にクラッシュするケースでは、無限再起動ループに入ってしまうためだ。
Chromiumの起動が遅い
PuppeteerでHeadless Chromeを使う場合、デスクトップ環境なら一瞬で起動するが、Android端末では20〜30秒かかる。CPUとストレージの速度がボトルネックになっている。
起動オプションも重要で、以下のフラグを付けないとそもそも起動しないか、すぐにクラッシュする。
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-gpu',
'--disable-dev-shm-usage',
'--single-process'
]
});
--disable-dev-shm-usageは共有メモリの制約を回避するためのフラグで、PRoot環境では特に重要。--single-processはメモリ消費を抑えるために付けている。
ブラウザインスタンスは使い終わったらすぐにbrowser.close()する。常駐させるとメモリが圧迫されてOOMキラーの餌食になる。
aptで入るパッケージは普通に使える
ここまでハマりポイントばかり書いてきたが、通常の開発ツールは問題なく動く。Node.js、Python、git、vim、curl、jq、ffmpeg。全部aptで入れて普通に使える。
apt update && apt install -y nodejs python3 python3-pip git curl jq ffmpeg
npmもpipも普通に動く。Webアプリの開発、スクリプトの実行、APIの呼び出しといった日常的な作業は問題ない。ハマるのは、カーネルに近い機能を使おうとした時だけだ。
結局のところ
PRootの制約を理解した上で使う分には、かなり実用的な環境が手に入る。Dockerが使えない、systemdが使えない、一部のシステムコールが通らない。これらを前提として設計すれば、24時間動くLinux環境がAndroid端末の上で動く。
ハマりポイントの大半は情報が少ないことに起因していた。PRootの制約に関するドキュメントは限られていて、「動かしてみて初めて分かる」ことが多い。この記事が同じことをやろうとしている人の助けになれば。
関連記事
参考リンク
- Termux — Android向けターミナルエミュレータ
- PRoot-Distro — Termux用Linuxディストリビューション管理
- Termux Wiki: PRoot
- Puppeteer
- Node.js