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
31using content::BrowserThread;
32
33namespace storage_monitor {
34
35typedef MtabWatcherLinux::MountPointDeviceMap MountPointDeviceMap;
36
37namespace {
38
39// udev device property constants.
40const char kBlockSubsystemKey[] = "block";
41const char kDiskDeviceTypeKey[] = "disk";
42const char kFsUUID[] = "ID_FS_UUID";
43const char kLabel[] = "ID_FS_LABEL";
44const char kModel[] = "ID_MODEL";
45const char kModelID[] = "ID_MODEL_ID";
46const char kRemovableSysAttr[] = "removable";
47const char kSerialShort[] = "ID_SERIAL_SHORT";
48const char kSizeSysAttr[] = "size";
49const char kVendor[] = "ID_VENDOR";
50const char kVendorID[] = "ID_VENDOR_ID";
51
52// Construct a device id using label or manufacturer (vendor and model) details.
53std::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.
79class 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.
99uint64 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.
119scoped_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
195MtabWatcherLinux* 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
203StorageMonitor::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
240StorageMonitorLinux::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
247StorageMonitorLinux::~StorageMonitorLinux() {
248  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
249}
250
251void 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
274bool 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
302device::MediaTransferProtocolManager*
303StorageMonitorLinux::media_transfer_protocol_manager() {
304  return media_transfer_protocol_manager_.get();
305}
306
307void StorageMonitorLinux::SetGetDeviceInfoCallbackForTest(
308    const GetDeviceInfoCallback& get_device_info_callback) {
309  get_device_info_callback_ = get_device_info_callback;
310}
311
312void StorageMonitorLinux::SetMediaTransferProtocolManagerForTest(
313    device::MediaTransferProtocolManager* test_manager) {
314  DCHECK(!media_transfer_protocol_manager_);
315  media_transfer_protocol_manager_.reset(test_manager);
316}
317
318void 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
358void StorageMonitorLinux::OnMtabWatcherCreated(MtabWatcherLinux* watcher) {
359  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
360  mtab_watcher_.reset(watcher);
361}
362
363void 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
463bool 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
469void 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
482void 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
502StorageMonitor* StorageMonitor::CreateInternal() {
503  const base::FilePath kDefaultMtabPath("/etc/mtab");
504  return new StorageMonitorLinux(kDefaultMtabPath);
505}
506
507}  // namespace storage_monitor
508