Подделка устройства ввода для целей тестирования - PullRequest
0 голосов
/ 05 декабря 2018

Что я хочу сделать

Я пишу демон, который прослушивает устройства ввода для нажатия клавиш и отправляет сигналы через D-Bus.Основная цель - управлять громкостью звука и уровнем подсветки экрана, запрашивая изменения или информируя об изменениях.Я использую libevdev для обработки событий устройства ввода.

Я написал функцию для открытия устройства ввода, расположенного по указанному пути:

Device device_open(const char *path);

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

То, что я уже сделал

Но для его тестирования на реальном устройстве ввода (расположенном в / dev / input / event *) необходимы root права доступа,Настройка доступа на чтение для всех файлов / dev / input / event * работает, но мне кажется рискованным.Выполнение моих тестов с правами root хуже!

Создание устройства с использованием mknod работает, но его необходимо выполнять с правами root.

Я также пытался использовать специальные символьные файлы (поскольку устройства ввода - это одноиз них) разрешить чтение для всех (например, / dev / random, / dev / zero, / dev / null и даже терминальное устройство, которое я сейчас использую: /dev/tty2).

Но эти устройства делаютnot обрабатывает ioctl запросы, необходимые для libevdev: EVIOCGBIT - это первый запрос, возвращающий ошибку «Неправильный ioctl для устройства».

То, что я ищу

Iхотите иметь возможность создавать файлы устройств как обычный пользователь (пользователь, выполняющий модульные тесты).Затем, установив права доступа, я смогу проверить поведение своей функции для разных типов файлов (только чтение, чтение не разрешено, неправильный тип устройства и т. Д.).Если это окажется невозможным, я, безусловно, проведу рефакторинг своей функции с помощью личных помощников.Но как это сделать.Есть примеры?

Спасибо.

Редактировать: Я пытался лучше выразить свои потребности.

1 Ответ

0 голосов
/ 05 декабря 2018

Создайте группу для пользователей, которым разрешен доступ к устройству, и правило udev, чтобы установить право собственности на это устройство ввода событий для этой группы.


Я использую teensy (система)group:

sudo groupadd -r teensy

и добавьте в него каждого пользователя, используя, например,

sudo usermod -a -g teensy my-user-name

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

Управляя какими пользователями и демонами службыпринадлежа к группе teensy, вы можете легко управлять доступом к устройствам.


Для моих микроконтроллеров Teensy (которые имеют собственный USB, и я использую для тестирования HID), у меня есть следующее /lib/udev/rules.d/49-teensy.rules:

ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789B]?", ENV{ID_MM_DEVICE_IGNORE}="1"
ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789A]?", ENV{MTP_NO_PROBE}="1"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789ABCD]?", GROUP:="teensy", MODE:="0660"
KERNEL=="ttyACM*", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789B]?", GROUP:="teensy", MODE:="0660"

Третья строка (SUBSYSTEMS=="usb", one) вам нужна только для устройств HID.Убедитесь, что idVendor и idProduct соответствуют вашему HID-устройству USB.Вы можете использовать lsusb для отображения списка подключенных в данный момент USB-устройств и номеров продуктов.При сопоставлении используются шаблоны глобуса, как и имена файлов.

После добавления вышеупомянутого не забудьте запустить sudo udevadm control --reload-rules && sudo udevadm trigger для перезагрузки правил.При следующем подключении устройства USB HID все члены вашей группы (teensy в приведенном выше) могут получить к нему прямой доступ.


Обратите внимание, что по умолчанию в большинстве дистрибутивов udev также создает постоянные символические ссылки.в /dev/input/by-id/ с использованием типа устройства USB и серийного номера.В моем случае один из моих Teensy LC (серийный 4298820) с комбинированным устройством клавиатура-мышь-джойстик обеспечивает /dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-event-kbd для устройства события клавиатуры, /dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-if01-event-mouse для устройства события мыши и /dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-if03-event-joystick и /dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-if04-event-joystick для устройства события.два интерфейса джойстика.

(Под «постоянным» я не подразумеваю, что эти символические ссылки всегда существуют; я имею в виду, что всякий раз, когда к этому конкретному устройству подключается, символическая ссылка именно этого имени существует, и указывает на фактическоеСимвольное устройство ввода события Linux.)


Устройство Linux uinput может использоваться для реализации виртуального устройства ввода события с использованием простого привилегированного демона.

