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