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