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