Runtime と PRoot
Runtime は、ABI ごとにビルド・配布されるイミュータブルな Ubuntu 24.04 ルートファイルシステムです。PRoot は、Root 権限のない一般的な Android アプリ環境下でこのファイルシステムを完全に稼働させる役割を担います。
なぜ PRoot を採用するのか
Android のセキュリティモデルでは、一般アプリが自身のプライベートディレクトリ内の実行可能ファイルを直接 execve することや、chroot を実行することは禁止されています。PRoot は ptrace によりシステムコールをインターセプトし、ファイルパスをユーザー空間内の仮想ルートディレクトリへ透過的に書き換えることで、「あたかも chroot されているかのような」Linux 環境を実現します。
重要な制約事項:
- 実行可能ファイルの配置場所:proot 関連バイナリ一式は
lib*.so形式で APK のjniLibsに格納され、インストール後にnativeLibraryDirに配置されます。これは targetSdk 35+ においてexecveの実行が保証される唯一の領域です。 - ローダーのパス:
PROOT_LOADERはnativeLibraryDir内のローダーを直接指定し、プライベートディレクトリの実行制限を回避します。 - SELinux 対策:Android の
/devには/dev/shmが存在せず、ハードリンクの作成も拒否されます。PRoot 起動コマンドはアプリ専用の共有メモリディレクトリをバインドし、--link2symlinkオプションを有効化することで、Python のマルチプロセスイベントやキューの正常動作を担保します。
セッション管理の契約
App(親プロセス) ──stdin パイプ──▶ proot wrapper ──▶ Runtime プロセスグループ- stdin パイプは常にオープン状態を維持しなければなりません。App プロセスが終了して EOF が送出されると、ラッパーは自律的に配下のプロセスグループ全体を kill し、孤児プロセスの残存を完全に防止します。
ProotHostの監視コルーチンをキャンセルする際は、待機のみを停止し、必ずCancellationExceptionを上位へ再送出してください。プロセスが終了したとみなしてsessionハンドルをクリアしてはなりません(正常停止処理時に stdin を閉じ、リソースを回収するためにこのハンドルが不可欠です)。- クラッシュ時はバックオフ待機を挟んで再起動を試行し、起動前には必ず残存ロックファイルをクリーンアップします。
バックグラウンド起動への変更は厳禁です
nohup 等を用いて親子関係を切り離すと、親プロセスの死亡検知プロトコルが失われます。親プロセス終了後も Runtime が動作し続けるため、次回起動時に同一の設定ファイルや SQLite データベースに対して競合が発生し、データ破壊の原因になります。
デプロイ状態マシン
ProvisionState の状態遷移:
| ステート | 状態の定義 |
|---|---|
Checking | 既存の導入済みバージョンを検証中 |
NotInstalled | 未デプロイ状態(ユーザーのアクション待ち) |
NotBundled | 軽量版を使用しているが、Release 上に該当アーキテクチャのファイルがない |
LowDisk | 空きストレージ容量が 2 GB 未満 |
Downloading | ダウンロード進行中(圧縮バイト単位で計測) |
Extracting | 展開処理中(圧縮バイト単位で計測) |
Ready | デプロイ完了(起動可能状態) |
Failed | デプロイ異常(ユーザー向けのエラー事由 reason を保持) |
インストール判定ロジック
isInstalled() は以下の条件をすべて満たした場合に true を返します:
.provisionedフラグの記録バージョンと、rootfs 内のopt/azurpilot/BUILD_MANIFESTが一致していること。rootfs_archが端末の ABI と合致していること(旧版パッケージで欠落している場合は arm64 と判定)。- Python のエントリポイントバイナリが存在すること。
導入済み状態のときは更新の案内のみを表示し、自動的に上書き置換されることはありません。
ダウンロードパイプライン
- マニフェストが 8 MiB 以上の場合:まず
bytes=0-0のプローブを行い、4つの独立した接続で互いに重複しない範囲を並列取得。 - 各セグメントごとに HTTP 206 レスポンス、
Content-Range、長さ、バージョンヘッダーを厳密に検証(レスポンス圧縮は無効化)。 - セグメント取得失敗時:全接続をキャンセルし、書き込みスレッドの終了を待機して進捗をゼロリセットした上で、単一接続によるフォールバック取得を1回試行。
- ダウンロード元変更時:100ms ごとに
shouldAbortをポーリングし、不完全な一時ファイルを削除して新ソースから再取得。 - ダウンロード完了後は、まずファイルサイズを照合し、次に SHA-256 ハッシュを照合。
展開パイプライン
rootfs.tmpディレクトリへ展開し、完了後にアトミックにリネーム。- 純粋な Java ストリーミング展開(commons-compress tar + tukaani xz)を使用。busybox tar は ubuntu-base のハードリンク前方参照で破損するため使用しません。
- シンボリックリンクは
Os.symlinkを使用。ハードリンクは実体コピーとして展開し、前方参照は2次パスで補完。 - ディレクトリトラバーサル攻撃や、展開済みシンボリックリンクを跨ぐ不正エントリは拒否。
- tar 内のパーミッションフラグに基づき、実行可能属性(proot および Python 依存バイナリ)を確実に復元。
config/、password.txt、log/を保持。テンプレートやシードは新バージョンのものを配置。.provisionedフラグを書き込み、ディレクトリ名をrootfsへ切り替えて完了。
初回起動が遅い理由
rootfs のサイズ圧縮のため、配布イメージからは __pycache__ が完全に剥離されています。新 rootfs の初回起動時は全モジュールのバイトコードが再生成されるため、WebUI の準備完了までに90秒以上かかる場合があります。この際、ProotHost のログには以下が出力されます:
AzurPilot WebUI not ready within 90000msこれは想定内の警告メッセージです。2回目以降の起動時はキャッシュが効くため通常の速度で起動します。
ビルドマニフェスト
BUILD_MANIFEST は Runtime の身元証明書です:
| フィールド | 用途 |
|---|---|
azurpilot_commit | 内包されている上流のコミットハッシュ |
rootfs_version | <コミット先頭12文字>-<ビルド入力ハッシュ先頭10文字> |
rootfs_arch | ターゲット ABI |
android_host_commit | ビルド時の Android ホスト側コミットハッシュ |
node_version | 内包されている Node.js ツールチェーン |
hot_update_mode=local | 端末内でのフロントエンドセルフビルドに対応している旨を宣言 |
rootfs_version のハッシュ値にはビルドスクリプト、overlay、シード、ツールチェーンの構成が含まれます。ハッシュ値が変更された場合は完全パッケージ更新が必須となり、ハッシュが同一でコミットのみが異なる場合はホットアップデートが可能です。
ホットアップデートトランザクション
/android/update/status を確認
→ 対象コミットを /opt/azurpilot-update/source へシャロークローン
→ uv.lock の差分を比較
├── 差分あり → requiresRuntimeUpdate=true(処理終了)
└── 差分なし → npm ci && npm run build(タイムアウト600秒)
→ トランザクションログ config/android-update-transaction.json を記録
→ インスタンス停止 → 旧 dist 退避 → ソース切り替え → 新 dist 配置 → マニフェスト更新
→ supervisor によるインスタンスの再ロードと復帰障害復旧シナリオ:
- ソース切り替え前の障害:作業ディレクトリを破棄。本番コードと dist は無傷で維持。
- 切り替え中の障害:退避していた旧ソースコード、dist、マニフェストを復元。
- プロセス自体の突然死:
android_host.pyが次回の WebUI 起動時にトランザクションログを読み込んで自動復旧。復旧不能時はログを保持して起動をブロック。
ホットアップデート処理は完全パッケージ更新と共通のパイプラインロックを共有します。
依存ライブラリの自動インストールは無効化されています
未検証の Python ライブラリが本番環境に混入することを防ぐため、ホットアップデート時の Python パッケージインストールは無効化されています。uv.lock に差分がある場合は完全パッケージ更新を行ってください。
オーバーレイレイヤーとシード
| ディレクトリ | 役割 | 制約事項 |
|---|---|---|
rootfs/overlays/ | Android / PRoot 環境固有の互換性パッチ | ユーザー設定には手を触れない |
rootfs/seeds/ | 初回起動時の設定テンプレート | 初回プロビジョニング時のみ配置 |
rootfs/build/ | build-azurpilot.sh 等のビルドスクリプト | 対象 ABI のネイティブ環境下で実行 |
オーバーレイには android_process_compat.py 等の互換レイヤーが含まれており、rootfs/tests/ 配下の単体テストで検証されます:
uv run --with psutil python -m unittest discover -s rootfs/testsストレージ容量とパフォーマンス
| 項目 | 容量規模 |
|---|---|
| rootfs 圧縮アーカイブ | 数百 MB |
| 展開後の実容量 | 1.5〜2.5 GB |
| ホットアップデート作業用領域 | 追加で数百 MB |
フロントエンドの node_modules | 数百 MB(ビルド完了後にクリーンアップ可能) |
端末内でのフロントエンドビルドは CPU、メモリ、ネットワークを消費します。容量不足やビルド失敗時は安全に旧版が維持されます。