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. 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: 77 ScopedGetDeviceInfoResultRecorder() : result_(false) {} 78 ~ScopedGetDeviceInfoResultRecorder() { 79 UMA_HISTOGRAM_BOOLEAN("MediaDeviceNotification.UdevRequestSuccess", 80 result_); 81 } 82 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. 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. 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 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 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 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 244 StorageMonitorLinux::~StorageMonitorLinux() { 245 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 246 } 247 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 271 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* 300 StorageMonitorLinux::media_transfer_protocol_manager() { 301 return media_transfer_protocol_manager_.get(); 302 } 303 304 void StorageMonitorLinux::SetGetDeviceInfoCallbackForTest( 305 const GetDeviceInfoCallback& get_device_info_callback) { 306 get_device_info_callback_ = get_device_info_callback; 307 } 308 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 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 355 void StorageMonitorLinux::OnMtabWatcherCreated(MtabWatcherLinux* watcher) { 356 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 357 mtab_watcher_.reset(watcher); 358 } 359 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 460 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 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 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 499 StorageMonitor* StorageMonitor::Create() { 500 const base::FilePath kDefaultMtabPath("/etc/mtab"); 501 return new StorageMonitorLinux(kDefaultMtabPath); 502 } 503