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.
5 // StorageMonitorLinux implementation.
7 #include "components/storage_monitor/storage_monitor_linux.h"
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/scoped_udev.h"
31 using content::BrowserThread
;
33 namespace storage_monitor
{
35 typedef MtabWatcherLinux::MountPointDeviceMap MountPointDeviceMap
;
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";
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",
61 return kFSUniqueIdPrefix
+ uuid
;
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
,
71 if (vendor
.empty() && model
.empty() && serial_short
.empty())
74 return kVendorModelSerialPrefix
+ vendor
+ ":" + model
+ ":" + serial_short
;
77 // Records GetDeviceInfo result on destruction, to see how often we fail to get
79 class ScopedGetDeviceInfoResultRecorder
{
81 ScopedGetDeviceInfoResultRecorder() : result_(false) {}
82 ~ScopedGetDeviceInfoResultRecorder() {
83 UMA_HISTOGRAM_BOOLEAN("MediaDeviceNotification.UdevRequestSuccess",
87 void set_result(bool result
) {
94 DISALLOW_COPY_AND_ASSIGN(ScopedGetDeviceInfoResultRecorder
);
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
= device::udev_device_get_sysattr_value(
103 device
, kSizeSysAttr
);
105 // Keep track of device size, to see how often this information is
107 UMA_HISTOGRAM_BOOLEAN(
108 "RemovableDeviceNotificationsLinux.device_partition_size_available",
109 !partition_size
.empty());
111 uint64 total_size_in_bytes
= 0;
112 if (!base::StringToUint64(partition_size
, &total_size_in_bytes
))
114 return (total_size_in_bytes
<= kuint64max
/ 512) ?
115 total_size_in_bytes
* 512 : 0;
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());
124 scoped_ptr
<StorageInfo
> storage_info
;
126 ScopedGetDeviceInfoResultRecorder results_recorder
;
128 device::ScopedUdevPtr
udev_obj(device::udev_new());
130 return storage_info
.Pass();
132 struct stat device_stat
;
133 if (stat(device_path
.value().c_str(), &device_stat
) < 0)
134 return storage_info
.Pass();
137 if (S_ISCHR(device_stat
.st_mode
))
139 else if (S_ISBLK(device_stat
.st_mode
))
142 return storage_info
.Pass(); // Not a supported type.
144 device::ScopedUdevDevicePtr
device(
145 device::udev_device_new_from_devnum(udev_obj
.get(), device_type
,
146 device_stat
.st_rdev
));
148 return storage_info
.Pass();
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
));
157 std::string unique_id
= MakeDeviceUniqueId(device
.get());
159 // Keep track of device info details to see how often we get invalid values.
160 MediaStorageUtil::RecordDeviceInfoHistogram(true, unique_id
, volume_label
);
163 device::udev_device_get_sysattr_value(device
.get(), kRemovableSysAttr
);
165 // |parent_device| is owned by |device| and does not need to be cleaned
167 struct udev_device
* parent_device
=
168 device::udev_device_get_parent_with_subsystem_devtype(
172 value
= device::udev_device_get_sysattr_value(parent_device
,
175 const bool is_removable
= (value
&& atoi(value
) == 1);
177 StorageInfo::Type type
= StorageInfo::FIXED_MASS_STORAGE
;
179 if (MediaStorageUtil::HasDcim(mount_point
))
180 type
= StorageInfo::REMOVABLE_MASS_STORAGE_WITH_DCIM
;
182 type
= StorageInfo::REMOVABLE_MASS_STORAGE_NO_DCIM
;
185 results_recorder
.set_result(true);
187 storage_info
.reset(new StorageInfo(
188 StorageInfo::MakeDeviceId(type
, unique_id
),
193 GetDeviceStorageSize(device_path
, device
.get())));
194 return storage_info
.Pass();
197 MtabWatcherLinux
* CreateMtabWatcherLinuxOnFileThread(
198 const base::FilePath
& mtab_path
,
199 base::WeakPtr
<MtabWatcherLinux::Delegate
> delegate
) {
200 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
202 return new MtabWatcherLinux(mtab_path
, delegate
);
205 StorageMonitor::EjectStatus
EjectPathOnFileThread(
206 const base::FilePath
& path
,
207 const base::FilePath
& device
) {
208 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
210 // Note: Linux LSB says umount should exist in /bin.
211 static const char kUmountBinary
[] = "/bin/umount";
212 std::vector
<std::string
> command
;
213 command
.push_back(kUmountBinary
);
214 command
.push_back(path
.value());
216 base::LaunchOptions options
;
217 base::ProcessHandle handle
;
218 if (!base::LaunchProcess(command
, options
, &handle
))
219 return StorageMonitor::EJECT_FAILURE
;
222 if (!base::WaitForExitCodeWithTimeout(handle
, &exit_code
,
223 base::TimeDelta::FromMilliseconds(3000))) {
224 base::KillProcess(handle
, -1, false);
225 base::EnsureProcessTerminated(base::Process(handle
));
226 return StorageMonitor::EJECT_FAILURE
;
229 // TODO(gbillock): Make sure this is found in documentation
230 // somewhere. Experimentally it seems to hold that exit code
231 // 1 means device is in use.
233 return StorageMonitor::EJECT_IN_USE
;
235 return StorageMonitor::EJECT_FAILURE
;
237 return StorageMonitor::EJECT_OK
;
242 StorageMonitorLinux::StorageMonitorLinux(const base::FilePath
& path
)
244 get_device_info_callback_(base::Bind(&GetDeviceInfo
)),
245 weak_ptr_factory_(this) {
246 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
249 StorageMonitorLinux::~StorageMonitorLinux() {
250 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
253 void StorageMonitorLinux::Init() {
254 DCHECK(!mtab_path_
.empty());
256 BrowserThread::PostTaskAndReplyWithResult(
257 BrowserThread::FILE, FROM_HERE
,
258 base::Bind(&CreateMtabWatcherLinuxOnFileThread
,
260 weak_ptr_factory_
.GetWeakPtr()),
261 base::Bind(&StorageMonitorLinux::OnMtabWatcherCreated
,
262 weak_ptr_factory_
.GetWeakPtr()));
264 if (!media_transfer_protocol_manager_
) {
265 scoped_refptr
<base::MessageLoopProxy
> loop_proxy
=
266 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE);
267 media_transfer_protocol_manager_
.reset(
268 device::MediaTransferProtocolManager::Initialize(loop_proxy
));
271 media_transfer_protocol_device_observer_
.reset(
272 new MediaTransferProtocolDeviceObserverLinux(
273 receiver(), media_transfer_protocol_manager_
.get()));
276 bool StorageMonitorLinux::GetStorageInfoForPath(
277 const base::FilePath
& path
,
278 StorageInfo
* device_info
) const {
280 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
282 // TODO(thestig) |media_transfer_protocol_device_observer_| should always be
284 if (media_transfer_protocol_device_observer_
&&
285 media_transfer_protocol_device_observer_
->GetStorageInfoForPath(
286 path
, device_info
)) {
290 if (!path
.IsAbsolute())
293 base::FilePath current
= path
;
294 while (!ContainsKey(mount_info_map_
, current
) && current
!= current
.DirName())
295 current
= current
.DirName();
297 MountMap::const_iterator mount_info
= mount_info_map_
.find(current
);
298 if (mount_info
== mount_info_map_
.end())
300 *device_info
= mount_info
->second
.storage_info
;
304 device::MediaTransferProtocolManager
*
305 StorageMonitorLinux::media_transfer_protocol_manager() {
306 return media_transfer_protocol_manager_
.get();
309 void StorageMonitorLinux::SetGetDeviceInfoCallbackForTest(
310 const GetDeviceInfoCallback
& get_device_info_callback
) {
311 get_device_info_callback_
= get_device_info_callback
;
314 void StorageMonitorLinux::SetMediaTransferProtocolManagerForTest(
315 device::MediaTransferProtocolManager
* test_manager
) {
316 DCHECK(!media_transfer_protocol_manager_
);
317 media_transfer_protocol_manager_
.reset(test_manager
);
320 void StorageMonitorLinux::EjectDevice(
321 const std::string
& device_id
,
322 base::Callback
<void(EjectStatus
)> callback
) {
323 StorageInfo::Type type
;
324 if (!StorageInfo::CrackDeviceId(device_id
, &type
, NULL
)) {
325 callback
.Run(EJECT_FAILURE
);
329 if (type
== StorageInfo::MTP_OR_PTP
) {
330 media_transfer_protocol_device_observer_
->EjectDevice(device_id
, callback
);
334 // Find the mount point for the given device ID.
336 base::FilePath device
;
337 for (MountMap::iterator mount_info
= mount_info_map_
.begin();
338 mount_info
!= mount_info_map_
.end(); ++mount_info
) {
339 if (mount_info
->second
.storage_info
.device_id() == device_id
) {
340 path
= mount_info
->first
;
341 device
= mount_info
->second
.mount_device
;
342 mount_info_map_
.erase(mount_info
);
348 callback
.Run(EJECT_NO_SUCH_DEVICE
);
352 receiver()->ProcessDetach(device_id
);
354 BrowserThread::PostTaskAndReplyWithResult(
355 BrowserThread::FILE, FROM_HERE
,
356 base::Bind(&EjectPathOnFileThread
, path
, device
),
360 void StorageMonitorLinux::OnMtabWatcherCreated(MtabWatcherLinux
* watcher
) {
361 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
362 mtab_watcher_
.reset(watcher
);
365 void StorageMonitorLinux::UpdateMtab(const MountPointDeviceMap
& new_mtab
) {
366 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
368 // Check existing mtab entries for unaccounted mount points.
369 // These mount points must have been removed in the new mtab.
370 std::list
<base::FilePath
> mount_points_to_erase
;
371 std::list
<base::FilePath
> multiple_mounted_devices_needing_reattachment
;
372 for (MountMap::const_iterator old_iter
= mount_info_map_
.begin();
373 old_iter
!= mount_info_map_
.end(); ++old_iter
) {
374 const base::FilePath
& mount_point
= old_iter
->first
;
375 const base::FilePath
& mount_device
= old_iter
->second
.mount_device
;
376 MountPointDeviceMap::const_iterator new_iter
= new_mtab
.find(mount_point
);
377 // |mount_point| not in |new_mtab| or |mount_device| is no longer mounted at
379 if (new_iter
== new_mtab
.end() || (new_iter
->second
!= mount_device
)) {
380 MountPriorityMap::iterator priority
=
381 mount_priority_map_
.find(mount_device
);
382 DCHECK(priority
!= mount_priority_map_
.end());
383 ReferencedMountPoint::const_iterator has_priority
=
384 priority
->second
.find(mount_point
);
385 if (StorageInfo::IsRemovableDevice(
386 old_iter
->second
.storage_info
.device_id())) {
387 DCHECK(has_priority
!= priority
->second
.end());
388 if (has_priority
->second
) {
389 receiver()->ProcessDetach(old_iter
->second
.storage_info
.device_id());
391 if (priority
->second
.size() > 1)
392 multiple_mounted_devices_needing_reattachment
.push_back(mount_device
);
394 priority
->second
.erase(mount_point
);
395 if (priority
->second
.empty())
396 mount_priority_map_
.erase(mount_device
);
397 mount_points_to_erase
.push_back(mount_point
);
401 // Erase the |mount_info_map_| entries afterwards. Erasing in the loop above
402 // using the iterator is slightly more efficient, but more tricky, since
403 // calling std::map::erase() on an iterator invalidates it.
404 for (std::list
<base::FilePath
>::const_iterator it
=
405 mount_points_to_erase
.begin();
406 it
!= mount_points_to_erase
.end();
408 mount_info_map_
.erase(*it
);
411 // For any multiply mounted device where the mount that we had notified
412 // got detached, send a notification of attachment for one of the other
414 for (std::list
<base::FilePath
>::const_iterator it
=
415 multiple_mounted_devices_needing_reattachment
.begin();
416 it
!= multiple_mounted_devices_needing_reattachment
.end();
418 ReferencedMountPoint::iterator first_mount_point_info
=
419 mount_priority_map_
.find(*it
)->second
.begin();
420 const base::FilePath
& mount_point
= first_mount_point_info
->first
;
421 first_mount_point_info
->second
= true;
423 const StorageInfo
& mount_info
=
424 mount_info_map_
.find(mount_point
)->second
.storage_info
;
425 DCHECK(StorageInfo::IsRemovableDevice(mount_info
.device_id()));
426 receiver()->ProcessAttach(mount_info
);
429 // Check new mtab entries against existing ones.
430 for (MountPointDeviceMap::const_iterator new_iter
= new_mtab
.begin();
431 new_iter
!= new_mtab
.end(); ++new_iter
) {
432 const base::FilePath
& mount_point
= new_iter
->first
;
433 const base::FilePath
& mount_device
= new_iter
->second
;
434 MountMap::iterator old_iter
= mount_info_map_
.find(mount_point
);
435 if (old_iter
== mount_info_map_
.end() ||
436 old_iter
->second
.mount_device
!= mount_device
) {
437 // New mount point found or an existing mount point found with a new
439 if (IsDeviceAlreadyMounted(mount_device
)) {
440 HandleDeviceMountedMultipleTimes(mount_device
, mount_point
);
442 BrowserThread::PostTaskAndReplyWithResult(
443 BrowserThread::FILE, FROM_HERE
,
444 base::Bind(get_device_info_callback_
, mount_device
, mount_point
),
445 base::Bind(&StorageMonitorLinux::AddNewMount
,
446 weak_ptr_factory_
.GetWeakPtr(),
452 // Note: relies on scheduled tasks on the file thread being sequential. This
453 // block needs to follow the for loop, so that the DoNothing call on the FILE
454 // thread happens after the scheduled metadata retrievals, meaning that the
455 // reply callback will then happen after all the AddNewMount calls.
456 if (!IsInitialized()) {
457 BrowserThread::PostTaskAndReply(
458 BrowserThread::FILE, FROM_HERE
,
459 base::Bind(&base::DoNothing
),
460 base::Bind(&StorageMonitorLinux::MarkInitialized
,
461 weak_ptr_factory_
.GetWeakPtr()));
465 bool StorageMonitorLinux::IsDeviceAlreadyMounted(
466 const base::FilePath
& mount_device
) const {
467 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
468 return ContainsKey(mount_priority_map_
, mount_device
);
471 void StorageMonitorLinux::HandleDeviceMountedMultipleTimes(
472 const base::FilePath
& mount_device
,
473 const base::FilePath
& mount_point
) {
474 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
476 MountPriorityMap::iterator priority
= mount_priority_map_
.find(mount_device
);
477 DCHECK(priority
!= mount_priority_map_
.end());
478 const base::FilePath
& other_mount_point
= priority
->second
.begin()->first
;
479 priority
->second
[mount_point
] = false;
480 mount_info_map_
[mount_point
] =
481 mount_info_map_
.find(other_mount_point
)->second
;
484 void StorageMonitorLinux::AddNewMount(const base::FilePath
& mount_device
,
485 scoped_ptr
<StorageInfo
> storage_info
) {
486 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
491 DCHECK(!storage_info
->device_id().empty());
493 bool removable
= StorageInfo::IsRemovableDevice(storage_info
->device_id());
494 const base::FilePath
mount_point(storage_info
->location());
496 MountPointInfo mount_point_info
;
497 mount_point_info
.mount_device
= mount_device
;
498 mount_point_info
.storage_info
= *storage_info
;
499 mount_info_map_
[mount_point
] = mount_point_info
;
500 mount_priority_map_
[mount_device
][mount_point
] = removable
;
501 receiver()->ProcessAttach(*storage_info
);
504 StorageMonitor
* StorageMonitor::CreateInternal() {
505 const base::FilePath
kDefaultMtabPath("/etc/mtab");
506 return new StorageMonitorLinux(kDefaultMtabPath
);
509 } // namespace storage_monitor