mcpField
S
SAIR

SAIR

ローカル
sair.run開発ツール12 日前に更新

SAIR について

SAIRはデバイスロック、ADBプロトコルプロキシ、ランナーごとの分離により、CIパイプラインで物理Android端末を共有し、テストの競合を防ぎます。コーディングエージェント向けMCPサーバーでは、実機テストや端末のリースと操作ができます。

主な機能

  1. 01端末ロックと待機ジョブのキューイング
  2. 02CIジョブごとのADBアクセス分離
  3. 03実機でのインストルメンテーションテスト
  4. 04APKのインストールとシェル実行
  5. 05スクリーンショットとlogcatの取得

活用シーン

  1. 01複数のCIジョブ間で端末利用を調整
  2. 02コーディングエージェントで実機テストを実行
  3. 03リースした端末にAPKを入れてアプリを確認
  4. 04スクリーンショットとlogcatでテスト障害を調査

MCP 対応

コーディングエージェント向けのstdio MCPサーバーを提供し、テスト実行、端末一覧とリース、APKインストール、シェル実行、スクリーンショット取得、logcat参照を可能にします。APIキーでオーケストレーターに接続し、エージェント側のマシンにプロキシやadbは不要です。

設定

ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。

インストール方法は未掲載です

このサーバーのパッケージやリモートエンドポイントはまだ取得できていません。プロジェクトの README の手順に従ってください。

リポジトリ
パッケージ
mcpbsair-mcp.mcpb

ツール

ツール一覧はまだ取得されていません

ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。

詳細

SAIR — 共有 Android インストゥルメントランナー

SAIR を使うと、CI パイプラインで物理 Android デバイスを安全に共有できます。 デバイスロック、ADB プロトコルの変換、ランナーごとの分離を提供するため、複数のジョブが同じデバイスを取り合うことはありません。

理由

SAIR がないと、スマートフォンを認識できるすべての CI ジョブが同時に使用します。

CI job 1 ──┐
           ├──▶ adb ──▶ Phone     ✗ both jobs install / uninstall the same APK
CI job 2 ──┘                      ✗ one job's reboot or settings change breaks
                                    the other's tests mid-run
                                  ✗ connectedCheck runs on every attached
                                    phone, including ones another job is using

SAIR があれば、ジョブはデバイスを認識する前にロックを取得する必要があり、待機中のジョブはキューに入ります。

CI job 1 ──┐                 ┌──▶ Phone A (locked by job 1)
           ├──▶ Proxy ───────┤
CI job 2 ──┘      ▲          └──▶ Phone B (locked by job 2)
                  │ locks
CI job 3 ···  Orchestrator: job 3 waits in the queue until
                            a phone is released

GitHub Action

このリポジトリは、sair-acquire、sair-release、sair-wifi、 sair-bluetooth、sair-locks、sair-label を PATH に追加する GitHub Action でもあります。これらのツールを使うにはオーケストレーターのアカウントが必要です。sair.run に登録して API キーを取得し、リポジトリシークレット SAIR_API_KEY に保存します。また、リポジトリ変数 SAIR_PROXY_URL にベンチのプロキシを設定します(実行を参照)。その後、次のようにします。

steps:
  - uses: actions/checkout@v4
  - uses: compscidr/[email protected]
  - run: sair-acquire --count 1 > /dev/null
    env:
      SAIR_PROXY_URL: ${{ vars.SAIR_PROXY_URL }}
      SAIR_API_KEY: ${{ secrets.SAIR_API_KEY }}
  - run: ./gradlew connectedCheck
  - if: always()
    run: sair-release --status ${{ job.status }}
    env:
      SAIR_API_KEY: ${{ secrets.SAIR_API_KEY }}

ワークフローの完全な例で各ステップを説明しています。

独自のプロキシを持たないランナー(たとえば、ほかのアカウントが共有するスマートフォンを使う GitHub ホストランナー)では、Action にプロキシを起動させることができます。Action と同じリリースのプロキシが起動します。SAIR_PROXY_URL は設定せず、プロキシがデフォルトの localhost:8550 で待ち受けるようにします。ログは $RUNNER_TEMP/sair-proxy.log にあります。

  - uses: compscidr/[email protected]
    with:
      proxy: true
    env:
      SAIR_API_KEY: ${{ secrets.SAIR_API_KEY }}

リリースタグではなくブランチまたは SHA を指定すると、Action は警告を表示し、代わりに最新リリースを起動します。

AI エージェント(MCP)

sair-mcp は、コーディングエージェント向けの MCP サーバー(stdio)です。アプリのインストゥルメントテストを実機でワンコールで実行したり、スマートフォンを借りて APK をインストールし、シェルコマンドを実行したり、スクリーンショットを撮ったり、logcat を読み取ったりできます。sair.run で取得した API キーを使って、オーケストレーターのエージェント API と通信します。エージェントのマシンにプロキシや adb は必要ありません。

go install github.com/compscidr/sair/cmd/sair-mcp@latest でインストールするか、リリースの tarball から sair-mcp を取得するか、リリース版の sair-mcp.mcpb を使用します(Claude Desktop などのクライアント向けの MCP バンドルです)。次に、エージェントに追加します。たとえば、Claude Code の場合は次のとおりです。

claude mcp add sair -e SAIR_API_KEY=sair_... -- sair-mcp

または、任意のクライアントの JSON 設定に追加します。

{ "mcpServers": { "sair": { "command": "sair-mcp", "env": { "SAIR_API_KEY": "sair_..." } } } }
ツール説明
run_testsapp_apk と test_apk(エージェントのマシン上のパス)をアップロードし、実行完了を待って、テストごとの結果、インストゥルメントの出力、logcat の末尾を返します
list_devicesキーで使用できるスマートフォンを一覧表示します。シリアル番号、モデル、sdk(API レベル)、共有状態、使用中かどうかを確認できます
lease_deviceスマートフォンを借りて(serial、wait_s は任意)、リース ID を返します
install_apkリース中のスマートフォンに APK をインストールし、そのパッケージを返します
shellコマンドを実行し、出力と終了コードを返します
screenshot画面の PNG を返します
logcat直近の lines(デフォルトは 500 行)を返します
release_deviceリースを終了します。インストールしたものをアンインストールし、スマートフォンを解放します

