Closed6

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

masa5714masa5714

Playwrightで device.shell() でコマンドを投げる際にエラーが起きることがある。

3台程度であれば問題起きにくいのだが、12台同時に動かしたところエラーが頻発するようになった。これはUSBへのアクセスが集中して処理が追いつかないことが原因であると推測される。

解決のために考えなければならないことをいくつか書いておく。
なお、これはあくまでメモ書きであり、完全に解決できているかはまだ答えが出ていない

masa5714masa5714

リトライ処理を含んだ関数経由で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回失敗したら例外エラー扱いにして処理を終了する。(何らかの問題が起きていることが考えられるため、成功するまでとはしなかった。)

masa5714masa5714

スクレイピング失敗する前提で再帰処理を必ず含める

スクレイピング中に失敗することも考慮しなければならない。
例えばデータ取得も失敗する可能性がある。

とにかくリトライ。
リトライ。リトライ。リトライ。リトライ。

だるすぎである。

仮にこの問題から解放されたいならば、
パソコン1台あたり5台接続程度にとどめる。パソコンも大量に必要になるだろう。であればパソコンだけでスクレイピングすればいいだけの話になるので本末転倒である。

処理結果

こんなことになっていた。
offlineになった場合には .shell() で例外エラーがキャッチできる。
このタイミングで再接続できる方法を見つけられれば解決できそうだ。

あと、ADBサーバー接続不安定によるエラーは今回起きなかった模様。

USBセレクティブサスペンドというものを無効にしてみて再試行する。

USBハブ側の問題の可能性が浮上

USBポートから接続が切れた場合、充電による動きは無い。
USBハブ側から接続が切れた場合、充電による画面の変化が発生する。つまり、USBハブ側に問題がある可能性が浮上した。

電源供給が不安定の可能性があるかもしれない。
この辺りを中心に調査を進める。

masa5714masa5714

結局だめだった。

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

masa5714masa5714

強制的に再実行を行うような仕組みを考える。

まずは手動でやり直しを行うときには下記の流れになる。

  1. adb kill-server でADBサーバーを停止する。
  2. Playwrightで起動したChromiumプロセスが残っていないかチェックする。残っていればキルしておく。
  3. adb start-server でADBサーバーを再度立ち上げる。
  4. adb devices でデバイス接続状況をチェックする。device というステータスになっていることを確認する。仮に12台接続しているならば、12台全てステータスが正常であることを確認しておく。
  5. Androidデバイスを再実行可能な状況に戻すため、Chromeを終了し、スクリーンを消灯する。
  6. プログラムの再実行を行う。

この流れが自動的に行われるようにすればOK。

masa5714masa5714

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);
  });
}
このスクラップは2025/03/03にクローズされました