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 #include "components/storage_monitor/volume_mount_watcher_win.h" 6 7 #include <windows.h> 8 9 #include <dbt.h> 10 #include <fileapi.h> 11 #include <shlobj.h> 12 #include <winioctl.h> 13 14 #include "base/bind_helpers.h" 15 #include "base/metrics/histogram.h" 16 #include "base/stl_util.h" 17 #include "base/strings/string_number_conversions.h" 18 #include "base/strings/string_util.h" 19 #include "base/strings/stringprintf.h" 20 #include "base/strings/utf_string_conversions.h" 21 #include "base/task_runner_util.h" 22 #include "base/time/time.h" 23 #include "base/win/scoped_handle.h" 24 #include "components/storage_monitor/media_storage_util.h" 25 #include "components/storage_monitor/storage_info.h" 26 #include "content/public/browser/browser_thread.h" 27 #include "content/public/browser/user_metrics.h" 28 29 using content::BrowserThread; 30 31 namespace storage_monitor { 32 33 namespace { 34 35 const DWORD kMaxPathBufLen = MAX_PATH + 1; 36 37 enum DeviceType { 38 FLOPPY, 39 REMOVABLE, 40 FIXED, 41 }; 42 43 // Histogram values for recording frequencies of eject attempts and 44 // outcomes. 45 enum EjectWinLockOutcomes { 46 LOCK_ATTEMPT, 47 LOCK_TIMEOUT, 48 LOCK_TIMEOUT2, 49 NUM_LOCK_OUTCOMES, 50 }; 51 52 // We are trying to figure out whether the drive is a fixed volume, 53 // a removable storage, or a floppy. A "floppy" here means "a volume we 54 // want to basically ignore because it won't fit media and will spin 55 // if we touch it to get volume metadata." GetDriveType returns DRIVE_REMOVABLE 56 // on either floppy or removable volumes. The DRIVE_CDROM type is handled 57 // as a floppy, as are DRIVE_UNKNOWN and DRIVE_NO_ROOT_DIR, as there are 58 // reports that some floppy drives don't report as DRIVE_REMOVABLE. 59 DeviceType GetDeviceType(const base::string16& mount_point) { 60 UINT drive_type = GetDriveType(mount_point.c_str()); 61 if (drive_type == DRIVE_FIXED || drive_type == DRIVE_REMOTE || 62 drive_type == DRIVE_RAMDISK) { 63 return FIXED; 64 } 65 if (drive_type != DRIVE_REMOVABLE) 66 return FLOPPY; 67 68 // Check device strings of the form "X:" and "\\.\X:" 69 // For floppy drives, these will return strings like "/Device/Floppy0" 70 base::string16 device = mount_point; 71 if (EndsWith(mount_point, L"\\", false)) 72 device = mount_point.substr(0, mount_point.length() - 1); 73 base::string16 device_path; 74 base::string16 device_path_slash; 75 DWORD dos_device = QueryDosDevice( 76 device.c_str(), WriteInto(&device_path, kMaxPathBufLen), kMaxPathBufLen); 77 base::string16 device_slash = base::string16(L"\\\\.\\"); 78 device_slash += device; 79 DWORD dos_device_slash = QueryDosDevice( 80 device_slash.c_str(), WriteInto(&device_path_slash, kMaxPathBufLen), 81 kMaxPathBufLen); 82 if (dos_device == 0 && dos_device_slash == 0) 83 return FLOPPY; 84 if (device_path.find(L"Floppy") != base::string16::npos || 85 device_path_slash.find(L"Floppy") != base::string16::npos) { 86 return FLOPPY; 87 } 88 89 return REMOVABLE; 90 } 91 92 // Returns 0 if the devicetype is not volume. 93 uint32 GetVolumeBitMaskFromBroadcastHeader(LPARAM data) { 94 DEV_BROADCAST_VOLUME* dev_broadcast_volume = 95 reinterpret_cast<DEV_BROADCAST_VOLUME*>(data); 96 if (dev_broadcast_volume->dbcv_devicetype == DBT_DEVTYP_VOLUME) 97 return dev_broadcast_volume->dbcv_unitmask; 98 return 0; 99 } 100 101 // Returns true if |data| represents a logical volume structure. 102 bool IsLogicalVolumeStructure(LPARAM data) { 103 DEV_BROADCAST_HDR* broadcast_hdr = 104 reinterpret_cast<DEV_BROADCAST_HDR*>(data); 105 return broadcast_hdr != NULL && 106 broadcast_hdr->dbch_devicetype == DBT_DEVTYP_VOLUME; 107 } 108 109 // Gets the total volume of the |mount_point| in bytes. 110 uint64 GetVolumeSize(const base::string16& mount_point) { 111 ULARGE_INTEGER total; 112 if (!GetDiskFreeSpaceExW(mount_point.c_str(), NULL, &total, NULL)) 113 return 0; 114 return total.QuadPart; 115 } 116 117 // Gets mass storage device information given a |device_path|. On success, 118 // returns true and fills in |info|. 119 // The following msdn blog entry is helpful for understanding disk volumes 120 // and how they are treated in Windows: 121 // http://blogs.msdn.com/b/adioltean/archive/2005/04/16/408947.aspx. 122 bool GetDeviceDetails(const base::FilePath& device_path, StorageInfo* info) { 123 DCHECK(info); 124 125 base::string16 mount_point; 126 if (!GetVolumePathName(device_path.value().c_str(), 127 WriteInto(&mount_point, kMaxPathBufLen), 128 kMaxPathBufLen)) { 129 return false; 130 } 131 mount_point.resize(wcslen(mount_point.c_str())); 132 133 // Note: experimentally this code does not spin a floppy drive. It 134 // returns a GUID associated with the device, not the volume. 135 base::string16 guid; 136 if (!GetVolumeNameForVolumeMountPoint(mount_point.c_str(), 137 WriteInto(&guid, kMaxPathBufLen), 138 kMaxPathBufLen)) { 139 return false; 140 } 141 // In case it has two GUID's (see above mentioned blog), do it again. 142 if (!GetVolumeNameForVolumeMountPoint(guid.c_str(), 143 WriteInto(&guid, kMaxPathBufLen), 144 kMaxPathBufLen)) { 145 return false; 146 } 147 148 // If we're adding a floppy drive, return without querying any more 149 // drive metadata -- it will cause the floppy drive to seek. 150 // Note: treats FLOPPY as FIXED_MASS_STORAGE. This is intentional. 151 DeviceType device_type = GetDeviceType(mount_point); 152 if (device_type == FLOPPY) { 153 info->set_device_id(StorageInfo::MakeDeviceId( 154 StorageInfo::FIXED_MASS_STORAGE, base::UTF16ToUTF8(guid))); 155 return true; 156 } 157 158 StorageInfo::Type type = StorageInfo::FIXED_MASS_STORAGE; 159 if (device_type == REMOVABLE) { 160 type = StorageInfo::REMOVABLE_MASS_STORAGE_NO_DCIM; 161 if (MediaStorageUtil::HasDcim(base::FilePath(mount_point))) 162 type = StorageInfo::REMOVABLE_MASS_STORAGE_WITH_DCIM; 163 } 164 165 // NOTE: experimentally, this function returns false if there is no volume 166 // name set. 167 base::string16 volume_label; 168 GetVolumeInformationW(device_path.value().c_str(), 169 WriteInto(&volume_label, kMaxPathBufLen), 170 kMaxPathBufLen, NULL, NULL, NULL, NULL, 0); 171 172 uint64 total_size_in_bytes = GetVolumeSize(mount_point); 173 std::string device_id = 174 StorageInfo::MakeDeviceId(type, base::UTF16ToUTF8(guid)); 175 176 // TODO(gbillock): if volume_label.empty(), get the vendor/model information 177 // for the volume. 178 *info = StorageInfo(device_id, mount_point, volume_label, base::string16(), 179 base::string16(), total_size_in_bytes); 180 return true; 181 } 182 183 // Returns a vector of all the removable mass storage devices that are 184 // connected. 185 std::vector<base::FilePath> GetAttachedDevices() { 186 std::vector<base::FilePath> result; 187 base::string16 volume_name; 188 HANDLE find_handle = FindFirstVolume(WriteInto(&volume_name, kMaxPathBufLen), 189 kMaxPathBufLen); 190 if (find_handle == INVALID_HANDLE_VALUE) 191 return result; 192 193 while (true) { 194 base::string16 volume_path; 195 DWORD return_count; 196 if (GetVolumePathNamesForVolumeName(volume_name.c_str(), 197 WriteInto(&volume_path, kMaxPathBufLen), 198 kMaxPathBufLen, &return_count)) { 199 result.push_back(base::FilePath(volume_path)); 200 } 201 if (!FindNextVolume(find_handle, WriteInto(&volume_name, kMaxPathBufLen), 202 kMaxPathBufLen)) { 203 if (GetLastError() != ERROR_NO_MORE_FILES) 204 DPLOG(ERROR); 205 break; 206 } 207 } 208 209 FindVolumeClose(find_handle); 210 return result; 211 } 212 213 // Eject a removable volume at the specified |device| path. This works by 214 // 1) locking the volume, 215 // 2) unmounting the volume, 216 // 3) ejecting the volume. 217 // If the lock fails, it will re-schedule itself. 218 // See http://support.microsoft.com/kb/165721 219 void EjectDeviceInThreadPool( 220 const base::FilePath& device, 221 base::Callback<void(StorageMonitor::EjectStatus)> callback, 222 scoped_refptr<base::SequencedTaskRunner> task_runner, 223 int iteration) { 224 base::FilePath::StringType volume_name; 225 base::FilePath::CharType drive_letter = device.value()[0]; 226 // Don't try to eject if the path isn't a simple one -- we're not 227 // sure how to do that yet. Need to figure out how to eject volumes mounted 228 // at not-just-drive-letter paths. 229 if (drive_letter < L'A' || drive_letter > L'Z' || 230 device != device.DirName()) { 231 BrowserThread::PostTask( 232 BrowserThread::UI, FROM_HERE, 233 base::Bind(callback, StorageMonitor::EJECT_FAILURE)); 234 return; 235 } 236 base::SStringPrintf(&volume_name, L"\\\\.\\%lc:", drive_letter); 237 238 base::win::ScopedHandle volume_handle(CreateFile( 239 volume_name.c_str(), 240 GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, 241 NULL, OPEN_EXISTING, 0, NULL)); 242 243 if (!volume_handle.IsValid()) { 244 BrowserThread::PostTask( 245 BrowserThread::UI, FROM_HERE, 246 base::Bind(callback, StorageMonitor::EJECT_FAILURE)); 247 return; 248 } 249 250 DWORD bytes_returned = 0; // Unused, but necessary for ioctl's. 251 252 // Lock the drive to be ejected (so that other processes can't open 253 // files on it). If this fails, it means some other process has files 254 // open on the device. Note that the lock is released when the volume 255 // handle is closed, and this is done by the ScopedHandle above. 256 BOOL locked = DeviceIoControl(volume_handle, FSCTL_LOCK_VOLUME, 257 NULL, 0, NULL, 0, &bytes_returned, NULL); 258 UMA_HISTOGRAM_ENUMERATION("StorageMonitor.EjectWinLock", 259 LOCK_ATTEMPT, NUM_LOCK_OUTCOMES); 260 if (!locked) { 261 UMA_HISTOGRAM_ENUMERATION("StorageMonitor.EjectWinLock", 262 iteration == 0 ? LOCK_TIMEOUT : LOCK_TIMEOUT2, 263 NUM_LOCK_OUTCOMES); 264 const int kNumLockRetries = 1; 265 const base::TimeDelta kLockRetryInterval = 266 base::TimeDelta::FromMilliseconds(500); 267 if (iteration < kNumLockRetries) { 268 // Try again -- the lock may have been a transient one. This happens on 269 // things like AV disk lock for some reason, or another process 270 // transient disk lock. 271 task_runner->PostDelayedTask( 272 FROM_HERE, 273 base::Bind(&EjectDeviceInThreadPool, 274 device, callback, task_runner, iteration + 1), 275 kLockRetryInterval); 276 return; 277 } 278 279 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, 280 base::Bind(callback, StorageMonitor::EJECT_IN_USE)); 281 return; 282 } 283 284 // Unmount the device from the filesystem -- this will remove it from 285 // the file picker, drive enumerations, etc. 286 BOOL dismounted = DeviceIoControl(volume_handle, FSCTL_DISMOUNT_VOLUME, 287 NULL, 0, NULL, 0, &bytes_returned, NULL); 288 289 // Reached if we acquired a lock, but could not dismount. This might 290 // occur if another process unmounted without locking. Call this OK, 291 // since the volume is now unreachable. 292 if (!dismounted) { 293 DeviceIoControl(volume_handle, FSCTL_UNLOCK_VOLUME, 294 NULL, 0, NULL, 0, &bytes_returned, NULL); 295 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, 296 base::Bind(callback, StorageMonitor::EJECT_OK)); 297 return; 298 } 299 300 PREVENT_MEDIA_REMOVAL pmr_buffer; 301 pmr_buffer.PreventMediaRemoval = FALSE; 302 // Mark the device as safe to remove. 303 if (!DeviceIoControl(volume_handle, IOCTL_STORAGE_MEDIA_REMOVAL, 304 &pmr_buffer, sizeof(PREVENT_MEDIA_REMOVAL), 305 NULL, 0, &bytes_returned, NULL)) { 306 BrowserThread::PostTask( 307 BrowserThread::UI, FROM_HERE, 308 base::Bind(callback, StorageMonitor::EJECT_FAILURE)); 309 return; 310 } 311 312 // Physically eject or soft-eject the device. 313 if (!DeviceIoControl(volume_handle, IOCTL_STORAGE_EJECT_MEDIA, 314 NULL, 0, NULL, 0, &bytes_returned, NULL)) { 315 BrowserThread::PostTask( 316 BrowserThread::UI, FROM_HERE, 317 base::Bind(callback, StorageMonitor::EJECT_FAILURE)); 318 return; 319 } 320 321 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, 322 base::Bind(callback, StorageMonitor::EJECT_OK)); 323 } 324 325 } // namespace 326 327 const int kWorkerPoolNumThreads = 3; 328 const char* kWorkerPoolNamePrefix = "DeviceInfoPool"; 329 330 VolumeMountWatcherWin::VolumeMountWatcherWin() 331 : device_info_worker_pool_(new base::SequencedWorkerPool( 332 kWorkerPoolNumThreads, kWorkerPoolNamePrefix)), 333 notifications_(NULL), 334 weak_factory_(this) { 335 task_runner_ = 336 device_info_worker_pool_->GetSequencedTaskRunnerWithShutdownBehavior( 337 device_info_worker_pool_->GetSequenceToken(), 338 base::SequencedWorkerPool::CONTINUE_ON_SHUTDOWN); 339 } 340 341 // static 342 base::FilePath VolumeMountWatcherWin::DriveNumberToFilePath(int drive_number) { 343 if (drive_number < 0 || drive_number > 25) 344 return base::FilePath(); 345 base::string16 path(L"_:\\"); 346 path[0] = L'A' + drive_number; 347 return base::FilePath(path); 348 } 349 350 // In order to get all the weak pointers created on the UI thread, and doing 351 // synchronous Windows calls in the worker pool, this kicks off a chain of 352 // events which will 353 // a) Enumerate attached devices 354 // b) Create weak pointers for which to send completion signals from 355 // c) Retrieve metadata on the volumes and then 356 // d) Notify that metadata to listeners. 357 void VolumeMountWatcherWin::Init() { 358 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 359 360 // When VolumeMountWatcherWin is created, the message pumps are not running 361 // so a posted task from the constructor would never run. Therefore, do all 362 // the initializations here. 363 base::PostTaskAndReplyWithResult(task_runner_, FROM_HERE, 364 GetAttachedDevicesCallback(), 365 base::Bind(&VolumeMountWatcherWin::AddDevicesOnUIThread, 366 weak_factory_.GetWeakPtr())); 367 } 368 369 void VolumeMountWatcherWin::AddDevicesOnUIThread( 370 std::vector<base::FilePath> removable_devices) { 371 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 372 373 for (size_t i = 0; i < removable_devices.size(); i++) { 374 if (ContainsKey(pending_device_checks_, removable_devices[i])) 375 continue; 376 pending_device_checks_.insert(removable_devices[i]); 377 task_runner_->PostTask( 378 FROM_HERE, 379 base::Bind(&VolumeMountWatcherWin::RetrieveInfoForDeviceAndAdd, 380 removable_devices[i], GetDeviceDetailsCallback(), 381 weak_factory_.GetWeakPtr())); 382 } 383 } 384 385 // static 386 void VolumeMountWatcherWin::RetrieveInfoForDeviceAndAdd( 387 const base::FilePath& device_path, 388 const GetDeviceDetailsCallbackType& get_device_details_callback, 389 base::WeakPtr<VolumeMountWatcherWin> volume_watcher) { 390 StorageInfo info; 391 if (!get_device_details_callback.Run(device_path, &info)) { 392 BrowserThread::PostTask( 393 BrowserThread::UI, FROM_HERE, 394 base::Bind(&VolumeMountWatcherWin::DeviceCheckComplete, 395 volume_watcher, device_path)); 396 return; 397 } 398 399 BrowserThread::PostTask( 400 BrowserThread::UI, FROM_HERE, 401 base::Bind(&VolumeMountWatcherWin::HandleDeviceAttachEventOnUIThread, 402 volume_watcher, device_path, info)); 403 } 404 405 void VolumeMountWatcherWin::DeviceCheckComplete( 406 const base::FilePath& device_path) { 407 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 408 pending_device_checks_.erase(device_path); 409 410 if (pending_device_checks_.size() == 0) { 411 if (notifications_) 412 notifications_->MarkInitialized(); 413 } 414 } 415 416 VolumeMountWatcherWin::GetAttachedDevicesCallbackType 417 VolumeMountWatcherWin::GetAttachedDevicesCallback() const { 418 return base::Bind(&GetAttachedDevices); 419 } 420 421 VolumeMountWatcherWin::GetDeviceDetailsCallbackType 422 VolumeMountWatcherWin::GetDeviceDetailsCallback() const { 423 return base::Bind(&GetDeviceDetails); 424 } 425 426 bool VolumeMountWatcherWin::GetDeviceInfo(const base::FilePath& device_path, 427 StorageInfo* info) const { 428 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 429 DCHECK(info); 430 base::FilePath path(device_path); 431 MountPointDeviceMetadataMap::const_iterator iter = 432 device_metadata_.find(path); 433 while (iter == device_metadata_.end() && path.DirName() != path) { 434 path = path.DirName(); 435 iter = device_metadata_.find(path); 436 } 437 438 if (iter == device_metadata_.end()) 439 return false; 440 441 *info = iter->second; 442 return true; 443 } 444 445 void VolumeMountWatcherWin::OnWindowMessage(UINT event_type, LPARAM data) { 446 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 447 switch (event_type) { 448 case DBT_DEVICEARRIVAL: { 449 if (IsLogicalVolumeStructure(data)) { 450 DWORD unitmask = GetVolumeBitMaskFromBroadcastHeader(data); 451 std::vector<base::FilePath> paths; 452 for (int i = 0; unitmask; ++i, unitmask >>= 1) { 453 if (!(unitmask & 0x01)) 454 continue; 455 paths.push_back(DriveNumberToFilePath(i)); 456 } 457 AddDevicesOnUIThread(paths); 458 } 459 break; 460 } 461 case DBT_DEVICEREMOVECOMPLETE: { 462 if (IsLogicalVolumeStructure(data)) { 463 DWORD unitmask = GetVolumeBitMaskFromBroadcastHeader(data); 464 for (int i = 0; unitmask; ++i, unitmask >>= 1) { 465 if (!(unitmask & 0x01)) 466 continue; 467 HandleDeviceDetachEventOnUIThread(DriveNumberToFilePath(i).value()); 468 } 469 } 470 break; 471 } 472 } 473 } 474 475 void VolumeMountWatcherWin::OnMediaChange(WPARAM wparam, LPARAM lparam) { 476 if (lparam == SHCNE_MEDIAINSERTED || lparam == SHCNE_MEDIAREMOVED) { 477 struct _ITEMIDLIST* pidl = *reinterpret_cast<struct _ITEMIDLIST**>( 478 wparam); 479 wchar_t sPath[MAX_PATH]; 480 if (!SHGetPathFromIDList(pidl, sPath)) { 481 DVLOG(1) << "MediaInserted: SHGetPathFromIDList failed"; 482 return; 483 } 484 switch (lparam) { 485 case SHCNE_MEDIAINSERTED: { 486 std::vector<base::FilePath> paths; 487 paths.push_back(base::FilePath(sPath)); 488 AddDevicesOnUIThread(paths); 489 break; 490 } 491 case SHCNE_MEDIAREMOVED: { 492 HandleDeviceDetachEventOnUIThread(sPath); 493 break; 494 } 495 } 496 } 497 } 498 499 void VolumeMountWatcherWin::SetNotifications( 500 StorageMonitor::Receiver* notifications) { 501 notifications_ = notifications; 502 } 503 504 VolumeMountWatcherWin::~VolumeMountWatcherWin() { 505 weak_factory_.InvalidateWeakPtrs(); 506 device_info_worker_pool_->Shutdown(); 507 } 508 509 void VolumeMountWatcherWin::HandleDeviceAttachEventOnUIThread( 510 const base::FilePath& device_path, 511 const StorageInfo& info) { 512 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 513 514 device_metadata_[device_path] = info; 515 516 if (notifications_) 517 notifications_->ProcessAttach(info); 518 519 DeviceCheckComplete(device_path); 520 } 521 522 void VolumeMountWatcherWin::HandleDeviceDetachEventOnUIThread( 523 const base::string16& device_location) { 524 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 525 526 MountPointDeviceMetadataMap::const_iterator device_info = 527 device_metadata_.find(base::FilePath(device_location)); 528 // If the device isn't type removable (like a CD), it won't be there. 529 if (device_info == device_metadata_.end()) 530 return; 531 532 if (notifications_) 533 notifications_->ProcessDetach(device_info->second.device_id()); 534 device_metadata_.erase(device_info); 535 } 536 537 void VolumeMountWatcherWin::EjectDevice( 538 const std::string& device_id, 539 base::Callback<void(StorageMonitor::EjectStatus)> callback) { 540 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 541 base::FilePath device = MediaStorageUtil::FindDevicePathById(device_id); 542 if (device.empty()) { 543 callback.Run(StorageMonitor::EJECT_FAILURE); 544 return; 545 } 546 if (device_metadata_.erase(device) == 0) { 547 callback.Run(StorageMonitor::EJECT_FAILURE); 548 return; 549 } 550 551 task_runner_->PostTask( 552 FROM_HERE, 553 base::Bind(&EjectDeviceInThreadPool, device, callback, task_runner_, 0)); 554 } 555 556 } // namespace storage_monitor 557