サーバーは取得したリースにハートビートを送るため、エージェントが呼び出しの合間にしばらく考えていても、スマートフォンを確保できます。リースはプランの最大時間を超えて継続することはありません(free は 15 分、solo は 1 時間、team は 2 時間、enterprise は 4 時間)。別のユーザーと共有しているスマートフォンでは、シェルコマンドは安全なコマンドに制限されます(オーケストレーターの README を参照)。SAIR_API_URL を設定すると、別のオーケストレーターを指定できます(デフォルトは https://api.sair.run)。

公式 MCP レジストリでは io.github.compscidr/sair として登録されています。すべてのリリースで公開されます。リリースワークフローは sair-mcp.mcpb をビルドし、server.json のバージョン、ダウンロード URL、SHA-256 を埋め込み(リポジトリ内の値はプレースホルダーです)、GitHub OIDC でログインした状態で mcp-publisher を使って公開します。次のコマンドで確認できます。 curl "https://registry.modelcontextprotocol.io/v0/servers?search=io.github.compscidr/sair"。

クイックスタート

インストール
curl -fsSL https://raw.githubusercontent.com/compscidr/sair/main/install.sh | bash

OS とアーキテクチャを検出し、最新リリースをダウンロードして、すべてのバイナリを ~/.local/bin にインストールします。

前提条件
  • Android デバイスを接続するマシンに adb がインストールされていること
  • USB デバッグを有効にした Android デバイスが 1 台以上、USB 経由で接続されていること (個人アカウントにはサインインしないでください。テスト環境のセキュリティ確保を参照してください)
実行
# 1. Start a real ADB server on a non-default port (5038), so it doesn't
#    conflict with the proxy which will own the standard port (5037).
adb -P 5038 start-server

# 2. Set your API key (shared by proxy and device-source)
export SAIR_API_KEY=your-api-key

# 3. Start the proxy and device source
sair-proxy &
sair-device-source

プロキシはデフォルトで、ホストされているオーケストレーター orchestrator.sair.run に接続します(TLS を使用)。ローカル開発で接続先を変更するには、ORCHESTRATOR_ADDR を設定してください。

使用方法

デバイスロックを取得し(利用可能になるまで待機します)、テストを実行してから解放します。

ACQUIRE_OUTPUT=$(sair-acquire)
eval "$ACQUIRE_OUTPUT"
./gradlew connectedCheck
sair-release

eval の後、標準の adb はスコープされたポートを介して自動的にプロキシと通信し、ロックされたデバイスのみを認識します。

テスト環境でデバイスソースに WIFI_SSID が設定されている場合、Wi-Fi は自動的に管理されます。

  • 取得時に各端末はベンチの Wi-Fi に接続し、取得処理は端末が オンライン(下記)になってから返ります。そうでなければ失敗します。--no-wifi を指定すると、端末は オフライン の状態で引き渡されます。前回の解放時の状態を信頼するのではなく、同じ方法で確認します。--wifi-band 2.4|5|6(または sair-wifi on --band)を指定すると、その GHz 帯で接続します。端末はスキャンを行い、指定された帯域で検出した WIFI_SSID のうち、最も強い BSSID を候補として固定します。該当するものが見つからない場合、接続は失敗します。接続先の BSSID と周波数は、端末の cmd wifi status で確認できます。
  • 解放時と、ロックの期限切れ時またはキャンセル時には、各端末がリセットされます。adb shell 経由で追加された候補と保存済みネットワークをすべて削除し、無線をいったん切り替えます。その後、端末がオフラインになり、無線が再びオンになるまで待機します。次のジョブが前のジョブのネットワーク設定を引き継ぐことはありません。その後、端末はベンチの Wi-Fi に再接続するため、リース中でない間はオンラインになり、アプリの更新をインストールできます(下記の Play ストアを参照)。次の取得時には、どちらの場合もジョブが指定した状態に設定されます。
  • 解放時にはさらに、デバイスソースが端末を最初に検出した時点で存在しなかったすべてのサードパーティ製パッケージ(テスト APK と .test パッケージを含み、アプリデータとログイン済みアカウントも対象)がアンインストールされ、ブラウザーとメールアプリが強制停止されます。BASELINE_PACKAGES、RESET_FORCE_STOP、RESET_CLEAR_PACKAGES を参照してください。削除されたものはプロキシログとオーケストレーター上のロックログに記録され、パッケージとともに追加された Wi-Fi の候補も記載されます。この処理は Wi-Fi のリセット前に実行されるため、テストアプリがベンチのネットワーク用に残した候補は、端末がオフラインになるのを待つ前に削除されます。
  • 取得時も、Wi-Fi の確認前に同じクリーンアップ(転送設定の解除、パッケージの削除)が実行されます。if: always() を指定した解放ステップがなかった、ランナーが終了した、またはロックの期限切れ時にプロキシが停止していたなどの理由でジョブが解放されなかった場合、次のジョブがクリーンアップを行います。そのロックログには sair: uninstalled … (left by an earlier job) と記録されます。
  • UiAutomation の残留を防止:解放時と取得時に、強制終了された am instrument や残留した uiautomator サーバーによって UiAutomation が登録されたままになることがあります。その場合、後続のすべてのインストゥルメンテーションが UiAutomationService … already registered! で失敗します。uiautomator dump で検出すると、sair はシェルユーザーの app_process/uiautomator プロセスを終了して登録を解除します。それでも確認に失敗した場合のみ再起動します(セキュアロックのある端末は再起動しません)。ロックログには sair: cleared a leaked UiAutomation registration (killed … | rebooted) と記録されます。
  • 画面を点灯してロック解除:取得時と、解放時のリセット後にもう一度、画面を点灯してキーガードを解除し、その状態を確認します(dumpsys power が Awake を示し、dumpsys window policy がキーガード非表示を示すこと)。端末によっては一度だけの解除処理が競合することがあるため、最大 3 回試行します。キーガードが解除されない端末は、取得に screen locked と表示されて失敗します。PIN、パターン、パスワードが設定されている場合、sair では解除できないため、端末の画面ロックを Swipe または None に設定してください。正常に点灯するまで、その理由はデバイスの warning(プロキシの /status、デバイスレポート)にも報告されます。stay_on_while_plugged_in がオフの端末は、ステップの途中でスリープして再ロックされるため、警告として記録されます。wm がない Android 4.x 端末では、端末固有の mShowingLockscreen フィールドを使って確認します。sair が認識できない形式のダンプは一度だけログに記録し、ロック解除済みとして扱います。
  • 稼働時間が REBOOT_UPTIME_HOURS を超えた端末は、解放時に再起動します(デフォルトは 24 時間)。端末が未取得の間に再起動し、次回の取得を進める前に、リセット処理が起動完了と上記の画面点灯を待機します。停止中の端末は提供されません。ロックログには sair: rebooted after release (uptime …) と記録され、プロキシの /status には各端末の last_reboot が表示されます。この処理は Wi-Fi のリセット前に実行されるため、オフラインになるまでの待機が二重に発生することはありません。PIN、パターン、パスワードが設定された端末は、この方法では再起動しません。再起動後に「起動するにはロック解除が必要」という状態になり、人手が必要になるためです。デバイスソースには、その理由と対処法が記録されます。画面点灯と再起動は、ベンチの Wi-Fi が利用できるかどうかにかかわらず実行されます。
  • 解放時に Bluetooth を一度オフにしてからオンにします。Wi-Fi のリセット後に svc bluetooth disable を実行し、dumpsys bluetooth_manager がアダプターの完全な OFF を示すまで待ってから、svc bluetooth enable を実行し、ON を示すまで待機します(それぞれ最大 30 秒)。前のジョブで BLE スタックが不安定になっていた場合(広告の停止やスキャナーが再起動しないなど)、次のジョブが「No peers discovered」の状態を引き継ぐことはありません。無効化だけでは BLE_ON(クラシック Bluetooth はオフでも、「付近のデバイスのスキャン」や BLE を要求したアプリにより BLE が有効な状態)で止まることがよくあります。そのため sair は、この処理中は ble_scan_always_enabled をオフにし、それでも BLE が保持されている場合は、ダンプに BLE アプリとして表示されるパッケージを強制停止します(One UI では com.samsung.android.beaconmanager と com.samsung.android.mdx.kit)。Bluetooth を再びオンにする前に設定を元に戻します。稼働時間超過による再起動後はスキップされます。また、端末の前回のジョブ以降(またはプロキシの起動以降)に解放時のリセットが実行されていない場合は、取得時に実行されます。ロックログには sair: Bluetooth cycled off and on (…) と記録されます。無線が完全にオフにならない、または再びオンにならない場合はロックログに記録され、サイクルが成功するまでデバイスの warning にも表示されます。ただし、ほとんどのジョブは Bluetooth を使用しないため、取得は失敗しません。Bluetooth は必ずオンのままにします。無線が完全にオフにならなかった場合でも、再びオンにします。その場合は Bluetooth back on but never fully off: ... adapter is BLE_ON と記録されます。アダプターのダンプを取得できない端末(Android 4.x)はスキップされます。
  • ジョブの途中で sair-bluetooth restart を実行すると、ジョブが使用する端末で同じサイクルを実行し、アダプターが ON になった時点で処理を返します。これは Bluetooth スタックの再起動からの復旧をテストするスイート向けです(sair#156)。代わりにテストから com.android.bluetooth 自体を強制停止すると、1 分前に sair が実行したサイクルと競合し、古いアダプター状態を取得してしまいます。ロックログには sair: Bluetooth cycled off and on (…) (requested by the job) と記録されます。失敗した場合は呼び出し元に返されますが、ロックは維持されます。端末からは、Wi-Fi と同様にロックのデバイストークンを使って呼び出せます。端末側からの制御を参照してください。
  • リース中は Play ストアの更新を行いません。取得時に Play ストアを無効化し(pm disable-user com.android.vending)、解放時に再び有効化します。これにより、端末がベンチの Wi-Fi(またはモバイルデータ)に再接続したリース間に更新がインストールされます。Google Play services の更新では、それを使用するすべてのアプリが強制停止されます。インストゥルメンテーション対象のアプリも含まれるため、ジョブは Process crashed で失敗します。ロックログには sair: Play Store disabled for the lease と記録されます。無効化できない端末(Xiaomi の場合は Cannot disable system packages)では、その旨がロックログに記録され、ロックは維持されます。Play ストアがない端末と Android 4.x 端末(pm がクラッシュするため)はスキップされます。Play ストア自体が必要なジョブ(課金、アプリ内更新、Play Integrity)は、これらの端末では実行できません。
  • 無線が不安定な場合は再起動します。svc wifi/svc bluetooth の切り替えと無線状態の読み取りには、それぞれタイムアウト(15 秒)が設定されています。Wi-Fi のリセットまたは Bluetooth のサイクルに失敗した場合、デバイスソースは無線のサービス(dumpsys wifi/dumpsys bluetooth_manager)を確認します。それも応答しない場合に限り、端末を再起動し(稼働時間による再起動と同じ規則)、リセットを再試行します。無線が正常なのに svc が停止した端末は再起動しません。ロックログには sair: radio wedged (wifi|bluetooth), rebooted と記録されます。再起動後も無線に問題がある場合は、従来どおりリセットが失敗します。 A ジョブをすでに実行していたベンチでは、端末に前回のジョブのパッケージが残っているため、「初回検出」ではそれらがベースラインに記録されます。pm list instrumentation にテストパッケージとそのターゲットの両方が表示され、どちらもインストールされている場合、これらはベースラインに記録されません。これにより、独自の instrumentation を同梱しているだけのアプリには触れずに、CI の残留物を処理できます。それ以外の場合や、確実にしたい場合は、各端末で pm list packages -3 を一度確認したうえで BASELINE_PACKAGES を明示的に設定してください(サードパーティーパッケージを一切残さない場合は空に設定します)。デバイスソースは、記録したベースラインを名前付きでログに出力するため、アップグレード後の最初のログで不要なエントリーを確認できます。
  • ジョブの実行中、sair-wifi off / sair-wifi on は、ジョブに認証情報を知らせることなくネットワークを切断・復元し、端末がオフラインまたはオンラインになってから戻ります。各端末は個別に切り替わるため、2 台の端末で同時に反対の状態を要求できます。
  • 端末から、ロックのデバイストークンを使って、テストで 2 つのアサーションの間に端末自身の Wi-Fi を切り替えられます。デバイス側の制御を参照してください。

オンラインとは、以下のすべてを満たす状態です。WIFI_SSID 自体に接続していること(SIM や残存ネットワークが代わりに応答していないこと)、その Wi-Fi が Android の検証済みデフォルトネットワークであること(テスト対象アプリが得る判定と同じです)、そして WIFI_PING_HOST が応答することです。デフォルトのホスト名を使う場合、これにより DNS も確認できます。その Wi-Fi 上の VPN は問題ありません。オフラインとは、WIFI_SSID に接続しておらず、WIFI_PING_HOST も応答しない状態です。テスト独自の Wi-Fi Direct グループなど、アップリンクのない別のネットワークに接続していても、オフラインとして扱われます。各待機には上限(WIFI_CONNECT_TIMEOUT_SECONDS、WIFI_DISCONNECT_TIMEOUT_SECONDS)があり、タイムアウト時には、失敗した箇所が示されます。例:"Cafe" に接続されています。"bench" ではありません、接続済みですが VALIDATED ではありません、"bench" にまだ接続されています。モバイルデータは管理対象外です。この定義では、SIM が入っている端末はオフラインになりません。

デバイスソースは、起動後に各端末で試行された Wi-Fi 接続について、失敗理由(not_associated、other_ssid、not_validated、no_route、other)ごとの回数、直近の失敗、オンラインになるまでの最新時間・中央値・最長時間を集計します。プロキシの /status では、これらが端末ごとに wifi として表示されます。また、最長接続時間が WIFI_CONNECT_TIMEOUT_SECONDS に近づいた場合(値を増やしてください)や、失敗の大半で接続が確立されなかった場合(端末または AP を移動してください)には、hint が表示されます。接続に 5 回以上失敗し、失敗率が 10 回に 1 回となった端末についても、警告がログに出力されます。

テストアプリ自身の UID で登録されたベンチネットワークの提案は、sair が削除するものではありませんが、そのアプリは対象になります。解放時にパッケージリセットでアプリがアンインストールされ、Android はそれに伴って提案を削除します。ベースラインに含まれるパッケージ(デバイスソースが初めて検出したときに端末にあったテストアプリ)だけが、ネットワークを削除するたびにベンチ Wi-Fi へ再接続できます。--no-wifi の取得は、その端末で still associated to "bench" となって失敗します。解決するには、アプリをアンインストールするか、BASELINE_PACKAGES から除外してください。

Wi-Fi のリセットでは、テストアプリが自身の UID で登録した提案には触れません。これらは、パッケージのリセットでアプリをアンインストールしたときに削除されます。別のプロキシから中継されたデバイスには触れません。

パスフレーズが adb コマンドラインに渡されることはありません。adbd は端末に提供するすべてのシェルサービスを端末の logcat に記録し、次のジョブから読み取られる可能性があるため、デバイスソースはコマンドを stdin 経由で sh に渡します。スコープ付きポートを通じて cmd wifi add-suggestion を自ら実行するジョブでは、そのコマンドはデバイスソースのログとロックログの両方で伏せられます。 スマートフォンで acquire または release のリセットに失敗すると、ロックログには、そのスマートフォンとロックに関するプロキシおよびデバイスソースの直近のログ行が記録されます(sair: proxy log before the failure と、デバイスソースについても同様です)。これにより、ベンチに SSH 接続しなくても、ポータルから失敗の内容を確認できます。ベンチにあるほかのスマートフォンやロックに関する行は含まれません。プロキシはまた、スマートフォンごとのポータルビューで表示するために、セッション上で小さなイベント(acquire の結果と待ち時間、Wi-Fi 接続時間または失敗原因、削除されたパッケージ、再起動、バージョン、デバイスソースの応答停止)をオーケストレーターに送信します。

アーキテクチャ

         ┌──────────────────────────┐
         │  DeviceSource            │
         │  + Phone                 │
         │  + real adb (port 5038)  │
         └────────────▲─────────────┘
                      │  gRPC
                      ▼
               ┌──────────────┐       ┌──────────────┐
               │    Proxy     │◄gRPC─▶│ Orchestrator │
               │  (port 5037) │       └──────────────┘
               └──────────────┘        (locks & sessions)
                  ▲       ▲
           ADB    │       │  HTTP
        (port 5037│       │(port 8550)
                  │       │
  ----------------+-------+---------------- CI runner --
                  ▼       │
         ┌────────────┐   │   ┌─────────────────┐
         │ adb client │   └───│ sair-acquire /  │
         │ (thinks it │       │ sair-release    │
         │  talks to  │       └─────────────────┘
         │  real adb) │
         └────────────┘

DeviceSource は、USB 接続された Android デバイスがある各マシン上で動作します。実際の adb サーバー(5038 などの標準以外のポートで動作)を介してデバイスを検出し、gRPC 経由でプロキシに登録します。デバイスソースは、必要な台数分のマシンで実行できます。

Proxy は中央ハブです。デバイスソースが登録され、デバイスを検出して、デバイスソース経由でコマンドをルーティングします。ロックとセッションの管理では、gRPC 経由でオーケストレーターと通信します。ポート 5037(標準の ADB ポート)でリッスンするため、CI ランナー上の標準の adb は、実際の ADB サーバーと通信していると認識します。

Orchestrator はデバイスのロック、セッション、調整を管理します。プロキシとは gRPC 経由で通信します。デバイスソースには接続せず、ADB も直接使用しません。ホスト型オーケストレーターは sair.run で利用できます。

Tools(sair-acquire / sair-release)は、プロキシの HTTP API(ポート 8550)を呼び出してデバイスロックを取得および解放する、シンプルな Bash ラッパーです。

ADB クライアント — CI ランナー上の標準の adb は、ポート 5037(標準の ADB ポート)でプロキシに接続します。プロキシは ADB プロトコルのメッセージを gRPC 呼び出しに変換するため、./gradlew connectedCheck などの CI ツールは変更なしで動作します。

設定

デバイスソース
変数デフォルト説明
DEVICE_SOURCE_PORT8080gRPC のリッスンポート
ADB_PORT5038実際の ADB サーバーのポート
DEVICE_SERIALS—ここに指定したシリアル番号(カンマ区切り)のみを管理します。残りのベンチ上のスマートフォンは sair から見えなくなります。設定して空にした場合:なし
EXCLUDE_SERIALS—これらを除く、接続済みのすべてのスマートフォンを管理します。DEVICE_SERIALS とは同時に使用できません
WIFI_SSID—ベンチの Wi-Fi。取得時に接続し、解放時にリセットします。未設定の場合:Wi-Fi は管理されません
WIFI_PASSPHRASE—WPA2 のパスフレーズ(未設定の場合:オープンネットワーク)
WIFI_PING_HOSTdns.googleスマートフォンがオンラインのときは ping に応答し、オフラインのときは応答しない必要があります。ホスト名を指定すると DNS も確認します。DNS を確認しない場合は IP アドレスを設定してください
WIFI_CONNECT_TIMEOUT_SECONDS60Wi-Fi のオン/取得時に、スマートフォンがオンラインになるまで待機する時間
WIFI_DISCONNECT_TIMEOUT_SECONDS20Wi-Fi のオフ/解放/--no-wifi での取得時に、スマートフォンがオフラインになるまで待機する時間
BASELINE_PACKAGES—解放後も保持するサードパーティ製パッケージ(カンマ区切り)。未設定の場合:各スマートフォンを初めて検出したときにパッケージを記録し、インストール済みのテストパッケージとターゲットのペアを除きます。設定して空にした場合:何も保持しません。このバージョンより前から使用しているベンチでは、明示的に設定してください
RESET_FORCE_STOPcom.android.chrome,com.google.android.gm解放時に強制停止するアプリ
RESET_CLEAR_PACKAGES—解放時にデータを消去するアプリ(pm clear)。デフォルトでは無効です。ログイン済みのスマートフォンで Chrome のデータを消去すると、ログアウトされます
REBOOT_UPTIME_HOURS24スマートフォンの起動時間がこの時間に達した場合、次のジョブに割り当てる前に、解放時に再起動します。0 の場合は再起動しません
SAIR_LOG_LEVELinfodebug に設定すると、転送が停滞した場合の追跡用に、すべての転送についてチャンクごとのバイト数を追加で記録します

DEVICE_SERIALS/EXCLUDE_SERIALS によって対象外になったスマートフォンは、 報告、ロック、リセット(Wi-Fi、リバースフォワード、パッケージ)の対象になりません。 そのため、数台のスマートフォンを CI ベンチに接続したまま、ローカル開発に使用できます。 このフィルターが決めるのは sair が管理する対象であり、USB ポートを所有する対象ではありません。 ベンチの sair-adb-server は引き続きすべてのスマートフォンを確保するため、 対象外のスマートフォンと通信するには、このサーバー経由で接続してください (ANDROID_ADB_SERVER_PORT=5038、または ADB_PORT の値)。 デバイスをめぐって競合する別の adb サーバーを起動しないでください。 デバイスソースは、フィルターによって対象外になった接続済みのスマートフォンをログに記録し、 DEVICE_SERIALS に指定されたシリアル番号が接続されていない場合は警告します。

動作を確認します:

grpcurl -plaintext localhost:8080 devicesource.DeviceSource/GetDevices
プロキシ
変数デフォルト説明
ORCHESTRATOR_ADDRorchestrator.sair.run:9090オーケストレーターの gRPC アドレス(ロック管理)
ORCHESTRATOR_TLSfalse(ホステッド環境では自動的に有効)オーケストレーターへの接続に TLS を使用します
SAIR_API_KEYdev-key-123認証用 API キー
SAIR_GUEST_API_KEYS空このプロキシのデバイスをロックできる、他のアカウントの API キーをカンマ区切りで指定します。このプロキシ自身のアカウントに属するすべてのキーと、ゲストとして指定されたアカウントに属する他のすべてのキーは、明示的に指定しなくても受け入れられます。詳しくは以下の「ゲストテナント」をご覧ください。
ADB_PROXY_PORT5037ADB プロトコルのリッスンポート
PROXY_HTTP_PORT8550HTTP API のリッスンポート
PROXY_HTTP_HOST0.0.0.0HTTP API のバインドアドレス
HEARTBEAT_INTERVAL_SECONDS60ロックのハートビート間隔
LOCK_IDLE_TIMEOUT_MINUTES20スコープ付きポートでこの時間 adb クライアントが接続していない場合にロックを解放します(ロックを保持したまま停止したジョブが対象です)。0 で無効になります
PROXY_IDホスト名アカウント内でこのプロキシを識別します。複数のマシンで同じ API キーを共有できます。ロックは、このプロキシが報告したデバイスに対してのみ許可されます。
SAIR_LOG_LEVELinfodebug、info、warn、または error

プロキシは次の 2 つのポートを公開します。

  • 5037(ADB プロトコル) — 標準の adb はここに接続しますが、ロックが取得されるまでデバイスは表示されません
  • 8550(HTTP API) — sair-acquire と sair-release はロックを管理するためにここを呼び出します

プロキシは、バージョン、デバイスの報告、ロックのハートビート、実行中のジョブの ADB ログ(1 秒以内にフラッシュ)を送信するストリームをオーケストレーターに対して 1 つ、長時間開いたままにします。このストリームに対応していない古いオーケストレーターに対しては、自動的に定期的な単項呼び出しに切り替わります。

ロックには、別のプロキシに接続されたデバイスや、別のアカウントから共有されたデバイスを含めることができます。オーケストレーターは 2 つのプロキシ間で ADB 接続を中継するため、ランナー上の adb からはローカルデバイスのように見えます。使用するのはアウトバウンド接続のみなので、NAT 配下のプロキシでも動作します。リモートデバイスの場合に限り、オーケストレーターがデータ経路に含まれます。

ゲストテナント

プロキシ自身のアカウントに登録されているキーはすべて、そのまま使用できます。新たに作成したキーも、招待されたチームメンバーが作成したキーも、設定を変更せずにベンチで使用できます。登録されていないキーについては、プロキシがオーケストレーターにどのアカウントのものか問い合わせ、その回答を30秒間キャッシュします。この確認に対応する前のオーケストレーターでは、SAIR_API_KEY と以下のゲストキーのみが使用できます。

1つのベンチで2つのアカウントに対応できます。よくあるケースとして、スマートフォンが個人アカウントに登録されているマシンで、独自のロック、キュー、ログ、課金を必要とする会社アカウントのCIも実行する場合があります。会社のAPIキーをプロキシのゲストキーとして設定します。

SAIR_API_KEY=owner-key                 # the account the phones belong to
SAIR_GUEST_API_KEYS=company-key,other  # accounts that may lock them here

ゲストキーを持つジョブは、ほかのジョブと同じようにプロキシを使用します(SAIR_API_KEY=company-key とプロキシのURLを指定して sair-acquire を実行します)。プロキシはロック要求にゲストキーを転送するため、スマートフォンはオーナーのプロキシ上に置いたまま、オーケストレーターはゲストアカウントのロックとして記録します。ADBトラフィックがマシンの外に出ることはありません。

信頼モデル:キーの登録はオーナーの同意を意味します。 ゲストキーはプロキシ自身の設定に保存され、編集できるのはそのマシンにアクセスできる人だけです。アカウントのキーをそこに設定することは、「このアカウントはこれらのスマートフォンを使用できる」と示すことになります。そのため、ポータルで共有設定にする必要はありません。デバイスは公開プールに登録されず、ゲストは引き続きロックできます。この2つの仕組みは独立しています。いずれの場合も、キーの所有者はテスト用APKだけでなく、スマートフォンにサインインしている任意のアカウントを使用できます(ベンチのセキュリティ保護を参照)。

プロキシのゲストキーポータルでの共有
ロックできるユーザー登録されたアカウントのみすべてのSAIRアカウント
ロックの実行場所オーナーのプロキシ上(ローカル)オーケストレーターのリレー経由
--count / allこのプロキシ上のすべてのスマートフォン借用者自身のスマートフォンのみ
許可方法プロキシの環境変数を編集ポータルでデバイスごとに切り替え

両方を同時に有効にできます。オーナー、ゲスト、公開プールの借用者は、同じロックドメイン内で同じ物理スマートフォンをロックするため、同時に保持することはできません。また、スマートフォンが空くと、オーナーのジョブがほかのユーザーのジョブより先に処理されます。

ゲストキーは、プロキシのすべてのエンドポイント(/acquire、/release、/wifi、/label、/status)で、オーナーのキーと同様に受け入れられます。登録されたゲストのアカウントにあるほかのキーもすべて受け入れられます。キーを1つ登録すると、そのアカウントに権限が付与されるため、ゲストはプロキシの環境変数を編集せずにキーを追加またはローテーションできます(オーケストレーターがアカウントを特定する際に使用するため、登録したキーは有効な状態を維持する必要があります)。それ以外のキーは拒否されます。 ゲストキーを使用するには、それに対応したオーケストレーターが必要です。古いオーケストレーターはこのフィールドを無視し、ロックをオーナーのものとして記録します。

ツール

GitHub Actions では、uses: compscidr/[email protected](またはそれ以降のリリースタグ)を指定すると、ツールが PATH に追加されます。以下のワークフロー例を参照してください。それ以外の環境では、tools/sair-acquire、tools/sair-release、tools/sair-wifi、tools/sair-bluetooth、tools/sair-locks、tools/sair-label を CI プロジェクトにコピーするか、このリポジトリの tools/ ディレクトリを PATH に追加してください。

デバイスロックを取得します(デバイスが使用可能になるまで待機します)。

eval $(sair-acquire)            # all devices
eval $(sair-acquire --count 1)  # any one free device

1 台のスマートフォンだけを必要とする CI ジョブでは、--count 1 を指定してください。これにより、プール内の残りのデバイスをほかのジョブで利用できます。

eval の後、次の環境変数が設定されます。

変数説明
SAIR_LOCK_IDロック ID(sair-release に渡します)
SAIR_SERIALS取得したデバイスのシリアル番号をカンマ区切りで並べたリスト
ANDROID_ADB_SERVER_PORTスコープ付き ADB ポート — 標準の adb が自動的に読み取ります
ANDROID_HOME / ANDROID_SDK_ROOT$RUNNER_TEMP(または $TMPDIR)配下のシャドウ SDK。ここにある platform-tools/adb は、実際の adb を -P <scoped port> 付きで呼び出します。SDK がすでに設定されている場合のみ設定されます。sair-release 後のステップでも引き続き動作するよう、そのまま残されます
ANDROID_SERIAL最初のシリアル番号(デバイスを 1 台だけ取得した場合のみ設定されます)
SAIR_PROXY_URLプロキシ URL(sair-release 用)
SAIR_DEVICE_TOKEN / SAIR_DEVICE_API_PORTロックにスコープされたトークンと、各スマートフォン上でプロキシの /wifi および /bluetooth に接続するポート。プロキシがこれらを設定した場合のみ設定されます。デバイス側の制御を参照してください

完了したら、ロックを解放します。

sair-release

よく使うオプション:

# Acquire specific device(s)
eval $(sair-acquire --serial DEVICE_A)
eval $(sair-acquire --serial DEVICE_A,DEVICE_B)

# Acquire any N free devices (mutually exclusive with --serial)
eval $(sair-acquire --count 2)

# Take every device from one proxy, for phones that must sit together (BLE,
# Wi-Fi Direct, adb reverse); waits until one proxy has enough free
eval $(sair-acquire --count 2 --same-proxy)

# Queue behind every default-priority job while devices are busy (higher
# goes first, ties are FIFO, default 0) — e.g. so bot PRs don't hold up humans
eval $(sair-acquire --priority -1)

# Phones verified offline (default: joined to the bench Wi-Fi and online)
eval $(sair-acquire --no-wifi)

# Join the bench Wi-Fi on one band of a dual/tri-band AP (2.4, 5 or 6 GHz)
eval $(sair-acquire --wifi-band 2.4)

# Tell matrix legs of one job apart (default: $SAIR_STAGE), and label the
# lease or one of its devices for the job views
eval $(sair-acquire --stage api-34 --label suite=smoke --label DEVICE_A:role=group-owner)

# Labels only known once the serials are granted; a key labeled again takes
# the new value (needs a proxy with /label)
sair-label DEVICE_A:role=group-owner DEVICE_B:role=client

# Drop and restore the network mid-job (all locked devices, or --serial)
sair-wifi off
sair-wifi on
sair-wifi on --band 5

# Restart the Bluetooth stack mid-job, sair's own off-then-on cycle; returns
# once the adapter is back on (all locked devices, or --serial)
sair-bluetooth restart

# Which release the tools (or a bench's binaries) came from
sair-acquire --version
sair-proxy --version   # also in the proxy's GET /status as "version"

# Point to a remote proxy
eval $(sair-acquire --url http://proxy-host:8550 --api-key my-key)

# Release with explicit lock ID
sair-release --lock-id <lock-id>

# Every lock your account holds (all proxies, all keys) and the ones that
# ended in the last hour, with how; asks the orchestrator, not the proxy
# (SAIR_ORCHESTRATOR_URL, default https://orchestrator.sair.run)
sair-locks
# Release every lock this key holds, e.g. after a crashed agent or killed job
sair-locks --release-mine
デバイス側の制御

ネットワークが必要になるタイミングをテストが適切に判断できる場合があります。あるテストではインターネット接続が必要で、次のテストでは不要であり、スマートフォン上の 2 つのアサーションの間で切り替える必要があります。テナント API キーをスマートフォンに渡してはいけません(このキーを使うと、すべてのジョブで取得/解放が可能になり、次のジョブが残された情報を読み取れるためです)。そのため、ロックごとに有効期間の短いトークンを発行します。

取得時に、プロキシはランダムなデバイストークンを発行します。このトークンは POST /wifi と POST /bluetooth にのみ使用でき、そのロック内のシリアル番号に対してのみ有効で、ロックが終了するまで有効です。また、各ローカルスマートフォンで adb reverse tcp:<port> tcp:<port> を実行します。ここで <port> はプロキシの HTTP API ポートです。これにより、スマートフォン上の 127.0.0.1:<port> からプロキシに接続できます(これには、デバイスソースのホスト上でプロキシの HTTP API が稼働している必要があります。これは通常のベンチ構成です)。sair-acquire はこれらを SAIR_DEVICE_TOKEN と SAIR_DEVICE_API_PORT としてエクスポートします。シリアル番号とともに、インストルメンテーションに渡してください。

./gradlew connectedCheck \
  -Pandroid.testInstrumentationRunnerArguments.sairDeviceToken="$SAIR_DEVICE_TOKEN" \
  -Pandroid.testInstrumentationRunnerArguments.sairDeviceApiPort="$SAIR_DEVICE_API_PORT" \
  -Pandroid.testInstrumentationRunnerArguments.sairLockId="$SAIR_LOCK_ID" \
  -Pandroid.testInstrumentationRunnerArguments.sairSerial="$ANDROID_SERIAL"

スマートフォン上のテストは、ライブラリを使わず、通常の HTTP 呼び出しを 1 回行います。on はスマートフォンがインターネットに接続されると応答するため、呼び出しが完了したらテストを続行できます。off はネットワークを切断します。

fun sairWifi(on: Boolean) {
    val args = InstrumentationRegistry.getArguments()
    val url = "http://127.0.0.1:${args.getString("sairDeviceApiPort")}/wifi" +
        "?lock_id=${args.getString("sairLockId")}" +
        "&serial=${Uri.encode(args.getString("sairSerial"))}" +
        "&state=${if (on) "on" else "off"}"
    val conn = URL(url).openConnection() as HttpURLConnection
    conn.requestMethod = "POST"
    conn.setRequestProperty("x-device-token", args.getString("sairDeviceToken"))
    conn.readTimeout = 90_000 // "on" waits for Internet, up to the bench's timeout
    check(conn.responseCode == 200) { "sair wifi: HTTP ${conn.responseCode}" }
}

同じヘッダーを付けて POST /bluetooth?lock_id=…&serial=…&action=restart を呼び出すと、sair が解放時に行うのと同じ方法で Bluetooth スタックを再起動し、アダプターが再び ON になると応答します(sair#156)。復旧テストでは、com.android.bluetooth を自分で強制停止する代わりに、この呼び出しを使用します。

これは 127.0.0.1 への通常の HTTP 通信であるため、テスト対象のアプリで平文通信を許可する必要があります。targetSdk 28 以降では、プラットフォームのデフォルト設定により、Cleartext HTTP traffic to 127.0.0.1 not permitted というエラーが発生します。デバッグビルドのネットワークセキュリティ設定で許可するか、ポリシーの対象外となる生の Socket にリクエスト行を書き込んでください。

1 つのロックで複数のスマートフォンを使用する場合は、それぞれに対応するシリアル番号を渡してください(SAIR_SERIALS に一覧があります)。デバイストークンを使用する場合はシリアル番号が必要です。プロキシは、リバース転送された接続がどのスマートフォンから来たのかを判別できないためです。別のプロキシ経由で中継されたデバイスには、トークンアクセスが許可されません。その Wi-Fi は一切管理されません。転送はロックとともに削除されます。これは、ジョブが残したほかのすべての adb reverse と同様です。

ベンチのセキュリティ保護

ロックを取得したジョブは、スマートフォンに対する完全な adb shell とインストルメンテーションアクセスを持ちます。UI 自動化を通じて、これだけで、そのスマートフォンにサインインしているあらゆるアプリ(メール、ブラウザーのサインイン済みセッション、Play ストア)を利用できます。正規の UiAutomator テストでも同じ操作ができるため、プロキシでこれを確実にフィルタリングすることはできません。**ベンチのスマートフォンにサインインしているアカウントは、そのスマートフォンのロックを取得できるすべての人が利用できます。**アカウント上のすべてのキー(あなた自身のキーと招待したチームのキー)、SAIR_GUEST_API_KEYS に含まれるすべてのキー、そしてスマートフォンをポータルで共有している場合はすべての SAIR アカウントが対象です。

  • スマートフォンにアカウントは本当に必要ですか? adb によるインストール、インストルメンテーション、BLE、Wi-Fi Direct、Nearby Connections は、いずれもアカウントなしで動作します。サインインする一般的な理由は、Play ストアからのインストールと Google Sign-In のテストです。これらが不要であれば、スマートフォンはサインアウトしたままにしてください。
  • 必要な場合は、個人のアカウントではなく、ベンチ専用のアカウントを使用してください。 お支払い方法を登録せず、Play での購入は常に認証を求める設定にし、実際のメールや連絡先は使わず、アカウントの復元先にはエイリアスを指定してください。スマートフォンのロックを取得できる人は、そのアカウントを利用できるものと想定してください。
  • Android のセカンダリユーザーや仕事用プロファイルは、隔離の境界にはなりません。 シェルユーザーはユーザー間で操作できるため、アカウントを隠す目的で利用しないでください。
  • ゲストキーとポータルでの共有によって渡されるのは、テスト APK だけではなく、スマートフォン全体です。 利用者は、サインイン済みのあらゆるアカウントを使えます。

デプロイ

本番環境では、SAIR コンポーネントを systemd サービスまたは Docker コンテナとして実行してください。

Systemd サービス

バイナリと systemd ユニットをインストールします。

curl -fsSL https://raw.githubusercontent.com/compscidr/sair/main/install.sh | bash -s -- --systemd

デバイスソースマシン — /etc/sair/device-source.env を編集してから、次を実行します。

sudo systemctl enable --now sair-adb-server sair-device-source

これにより、実際の ADB サーバーがポート 5038 で起動し、デバイスソースとともにブート時に開始されます。

プロキシマシン — /etc/sair/proxy.env(ORCHESTRATOR_ADDR、SAIR_API_KEY などを設定)を編集してから、次を実行します。

sudo systemctl enable --now sair-proxy

ステータスとログを確認します。

sudo systemctl status sair-device-source
sudo journalctl -u sair-proxy -f

プロキシまたはデバイスソースが fatal error: removed GODEBUG ... set to old value というエラーでループ再起動する場合、ホスト OS が古い GODEBUG をすべてのサービスにエクスポートしています(UGOS は tlskyber=0 を設定します)。同梱のユニットは UnsetEnvironment=GODEBUG でこれをクリアします(systemd ≥ 235)。ユニットを再インストールするか、ドロップインにその行を追加してください(sudo systemctl edit sair-proxy)。

Docker コンテナ

リリースごとに、ビルド済みイメージが GitHub Container Registry に公開されます。

ghcr.io/compscidr/sair-device-source:latest
ghcr.io/compscidr/sair-proxy:latest

デバイスソースマシン — 実際の ADB サーバーは、コンテナ内ではなくホスト上で実行する必要があります。ブート時に起動するには systemd ユニットを使用するか、手動で起動します。

# Option A: Install the ADB systemd service (starts on boot)
curl -fsSL https://raw.githubusercontent.com/compscidr/sair/main/install.sh | bash -s -- \
  --systemd-adb-only

# Option B: Start manually
adb -P 5038 start-server

デバイスソースコンテナからホストの ADB サーバーに接続できる必要があります。

docker run -d --name sair-device-source \
  --network host \
  -e ADB_PORT=5038 \
  ghcr.io/compscidr/sair-device-source:latest

--network host が最も簡単な方法です。これにより、コンテナから localhost:5038 のホスト ADB サーバーに接続でき、gRPC ポートも直接公開されます。

プロキシマシン:

docker run -d --name sair-proxy \
  -p 5037:5037 \
  -p 8550:8550 \
  -e ORCHESTRATOR_ADDR=your-orchestrator:9090 \
  -e SAIR_API_KEY=your-api-key \
  ghcr.io/compscidr/sair-proxy:latest

特定のバージョンに固定するには、latest をリリースタグ(例:v0.1.0)に置き換えてください。

例:GitHub Actions ワークフロー

name: Android Tests

on: [push, pull_request]

jobs:
  connected-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21

      # Puts sair-acquire and sair-release on PATH, pinned to a release tag
      # (the action ships from v0.0.21 on). Bump the tag to upgrade; the
      # release notes list breaking changes.
      - uses: compscidr/[email protected]

      - name: Acquire device
        env:
          SAIR_PROXY_URL: ${{ vars.SAIR_PROXY_URL }}
          SAIR_API_KEY: ${{ secrets.SAIR_API_KEY }}
        run: |
          # Also appends every variable to $GITHUB_ENV for the later steps
          sair-acquire --count 1 > /dev/null

      - name: Run connected tests
        run: ./gradlew connectedCheck

      - name: Release device
        if: always()
        env:
          SAIR_API_KEY: ${{ secrets.SAIR_API_KEY }}
        run: sair-release --status ${{ job.status }}

ワークフローの主なポイント:

  • sair-acquire はデバイスが使用可能になるまで待機するため、すべてのデバイスが使用中の場合、ジョブは自然にキューに入ります。--priority N はそのキューの順序を指定します(優先度の高いものが先で、同じ値の中では FIFO、デフォルトは 0)。たとえば、--priority ${{ github.actor == 'dependabot[bot]' && -1 || 0 }} を渡すと、ボットの PR は人間による PR の後に処理されます。
  • --count 1 はプール全体ではなく、空いているデバイスを 1 台だけロックするため、ほかのデバイスを並行ジョブに残せます。また、ANDROID_SERIAL も設定されるため、Gradle はそのデバイスをターゲットにします。
  • ANDROID_ADB_SERVER_PORT は、標準の adb にプロキシのスコープ付きポートへ接続するよう指示します。このポートからは、ロックされたデバイスだけが公開されます。
  • ANDROID_HOME は、スコープ付きポートが組み込まれた adb を含むシャドウ SDK を指すように変更されます。AGP 9.4+ は、空の環境で起動する Gradle ワーカーデーモン内でインストルメンテーションテストを実行するため、ANDROID_ADB_SERVER_PORT は、デーモンがシェルから呼び出す adb に渡されません。この経路でも動作するよう、シムが用意されています。sair-acquire の実行時に ANDROID_HOME または ANDROID_SDK_ROOT が設定されている必要があり、設定されていない場合は標準エラーに警告が出力されます。
  • GitHub Actions では(GITHUB_ENV が設定されている場合)、sair-acquire がエクスポートするすべての変数を生の値のまま $GITHUB_ENV に追記するため、後続のステップで eval 出力を解析しなくても変数を利用できます。
  • ロックはテストに失敗した場合でも解放されるよう、sair-release を if: always() ステップにします。--status ${{ job.status }} を指定すると、ダッシュボード上のジョブに結果(success、failure、cancelled)が記録されます。指定しない場合、解放済みのジョブは unknown と表示され、解放されずに期限切れになったロックは timeout と表示されます。
  • ロック中、プロキシはスコープ付きポートで行われた ADB リクエストをすべて記録します(サービス文字列、発生時刻、デバイスセッションの継続時間、返されたバイト数)。記録は各ハートビート時と解放時にオーケストレーターへ送信されるため、タイムアウトしたジョブでも、ジョブページでデバイス上で実際に実行された内容を確認できます。通常のポートでは何も記録されません。
  • GitHub Actions では、sair-acquire はリポジトリ、実行へのリンク、ジョブ ID、実行試行、コミット、ブランチ、ワークフロー(GITHUB_REPOSITORY、GITHUB_RUN_ID、GITHUB_JOB、GITHUB_RUN_ATTEMPT、GITHUB_SHA、GITHUB_HEAD_REF または GITHUB_REF_NAME、GITHUB_WORKFLOW から取得)も送信し、ダッシュボードにはジョブごとにこれらが表示されます。それ以外の環境では、--repo、--run-url、--job、--run-attempt、--sha、--branch、--workflow を渡してください。GITHUB_JOB はマトリックスジョブのすべてのレッグで同じなので、レッグを区別するには SAIR_STAGE を設定するか(例:SAIR_STAGE: api-${{ matrix.api }})、--stage を渡してください。これらのフィールドに対応していない古いプロキシは、渡された値を無視します。
  • Actions 以外では、取得に失敗した場合にステップも失敗するよう、eval $(sair-acquire) の代わりに ACQUIRE_OUTPUT=$(sair-acquire) を使用し、成功した場合に出力を eval してください。

複数マシンの例

複数のマシンにデバイスを分散した、一般的な本番環境の構成:

┌──────────────────────────┐   ┌──────────────────────────┐
│  Machine A               │   │  Machine B               │
│  DeviceSource            │   │  DeviceSource            │
│  + Phone A, Phone B      │   │  + Phone C               │
│  + real adb (port 5038)  │   │  + real adb (port 5038)  │
└────────────▲─────────────┘   └────────────▲─────────────┘
             │  gRPC                        │  gRPC
             └──────────┬───────────────────┘
                        ▼
                 ┌──────────────┐       ┌──────────────┐
                 │    Proxy     │◄gRPC─▶│ Orchestrator │
                 │  (port 5037) │       └──────────────┘
                 └──────────────┘        (locks & sessions)
                    ▲       ▲
             ADB    │       │  HTTP
          (port 5037│       │(port 8550)
                    │       │
  ------------------+-------+-------------- CI runner --
                    ▼       │
           ┌────────────┐   │   ┌─────────────────┐
           │ adb client │   └───│ sair-acquire /  │
           │ (thinks it │       │ sair-release    │
           │  talks to  │       └─────────────────┘
           │  real adb) │
           └────────────┘

CI から見ると、3 台のデバイスは単一のプールとして扱われます。sair-acquire を呼び出すと、デバイスが物理的にどのマシンに接続されているかにかかわらず、空いているデバイスがロックされます。

ソースからのビルド

Go 1.24+ が必要です。

go build ./cmd/sair-device-source
go build ./cmd/sair-proxy

または、Docker を使用します:

# Device source image
docker build --target device-source -t sair-device-source .

# Proxy image
docker build --target proxy -t sair-proxy .

ライセンス

詳細については、LICENSE を参照してください。

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。