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