HID: uhid: implement feature requests
David Herrmann [Sun, 10 Jun 2012 13:16:25 +0000 (15:16 +0200)]
HID standard allows sending a feature request to the device which is
answered by an HID report. uhid implements this by sending a UHID_FEATURE
event to user-space which then must answer with UHID_FEATURE_ANSWER. If it
doesn't do this in a timely manner, the request is discarded silently.

We serialize the feature requests, that is, there is always only a single
active feature-request sent to user-space, other requests have to wait.
HIDP and USB-HID do it the same way.

Because we discard feature-requests silently, we must make sure to match
a response to the corresponding request. We use sequence-IDs for this so
user-space must copy the ID from the request into the answer.
Feature-answers are ignored if they do not contain the same ID as the
currently pending feature request.

Internally, we must make sure that feature-requests are synchronized with
UHID_DESTROY and close() events. We must not dead-lock when closing the
HID device, either, so we have to use separate locks.

Signed-off-by: David Herrmann <dh.herrmann@googlemail.com>
Signed-off-by: Jiri Kosina <jkosina@suse.cz>

drivers/hid/uhid.c
include/linux/uhid.h

index 421c492..ea560bf 100644 (file)
@@ -42,6 +42,12 @@ struct uhid_device {
        __u8 head;
        __u8 tail;
        struct uhid_event *outq[UHID_BUFSIZE];
+
+       struct mutex report_lock;
+       wait_queue_head_t report_wait;
+       atomic_t report_done;
+       atomic_t report_id;
+       struct uhid_event report_buf;
 };
 
 static struct miscdevice uhid_misc;
@@ -143,7 +149,84 @@ static int uhid_hid_parse(struct hid_device *hid)
 static int uhid_hid_get_raw(struct hid_device *hid, unsigned char rnum,
                            __u8 *buf, size_t count, unsigned char rtype)
 {
-       return 0;
+       struct uhid_device *uhid = hid->driver_data;
+       __u8 report_type;
+       struct uhid_event *ev;
+       unsigned long flags;
+       int ret;
+       size_t len;
+       struct uhid_feature_answer_req *req;
+
+       if (!uhid->running)
+               return -EIO;
+
+       switch (rtype) {
+       case HID_FEATURE_REPORT:
+               report_type = UHID_FEATURE_REPORT;
+               break;
+       case HID_OUTPUT_REPORT:
+               report_type = UHID_OUTPUT_REPORT;
+               break;
+       case HID_INPUT_REPORT:
+               report_type = UHID_INPUT_REPORT;
+               break;
+       default:
+               return -EINVAL;
+       }
+
+       ret = mutex_lock_interruptible(&uhid->report_lock);
+       if (ret)
+               return ret;
+
+       ev = kzalloc(sizeof(*ev), GFP_KERNEL);
+       if (!ev) {
+               ret = -ENOMEM;
+               goto unlock;
+       }
+
+       spin_lock_irqsave(&uhid->qlock, flags);
+       ev->type = UHID_FEATURE;
+       ev->u.feature.id = atomic_inc_return(&uhid->report_id);
+       ev->u.feature.rnum = rnum;
+       ev->u.feature.rtype = report_type;
+
+       atomic_set(&uhid->report_done, 0);
+       uhid_queue(uhid, ev);
+       spin_unlock_irqrestore(&uhid->qlock, flags);
+
+       ret = wait_event_interruptible_timeout(uhid->report_wait,
+                               atomic_read(&uhid->report_done), 5 * HZ);
+
+       /*
+        * Make sure "uhid->running" is cleared on shutdown before
+        * "uhid->report_done" is set.
+        */
+       smp_rmb();
+       if (!ret || !uhid->running) {
+               ret = -EIO;
+       } else if (ret < 0) {
+               ret = -ERESTARTSYS;
+       } else {
+               spin_lock_irqsave(&uhid->qlock, flags);
+               req = &uhid->report_buf.u.feature_answer;
+
+               if (req->err) {
+                       ret = -EIO;
+               } else {
+                       ret = 0;
+                       len = min(count,
+                               min_t(size_t, req->size, UHID_DATA_MAX));
+                       memcpy(buf, req->data, len);
+               }
+
+               spin_unlock_irqrestore(&uhid->qlock, flags);
+       }
+
+       atomic_set(&uhid->report_done, 1);
+
+unlock:
+       mutex_unlock(&uhid->report_lock);
+       return ret ? ret : len;
 }
 
 static int uhid_hid_output_raw(struct hid_device *hid, __u8 *buf, size_t count,
@@ -265,7 +348,11 @@ static int uhid_dev_destroy(struct uhid_device *uhid)
        if (!uhid->running)
                return -EINVAL;
 
+       /* clear "running" before setting "report_done" */
        uhid->running = false;
+       smp_wmb();
+       atomic_set(&uhid->report_done, 1);
+       wake_up_interruptible(&uhid->report_wait);
 
        hid_destroy_device(uhid->hid);
        kfree(uhid->rd_data);
@@ -284,6 +371,31 @@ static int uhid_dev_input(struct uhid_device *uhid, struct uhid_event *ev)
        return 0;
 }
 
