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