company_banner

Группировка моделей телефонов Android по контейнерам Docker


    Немного предыстории


    Мобильное приложение Badoo существует для основных «нативных» платформ (Android, iOS и Windows Phone) и для мобильного веба. Несмотря на то, что в разработке мы не используем никаких кроссплатформенных фрэймворков, подавляющая часть бизнес-логики в приложениях схожа, и чтобы не дублировать функциональные тесты для всех платформ, мы пишем кроссплатформенные тесты с помощью Cucumber, Calabash и Appium. Это позволяет нам выносить в общую часть и переиспользовать в тестах для всех платформ код, отвечающий за проверку этой самой бизнес-логики. Различной же остается лишь реализация взаимодействия с приложением (более подробно мы рассказывали об этом здесь).

    Когда кроссплатформенная автоматизация только начиналась (на iOS и Android), было принято решение использовать в качестве серверов Mac Mini. Это позволило сделать каждую из 8 билд-машин универсальной: на ней можно было собирать и запускать функциональные и юнит-тесты как для приложений на iOS, так и на Android. Такое решение устраивало нас практически всем до тех пор, пока количество функциональных тестов не перевалило за пять сотен для каждой платформы, а прогоны не стали требовать все больше времени. Для того чтобы удержать время прогона в разумных границах, мы постоянно работаем над оптимизацией тестов, а также добавляем новые Android-устройства (для iOS мы добавляем симуляторы по-другому). Со временем у нас появились Mac Mini с более чем 8 смартфонами. Важно отметить, что мы подключаем устройства одной модели к одному серверу, чтобы прогоны тестов были консистентны на одном агенте.

    По существу


    У себя в Badoo мы решили перенести тестирование устройств Android на Linux-хосты — необходимое оборудование стоит дешевле, а кроме того, компьютеры Mac Mini, используемые для сборки, часто прерывают USB-подключения к устройствам Android, и те внезапно исчезают во время тестирования. Для управления серверами Linux мы в основном используем контейнеры Docker, поэтому решили попробовать создать контейнер для тестирования реальных устройств Android и клонировать его для каждой модели или группы телефонов, чтобы интегрировать контейнер в существующую конфигурацию серверов.

    Небольшое замечание: одно из преимуществ Linux по сравнению с Mac заключается в том, что Linux — открытая система. Она показала нам, что причина таинственного исчезновения телефонов при тестировании кроется в разрывах соединений, длящихся доли секунды. Мы исправили тесты, добавив в них повторную попытку подключения, что в значительной степени решило проблему.

    По существу: Docker


    Docker — это система, которая содержит в себе методы сборки и распространения конфигураций ПО с инфраструктурой операционной системы, изолирующей каждый программный контейнер от остальных компонентов компьютера. Контейнер имеет собственную файловую систему, адресное пространство и пр. Контейнеры исполняются в одном экземпляре операционной системы, но поскольку система сильнее изолирует процессы, это все работает как набор виртуальных машин.

    Поясняющие диаграммы, опубликованные на сайте Docker:

    На хост-компьютере используется система виртуализации, в которой запущены гостевые экземпляры ОС:


    Контейнеры Docker выполняются на одной ОС:



    По существу: группировка adb/adbd


    Каждый контейнер должен был управлять собственным набором телефонов. Чтобы реализовать это наиболее естественным способом, нужно сопоставить группы разъемов USB разным контейнерам. Устройства, подключенные к разъемам на передней панели компьютера, появляются в каталоге /dev/bus/usb/001, который доступен контейнеру 1; устройства, подключенные к разъемам на задней панели, появляются в каталоге /dev/bus/usb/002, который доступен контейнеру 2. Чтобы увеличить количество подключаемых устройств, была заказана дополнительная плата расширения.
    Все это выглядит неплохо, однако команда adb взаимодействует с телефоном через демон, который использует порт по умолчанию 5037 и работает на уровне всего компьютера. Это означает, что первый контейнер, в котором выполняется команда adb, запускает и демон adb (adbd) — в результате остальные контейнеры, подключаемые к этому демону, видят телефоны первого контейнера. Эту проблему можно было бы решить с помощью сетевых возможностей Docker (каждый контейнер получает собственный IP-адрес, а, следовательно, и собственный набор портов), однако мы воспользовались другим механизмом. Для каждого контейнера было присвоено отдельное значение переменной окружения ANDROID_ADB_SERVER_PORT. Мы выделили порт каждому контейнеру, чтобы он запускал собственный демон adb, который видит только телефоны этого контейнера.

    В процессе разработки мы поняли, что нельзя выполнять команду adb на уровне хоста, не задав переменную ANDROID_ADB_SERVER_PORT, поскольку демон adbd уровня хоста, способный видеть все порты USB, «крадет» телефоны у контейнеров Docker (телефоны могут взаимодействовать только с одним демоном adbd в каждый момент времени).
    Если бы мы использовали только эмуляторы, можно было бы обойтись отдельными процессами adbd, но поскольку мы работаем с реальными устройствами…

    По существу: обновление контейнеров при горячем подключении устройств USB


    Вторая проблема (и главная причина написания этой статьи) заключалась в том, что при перезагрузке телефона во время обычной процедуры сборки он исчезал из файловой системы и списка телефонов контейнера и больше не появлялся!

    Отслеживать добавление и удаление телефонов на хост-компьютере можно по файлам в каталоге /dev/bus/usb, в котором система создает и удаляет файлы, соответствующие телефонам:

    while sleep 3; do
      find /dev/bus/usb > /tmp/a
      diff /tmp/a /tmp/b
      mv /tmp/a /tmp/b
     done
    

    К сожалению, в контейнерах Docker телефоны не только не создаются и не удаляются подобным образом; если настроить создание и удаление узлов, то они на самом деле не взаимодействуют с телефонами!

    Мы решили этот вопрос «в лоб»: поместили контейнеры в режим --privileged и открыли им доступ ко всему каталогу /dev/bus/usb.

    Теперь понадобился другой механизм распределения телефонов по интерфейсным шинам. Я скачал исходный код Android и внес небольшие изменения в файл platform/system/core/adb/usb_linux.cpp

        std::string bus_name = base + "/" + de->d_name;
    
    +    const char* filter = getenv("ADB_DEV_BUS_USB");
    +    if (filter && *filter && strcmp(filter, bus_name.c_str())) continue;
    
        std::unique_ptr<DIR, int(*)(DIR*)> dev_dir(opendir(bus_name.c_str()), closedir);
        if (!dev_dir) continue;
    

    Переменной ADB_DEV_BUS_USB присваивается отдельное значение для каждого контейнера, указывающее на шину, с которой должен работать контейнер.

    Отступление: хотя исправление было совсем несложным, сборку adb пришлось выполнять методом проб и ошибок, поскольку большинство людей включает в сборку все подряд. Мое окончательное решение выглядело так (в чувствительной к регистру файловой системе — я работаю на Mac):

    cd src/android-src
    source build/envsetup.sh
    lunch 6
    vi system/core/adb/usb_linux.cpp
    JAVA_NOT_REQUIRED=true make adb
    out/host/linux-x86/bin/adb
    

    По существу: мультиплексирование портов USB


    Дела шли неплохо, но после установки платы расширения USB мы обнаружили, что на ней только одна шина USB — на компьютере теперь три шины, а у нас пять групп устройств.

    Поскольку я уже влез в код adb, то решил просто добавить еще одну переменную окружения: переменная ADB_VID_PID_FILTER получает список пар идентификаторов vid:pid, и adb игнорирует любые несоответствующие устройства.

    Исправление приведено ниже. При сканировании шины USB для обнаружения подключенных телефонов процессы adbd могут вступить в состояние гонки, однако на практике это не вызывает проблем.

    diff --git a/adb/usb_linux.cpp b/adb/usb_linux.cpp
    index 500898a..92e15ca 100644
    --- a/adb/usb_linux.cpp
    +++ b/adb/usb_linux.cpp
    @@ -115,6 +115,71 @@ static inline bool contains_non_digit(const char* name) {
         return false;
     }
    
    +static int iterate_numbers(const char* list, int* rejects) {
    +  const char* p = list;
    +  char* end;
    +  int count = 0;
    +  while(true) {
    +    long value = strtol(p, &end, 16);
    +//printf("%d, %p ... %p (%c) = %ld (...%s)\n", count, p, end, *end, value, p);
    +    if (p == end) return count;
    +    p = end + 1;
    +    count++;
    +    if (rejects) rejects[count] = value;
    +    if (!*end || !*p) return count;
    +  }
    +}
    +
    +int* compute_reject_filter() {
    +    char* filter = getenv("ADB_VID_PID_FILTER");
    +    if (!filter || !*filter) {
    +        filter = getenv("HOME");
    +        if (filter) {
    +            const char* suffix = "/.android/vidpid.filter";
    +            filter = (char*) malloc(strlen(filter) + strlen(suffix) + 1);
    +            *filter = 0;
    +            strcat(filter, getenv("HOME"));
    +            strcat(filter, suffix);
    +        }
    +    }
    +    if (!filter || !*filter) {
    +        return (int*) calloc(sizeof(int), 1);
    +    }
    +    if (*filter == '.' || *filter == '/') {
    +        FILE *f = fopen(filter, "r");
    +        if (!f) {
    +            if (getenv("ADB_VID_PID_FILTER")) {
    +                // Only report failure for non-default value
    +                printf("Unable to open file '%s'\n", filter);
    +            }
    +            return (int*) calloc(sizeof(int), 1);
    +        }
    +        fseek(f, 0, SEEK_END);
    +        long fsize = ftell(f);
    +        fseek(f, 0, SEEK_SET);  //same as rewind(f);
    +        filter = (char*) malloc(fsize + 1);  // Yes, it's a leak.
    +        fsize = fread(filter, 1, fsize, f);
    +        fclose(f);
    +        filter[fsize] = 0;
    +    }
    +    int count = iterate_numbers(filter, 0);
    +    if (count % 2) printf("WARNING: ADB_VID_PID_FILTER contained %d items\n", count);
    +    int* rejects = (int*)malloc((count + 1) * sizeof(int));
    +    *rejects = count;
    +    iterate_numbers(filter, rejects);
    +    return rejects;
    +}
    +
    +static int* rejects = 0;
    +static bool reject_this_device(int vid, int pid) {
    +    if (!*rejects) return false;
    +    for ( int len = *rejects; len > 0; len -= 2 ) {
    +//printf("%4x:%4x vs %4x:%4x\n", vid, pid, rejects[len - 1], rejects[len]);
    +        if ( vid == rejects[len - 1] && pid == rejects[len] ) return false;
    +    }
    +    return true;
    +}
    +
     static void find_usb_device(const std::string& base,
             void (*register_device_callback)
                     (const char*, const char*, unsigned char, unsigned char, int, int, unsigned))
    @@ -127,6 +192,8 @@ static void find_usb_device(const std::string& base,
             if (contains_non_digit(de->d_name)) continue;
    
             std::string bus_name = base + "/" + de->d_name;
    +        const char* filter = getenv("ADB_DEV_BUS_USB");
    +        if (filter && *filter && strcmp(filter, bus_name.c_str())) continue;
    
             std::unique_ptr<DIR, int(*)(DIR*)> dev_dir(opendir(bus_name.c_str()), closedir);
             if (!dev_dir) continue;
    @@ -176,6 +243,12 @@ static void find_usb_device(const std::string& base,
                 pid = device->idProduct;
                 DBGX("[ %s is V:%04x P:%04x ]\n", dev_name.c_str(), vid, pid);
    
    +            if(reject_this_device(vid, pid)) {
    +                D("usb_config_vid_pid_reject");
    +                unix_close(fd);
    +                continue;
    +            }
    +
                     // should have config descriptor next
                 config = (struct usb_config_descriptor *)bufptr;
                 bufptr += USB_DT_CONFIG_SIZE;
    @@ -574,6 +647,7 @@ static void register_device(const char* dev_name, const char* dev_path,
     static void device_poll_thread(void*) {
         adb_thread_setname("device poll");
         D("Created device thread");
    +    rejects = compute_reject_filter();
         while (true) {
             // TODO: Use inotify.
             find_usb_device("/dev/bus/usb", register_device);
    

    Надеюсь, что мои идеи пригодятся вам, если вы реализуете похожий проект. Оставляйте вопросы в комментариях ниже.

    Tim Baverstock,
    QA automation engineer
    • +23
    • 13.8k
    • 2
    Badoo
    410.32
    Big Dating
    Share post

    Comments 2

      0
      А нельзя взять прошивку с реального телефона, сделать из нее образ ВМ и гонять через QEMU? Ясное дело, что целиком аппаратную часть эмулировать не выйдет, но при некоторых усилиях система останется работоспособной (замена libEGL на софтварную, обход или перекомпиляция падающих pixelflinger и пр.).
        0
        Мы не пробовали, но я думаю, в целом можно, только вот силы и время потраченные на то, чтобы сделать её рабочей могут оказаться несовоставимо велики.
        В качестве эмулятора андроида мы сейчас активно исследуем Genymotion. Прошлый раз пробовали запускать на нём функциональные тесты около полутора лет назад и тогда он показал себя не случшей стороны — было много крашей и не совсем адекватного поведения в целом. Сейчас же ситуация с ним стала гораздо лучше. Поэтому с большой вероятностью мы переведём (существенную) часть прогонов тестов на него.

      Only users with full accounts can post comments. Log in, please.