shill: Differentiate between various cdc_ether devices.

Add code to distinguish between a cdc_ether cellular device and a
cdc_ether ethernet device.

BUG=chromium-os:28499
TEST=New unit tests, manually verify detection of cdc_ether modem devices
on y3300 and y3400.

Change-Id: I5ca68b68edaac25bdb4b86abb96366c13f0e6e23
Reviewed-on: https://gerrit.chromium.org/gerrit/20317
Reviewed-by: Jason Glasgow <jglasgow@chromium.org>
Commit-Ready: Thieu Le <thieule@chromium.org>
Tested-by: Thieu Le <thieule@chromium.org>
diff --git a/device_info.cc b/device_info.cc
index a424a56..34680bc 100644
--- a/device_info.cc
+++ b/device_info.cc
@@ -41,6 +41,7 @@
 #include "shill/wifi.h"
 
 using base::Bind;
+using base::StringPrintf;
 using base::Unretained;
 using std::map;
 using std::string;
@@ -50,25 +51,18 @@
 
 // static
 const char DeviceInfo::kInterfaceUevent[] = "/sys/class/net/%s/uevent";
-// static
 const char DeviceInfo::kInterfaceUeventWifiSignature[] = "DEVTYPE=wlan\n";
-// static
+const char DeviceInfo::kInterfaceDevice[] = "/sys/class/net/%s/device";
 const char DeviceInfo::kInterfaceDriver[] = "/sys/class/net/%s/device/driver";
-// static
 const char DeviceInfo::kInterfaceTunFlags[] = "/sys/class/net/%s/tun_flags";
-// static
 const char DeviceInfo::kInterfaceType[] = "/sys/class/net/%s/type";
-// static
 const char DeviceInfo::kLoopbackDeviceName[] = "lo";
-// static
+const char DeviceInfo::kDriverCdcEther[] = "cdc_ether";
 const char *DeviceInfo::kModemDrivers[] = {
     "gobi",
     "QCUSBNet2k",
-    "GobiNet",
-    "cdc_ether",
-    NULL
+    "GobiNet"
 };
-// static
 const char DeviceInfo::kTunDeviceName[] = "/dev/net/tun";
 
 DeviceInfo::DeviceInfo(ControlInterface *control_interface,
@@ -140,6 +134,7 @@
   }
 }
 
+// static
 Technology::Identifier DeviceInfo::GetDeviceTechnology(
     const string &iface_name) {
   FilePath uevent_file(StringPrintf(kInterfaceUevent, iface_name.c_str()));
@@ -150,11 +145,9 @@
     return Technology::kUnknown;
   }
 
-  /*
-   * If the "uevent" file contains the string "DEVTYPE=wlan\n" at the
-   * start of the file or after a newline, we can safely assume this
-   * is a wifi device.
-   */
+  // If the "uevent" file contains the string "DEVTYPE=wlan\n" at the
+  // start of the file or after a newline, we can safely assume this
+  // is a wifi device.
   if (contents.find(kInterfaceUeventWifiSignature) != string::npos) {
     VLOG(2) << StringPrintf("%s: device %s has wifi signature in uevent file",
                             __func__, iface_name.c_str());
@@ -198,11 +191,9 @@
   }
 
   string driver_name(driver_path.BaseName().value());
