Your browser is not supported anymore. Please update to a more recent one.


Download Chrome

Download Firefox

Download
Internet Explorer

Download Safari

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

13 сентября 2016

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


Мобильное приложение 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