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"
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
{
35 const DWORD kMaxPathBufLen
= MAX_PATH
+ 1;
43 // Histogram values for recording frequencies of eject attempts and
45 enum EjectWinLockOutcomes
{
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
) {
65 if (drive_type
!= DRIVE_REMOVABLE
)
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
),
82 if (dos_device
== 0 && dos_device_slash
== 0)
84 if (device_path
.find(L
"Floppy") != base::string16::npos
||
85 device_path_slash
.find(L
"Floppy") != base::string16::npos
) {
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
;
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
))
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
) {
125 base::string16 mount_point
;
126 if (!GetVolumePathName(device_path
.value().c_str(),
127 WriteInto(&mount_point
, kMaxPathBufLen
),
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.
136 if (!GetVolumeNameForVolumeMountPoint(mount_point
.c_str(),
137 WriteInto(&guid
, kMaxPathBufLen
),
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
),
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
)));
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
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
178 *info
= StorageInfo(device_id
, mount_point
, volume_label
, base::string16(),
179 base::string16(), total_size_in_bytes
);
183 // Returns a vector of all the removable mass storage devices that are
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
),
190 if (find_handle
== INVALID_HANDLE_VALUE
)
194 base::string16 volume_path
;
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
),
203 if (GetLastError() != ERROR_NO_MORE_FILES
)
209 FindVolumeClose(find_handle
);
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
,
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
));
236 base::SStringPrintf(&volume_name
, L
"\\\\.\\%lc:", drive_letter
);
238 base::win::ScopedHandle
volume_handle(CreateFile(
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
));
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
.Get(), FSCTL_LOCK_VOLUME
,
257 NULL
, 0, NULL
, 0, &bytes_returned
, NULL
);
258 UMA_HISTOGRAM_ENUMERATION("StorageMonitor.EjectWinLock",
259 LOCK_ATTEMPT
, NUM_LOCK_OUTCOMES
);
261 UMA_HISTOGRAM_ENUMERATION("StorageMonitor.EjectWinLock",
262 iteration
== 0 ? LOCK_TIMEOUT
: LOCK_TIMEOUT2
,
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(
273 base::Bind(&EjectDeviceInThreadPool
,
274 device
, callback
, task_runner
, iteration
+ 1),
279 BrowserThread::PostTask(BrowserThread::UI
, FROM_HERE
,
280 base::Bind(callback
, StorageMonitor::EJECT_IN_USE
));
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
.Get(), 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.
293 DeviceIoControl(volume_handle
.Get(), 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
));
300 PREVENT_MEDIA_REMOVAL pmr_buffer
;
301 pmr_buffer
.PreventMediaRemoval
= FALSE
;
302 // Mark the device as safe to remove.
303 if (!DeviceIoControl(volume_handle
.Get(), 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
));
312 // Physically eject or soft-eject the device.
313 if (!DeviceIoControl(volume_handle
.Get(), 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
));
321 BrowserThread::PostTask(BrowserThread::UI
, FROM_HERE
,
322 base::Bind(callback
, StorageMonitor::EJECT_OK
));
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 notifications_(NULL
),
334 weak_factory_(this) {
336 device_info_worker_pool_
->GetSequencedTaskRunnerWithShutdownBehavior(
337 device_info_worker_pool_
->GetSequenceToken(),
338 base::SequencedWorkerPool::CONTINUE_ON_SHUTDOWN
);
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
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
]))
376 pending_device_checks_
.insert(removable_devices
[i
]);
377 task_runner_
->PostTask(
379 base::Bind(&VolumeMountWatcherWin::RetrieveInfoForDeviceAndAdd
,
380 removable_devices
[i
], GetDeviceDetailsCallback(),
381 weak_factory_
.GetWeakPtr()));
386 void VolumeMountWatcherWin::RetrieveInfoForDeviceAndAdd(
387 const base::FilePath
& device_path
,
388 const GetDeviceDetailsCallbackType
& get_device_details_callback
,
389 base::WeakPtr
<VolumeMountWatcherWin
> volume_watcher
) {
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
));
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) {
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
));
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())
441 *info
= iter
->second
;
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))
455 paths
.push_back(DriveNumberToFilePath(i
));
457 AddDevicesOnUIThread(paths
);
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))
467 HandleDeviceDetachEventOnUIThread(DriveNumberToFilePath(i
).value());
475 void VolumeMountWatcherWin::OnMediaChange(WPARAM wparam
, LPARAM lparam
) {
476 if (lparam
== SHCNE_MEDIAINSERTED
|| lparam
== SHCNE_MEDIAREMOVED
) {
477 struct _ITEMIDLIST
* pidl
= *reinterpret_cast<struct _ITEMIDLIST
**>(
479 wchar_t sPath
[MAX_PATH
];
480 if (!SHGetPathFromIDList(pidl
, sPath
)) {
481 DVLOG(1) << "MediaInserted: SHGetPathFromIDList failed";
485 case SHCNE_MEDIAINSERTED
: {
486 std::vector
<base::FilePath
> paths
;
487 paths
.push_back(base::FilePath(sPath
));
488 AddDevicesOnUIThread(paths
);
491 case SHCNE_MEDIAREMOVED
: {
492 HandleDeviceDetachEventOnUIThread(sPath
);
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
;
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())
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
);
546 if (device_metadata_
.erase(device
) == 0) {
547 callback
.Run(StorageMonitor::EJECT_FAILURE
);
551 task_runner_
->PostTask(
553 base::Bind(&EjectDeviceInThreadPool
, device
, callback
, task_runner_
, 0));
556 } // namespace storage_monitor