-  // See if driver for this interface is in a list of known modem driver names
-  for (int modem_idx = 0; kModemDrivers[modem_idx] != NULL; ++modem_idx) {
-    // TODO(ers): should have additional checks to make sure a cdc_ether
-    // device is really a modem. flimflam uses udev to make such checks,
-    // looking to see whether a ttyACM or ttyUSB device is associated.
+  // See if driver for this interface is in a list of known modem driver names.
+  for (size_t modem_idx = 0; modem_idx < arraysize(kModemDrivers);
+       ++modem_idx) {
     if (driver_name == kModemDrivers[modem_idx]) {
       VLOG(2) << StringPrintf("%s: device %s is matched with modem driver %s",
                               __func__, iface_name.c_str(),
@@ -211,11 +202,81 @@
     }
   }
 
+  // For cdc_ether devices, make sure it's a modem because this driver
+  // can be used for other ethernet devices.
+  if (driver_name == kDriverCdcEther &&
+      IsCdcEtherModemDevice(iface_name)) {
+    VLOG(2) << StringPrintf("%s: device %s is a modem cdc_ether device",
+                            __func__, iface_name.c_str());
+    return Technology::kCellular;
+  }
+
   VLOG(2) << StringPrintf("%s: device %s is defaulted to type ethernet",
                           __func__, iface_name.c_str());
   return Technology::kEthernet;
 }
 
+// static
+bool DeviceInfo::IsCdcEtherModemDevice(const std::string &iface_name) {
+  // A cdc_ether device is a modem device if it also exposes tty interfaces.
+  // To determine this, we look for the existence of the tty interface in the
+  // USB device sysfs tree.
+  //
+  // A typical sysfs dir hierarchy for a cdc_ether modem USB device is as
+  // follows:
+  //
+  //   /sys/devices/pci0000:00/0000:00:1d.7/usb1/1-2
+  //     1-2:1.0
+  //       tty
+  //         ttyACM0
+  //     1-2:1.1
+  //       net
+  //         usb0
+  //     1-2:1.2
+  //       tty
+  //         ttyACM1
+  //       ...
+  //
+  // /sys/class/net/usb0/device symlinks to
+  // /sys/devices/pci0000:00/0000:00:1d.7/usb1/1-2/1-2:1.1
+
+  FilePath device_file(StringPrintf(kInterfaceDevice, iface_name.c_str()));
+  FilePath device_path;
+  if (!file_util::ReadSymbolicLink(device_file, &device_path)) {
+    VLOG(2) << StringPrintf("%s: device %s has no device symlink",
+                            __func__, iface_name.c_str());
+    return false;
+  }
+  if (!device_path.IsAbsolute()) {
+    device_path = device_file.DirName().Append(device_path);
+    file_util::AbsolutePath(&device_path);
+  }
+
+  // Look for tty interface by enumerating all directories under the parent
+  // USB device and seeing if there's a subdirectory "tty" inside of the
+  // enumerated directory.  In other words, using the example dir hierarchy
+  // above, find /sys/devices/pci0000:00/0000:00:1d.7/usb1/1-2/1-2*/tty.
+  // If this exists, then this is a modem device.
+  FilePath usb_device_path(device_path.DirName());
+  string search_pattern(usb_device_path.BaseName().MaybeAsASCII() + "*");
+  return IsGrandchildSubdir(usb_device_path, search_pattern, "tty");
+}
+
+// static
+bool DeviceInfo::IsGrandchildSubdir(const FilePath &base_dir,
+                                    const string &children_filter,
+                                    const string &grandchild) {
+  file_util::FileEnumerator dir_enum(base_dir, false,
+                                     file_util::FileEnumerator::DIRECTORIES,
+                                     children_filter);
+  for (FilePath curr_dir = dir_enum.Next(); !curr_dir.empty();
+       curr_dir = dir_enum.Next()) {
+    if (file_util::DirectoryExists(curr_dir.Append(grandchild)))
+      return true;
+  }
+  return false;
+}
+
 void DeviceInfo::AddLinkMsgHandler(const RTNLMessage &msg) {
   DCHECK(msg.type() == RTNLMessage::kTypeLink &&
          msg.mode() == RTNLMessage::kModeAdd);
diff --git a/device_info.h b/device_info.h
index 441573a..91c354a 100644
--- a/device_info.h
+++ b/device_info.h
@@ -21,6 +21,8 @@
 #include "shill/rtnl_listener.h"
 #include "shill/technology.h"
 
+class FilePath;
+
 namespace shill {
 
 class Manager;
@@ -76,6 +78,7 @@
   friend class DeviceInfoTest;
   FRIEND_TEST(CellularTest, StartLinked);
   FRIEND_TEST(DeviceInfoTest, AddLoopbackDevice);  // For kLoopbackDeviceName.
+  FRIEND_TEST(DeviceInfoTest, GrandchildSubdir);  // For IsGrandChildSubdir.
 
   struct Info {
     Info() : flags(0) {}
@@ -86,17 +89,41 @@
     unsigned int flags;
   };
 
+  // Sysfs path to a device uevent file.
   static const char kInterfaceUevent[];
+  // Content of a device uevent file that indicates it is a wifi device.
   static const char kInterfaceUeventWifiSignature[];
+  // Sysfs path to a device via its interface name.
+  static const char kInterfaceDevice[];
+  // Sysfs path to the driver of a device via its interface name.
   static const char kInterfaceDriver[];
+  // Sysfs path to the file that is used to determine if this is tun device.
   static const char kInterfaceTunFlags[];
+  // Sysfs path to the file that is used to determine if a wifi device is
+  // operating in monitor mode.
   static const char kInterfaceType[];
+  // Name of the interface for the loopback device.
   static const char kLoopbackDeviceName[];
+  // Name of the "cdc_ether" driver.  This driver is not included in the
+  // kModemDrivers list because we need to do additional checking.
+  static const char kDriverCdcEther[];
+  // Modem drivers that we support.
   static const char *kModemDrivers[];
+  // Path to the tun device.
   static const char kTunDeviceName[];
 
   static Technology::Identifier GetDeviceTechnology(
-      const std::string &face_name);
+      const std::string &iface_name);
+  // Checks the device specified by |iface_name| to see if it's a modem device.
+  // This method assumes that |iface_name| has already been determined to be
+  // using the cdc_ether driver.
+  static bool IsCdcEtherModemDevice(const std::string &iface_name);
+  // Returns true if |grandchild| is a grandchild of the |base_dir|.
+  // This method only searches for |grandchild| in children specified by
+  // the regex in |children_filter|.
+  static bool IsGrandchildSubdir(const FilePath &base_dir,
+                                 const std::string &children_filter,
+                                 const std::string &grandchild);
 
   void AddLinkMsgHandler(const RTNLMessage &msg);
   void DelLinkMsgHandler(const RTNLMessage &msg);
diff --git a/device_info_unittest.cc b/device_info_unittest.cc
index 0df149f..b918d95 100644
--- a/device_info_unittest.cc
+++ b/device_info_unittest.cc
@@ -10,9 +10,11 @@
 #include <linux/netlink.h>  // Needs typedefs from sys/socket.h.
 #include <linux/rtnetlink.h>
 
+#include <base/file_util.h>
 #include <base/logging.h>
 #include <base/memory/ref_counted.h>
 #include <base/message_loop.h>
+#include <base/scoped_temp_dir.h>
 #include <base/stl_util.h>
 #include <gtest/gtest.h>
 #include <gmock/gmock.h>
@@ -322,4 +324,32 @@
   SendMessageToDeviceInfo(*message);
 }
 
+TEST_F(DeviceInfoTest, GrandchildSubdir) {
+  device_info_.Start();
+  ScopedTempDir temp_dir;
+  EXPECT_TRUE(temp_dir.CreateUniqueTempDir());
+  EXPECT_TRUE(file_util::CreateDirectory(temp_dir.path().Append("child11")));
+  EXPECT_TRUE(file_util::CreateDirectory(temp_dir.path().Append("child12")));
+  FilePath child21 = temp_dir.path().Append("child21");
+  EXPECT_TRUE(file_util::CreateDirectory(child21));
+  FilePath grandchild = child21.Append("grandchild");
+  EXPECT_TRUE(file_util::CreateDirectory(grandchild));
+  EXPECT_TRUE(file_util::CreateDirectory(grandchild.Append("greatgrandchild")));
+  EXPECT_TRUE(DeviceInfo::IsGrandchildSubdir(temp_dir.path(),
+                                             "*",
+                                             "grandchild"));
+  EXPECT_FALSE(DeviceInfo::IsGrandchildSubdir(temp_dir.path(),
+                                              "*",
+                                              "nonexistent"));
+  EXPECT_FALSE(DeviceInfo::IsGrandchildSubdir(temp_dir.path(),
+                                              "child1*",
+                                              "grandchild"));
+  EXPECT_TRUE(DeviceInfo::IsGrandchildSubdir(temp_dir.path(),
+                                             "child2*",
+                                             "grandchild"));
+  EXPECT_FALSE(DeviceInfo::IsGrandchildSubdir(temp_dir.path(),
+                                              "*",
+                                              "greatgrandchild"));
+}
+
 }  // namespace shill