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