Подделка устройства ввода для целей тестирования
Что я хочу сделать
Я пишу демон, который слушает устройства ввода для нажатия клавиш и отправляет сигналы через 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).
Но эти устройства не обрабатывает ioctl
запросы, необходимые для libevdev: EVIOCGBIT является первым запросом, возвращающим ошибку "Неправильный ioctl для устройства".
Что я ищу
Я хочу иметь возможность создавать файлы устройств как обычный пользователь (пользователь, выполняющий модульные тесты). Затем, установив права доступа, я смогу проверить поведение своей функции для разных типов файлов (только чтение, чтение не разрешено, неправильный тип устройства и т. Д.). Если это окажется невозможным, я, безусловно, проведу рефакторинг своей функции с помощью личных помощников. Но как это сделать. Есть примеры?
Благодарю.
Изменить: я пытался лучше выразить свои потребности.
1 ответ
Создайте группу для пользователей, которым разрешен доступ к устройству, и правило udev, чтобы установить право собственности на это устройство ввода событий для этой группы.
я использую teensy
(системная) группа:
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",
1) для устройств HID. Убедитесь, что idVendor
а также idProduct
соответствует вашему USB HID устройству. Ты можешь использовать 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 device может использоваться для реализации виртуального устройства ввода событий с использованием простого привилегированного демона.
Процесс создания нового виртуального устройства ввода USB-событий происходит следующим образом.
открыто
/dev/uinput
для письма (или чтения и письма):fd = open("/dev/uinput", O_RDWR); if (fd == -1) { fprintf(stderr, "Cannot open /dev/uinput: %s.\n", strerror(errno)); exit(EXIT_FAILURE); }
Вышесказанное требует привилегий суперпользователя. Однако сразу после открытия устройства вы можете отказаться от всех привилегий, и вместо этого ваш демон / служба будет работать как выделенный пользователь.
Использовать
UI_SET_EVBIT
ioctl для каждого типа событий допускается.Вы хотите разрешить хотя бы
EV_SYN
; а такжеEV_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); }
Я лично использую статический константный массив с кодами для более легкого управления.
Использовать
UI_SET_KEYBIT
ioctl для каждого кода ключа, который может выдавать устройство, иUI_SET_RELBIT
ioctl для каждого кода относительного движения (код мыши). Например, чтобы оставить пробел, левую кнопку мыши, горизонтальное и вертикальное движение мыши и колесо мыши: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); }
Опять же, статические константные массивы (один для
UI_SET_KEYBIT
и один дляUI_SET_RELBIT
коды) гораздо проще в обслуживании.Определить
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 (или минималистский подход) и считаю такие бородавки совершенно ненужными. И слишком часто из той же слабо связанной группы разработчиков. Гм. Личное оскорбление не предусмотрено; Я просто думаю, что они делают плохую работу.
Создайте виртуальное устройство, выполнив
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 создаст узел устройства и символическую ссылку (ссылки) в соответствии с его конфигурацией. Все это займет немного времени - доли секунды в реальном мире, но этого достаточно, чтобы попытка немедленно вызвать события могла привести к потере некоторых из них.
Испускать входные события (
struct input_event
) записав на входное устройство.Вы можете написать один или несколько
struct input_event
s за один раз, и никогда не должны видеть короткие записи (если вы не пытаетесь написать частичную структуру события). Частичные структуры событий полностью игнорируются. (См. Драйверы /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");
Случай неудачи не смертелен; это только означает, что события не были введены (хотя я не понимаю, как это могло бы произойти в современных ядрах; на всякий случай лучше защититься). Вы можете просто повторить то же самое снова или проигнорировать сбой (но сообщите об этом пользователю, чтобы он мог провести расследование, если это когда-нибудь произойдет). Так что регистрируйте его или выводите предупреждение, но нет необходимости вызывать демон / сервис для выхода.
Уничтожить устройство:
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 не одно).