1

我想做的事

我正在编写一个守护进程,它监听输入设备的按键并通过 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)。

但是这些设备不处理ioctllibevdev 所需的请求:EVIOCGBIT是第一个返回错误“设备的 ioctl 不合适”的请求。

我在寻找什么

我希望能够以普通用户(执行单元测试的用户)的身份创建设备文件。然后,通过设置访问权限,我应该能够针对不同类型的文件(只读、不允许读取、错误的设备类型等)测试我的函数行为。如果看起来不可能,我肯定会使用私有助手重构我的函数。但是怎么做。有什么例子吗?

谢谢。

编辑:我试图更好地表达我的需求。

4

1 回答 1

7

为允许访问设备的用户创建一个组,并创建一个 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",不过,对于 HID 设备,您只需要第三行(一)。确保idVendoridProduct匹配您的 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)与组合的键盘-鼠标-游戏设备提供/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. 对允许的每种事件类型使用UI_SET_EVBITioctl。

    您至少要允许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);
    }
    

    我个人在代码中使用了一个静态常量数组,以便于管理。
     

  3. 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);
    }
    

    同样,静态 const 数组(一个用于代码UI_SET_KEYBIT,一个用于UI_SET_RELBIT代码)更易于维护。
     

  4. 定义 a struct uinput_user_dev,并将其写入设备。

    如果您name包含设备名称字符串、vendorUSBproduct供应商和产品 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);
    }
    

    后来的内核有一个 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_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);
    }
    

    此时,内核将构造设备,将相应的事件提供给 udev 守护进程,udev 守护进程将根据其配置构造设备节点和符号链接。所有这一切都需要一点时间——在现实世界中只有几分之一秒,但足以尝试立即发出事件可能会导致其中一些事件丢失。
     

  6. 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");
    

    失败案例不是致命的;这仅意味着事件没有被注入(尽管我不知道在当前内核中如何发生这种情况;最好是防御性的,以防万一)。您可以简单地再次重试,或忽略失败(但让用户知道,以便他们可以调查,如果它发生了)。所以记录它或输出警告,但不需要它导致守护程序/服务退出。
     

  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 的保护伞下。)

于 2018-12-05T20:13:11.667 回答