ПроцессСоздание нового виртуального устройства ввода USB-событий происходит следующим образом.

  1. Открыть /dev/uinput для записи (или чтения и записи):

    fd = open("/dev/uinput", O_RDWR);
    if (fd == -1) {
        fprintf(stderr, "Cannot open /dev/uinput: %s.\n", strerror(errno));
        exit(EXIT_FAILURE);
    }
    

    Вышеуказанное требуетпривилегии суперпользователя.Однако сразу же после открытия устройства вы можете отказаться от всех привилегий и вместо этого запустить свой демон / службу в качестве выделенного пользователя.

  2. Используйте ioctl UI_SET_EVBIT для каждогоразрешен тип события.

    Вы хотите разрешить хотя бы EV_SYNEV_KEY для клавиатур и кнопок мыши, EV_REL для движения мыши и т. д.

    if (ioctl(fd, UI_SET_EVBIT, EV_SYN) == -1 ||
        ioctl(fd, UI_SET_EVBIT, EV_KEY) == -1 ||
        ioctl(fd, UI_SET_EVBIT, EV_REL) == -1) {
        fprintf(stderr, "Uinput event types not allowed: %s.\n", strerror(errno));
        close(fd);
        exit(EXIT_FAILURE);
    }
    

    Лично я использую массив статических констант с кодами для упрощения управления.

  3. Используйте ioctl UI_SET_KEYBIT для каждого кода клавиши, который может выдавать устройство, и ioctl UI_SET_RELBIT для каждого кода относительного движения (кода мыши).Например, чтобы оставить пробел, левая кнопка мыши, горизонтальное и вертикальное движение мыши и колесо мыши:

    if (ioctl(fd, UI_SET_KEYBIT, KEY_SPACE) == -1 ||
        ioctl(fd, UI_SET_KEYBIT, BTN_LEFT) == -1 ||
        ioctl(fd, UI_SET_RELBIT, REL_X) == -1 ||
        ioctl(fd, UI_SET_RELBIT, REL_Y) == -1 ||
        ioctl(fd, UI_SET_RELBIT, REL_WHEEL) == -1) {
        fprintf(stderr, "Uinput event types not allowed: %s.\n", strerror(errno));
        close(fd);
        exit(EXIT_FAILURE);
    }
    

    Опять же, статические массивы const (один для UI_SET_KEYBIT и один для UI_SET_RELBIT кодов)намного проще в обслуживании.

  4. Определите struct uinput_user_dev и запишите его на устройство.

    Если у вас есть name, содержащий строку имени устройства, vendor и product с указанием идентификатора производителя USB и идентификатора продукта, version с номером версии (нормально 0), используйте

    struct uinput_user_dev  dev;
    
    memset(&dev, 0, sizeof dev);
    strncpy(dev.name, name, UINPUT_MAX_NAME_SIZE);
    dev.id.bustype = BUS_USB;
    dev.id.vendor = vendor;
    dev.id.product = product;
    dev.id.version = version;
    
    if (write(fd, &dev, sizeof dev) != sizeof dev) {
        fprintf(stderr, "Cannot write an uinput device description: %s.\n", strerror(errno));
        close(fd);
        exit(EXIT_FAILURE);
    }
    

    В более поздних ядрах ioctl используется для того же действия(очевидно, что участие в разработке systemd приводит к такому «сливному повреждению»);

    struct uinput_setup  dev;
    
    memset(&dev, 0, sizeof dev);
    strncpy(dev.name, name, UINPUT_MAX_NAME_SIZE);
    dev.id.bustype = BUS_USB;
    dev.id.vendor = vendor;
    dev.id.product = product;
    dev.id.version = version;
    
    if (ioctl(fd, UI_DEV_SETUP, &dev) == -1) {
        fprintf(stderr, "Cannot write an uinput device description: %s.\n", strerror(errno));
        close(fd);
        exit(EXIT_FAILURE);
    }
    

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

    Может показаться немного капризным, но это только потому, что я поддерживаю и Unix философия , и KISS принцип (или минималистский подход), и видеть такие бородавки совершенно не нужно.И слишком часто из той же слабо связанной группы разработчиков.Гм.Личное оскорбление не предусмотрено;Я просто думаю, что они плохо выполняют свою работу.

  5. Создайте виртуальное устройство, выполнив UI_DEV_CREATE ioctl:

    if (ioctl(fd, UI_DEV_CREATE) == -1) {
        fprintf(stderr, "Cannot create the virtual uinput device: %s.\n", strerror(errno));
        close(fd);
        exit(EXIT_FAILURE);
    }
    

    На этом этапеядро создаст устройство, предоставит соответствующее событие демону udev, а демон udev создаст узел устройства и символическую ссылку (и) в соответствии с его конфигурацией.Все это займет немного времени - доли секунды в реальном мире, но этого достаточно, чтобы попытка немедленно вызвать события могла привести к потере некоторых из них.

  6. Излучение входных событий (struct input_event) путем записи на устройство uinput.

    Вы можете записывать по одному или нескольким struct input_event с одновременно, и никогда не должны видеть короткие записи (если вы не пытаетесь писатьчастичная структура события).Частичные структуры событий полностью игнорируются.(См. drivers / input / misc / uinput.c: uinput_write () uinput_inject_events () , чтобы узнать, как ядро ​​обрабатывает такие записи.)

    Многие действия состоят из нескольких struct input_event.Например, вы можете перемещать мышь по диагонали (испуская { .type == EV_REL, .code == REL_X, .value = xdelta } и { .type == EV_REL, .code == REL_Y, .value = ydelta } для этого единственного движения).События синхронизации ({ .type == EV_SYN, .code == 0, .value == 0 }) используются в качестве часового или разделителя, обозначая конец связанных событий.

    Из-за этого вам необходимо добавлять входное событие { .type == EV_SYN, .code == 0, .value == 0 } после каждого отдельного действия (движение мыши, нажатие клавиши, отпускание клавиши и т. д.).Думайте об этом как о эквиваленте новой строки для буферизованного ввода.

    Например, следующий код перемещает мышь по диагонали вниз на один пиксель вправо.

    struct input_event  event[3];
    memset(event, 0, sizeof event);
    
    event[0].type  = EV_REL;
    event[0].code  = REL_X;
    event[0].value = +1; /* Right */
    
    event[1].type  = EV_REL;
    event[1].code  = REL_Y;
    event[1].value = +1; /* Down */
    
    event[2].type  = EV_SYN;
    event[2].code  = 0;
    event[2].value = 0;
    
    if (write(fd, event, sizeof event) != sizeof event)
        fprintf(stderr, "Failed to inject mouse movement event.\n");
    

    Ошибкадело не смертельно;это только означает, что события не были введены (хотя я не понимаю, как это могло бы произойти в современных ядрах; на всякий случай лучше защититься).Вы можете просто повторить то же самое снова или проигнорировать сбой (но сообщите об этом пользователю, чтобы он мог провести расследование, если это когда-нибудь произойдет).Поэтому зарегистрируйте его или выведите предупреждение, но не нужно, чтобы демон / служба завершали работу.

  7. Удалите устройство:

    ioctl(fd, UI_DEV_DESTROY);
    close(fd);
    

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

