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