Home | History | Annotate | Download | only in storage_monitor
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 // StorageMonitorLinux implementation.
      6 
      7 #include "chrome/browser/storage_monitor/storage_monitor_linux.h"
      8 
      9 #include <mntent.h>
     10 #include <stdio.h>
     11 
     12 #include <list>
     13 
     14 #include "base/basictypes.h"
     15 #include "base/bind.h"
     16 #include "base/metrics/histogram.h"
     17 #include "base/process/kill.h"
     18 #include "base/process/launch.h"
     19 #include "base/stl_util.h"
     20 #include "base/strings/string_number_conversions.h"
     21 #include "base/strings/string_util.h"
     22 #include "base/strings/utf_string_conversions.h"
     23 #include "chrome/browser/storage_monitor/media_storage_util.h"
     24 #include "chrome/browser/storage_monitor/media_transfer_protocol_device_observer_linux.h"
     25 #include "chrome/browser/storage_monitor/removable_device_constants.h"
     26 #include "chrome/browser/storage_monitor/storage_info.h"
     27 #include "chrome/browser/storage_monitor/udev_util_linux.h"
     28 #include "device/media_transfer_protocol/media_transfer_protocol_manager.h"
     29 
     30 namespace chrome {
     31 
     32 using content::BrowserThread;
     33 typedef MtabWatcherLinux::MountPointDeviceMap MountPointDeviceMap;
     34 
     35 namespace {
     36 
     37 // udev device property constants.
     38 const char kBlockSubsystemKey[] = "block";
     39 const char kDiskDeviceTypeKey[] = "disk";
     40 const char kFsUUID[] = "ID_FS_UUID";
     41 const char kLabel[] = "ID_FS_LABEL";
     42 const char kModel[] = "ID_MODEL";
     43 const char kModelID[] = "ID_MODEL_ID";
     44 const char kRemovableSysAttr[] = "removable";
     45 const char kSerialShort[] = "ID_SERIAL_SHORT";
     46 const char kSizeSysAttr[] = "size";
     47 const char kVendor[] = "ID_VENDOR";
     48 const char kVendorID[] = "ID_VENDOR_ID";
     49 
     50 // Construct a device id using label or manufacturer (vendor and model) details.
     51 std::string MakeDeviceUniqueId(struct udev_device* device) {
     52   std::string uuid = GetUdevDevicePropertyValue(device, kFsUUID);
     53   // Keep track of device uuid, to see how often we receive empty uuid values.
     54   UMA_HISTOGRAM_BOOLEAN(
     55       "RemovableDeviceNotificationsLinux.device_file_system_uuid_available",
     56       !uuid.empty());
     57 
     58   if (!uuid.empty())
     59     return kFSUniqueIdPrefix + uuid;
     60 
     61   // If one of the vendor, model, serial information is missing, its value
     62   // in the string is empty.
     63   // Format: VendorModelSerial:VendorInfo:ModelInfo:SerialShortInfo
     64   // E.g.: VendorModelSerial:Kn:DataTravel_12.10:8000000000006CB02CDB
     65   std::string vendor = GetUdevDevicePropertyValue(device, kVendorID);
     66   std::string model = GetUdevDevicePropertyValue(device, kModelID);
     67   std::string serial_short = GetUdevDevicePropertyValue(device,
     68                                                         kSerialShort);
     69   if (vendor.empty() && model.empty() && serial_short.empty())
     70     return std::string();
     71 
     72   return kVendorModelSerialPrefix + vendor + ":" + model + ":" + serial_short;
     73 }
     74 
     75 // Records GetDeviceInfo result on destruction, to see how often we fail to get
     76 // device details.
     77 class ScopedGetDeviceInfoResultRecorder {
     78  public:
     79   ScopedGetDeviceInfoResultRecorder() : result_(false) {}
     80   ~ScopedGetDeviceInfoResultRecorder() {
     81     UMA_HISTOGRAM_BOOLEAN("MediaDeviceNotification.UdevRequestSuccess",
     82                           result_);
     83   }
     84 
     85   void set_result(bool result) {
     86     result_ = result;
     87   }
     88 
     89  private:
     90   bool result_;
     91 
     92   DISALLOW_COPY_AND_ASSIGN(ScopedGetDeviceInfoResultRecorder);
     93 };
     94 
     95 // Returns the storage partition size of the device specified by |device_path|.
     96 // If the requested information is unavailable, returns 0.
     97 uint64 GetDeviceStorageSize(const base::FilePath& device_path,
     98                             struct udev_device* device) {
     99   // sysfs provides the device size in units of 512-byte blocks.
    100   const std::string partition_size = udev_device_get_sysattr_value(
    101       device, kSizeSysAttr);
    102 
    103   // Keep track of device size, to see how often this information is
    104   // unavailable.
    105   UMA_HISTOGRAM_BOOLEAN(
    106       "RemovableDeviceNotificationsLinux.device_partition_size_available",
    107       !partition_size.empty());
    108 
    109   uint64 total_size_in_bytes = 0;
    110   if (!base::StringToUint64(partition_size, &total_size_in_bytes))
    111     return 0;
    112   return (total_size_in_bytes <= kuint64max / 512) ?
    113       total_size_in_bytes * 512 : 0;
    114 }
    115 
    116 // Constructs the device name from the device properties. If the device details
    117 // are unavailable, returns an empty string.
    118 void GetDeviceName(struct udev_device* device,
    119                    string16* out_volume_label,
    120                    string16* out_vendor_name,
    121                    string16* out_model_name) {
    122   std::string device_label = GetUdevDevicePropertyValue(device, kLabel);
    123   std::string vendor_name = GetUdevDevicePropertyValue(device, kVendor);
    124   std::string model_name = GetUdevDevicePropertyValue(device, kModel);
    125   if (out_volume_label)
    126     *out_volume_label = UTF8ToUTF16(device_label);
    127   if (out_vendor_name)
    128     *out_vendor_name = UTF8ToUTF16(vendor_name);
    129   if (out_model_name)
    130     *out_model_name = UTF8ToUTF16(model_name);
    131 }
    132 
    133 // Gets the device information using udev library.
    134 scoped_ptr<StorageInfo> GetDeviceInfo(const base::FilePath& device_path,
    135                                       const base::FilePath& mount_point) {
    136   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
    137   DCHECK(!device_path.empty());
    138 
    139   scoped_ptr<StorageInfo> storage_info;
    140 
    141   ScopedGetDeviceInfoResultRecorder results_recorder;
    142 
    143   ScopedUdevObject udev_obj(udev_new());
    144   if (!udev_obj.get())
    145     return storage_info.Pass();
    146 
    147   struct stat device_stat;
    148   if (stat(device_path.value().c_str(), &device_stat) < 0)
    149     return storage_info.Pass();
    150 
    151   char device_type;
    152   if (S_ISCHR(device_stat.st_mode))
    153     device_type = 'c';
    154   else if (S_ISBLK(device_stat.st_mode))
    155     device_type = 'b';
    156   else
    157     return storage_info.Pass();  // Not a supported type.
    158 
    159   ScopedUdevDeviceObject device(
    160       udev_device_new_from_devnum(udev_obj.get(), device_type,
    161                                   device_stat.st_rdev));
    162   if (!device.get())
    163     return storage_info.Pass();
    164 
    165   string16 volume_label = UTF8ToUTF16(GetUdevDevicePropertyValue(device.get(),
    166                                                                  kLabel));
    167   string16 vendor_name = UTF8ToUTF16(GetUdevDevicePropertyValue(device.get(),
    168                                                                 kVendor));
    169   string16 model_name = UTF8ToUTF16(GetUdevDevicePropertyValue(device.get(),
    170                                                                kModel));
    171 
    172   std::string unique_id = MakeDeviceUniqueId(device.get());
    173 
    174   // Keep track of device info details to see how often we get invalid values.
    175   MediaStorageUtil::RecordDeviceInfoHistogram(true, unique_id, volume_label);
    176 
    177   const char* value =
    178       udev_device_get_sysattr_value(device.get(), kRemovableSysAttr);
    179   if (!value) {
    180     // |parent_device| is owned by |device| and does not need to be cleaned
    181     // up.
    182     struct udev_device* parent_device =
    183         udev_device_get_parent_with_subsystem_devtype(device.get(),
    184                                                       kBlockSubsystemKey,
    185                                                       kDiskDeviceTypeKey);
    186     value = udev_device_get_sysattr_value(parent_device, kRemovableSysAttr);
    187   }
    188   const bool is_removable = (value && atoi(value) == 1);
    189 
    190   StorageInfo::Type type = StorageInfo::FIXED_MASS_STORAGE;
    191   if (is_removable) {
    192     if (MediaStorageUtil::HasDcim(mount_point))
    193       type = StorageInfo::REMOVABLE_MASS_STORAGE_WITH_DCIM;
    194     else
    195       type = StorageInfo::REMOVABLE_MASS_STORAGE_NO_DCIM;
    196   }
    197 
    198   results_recorder.set_result(true);
    199 
    200   storage_info.reset(new StorageInfo(
    201       StorageInfo::MakeDeviceId(type, unique_id),
    202       string16(),
    203       mount_point.value(),
    204       volume_label,
    205       vendor_name,
    206       model_name,
    207       GetDeviceStorageSize(device_path, device.get())));
    208   return storage_info.Pass();
    209 }
    210 
    211 MtabWatcherLinux* CreateMtabWatcherLinuxOnFileThread(
    212     const base::FilePath& mtab_path,
    213     base::WeakPtr<MtabWatcherLinux::Delegate> delegate) {
    214   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
    215   // Owned by caller.
    216   return new MtabWatcherLinux(mtab_path, delegate);
    217 }
    218 
    219 StorageMonitor::EjectStatus EjectPathOnFileThread(
    220     const base::FilePath& path,
    221     const base::FilePath& device) {
    222   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
    223 
    224   // Note: Linux LSB says umount should exist in /bin.
    225   static const char kUmountBinary[] = "/bin/umount";
    226   std::vector<std::string> command;
    227   command.push_back(kUmountBinary);
    228   command.push_back(path.value());
    229 
    230   base::LaunchOptions options;
    231   base::ProcessHandle handle;
    232   if (!base::LaunchProcess(command, options, &handle))
    233     return StorageMonitor::EJECT_FAILURE;
    234 
    235   int exit_code = -1;
    236   if (!base::WaitForExitCodeWithTimeout(handle, &exit_code,
    237       base::TimeDelta::FromMilliseconds(3000))) {
    238     base::KillProcess(handle, -1, false);
    239     base::EnsureProcessTerminated(handle);
    240     return StorageMonitor::EJECT_FAILURE;
    241   }
    242 
    243   // TODO(gbillock): Make sure this is found in documentation
    244   // somewhere. Experimentally it seems to hold that exit code
    245   // 1 means device is in use.
    246   if (exit_code == 1)
    247     return StorageMonitor::EJECT_IN_USE;
    248   if (exit_code != 0)
    249     return StorageMonitor::EJECT_FAILURE;
    250 
    251   return StorageMonitor::EJECT_OK;
    252 }
    253 
    254 }  // namespace
    255 
    256 StorageMonitorLinux::StorageMonitorLinux(const base::FilePath& path)
    257     : mtab_path_(path),
    258       get_device_info_callback_(base::Bind(&GetDeviceInfo)),
    259       weak_ptr_factory_(this) {
    260   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    261 }
    262 
    263 StorageMonitorLinux::~StorageMonitorLinux() {
    264   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    265 }
    266 
    267 void StorageMonitorLinux::Init() {
    268   DCHECK(!mtab_path_.empty());
    269 
    270   BrowserThread::PostTaskAndReplyWithResult(
    271       BrowserThread::FILE, FROM_HERE,
    272       base::Bind(&CreateMtabWatcherLinuxOnFileThread,
    273                  mtab_path_,
    274                  weak_ptr_factory_.GetWeakPtr()),
    275       base::Bind(&StorageMonitorLinux::OnMtabWatcherCreated,
    276                  weak_ptr_factory_.GetWeakPtr()));
    277 
    278   if (!media_transfer_protocol_manager_) {
    279     scoped_refptr<base::MessageLoopProxy> loop_proxy =
    280         BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE);
    281     media_transfer_protocol_manager_.reset(
    282         device::MediaTransferProtocolManager::Initialize(loop_proxy));
    283   }
    284 
    285   media_transfer_protocol_device_observer_.reset(
    286       new MediaTransferProtocolDeviceObserverLinux(
    287           receiver(), media_transfer_protocol_manager_.get()));
    288 }
    289 
    290 bool StorageMonitorLinux::GetStorageInfoForPath(
    291     const base::FilePath& path,
    292     StorageInfo* device_info) const {
    293   DCHECK(device_info);
    294   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    295 
    296   // TODO(thestig) |media_transfer_protocol_device_observer_| should always be
    297   // valid.
    298   if (media_transfer_protocol_device_observer_ &&
    299       media_transfer_protocol_device_observer_->GetStorageInfoForPath(
    300           path, device_info)) {
    301     return true;
    302   }
    303 
    304   if (!path.IsAbsolute())
    305     return false;
    306 
    307   base::FilePath current = path;
    308   while (!ContainsKey(mount_info_map_, current) && current != current.DirName())
    309     current = current.DirName();
    310 
    311   MountMap::const_iterator mount_info = mount_info_map_.find(current);
    312   if (mount_info == mount_info_map_.end())
    313     return false;
    314   *device_info = mount_info->second.storage_info;
    315   return true;
    316 }
    317 
    318 device::MediaTransferProtocolManager*
    319 StorageMonitorLinux::media_transfer_protocol_manager() {
    320   return media_transfer_protocol_manager_.get();
    321 }
    322 
    323 void StorageMonitorLinux::SetGetDeviceInfoCallbackForTest(
    324     const GetDeviceInfoCallback& get_device_info_callback) {
    325   get_device_info_callback_ = get_device_info_callback;
    326 }
    327 
    328 void StorageMonitorLinux::SetMediaTransferProtocolManagerForTest(
    329     device::MediaTransferProtocolManager* test_manager) {
    330   DCHECK(!media_transfer_protocol_manager_);
    331   media_transfer_protocol_manager_.reset(test_manager);
    332 }
    333 
    334 void StorageMonitorLinux::EjectDevice(
    335     const std::string& device_id,
    336     base::Callback<void(EjectStatus)> callback) {
    337   // Find the mount point for the given device ID.
    338   base::FilePath path;
    339   base::FilePath device;
    340   for (MountMap::iterator mount_info = mount_info_map_.begin();
    341        mount_info != mount_info_map_.end(); ++mount_info) {
    342     if (mount_info->second.storage_info.device_id() == device_id) {
    343       path = mount_info->first;
    344       device = mount_info->second.mount_device;
    345       mount_info_map_.erase(mount_info);
    346       break;
    347     }
    348   }
    349 
    350   if (path.empty()) {
    351     callback.Run(EJECT_NO_SUCH_DEVICE);
    352     return;
    353   }
    354 
    355   receiver()->ProcessDetach(device_id);
    356 
    357   BrowserThread::PostTaskAndReplyWithResult(
    358       BrowserThread::FILE, FROM_HERE,
    359       base::Bind(&EjectPathOnFileThread, path, device),
    360       callback);
    361 }
    362 
    363 void StorageMonitorLinux::OnMtabWatcherCreated(MtabWatcherLinux* watcher) {
    364   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    365   mtab_watcher_.reset(watcher);
    366 }
    367 
    368 void StorageMonitorLinux::UpdateMtab(const MountPointDeviceMap& new_mtab) {
    369   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    370 
    371   // Check existing mtab entries for unaccounted mount points.
    372   // These mount points must have been removed in the new mtab.
    373   std::list<base::FilePath> mount_points_to_erase;
    374   std::list<base::FilePath> multiple_mounted_devices_needing_reattachment;
    375   for (MountMap::const_iterator old_iter = mount_info_map_.begin();
    376        old_iter != mount_info_map_.end(); ++old_iter) {
    377     const base::FilePath& mount_point = old_iter->first;
    378     const base::FilePath& mount_device = old_iter->second.mount_device;
    379     MountPointDeviceMap::const_iterator new_iter = new_mtab.find(mount_point);
    380     // |mount_point| not in |new_mtab| or |mount_device| is no longer mounted at
    381     // |mount_point|.
    382     if (new_iter == new_mtab.end() || (new_iter->second != mount_device)) {
    383       MountPriorityMap::iterator priority =
    384           mount_priority_map_.find(mount_device);
    385       DCHECK(priority != mount_priority_map_.end());
    386       ReferencedMountPoint::const_iterator has_priority =
    387           priority->second.find(mount_point);
    388       if (StorageInfo::IsRemovableDevice(
    389               old_iter->second.storage_info.device_id())) {
    390         DCHECK(has_priority != priority->second.end());
    391         if (has_priority->second) {
    392           receiver()->ProcessDetach(old_iter->second.storage_info.device_id());
    393         }
    394         if (priority->second.size() > 1)
    395           multiple_mounted_devices_needing_reattachment.push_back(mount_device);
    396       }
    397       priority->second.erase(mount_point);
    398       if (priority->second.empty())
    399         mount_priority_map_.erase(mount_device);
    400       mount_points_to_erase.push_back(mount_point);
    401     }
    402   }
    403 
    404   // Erase the |mount_info_map_| entries afterwards. Erasing in the loop above
    405   // using the iterator is slightly more efficient, but more tricky, since
    406   // calling std::map::erase() on an iterator invalidates it.
    407   for (std::list<base::FilePath>::const_iterator it =
    408            mount_points_to_erase.begin();
    409        it != mount_points_to_erase.end();
    410        ++it) {
    411     mount_info_map_.erase(*it);
    412   }
    413 
    414   // For any multiply mounted device where the mount that we had notified
    415   // got detached, send a notification of attachment for one of the other
    416   // mount points.
    417   for (std::list<base::FilePath>::const_iterator it =
    418            multiple_mounted_devices_needing_reattachment.begin();
    419        it != multiple_mounted_devices_needing_reattachment.end();
    420        ++it) {
    421     ReferencedMountPoint::iterator first_mount_point_info =
    422         mount_priority_map_.find(*it)->second.begin();
    423     const base::FilePath& mount_point = first_mount_point_info->first;
    424     first_mount_point_info->second = true;
    425 
    426     const StorageInfo& mount_info =
    427         mount_info_map_.find(mount_point)->second.storage_info;
    428     DCHECK(StorageInfo::IsRemovableDevice(mount_info.device_id()));
    429     receiver()->ProcessAttach(mount_info);
    430   }
    431 
    432   // Check new mtab entries against existing ones.
    433   for (MountPointDeviceMap::const_iterator new_iter = new_mtab.begin();
    434        new_iter != new_mtab.end(); ++new_iter) {
    435     const base::FilePath& mount_point = new_iter->first;
    436     const base::FilePath& mount_device = new_iter->second;
    437     MountMap::iterator old_iter = mount_info_map_.find(mount_point);
    438     if (old_iter == mount_info_map_.end() ||
    439         old_iter->second.mount_device != mount_device) {
    440       // New mount point found or an existing mount point found with a new
    441       // device.
    442       if (IsDeviceAlreadyMounted(mount_device)) {
    443         HandleDeviceMountedMultipleTimes(mount_device, mount_point);
    444       } else {
    445         BrowserThread::PostTaskAndReplyWithResult(
    446             BrowserThread::FILE, FROM_HERE,
    447             base::Bind(get_device_info_callback_, mount_device, mount_point),
    448             base::Bind(&StorageMonitorLinux::AddNewMount,
    449                        weak_ptr_factory_.GetWeakPtr(),
    450                        mount_device));
    451       }
    452     }
    453   }
    454 
    455   // Note: relies on scheduled tasks on the file thread being sequential. This
    456   // block needs to follow the for loop, so that the DoNothing call on the FILE
    457   // thread happens after the scheduled metadata retrievals, meaning that the
    458   // reply callback will then happen after all the AddNewMount calls.
    459   if (!IsInitialized()) {
    460     BrowserThread::PostTaskAndReply(
    461         BrowserThread::FILE, FROM_HERE,
    462         base::Bind(&base::DoNothing),
    463         base::Bind(&StorageMonitorLinux::MarkInitialized,
    464                    weak_ptr_factory_.GetWeakPtr()));
    465   }
    466 }
    467 
    468 bool StorageMonitorLinux::IsDeviceAlreadyMounted(
    469     const base::FilePath& mount_device) const {
    470   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    471   return ContainsKey(mount_priority_map_, mount_device);
    472 }
    473 
    474 void StorageMonitorLinux::HandleDeviceMountedMultipleTimes(
    475     const base::FilePath& mount_device,
    476     const base::FilePath& mount_point) {
    477   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    478 
    479   MountPriorityMap::iterator priority = mount_priority_map_.find(mount_device);
    480   DCHECK(priority != mount_priority_map_.end());
    481   const base::FilePath& other_mount_point = priority->second.begin()->first;
    482   priority->second[mount_point] = false;
    483   mount_info_map_[mount_point] =
    484       mount_info_map_.find(other_mount_point)->second;
    485 }
    486 
    487 void StorageMonitorLinux::AddNewMount(const base::FilePath& mount_device,
    488                                       scoped_ptr<StorageInfo> storage_info) {
    489   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    490 
    491   if (!storage_info)
    492     return;
    493 
    494   DCHECK(!storage_info->device_id().empty());
    495 
    496   bool removable = StorageInfo::IsRemovableDevice(storage_info->device_id());
    497   const base::FilePath mount_point(storage_info->location());
    498 
    499   MountPointInfo mount_point_info;
    500   mount_point_info.mount_device = mount_device;
    501   mount_point_info.storage_info = *storage_info;
    502   mount_info_map_[mount_point] = mount_point_info;
    503   mount_priority_map_[mount_device][mount_point] = removable;
    504   receiver()->ProcessAttach(*storage_info);
    505 }
    506 
    507 StorageMonitor* StorageMonitor::Create() {
    508   const base::FilePath kDefaultMtabPath("/etc/mtab");
    509   return new StorageMonitorLinux(kDefaultMtabPath);
    510 }
    511 
    512 }  // namespace chrome
    513