2

I'm creating a Linux module for a game library that let's you hotplug multiple joysticks, it uses inotify to watch /dev/input.

I am testing it with 3 joysticks:

  • First I connect 2 joysticks.
  • Then I start the application, the joysticks work and I don't get a error.
  • After that I connect the third joystick, perror gives: /dev/input/js1: Permission denied.
  • When I check ls -l /proc/<pid-of-process>/fd it lists /dev/input/js0 and /dev/input/js2.

All the joysticks work fine when I run it as root.

This is how it's initialized:

static void createGamepad(char *locName){
    char dirName[30];
    int fd;

    snprintf(dirName, 30, "/dev/input/%s", locName);

    fd = open(dirName, O_RDONLY | O_NONBLOCK, 0);
    if(fd < 0){
        perror(dirName);
    }
}

struct dirent *dir;
DIR *d;
int i, notifyfd, watch;

// Attach notifications to check if a device connects/disconnects
notifyfd = inotify_init();

watch = inotify_add_watch(notifyfd, "/dev/input", IN_CREATE | IN_DELETE);

d = opendir("/dev/input");

i = 0;
while((dir = readdir(d)) != NULL){
    if(*dir->d_name == 'j' && *(dir->d_name + 1) == 's'){
        createGamepad(dir->d_name, i);
        i++;
    }
}

closedir(d);

After that inotify handles it like this in the while(1) loop:

static bool canReadINotify(){
    fd_set set;
    struct timeval timeout;

    FD_ZERO(&set);
    FD_SET(notifyfd, &set);
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;

    return select(notifyfd + 1, &set, NULL, NULL, &timeout) > 0 && 
        FD_ISSET(notifyfd, &set);
 }

// Inside the event loop
struct inotify_event ne;

while(canReadINotify()){
    if(read(notifyfd, &ne, sizeof(struct inotify_event) + 16) >= 0){
        if(*ne.name != 'j' || *(ne.name + 1) != 's'){
            continue;
        }

        if(ne.mask & IN_CREATE){
            createGamepad(ne.name);
        }
    }
}

Is it even possible with inotify or should I use udev? And if it's possible, how can I solve this?

tversteeg
  • 4,717
  • 10
  • 42
  • 77

1 Answers1

4

It is very likely a race condition. You see, you get the inotify event when the device node is created (by udev using a mknod() call), but the access permissions are set by udev using a separate chown() call, just a tiny bit later.

See systemd src/udev/udev-node.c, node_permissions_apply(). In this particular case, /dev/input/jsX is not a symlink, but the actual device node; at least with systemd the device node access mode gets set sometime later, after the actual node is created.

One robust solution would be to modify your createGamepad() function, so that instead of failing completely at fd == -1 && errno == EACCES, you instead retry after a short while; at least a few times, say for up to a second or two.

However, ninjalj pointed out a better suggestion: use also the access permissions change as a trigger to check the device node. This is trivially accomplished, by using IN_CREATE | IN_DELETE | IN_ATTRIBUTE in the inotify_add_watch() function!

(You'll also want to ignore open()==-1, errno==EACCES errors in createGamepad(), as they are likely caused by this race condition, and the following IN_ATTRIBUTE inotify event will yield access to the same device.)

Prior to ninjalj's comment, I'd personally have used an array of input devices, and another for "possible" input devices that can/need to be retried after a short timeout to decide whether they are available or not, but I think his suggestion is much better.

Need/want an example?

Community
  • 1
  • 1
Nominal Animal
  • 38,216
  • 5
  • 59
  • 86
  • 1
    Don't joystick device nodes get an IN_ATTRIB inotify event when their permissions change? If so, the OP could watch also for that event, no need for retrying. – ninjalj Sep 04 '14 at 19:13
  • 1
    @ninjalj: I checked, and indeed you do get the `IN_ATTRIB` inotify events. I think that should work just fine; excellent suggestion! motash, you only need to use `IN_CREATE | IN_DELETE | IN_ATTRIB` instead of `IN_CREATE | IN_DELETE`, and ignore `errno==EACCES` errors, to try ninjalj's suggested solution. – Nominal Animal Sep 04 '14 at 20:39
  • Thank you both, the `IN_ATTRIB` events work like a charm! If you could put it in an answer I can accept it and award the points. – tversteeg Sep 05 '14 at 08:56
  • 1
    @ninjalj: It was your idea; feel free to submit an answer to get the points! – Nominal Animal Sep 05 '14 at 15:03
  • 1
    @NominalAnimal: you diagnosed the problem. I merely suggested a possible improvement to your solution. – ninjalj Sep 05 '14 at 19:38
  • IN_ATTRIB doesn't produce any events with my testing. – AldaronLau May 22 '19 at 21:02