PlaywrightによるADBで複数端末を管理する場合に起きる特有の問題について

Playwrightで device.shell()
でコマンドを投げる際にエラーが起きることがある。
3台程度であれば問題起きにくいのだが、12台同時に動かしたところエラーが頻発するようになった。これはUSBへのアクセスが集中して処理が追いつかないことが原因であると推測される。
解決のために考えなければならないことをいくつか書いておく。
なお、これはあくまでメモ書きであり、完全に解決できているかはまだ答えが出ていない。

リトライ処理を含んだ関数経由でshellコマンドを発出すべき
device.shell()
だけで実行すると問題が起きた時に例外エラー扱いになってしまう。リトライ処理を自作して、その関数経由でコマンドを発出するようにした。
async function adbShellWithRetry(command: string) {
let retryCount = 0;
const retryLimit = 8;
while (retryCount < retryLimit) {
try {
const result = await device.shell(command);
return result;
} catch (e) {
retryCount++;
console.log(`【ADB: ${device.serial()}】${retryCount}回目のADBリトライ`);
await new Promise((resolve) => setTimeout(resolve, 1000 * 8)); // リトライ間隔を待つ
}
}
throw new Error("【ADBエラー】ADBコマンド発出の上限を越えました。");
}
8秒間隔でリトライを行う。
8回失敗したら例外エラー扱いにして処理を終了する。(何らかの問題が起きていることが考えられるため、成功するまでとはしなかった。)

スクレイピング失敗する前提で再帰処理を必ず含める
スクレイピング中に失敗することも考慮しなければならない。
例えばデータ取得も失敗する可能性がある。
とにかくリトライ。
リトライ。リトライ。リトライ。リトライ。
だるすぎである。
仮にこの問題から解放されたいならば、
パソコン1台あたり5台接続程度にとどめる。パソコンも大量に必要になるだろう。であればパソコンだけでスクレイピングすればいいだけの話になるので本末転倒である。
処理結果
こんなことになっていた。
offlineになった場合には .shell()
で例外エラーがキャッチできる。
このタイミングで再接続できる方法を見つけられれば解決できそうだ。
あと、ADBサーバー接続不安定によるエラーは今回起きなかった模様。
USBセレクティブサスペンドというものを無効にしてみて再試行する。
USBハブ側の問題の可能性が浮上
USBポートから接続が切れた場合、充電による動きは無い。
USBハブ側から接続が切れた場合、充電による画面の変化が発生する。つまり、USBハブ側に問題がある可能性が浮上した。
電源供給が不安定の可能性があるかもしれない。
この辺りを中心に調査を進める。

結局だめだった。
根本的な解決はできず敗北した。
無理やりタスクを完了させるような作り方をしていこうと思う。

強制的に再実行を行うような仕組みを考える。
まずは手動でやり直しを行うときには下記の流れになる。
-
adb kill-server
でADBサーバーを停止する。 - Playwrightで起動したChromiumプロセスが残っていないかチェックする。残っていればキルしておく。
-
adb start-server
でADBサーバーを再度立ち上げる。 -
adb devices
でデバイス接続状況をチェックする。device
というステータスになっていることを確認する。仮に12台接続しているならば、12台全てステータスが正常であることを確認しておく。 - Androidデバイスを再実行可能な状況に戻すため、Chromeを終了し、スクリーンを消灯する。
- プログラムの再実行を行う。
この流れが自動的に行われるようにすればOK。

ADBサーバーを起動する
import { exec } from "child_process";
function adbStartServer() {
return new Promise((resolve) => {
exec("adb start-server", () => {
resolve(true);
});
});
}
ADBサーバーを停止する
import { exec } from "child_process";
function adbKillServer() {
return new Promise((resolve) => {
exec("adb kill-server", () => {
resolve(true);
});
});
}
adb devicesで正常接続された端末のシリアル番号を配列で返す
import { exec } from "child_process";
const sleep = (ms: number) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
function getAdbDevices(): Promise<string[]> {
return new Promise(async (resolve, reject) => {
await adbStartServer();
await sleep(1000 * 5);
exec("adb devices", (error, stdout, stderr) => {
const regex = /^([A-Z0-9]+)\s+device$/gm;
const matches = stdout.matchAll(regex);
// シリアル番号だけを配列に格納
const serials = Array.from(matches, (match) => match[1]);
resolve(serials);
});
});
}
上記の関数を使って端末全ての接続を保証しつつADBサーバー立ち上げ
import { exec } from "child_process";
// ここに物理的な端末数を書いておく
// 本当は getAdbDevices でカウントするといいんだけどね...。
const ADB_DEVICE_LENGTH = 12;
export async function launchAdbServer() {
const physicalDevices = Number(ADB_DEVICE_LENGTH);
return new Promise(async (resolve) => {
while (true) {
const adbDevices = await getAdbDevices();
if (physicalDevices === adbDevices.length) break;
await adbKillServer();
await sleep(1000 * 5);
}
console.log("ADBサーバーを起動して正常にデバイス接続が完了したことを確認しました。");
resolve(true);
});
}