Dam*_*ent 0 c linux input-devices evdev
我正在编写一个守护进程,它监听输入设备的按键按下并通过 D-Bus 发送信号。主要目标是通过请求更改或通知更改来管理音频音量和屏幕背光级别。我使用libevdev来处理输入设备事件。
我写了一个函数来打开位于指定路径的输入设备:
Device device_open(const char *path);
Run Code Online (Sandbox Code Playgroud)
该函数运行良好,但是当我为它编写单元测试时,我想创建具有不同属性(文件的存在、读取访问等)的文件装置来检查我的函数和内存管理的错误处理(如我将数据存储在结构中)。
但是使用真实的输入设备(位于 /dev/input/event*)测试它需要root访问权限。在 /dev/input/event* 文件上为每个人设置读取访问权限有效,但对我来说似乎有风险。以 root 身份执行我的测试更糟!
使用mknod作品创建设备,但需要以 root 身份完成。
我还尝试使用字符特殊文件(因为输入设备是其中之一)允许所有人读取(例如 /dev/random、/dev/zero、/dev/null 甚至我当前使用的终端设备:/dev /tty2)。
但是这些设备不处理ioctllibevdev 所需的请求:EVIOCGBIT是第一个返回错误“设备的 ioctl 不合适”的请求。
我希望能够以普通用户(执行单元测试的用户)的身份创建设备文件。然后,通过设置访问权限,我应该能够针对不同类型的文件(只读、不允许读取、错误设备类型等)测试我的函数行为。如果这看起来不可能,我肯定会使用私人助手重构我的功能。但是怎么做。有什么例子吗?
谢谢。
编辑:我试图更好地表达我的需求。
为被允许访问设备的用户创建一个组,并创建一个 udev 规则以将该输入事件设备的所有权设置为该组。
我使用teensy(系统)组:
sudo groupadd -r teensy
Run Code Online (Sandbox Code Playgroud)
并使用例如将每个用户添加到其中
sudo usermod -a -g teensy my-user-name
Run Code Online (Sandbox Code Playgroud)
或任何我可用的图形用户界面。
通过管理哪些用户和服务守护进程属于该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"
Run Code Online (Sandbox Code Playgroud)
SUBSYSTEMS=="usb",但是,对于 HID 设备,您只需要第三行(一)。确保idVendor和idProduct满足您的USB HID设备。您可以使用lsusb列出当前连接的 USB 设备供应商和产品编号。匹配使用 glob 模式,就像文件名一样。
添加以上后,不要忘记运行sudo udevadm control --reload-rules && sudo udevadm trigger重新加载规则。下次插入 USB HID 设备时,您组的所有成员(teensy如上所示)都可以直接访问它。
请注意,默认情况下,在大多数发行版中,udev 还会在/dev/input/by-id/使用 USB 设备类型和串行时创建持久符号链接。就我而言,我的Teensy LC的(串行4298820)用组合的键盘鼠标joystic设备提供一个/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 输入事件设备的过程如下。
开放/dev/uinput写作(或读写):
fd = open("/dev/uinput", O_RDWR);
if (fd == -1) {
fprintf(stderr, "Cannot open /dev/uinput: %s.\n", strerror(errno));
exit(EXIT_FAILURE);
}
Run Code Online (Sandbox Code Playgroud)
以上需要超级用户权限。但是,打开设备后,您可以立即放弃所有权限,并让您的守护程序/服务以专用用户身份运行。
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);
}
Run Code Online (Sandbox Code Playgroud)
我个人使用带有代码的静态常量数组,以便于管理。
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);
}
Run Code Online (Sandbox Code Playgroud)
同样,静态常量数组(一个UI_SET_KEYBIT用于UI_SET_RELBIT代码,一个用于代码)更容易维护。
定义一个struct uinput_user_dev,并将其写入设备。
如果你已经name包含设备名称的字符串,vendor并product与USB供应商和产品ID号,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);
}
Run Code Online (Sandbox Code Playgroud)
后来的内核有一个 ioctl 来做同样的事情(显然参与 systemd 开发会导致这种流失 bamage);
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);
}
Run Code Online (Sandbox Code Playgroud)
这个想法似乎是,您可以先尝试后者,而不是使用前者,如果失败,则改为使用前者。您知道,因为有一天,单个界面可能还不够。(无论如何,这就是文档和提交所说的。)
在这里,我可能听起来有点古怪,但这只是因为我确实订阅了Unix 哲学和KISS 原则(或极简主义方法),并且认为这种疣完全没有必要。并且经常来自同一个松散相关的开发人员群体。咳咳。没有人身侮辱的意图;我只是觉得他们做得不好。
通过发出UI_DEV_CREATEioctl创建虚拟设备:
if (ioctl(fd, UI_DEV_CREATE) == -1) {
fprintf(stderr, "Cannot create the virtual uinput device: %s.\n", strerror(errno));
close(fd);
exit(EXIT_FAILURE);
}
Run Code Online (Sandbox Code Playgroud)
此时,内核将构造设备,向 udev 守护进程提供相应的事件,而 udev 守护进程将根据其配置构造设备节点和符号链接。所有这一切都需要一点时间——在现实世界中只有几分之一秒,但足以立即发出事件可能会导致其中一些事件丢失。
struct input_event通过写入 uinput 设备来发出输入事件 ( )。
您可以一次写入一个或多个struct input_events,并且永远不会看到短写入(除非您尝试编写部分事件结构)。部分事件结构被完全忽略。(有关内核如何处理此类写入,请参阅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");
Run Code Online (Sandbox Code Playgroud)
失败案例并不致命;这仅意味着未注入事件(尽管我不知道在当前内核中会发生这种情况;最好是防御性的,以防万一)。您可以简单地再次重试,或者忽略失败(但让用户知道,以便他们可以进行调查,如果发生了)。所以记录它或输出警告,但不需要它导致守护进程/服务退出。
销毁设备:
ioctl(fd, UI_DEV_DESTROY);
close(fd);
Run Code Online (Sandbox Code Playgroud)
当原始打开的描述符的最后一个副本关闭时,设备确实会自动销毁,但我建议按上述方式明确执行此操作。
将步骤 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);
}
Run Code Online (Sandbox Code Playgroud)
这似乎工作正常,并且不需要库(标准 C 库除外)。
重要的是要意识到 Linux 输入子系统,包括 uinput 和struct input_event,是Linux 内核的二进制接口,因此将保持向后兼容(除了紧迫的技术原因,如安全问题或与内核其他部分的严重冲突) . (将所有内容都放在 freedesktop.org 或 systemd 保护伞下的愿望不是一个。)
| 归档时间: |
|
| 查看次数: |
2232 次 |
| 最近记录: |