Home | History | Annotate | Download | only in storage_monitor
      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