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