Помещение шагов 1-5 в функциювы получаете что-то вроде

#define  _POSIX_C_SOURCE 200809L
#define  _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/uinput.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>

static const unsigned int  allow_event_type[] = {
    EV_KEY,
    EV_SYN,
    EV_REL,
};
#define  ALLOWED_EVENT_TYPES  (sizeof allow_event_type / sizeof allow_event_type[0])

static const unsigned int  allow_key_code[] = {
    KEY_SPACE,
    BTN_LEFT,
    BTN_MIDDLE,
    BTN_RIGHT,
};
#define  ALLOWED_KEY_CODES  (sizeof allow_key_code / sizeof allow_key_code[0])

static const unsigned int  allow_rel_code[] = {
    REL_X,
    REL_Y,
    REL_WHEEL,
};
#define  ALLOWED_REL_CODES  (sizeof allow_rel_code / sizeof allow_rel_code[0])

static int uinput_open(const char *name, const unsigned int vendor, const unsigned int product, const unsigned int version)
{
    struct uinput_user_dev  dev;
    int                     fd;
    size_t                  i;

    if (!name || strlen(name) < 1 || strlen(name) >= UINPUT_MAX_NAME_SIZE) {
        errno = EINVAL;
        return -1;
    }

    fd = open("/dev/uinput", O_RDWR);
    if (fd == -1)
        return -1;

    memset(&dev, 0, sizeof dev);
    strncpy(dev.name, name, UINPUT_MAX_NAME_SIZE);
    dev.id.bustype = BUS_USB;
    dev.id.vendor  = vendor;
    dev.id.product = product;
    dev.id.version = version;

    do {

        for (i = 0; i < ALLOWED_EVENT_TYPES; i++)
            if (ioctl(fd, UI_SET_EVBIT, allow_event_type[i]) == -1)
                break;
        if (i < ALLOWED_EVENT_TYPES)
            break;

        for (i = 0; i < ALLOWED_KEY_CODES; i++)
            if (ioctl(fd, UI_SET_KEYBIT, allow_key_code[i]) == -1)
                break;
        if (i < ALLOWED_KEY_CODES)
            break;

        for (i = 0; i < ALLOWED_REL_CODES; i++)
            if (ioctl(fd, UI_SET_RELBIT, allow_rel_code[i]) == -1)
                break;
        if (i < ALLOWED_REL_CODES)
            break;

        if (write(fd, &dev, sizeof dev) != sizeof dev)
            break;

        if (ioctl(fd, UI_DEV_CREATE) == -1)
            break;

        /* Success. */
        return fd;

    } while (0);

    /* FAILED: */
    {
        const int saved_errno = errno;
        close(fd);
        errno = saved_errno;
        return -1;
    }
}

static void uinput_close(const int fd)
{
    ioctl(fd, UI_DEV_DESTROY);
    close(fd);
}

, которое, кажется, работает нормально и не требует никаких библиотек (кроме стандартной библиотеки C).

Важно понимать, что подсистема ввода Linux,включая uinput и struct input_event, являются двоичными интерфейсами для ядра Linux и, следовательно, будут обратно совместимыми (за исключением неотложных технических причин, таких как проблемы безопасности или серьезные конфликты с другими частями ядра).(Желание обернуть все под зонтик freedesktop.org или systemd не одно).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...