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"
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 "components/storage_monitor/media_storage_util.h"
24 #include "components/storage_monitor/storage_info.h"
25 #include "content/public/browser/browser_thread.h"
26 #include "content/public/browser/user_metrics.h"
28 using content::BrowserThread
;
30 namespace storage_monitor
{
34 const DWORD kMaxPathBufLen
= MAX_PATH
+ 1;
42 // Histogram values for recording frequencies of eject attempts and
44 enum EjectWinLockOutcomes
{
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
) {
64 if (drive_type
!= DRIVE_REMOVABLE
)
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
),
81 if (dos_device
== 0 && dos_device_slash
== 0)
83 if (device_path
.find(L
"Floppy") != base::string16::npos
||
84 device_path_slash
.find(L
"Floppy") != base::string16::npos
) {
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
;
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
;
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
))
113 return total
.QuadPart
;
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
) {
124 base::string16 mount_point
;
125 if (!GetVolumePathName(device_path
.value().c_str(),
126 WriteInto(&mount_point
, kMaxPathBufLen
),
130 mount_point
.resize(wcslen(mount_point
.c_str()));
132 // Note: experimentally this code does not spin a floppy drive. It
133 // returns a GUID associated with the device, not the volume.
135 if (!GetVolumeNameForVolumeMountPoint(mount_point
.c_str(),
136 WriteInto(&guid
, kMaxPathBufLen
),
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
),
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
, base::UTF16ToUTF8(guid
)));
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
;
164 // NOTE: experimentally, this function returns false if there is no volume
166 base::string16 volume_label
;
167 GetVolumeInformationW(device_path
.value().c_str(),
168 WriteInto(&volume_label
, kMaxPathBufLen
),
169 kMaxPathBufLen
, NULL
, NULL
, NULL
, NULL
, 0);
171 uint64 total_size_in_bytes
= GetVolumeSize(mount_point
);
172 std::string device_id
=
173 StorageInfo::MakeDeviceId(type
, base::UTF16ToUTF8(guid
));
175 // TODO(gbillock): if volume_label.empty(), get the vendor/model information
177 *info
= StorageInfo(device_id
, mount_point
, volume_label
, base::string16(),
178 base::string16(), total_size_in_bytes
);
182 // Returns a vector of all the removable mass storage devices that are
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
),
189 if (find_handle
== INVALID_HANDLE_VALUE
)
193 base::string16 volume_path
;
195 if (GetVolumePathNamesForVolumeName(volume_name
.c_str(),
196 WriteInto(&volume_path
, kMaxPathBufLen
),
197 kMaxPathBufLen
, &return_count
)) {
198 result
.push_back(base::FilePath(volume_path
));
200 if (!FindNextVolume(find_handle
, WriteInto(&volume_name
, kMaxPathBufLen
),
202 if (GetLastError() != ERROR_NO_MORE_FILES
)
208 FindVolumeClose(find_handle
);
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
,
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
));
235 base::SStringPrintf(&volume_name
, L
"\\\\.\\%lc:", drive_letter
);
237 base::win::ScopedHandle
volume_handle(CreateFile(
239 GENERIC_READ
, FILE_SHARE_READ
| FILE_SHARE_WRITE
,
240 NULL
, OPEN_EXISTING
, 0, NULL
));
242 if (!volume_handle
.IsValid()) {
243 BrowserThread::PostTask(
244 BrowserThread::UI
, FROM_HERE
,
245 base::Bind(callback
, StorageMonitor::EJECT_FAILURE
));
249 DWORD bytes_returned
= 0; // Unused, but necessary for ioctl's.
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
);
260 UMA_HISTOGRAM_ENUMERATION("StorageMonitor.EjectWinLock",
261 iteration
== 0 ? LOCK_TIMEOUT
: LOCK_TIMEOUT2
,
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(
272 base::Bind(&EjectDeviceInThreadPool
,
273 device
, callback
, task_runner
, iteration
+ 1),
278 BrowserThread::PostTask(BrowserThread::UI
, FROM_HERE
,
279 base::Bind(callback
, StorageMonitor::EJECT_IN_USE
));
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
);
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.
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
));
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
));
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
));
320 BrowserThread::PostTask(BrowserThread::UI
, FROM_HERE
,
321 base::Bind(callback
, StorageMonitor::EJECT_OK
));
326 const int kWorkerPoolNumThreads
= 3;
327 const char* kWorkerPoolNamePrefix
= "DeviceInfoPool";
329 VolumeMountWatcherWin::VolumeMountWatcherWin()
330 : device_info_worker_pool_(new base::SequencedWorkerPool(
331 kWorkerPoolNumThreads
, kWorkerPoolNamePrefix
)),
333 notifications_(NULL
) {
335 device_info_worker_pool_
->GetSequencedTaskRunnerWithShutdownBehavior(
336 device_info_worker_pool_
->GetSequenceToken(),
337 base::SequencedWorkerPool::CONTINUE_ON_SHUTDOWN
);
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
);
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
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
));
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()));
368 void VolumeMountWatcherWin::AddDevicesOnUIThread(
369 std::vector
<base::FilePath
> removable_devices
) {
370 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
372 for (size_t i
= 0; i
< removable_devices
.size(); i
++) {
373 if (ContainsKey(pending_device_checks_
, removable_devices
[i
]))
375 pending_device_checks_
.insert(removable_devices
[i
]);
376 task_runner_
->PostTask(
378 base::Bind(&VolumeMountWatcherWin::RetrieveInfoForDeviceAndAdd
,
379 removable_devices
[i
], GetDeviceDetailsCallback(),
380 weak_factory_
.GetWeakPtr()));
385 void VolumeMountWatcherWin::RetrieveInfoForDeviceAndAdd(
386 const base::FilePath
& device_path
,
387 const GetDeviceDetailsCallbackType
& get_device_details_callback
,
388 base::WeakPtr
<VolumeMountWatcherWin
> volume_watcher
) {
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
));
398 BrowserThread::PostTask(
399 BrowserThread::UI
, FROM_HERE
,
400 base::Bind(&VolumeMountWatcherWin::HandleDeviceAttachEventOnUIThread
,
401 volume_watcher
, device_path
, info
));
404 void VolumeMountWatcherWin::DeviceCheckComplete(
405 const base::FilePath
& device_path
) {
406 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
407 pending_device_checks_
.erase(device_path
);
409 if (pending_device_checks_
.size() == 0) {
411 notifications_
->MarkInitialized();
415 VolumeMountWatcherWin::GetAttachedDevicesCallbackType
416 VolumeMountWatcherWin::GetAttachedDevicesCallback() const {
417 return base::Bind(&GetAttachedDevices
);
420 VolumeMountWatcherWin::GetDeviceDetailsCallbackType
421 VolumeMountWatcherWin::GetDeviceDetailsCallback() const {
422 return base::Bind(&GetDeviceDetails
);
425 bool VolumeMountWatcherWin::GetDeviceInfo(const base::FilePath
& device_path
,
426 StorageInfo
* info
) const {
427 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
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
);
437 if (iter
== device_metadata_
.end())
440 *info
= iter
->second
;
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))
454 paths
.push_back(DriveNumberToFilePath(i
));
456 AddDevicesOnUIThread(paths
);
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))
466 HandleDeviceDetachEventOnUIThread(DriveNumberToFilePath(i
).value());
474 void VolumeMountWatcherWin::SetNotifications(
475 StorageMonitor::Receiver
* notifications
) {
476 notifications_
= notifications
;
479 VolumeMountWatcherWin::~VolumeMountWatcherWin() {
480 weak_factory_
.InvalidateWeakPtrs();
481 device_info_worker_pool_
->Shutdown();
484 void VolumeMountWatcherWin::HandleDeviceAttachEventOnUIThread(
485 const base::FilePath
& device_path
,
486 const StorageInfo
& info
) {
487 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
489 device_metadata_
[device_path
] = info
;
492 notifications_
->ProcessAttach(info
);
494 DeviceCheckComplete(device_path
);
497 void VolumeMountWatcherWin::HandleDeviceDetachEventOnUIThread(
498 const base::string16
& device_location
) {
499 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
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())
508 notifications_
->ProcessDetach(device_info
->second
.device_id());
509 device_metadata_
.erase(device_info
);
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
);
521 if (device_metadata_
.erase(device
) == 0) {
522 callback
.Run(StorageMonitor::EJECT_FAILURE
);
526 task_runner_
->PostTask(
528 base::Bind(&EjectDeviceInThreadPool
, device
, callback
, task_runner_
, 0));
531 } // namespace storage_monitor