Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / components / storage_monitor / volume_mount_watcher_win.cc
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.
4
5 #include "components/storage_monitor/volume_mount_watcher_win.h"
6
7 #include <windows.h>
8
9 #include <dbt.h>
10 #include <fileapi.h>
11 #include <shlobj.h>
12 #include <winioctl.h>
13
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"
28
29 using content::BrowserThread;
30
31 namespace storage_monitor {
32
33 namespace {
34
35 const DWORD kMaxPathBufLen = MAX_PATH + 1;
36
37 enum DeviceType {
38   FLOPPY,
39   REMOVABLE,
40   FIXED,
41 };
42
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,
50 };
51
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;
64   }
65   if (drive_type != DRIVE_REMOVABLE)
66     return FLOPPY;
67
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;
87   }
88
89   return REMOVABLE;
90 }
91
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;
99 }
100
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;
107 }
108
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;
115 }
116
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);
124
125   base::string16 mount_point;
126   if (!GetVolumePathName(device_path.value().c_str(),
127                          WriteInto(&mount_point, kMaxPathBufLen),
128                          kMaxPathBufLen)) {
129     return false;
130   }
131   mount_point.resize(wcslen(mount_point.c_str()));
132
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;
140   }
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;
146   }
147
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;
156   }
157
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;
163   }
164
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);
171
172   uint64 total_size_in_bytes = GetVolumeSize(mount_point);
173   std::string device_id =
174       StorageInfo::MakeDeviceId(type, base::UTF16ToUTF8(guid));
175
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;
181 }
182
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;
192
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));
200     }
201     if (!FindNextVolume(find_handle, WriteInto(&volume_name, kMaxPathBufLen),
202                         kMaxPathBufLen)) {
203       if (GetLastError() != ERROR_NO_MORE_FILES)
204         DPLOG(ERROR);
205       break;
206     }
207   }
208
209   FindVolumeClose(find_handle);
210   return result;
211 }
212
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;
235   }
236   base::SStringPrintf(&volume_name, L"\\\\.\\%lc:", drive_letter);
237
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));
242
243   if (!volume_handle.IsValid()) {
244     BrowserThread::PostTask(
245         BrowserThread::UI, FROM_HERE,
246         base::Bind(callback, StorageMonitor::EJECT_FAILURE));
247     return;
248   }
249
250   DWORD bytes_returned = 0;  // Unused, but necessary for ioctl's.
251
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;
277     }
278
279     BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
280                             base::Bind(callback, StorageMonitor::EJECT_IN_USE));
281     return;
282   }
283
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);
288
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;
298   }
299
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;
310   }
311
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;
319   }
320
321   BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
322                           base::Bind(callback, StorageMonitor::EJECT_OK));
323 }
324
325 }  // namespace
326
327 const int kWorkerPoolNumThreads = 3;
328 const char* kWorkerPoolNamePrefix = "DeviceInfoPool";
329
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);
339 }
340
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);
348 }
349
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));
359
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()));
367 }
368
369 void VolumeMountWatcherWin::AddDevicesOnUIThread(
370     std::vector<base::FilePath> removable_devices) {
371   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
372
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()));
382   }
383 }
384
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;
397   }
398
399   BrowserThread::PostTask(
400       BrowserThread::UI, FROM_HERE,
401       base::Bind(&VolumeMountWatcherWin::HandleDeviceAttachEventOnUIThread,
402                  volume_watcher, device_path, info));
403 }
404
405 void VolumeMountWatcherWin::DeviceCheckComplete(
406     const base::FilePath& device_path) {
407   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
408   pending_device_checks_.erase(device_path);
409
410   if (pending_device_checks_.size() == 0) {
411     if (notifications_)
412       notifications_->MarkInitialized();
413   }
414 }
415
416 VolumeMountWatcherWin::GetAttachedDevicesCallbackType
417     VolumeMountWatcherWin::GetAttachedDevicesCallback() const {
418   return base::Bind(&GetAttachedDevices);
419 }
420
421 VolumeMountWatcherWin::GetDeviceDetailsCallbackType
422     VolumeMountWatcherWin::GetDeviceDetailsCallback() const {
423   return base::Bind(&GetDeviceDetails);
424 }
425
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);
436   }
437
438   if (iter == device_metadata_.end())
439     return false;
440
441   *info = iter->second;
442   return true;
443 }
444
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));
456         }
457         AddDevicesOnUIThread(paths);
458       }
459       break;
460     }
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());
468         }
469       }
470       break;
471     }
472   }
473 }
474
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;
483     }
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;
490       }
491       case SHCNE_MEDIAREMOVED: {
492         HandleDeviceDetachEventOnUIThread(sPath);
493         break;
494       }
495     }
496   }
497 }
498
499 void VolumeMountWatcherWin::SetNotifications(
500     StorageMonitor::Receiver* notifications) {
501   notifications_ = notifications;
502 }
503
504 VolumeMountWatcherWin::~VolumeMountWatcherWin() {
505   weak_factory_.InvalidateWeakPtrs();
506   device_info_worker_pool_->Shutdown();
507 }
508
509 void VolumeMountWatcherWin::HandleDeviceAttachEventOnUIThread(
510     const base::FilePath& device_path,
511     const StorageInfo& info) {
512   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
513
514   device_metadata_[device_path] = info;
515
516   if (notifications_)
517     notifications_->ProcessAttach(info);
518
519   DeviceCheckComplete(device_path);
520 }
521
522 void VolumeMountWatcherWin::HandleDeviceDetachEventOnUIThread(
523     const base::string16& device_location) {
524   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
525
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;
531
532   if (notifications_)
533     notifications_->ProcessDetach(device_info->second.device_id());
534   device_metadata_.erase(device_info);
535 }
536
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;
545   }
546   if (device_metadata_.erase(device) == 0) {
547     callback.Run(StorageMonitor::EJECT_FAILURE);
548     return;
549   }
550
551   task_runner_->PostTask(
552       FROM_HERE,
553       base::Bind(&EjectDeviceInThreadPool, device, callback, task_runner_, 0));
554 }
555
556 }  // namespace storage_monitor