+static int uhid_dev_feature_answer(struct uhid_device *uhid,
+                                  struct uhid_event *ev)
+{
+       unsigned long flags;
+
+       if (!uhid->running)
+               return -EINVAL;
+
+       spin_lock_irqsave(&uhid->qlock, flags);
+
+       /* id for old report; drop it silently */
+       if (atomic_read(&uhid->report_id) != ev->u.feature_answer.id)
+               goto unlock;
+       if (atomic_read(&uhid->report_done))
+               goto unlock;
+
+       memcpy(&uhid->report_buf, ev, sizeof(*ev));
+       atomic_set(&uhid->report_done, 1);
+       wake_up_interruptible(&uhid->report_wait);
+
+unlock:
+       spin_unlock_irqrestore(&uhid->qlock, flags);
+       return 0;
+}
+
 static int uhid_char_open(struct inode *inode, struct file *file)
 {
        struct uhid_device *uhid;
@@ -293,9 +405,12 @@ static int uhid_char_open(struct inode *inode, struct file *file)
                return -ENOMEM;
 
        mutex_init(&uhid->devlock);
+       mutex_init(&uhid->report_lock);
        spin_lock_init(&uhid->qlock);
        init_waitqueue_head(&uhid->waitq);
+       init_waitqueue_head(&uhid->report_wait);
        uhid->running = false;
+       atomic_set(&uhid->report_done, 1);
 
        file->private_data = uhid;
        nonseekable_open(inode, file);
@@ -398,6 +513,9 @@ static ssize_t uhid_char_write(struct file *file, const char __user *buffer,
        case UHID_INPUT:
                ret = uhid_dev_input(uhid, &uhid->input_buf);
                break;
+       case UHID_FEATURE_ANSWER:
+               ret = uhid_dev_feature_answer(uhid, &uhid->input_buf);
+               break;
        default:
                ret = -EOPNOTSUPP;
        }
index 2c97255..9c6974f 100644 (file)
@@ -32,6 +32,8 @@ enum uhid_event_type {
        UHID_OUTPUT,
        UHID_OUTPUT_EV,
        UHID_INPUT,
+       UHID_FEATURE,
+       UHID_FEATURE_ANSWER,
 };
 
 struct uhid_create_req {
@@ -73,6 +75,19 @@ struct uhid_output_ev_req {
        __s32 value;
 } __attribute__((__packed__));
 
+struct uhid_feature_req {
+       __u32 id;
+       __u8 rnum;
+       __u8 rtype;
+} __attribute__((__packed__));
+
+struct uhid_feature_answer_req {
+       __u32 id;
+       __u16 err;
+       __u16 size;
+       __u8 data[UHID_DATA_MAX];
+};
+
 struct uhid_event {
        __u32 type;
 
@@ -81,6 +96,8 @@ struct uhid_event {
                struct uhid_input_req input;
                struct uhid_output_req output;
                struct uhid_output_ev_req output_ev;
+               struct uhid_feature_req feature;
+               struct uhid_feature_answer_req feature_answer;
        } u;
 } __attribute__((__packed__));