clinuxevdevinput-devices

Faking an input device for testing purpose


What I want to do

I'm writing a daemon which listen to the input devices for keys presses and send signals via D-Bus. The main goal is to manage audio volume and screen backlight level by requesting changes or informing about changes. I use libevdev to handle the input device events.

I wrote a function for opening an input device located at a specified path:

Device device_open(const char *path);

That function works well, but while I'm writing unit tests for it, I wanted to create file fixtures with different properties (existence of the file, read access, etc.) to check the error handling of my function and memory management (as I store data in a structure).

What I have already done

But testing it with a real input device (located at /dev/input/event*) needs root access rights. Setting read access for everyone on /dev/input/event* files works but seems risky to me. Executing my tests as root is worse !

Creating a device using mknod works but needs to be done as root.

I also tried to use character special files (because input devices are one of those) allowing read for everyone (like /dev/random, /dev/zero, /dev/null and even the terminal device i'm currently using: /dev/tty2).

But those devices does not handles ioctl requests needed by libevdev: EVIOCGBIT is the first request returning an error "Inappropriate ioctl for device".

What I'm looking for

I want to be able to create device files as a regular user (the user executing the unit tests). Then, by setting access rights I should be able to test my function behavior for different kinds of file (read only, no read allowed, bad device type, etc.). If it appears to be impossible, I will certainly refactor my function using private helpers. But how to do it. Any examples ?

Thanks.

Edit: I tried to express better my needs.


Solution

  • Create a group for users who are allowed to access the device, and an udev rule to set the ownership of that input event device to that group.


    I use teensy (system) group:

    sudo groupadd -r teensy
    

    and add each user into it using e.g.

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

    or whatever graphical user interface I have available.

    By managing which users and service daemons belong to the teensy group, you can easily manage the access to the devices.


    For my Teensy microcontrollers (that have native USB, and I use for HID testing), I have the following /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"
    

    You only need the third line (SUBSYSTEMS=="usb", one) for HID devices, though. Make sure the idVendor and idProduct match your USB HID device. You can use lsusb to list the currently connected USB devices vendor and product numbers. The matching uses glob patterns, just like file names.

    After adding the above, don't forget running sudo udevadm control --reload-rules && sudo udevadm trigger to reload the rules. Next time you plug in your USB HID device, all members of your group (teensy in the above) can access it directly.


    Note that by default in most distributions, udev also creates persistent symlinks in /dev/input/by-id/ using the USB device type and serial. In my case, one of my Teensy LC's (serial 4298820) with a combined keyboard-mouse-joystic device provides /dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-event-kbd for the keyboard event device, /dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-if01-event-mouse for the mouse event device, and /dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-if03-event-joystick and /dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-if04-event-joystick for the two joystick interfaces.

    (By "persistent", I do not mean these symlinks always exist; I mean that whenever that particular device is plugged in, the symlink of exactly that name exists, and points to the actual Linux input event character device.)


    The Linux uinput device can be used to implement a virtual input event device using a simple privileged daemon.

    The process to create a new virtual USB input event device goes as follows.

    1. Open /dev/uinput for writing (or reading and writing):

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

      The above requires superuser privileges. However, immediately after opening the device, you can drop all privileges, and have your daemon/service run as a dedicated user instead.
       

    2. Use the UI_SET_EVBIT ioctl for each event type allowed.

      You will want to allow at least EV_SYN; and EV_KEY for keyboards and mouse buttons, and EV_REL for mouse movement, and so on.

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

      I personally use a static constant array with the codes, for easier management.
       

    3. Use the UI_SET_KEYBIT ioctl for each key code the device may emit, and UI_SET_RELBIT ioctl for each relative movement code (mouse code). For example, to allow space, left mouse button, horizontal and vertical mouse movement, and mouse wheel:

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

      Again, static const arrays (one for UI_SET_KEYBIT and one for UI_SET_RELBIT codes) is much easier to maintain.
       

    4. Define a struct uinput_user_dev, and write it to the device.

      If you have name containing the device name string, vendor and product with the USB vendor and product ID numbers, version with a version number (0 is fine), use

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

      Later kernels have an ioctl to do the same thing (apparently being involved in systemd development causes this kind of drain 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);
      }
      

      The idea seems to be that instead of using the former, you can try the latter first, and if it fails, do the former instead. You know, because a single interface might some day not be enough. (That's what the documentation and commit say, anyway.)

      I might sound a bit cranky, here, but that's just because I do subscribe to both the Unix philosophy and the KISS principle (or minimalist approach), and see such warts completely unnecessary. And too often coming from the same loosely related group of developers. Ahem. No personal insult intended; I just think they are doing poor job.
       

    5. Create the virtual device, by issuing an 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);
      }
      

      At this point, the kernel will construct the device, provide the corresponding event to the udev daemon, and the udev daemon will construct the device node and symlink(s) according to its configuration. All this will take a bit of time -- a fraction of a second in the real world, but enough that trying to emit events immediately might cause some of them to be lost.
       

    6. Emit the input events (struct input_event) by writing to the uinput device.

      You can write one or more struct input_events at a time, and should never see short writes (unless you try to write a partial event structure). Partial event structures are completely ignored. (See drivers/input/misc/uinput.c:uinput_write() uinput_inject_events() for how the kernel handles such writes.)

      Many actions consists of more than one struct input_event. For example, you might move the mouse diagonally (emitting both { .type == EV_REL, .code == REL_X, .value = xdelta } and { .type == EV_REL, .code == REL_Y, .value = ydelta } for that single movement). The synchronization events ({ .type == EV_SYN, .code == 0, .value == 0 }) are used as a sentinel or separator, denoting the end of related events.

      Because of this, you'll need to append an { .type == EV_SYN, .code == 0, .value == 0 } input event after each individual action (mouse movement, key press, key release, and so on). Think of it as the equivalent of a newline, for line-buffered input.

      For example, the following code moves the mouse diagonally down right by a single pixel.

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

      The failure case is not fatal; it only means the events were not injected (although I don't see how that could happen in current kernels; better be defensive, just in case). You can simply retry the same again, or ignore the failure (but letting the user know, so they can investigate, if it ever happens). So log it or output a warning, but no need for it to cause the daemon/service to exit.
       

    7. Destroy the device:

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

      The device does get automatically destroyed when the last duplicate of the original opened descriptor gets closed, but I recommend doing it explicitly as above.
       

    Putting steps 1-5 in a function, you get something like

    #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);
    }
    

    which seem to work fine, and requires no libraries (other than the standard C library).

    It is important to realize that the Linux input subsystem, including uinput and struct input_event, are binary interfaces to the Linux kernel, and therefore will be kept backwards compatible (except for pressing technical reasons, like security issues or serious conflicts with other parts of the kernel). (The desire to wrap everything under the freedesktop.org or systemd umbrella is not one.)