ethtool: Change ETHTOOL_PHYS_ID implementation to allow dropping RTNL
[pandora-kernel.git] / net / core / ethtool.c
index 74ead9e..1c95f0f 100644 (file)
@@ -21,6 +21,8 @@
 #include <linux/uaccess.h>
 #include <linux/vmalloc.h>
 #include <linux/slab.h>
+#include <linux/rtnetlink.h>
+#include <linux/sched.h>
 
 /*
  * Some useful ethtool_ops methods that're device independent.
@@ -1618,14 +1620,63 @@ out:
 static int ethtool_phys_id(struct net_device *dev, void __user *useraddr)
 {
        struct ethtool_value id;
+       static bool busy;
+       int rc;
 
-       if (!dev->ethtool_ops->phys_id)
+       if (!dev->ethtool_ops->set_phys_id && !dev->ethtool_ops->phys_id)
                return -EOPNOTSUPP;
 
+       if (busy)
+               return -EBUSY;
+
        if (copy_from_user(&id, useraddr, sizeof(id)))
                return -EFAULT;
 
-       return dev->ethtool_ops->phys_id(dev, id.data);
+       if (!dev->ethtool_ops->set_phys_id)
+               /* Do it the old way */
+               return dev->ethtool_ops->phys_id(dev, id.data);
+
+       rc = dev->ethtool_ops->set_phys_id(dev, ETHTOOL_ID_ACTIVE);
+       if (rc && rc != -EINVAL)
+               return rc;
+
+       /* Drop the RTNL lock while waiting, but prevent reentry or
+        * removal of the device.
+        */
+       busy = true;
+       dev_hold(dev);
+       rtnl_unlock();
+
+       if (rc == 0) {
+               /* Driver will handle this itself */
+               schedule_timeout_interruptible(
+                       id.data ? id.data : MAX_SCHEDULE_TIMEOUT);
+       } else {
+               /* Driver expects to be called periodically */
+               do {
+                       rtnl_lock();
+                       rc = dev->ethtool_ops->set_phys_id(dev, ETHTOOL_ID_ON);
+                       rtnl_unlock();
+                       if (rc)
+                               break;
+                       schedule_timeout_interruptible(HZ / 2);
+
+                       rtnl_lock();
+                       rc = dev->ethtool_ops->set_phys_id(dev, ETHTOOL_ID_OFF);
+                       rtnl_unlock();
+                       if (rc)
+                               break;
+                       schedule_timeout_interruptible(HZ / 2);
+               } while (!signal_pending(current) &&
+                        (id.data == 0 || --id.data != 0));
+       }
+
+       rtnl_lock();
+       dev_put(dev);
+       busy = false;
+
+       (void)dev->ethtool_ops->set_phys_id(dev, ETHTOOL_ID_INACTIVE);
+       return rc;
 }
 
 static int ethtool_get_stats(struct net_device *dev, void __user *useraddr)