blob: f2b288e2a6431502350d2c3df6385534221e3a34 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// StorageMonitorLinux implementation.
#include "components/storage_monitor/storage_monitor_linux.h"
#include <mntent.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/stat.h>
#include <limits>
#include <list>
#include <memory>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/macros.h"
#include "base/metrics/histogram_macros.h"
#include "base/process/kill.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/task_runner_util.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "components/storage_monitor/media_storage_util.h"
#include "components/storage_monitor/removable_device_constants.h"
#include "components/storage_monitor/storage_info.h"
#include "components/storage_monitor/udev_util_linux.h"
#include "device/udev_linux/scoped_udev.h"
namespace storage_monitor {
using MountPointDeviceMap = MtabWatcherLinux::MountPointDeviceMap;
namespace {
// udev device property constants.
const char kBlockSubsystemKey[] = "block";
const char kDiskDeviceTypeKey[] = "disk";
const char kFsUUID[] = "ID_FS_UUID";
const char kLabel[] = "ID_FS_LABEL";
const char kModel[] = "ID_MODEL";
const char kModelID[] = "ID_MODEL_ID";
const char kRemovableSysAttr[] = "removable";
const char kSerialShort[] = "ID_SERIAL_SHORT";
const char kSizeSysAttr[] = "size";
const char kVendor[] = "ID_VENDOR";
const char kVendorID[] = "ID_VENDOR_ID";
// Construct a device id using label or manufacturer (vendor and model) details.
std::string MakeDeviceUniqueId(struct udev_device* device) {
std::string uuid = device::UdevDeviceGetPropertyValue(device, kFsUUID);
if (!uuid.empty())
return kFSUniqueIdPrefix + uuid;
// If one of the vendor, model, serial information is missing, its value
// in the string is empty.
// Format: VendorModelSerial:VendorInfo:ModelInfo:SerialShortInfo
// E.g.: VendorModelSerial:Kn:DataTravel_12.10:8000000000006CB02CDB
std::string vendor = device::UdevDeviceGetPropertyValue(device, kVendorID);
std::string model = device::UdevDeviceGetPropertyValue(device, kModelID);
std::string serial_short =
device::UdevDeviceGetPropertyValue(device, kSerialShort);
if (vendor.empty() && model.empty() && serial_short.empty())
return std::string();
return kVendorModelSerialPrefix + vendor + ":" + model + ":" + serial_short;
}
// Records GetDeviceInfo result on destruction, to see how often we fail to get
// device details.
class ScopedGetDeviceInfoResultRecorder {
public:
ScopedGetDeviceInfoResultRecorder() : result_(false) {}
~ScopedGetDeviceInfoResultRecorder() {
UMA_HISTOGRAM_BOOLEAN("MediaDeviceNotification.UdevRequestSuccess",
result_);
}
void set_result(bool result) {
result_ = result;
}
private:
bool result_;
DISALLOW_COPY_AND_ASSIGN(ScopedGetDeviceInfoResultRecorder);
};
// Returns the storage partition size of the device specified by |device_path|.
// If the requested information is unavailable, returns 0.
uint64_t GetDeviceStorageSize(const base::FilePath& device_path,
struct udev_device* device) {
// sysfs provides the device size in units of 512-byte blocks.
const std::string partition_size =
device::UdevDeviceGetSysattrValue(device, kSizeSysAttr);
uint64_t total_size_in_bytes = 0;
if (!base::StringToUint64(partition_size, &total_size_in_bytes))
return 0;
return (total_size_in_bytes <= std::numeric_limits<uint64_t>::max() / 512)
? total_size_in_bytes * 512
: 0;
}
// Gets the device information using udev library.
std::unique_ptr<StorageInfo> GetDeviceInfo(const base::FilePath& device_path,
const base::FilePath& mount_point) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
DCHECK(!device_path.empty());
std::unique_ptr<StorageInfo> storage_info;
ScopedGetDeviceInfoResultRecorder results_recorder;
device::ScopedUdevPtr udev_obj(device::udev_new());
if (!udev_obj.get())
return storage_info;
struct stat device_stat;
if (stat(device_path.value().c_str(), &device_stat) < 0)
return storage_info;
char device_type;
if (S_ISCHR(device_stat.st_mode))
device_type = 'c';
else if (S_ISBLK(device_stat.st_mode))
device_type = 'b';
else
return storage_info; // Not a supported type.
device::ScopedUdevDevicePtr device(
device::udev_device_new_from_devnum(udev_obj.get(), device_type,
device_stat.st_rdev));
if (!device.get())
return storage_info;
base::string16 volume_label = base::UTF8ToUTF16(
device::UdevDeviceGetPropertyValue(device.get(), kLabel));
base::string16 vendor_name = base::UTF8ToUTF16(
device::UdevDeviceGetPropertyValue(device.get(), kVendor));
base::string16 model_name = base::UTF8ToUTF16(
device::UdevDeviceGetPropertyValue(device.get(), kModel));
std::string unique_id = MakeDeviceUniqueId(device.get());
const char* value =
device::udev_device_get_sysattr_value(device.get(), kRemovableSysAttr);
if (!value) {
// |parent_device| is owned by |device| and does not need to be cleaned
// up.
struct udev_device* parent_device =
device::udev_device_get_parent_with_subsystem_devtype(
device.get(),
kBlockSubsystemKey,
kDiskDeviceTypeKey);
value = device::udev_device_get_sysattr_value(parent_device,
kRemovableSysAttr);
}
const bool is_removable = (value && atoi(value) == 1);
StorageInfo::Type type = StorageInfo::FIXED_MASS_STORAGE;
if (is_removable) {
type = MediaStorageUtil::HasDcim(mount_point)
? StorageInfo::REMOVABLE_MASS_STORAGE_WITH_DCIM
: StorageInfo::REMOVABLE_MASS_STORAGE_NO_DCIM;
}
results_recorder.set_result(true);
storage_info = std::make_unique<StorageInfo>(
StorageInfo::MakeDeviceId(type, unique_id), mount_point.value(),
volume_label, vendor_name, model_name,
GetDeviceStorageSize(device_path, device.get()));
return storage_info;
}
// Runs |callback| with the |new_mtab| on |storage_monitor_task_runner|.
void BounceMtabUpdateToStorageMonitorTaskRunner(
scoped_refptr<base::SequencedTaskRunner> storage_monitor_task_runner,
const MtabWatcherLinux::UpdateMtabCallback& callback,
const MtabWatcherLinux::MountPointDeviceMap& new_mtab) {
storage_monitor_task_runner->PostTask(FROM_HERE,
base::BindOnce(callback, new_mtab));
}
MtabWatcherLinux* CreateMtabWatcherLinuxOnMtabWatcherTaskRunner(
const base::FilePath& mtab_path,
scoped_refptr<base::SequencedTaskRunner> storage_monitor_task_runner,
const MtabWatcherLinux::UpdateMtabCallback& callback) {
// Owned by caller.
return new MtabWatcherLinux(
mtab_path, base::Bind(&BounceMtabUpdateToStorageMonitorTaskRunner,
storage_monitor_task_runner, callback));
}
StorageMonitor::EjectStatus EjectPathOnBlockingTaskRunner(
const base::FilePath& path,
const base::FilePath& device) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
// Note: Linux LSB says umount should exist in /bin.
static const char kUmountBinary[] = "/bin/umount";
std::vector<std::string> command;
command.push_back(kUmountBinary);
command.push_back(path.value());
base::LaunchOptions options;
base::Process process = base::LaunchProcess(command, options);
if (!process.IsValid())
return StorageMonitor::EJECT_FAILURE;
int exit_code = -1;
if (!process.WaitForExitWithTimeout(base::TimeDelta::FromMilliseconds(3000),
&exit_code)) {
process.Terminate(-1, false);
base::EnsureProcessTerminated(std::move(process));
return StorageMonitor::EJECT_FAILURE;
}
// TODO(gbillock): Make sure this is found in documentation
// somewhere. Experimentally it seems to hold that exit code
// 1 means device is in use.
if (exit_code == 1)
return StorageMonitor::EJECT_IN_USE;
if (exit_code != 0)
return StorageMonitor::EJECT_FAILURE;
return StorageMonitor::EJECT_OK;
}
} // namespace
StorageMonitorLinux::StorageMonitorLinux(const base::FilePath& path)
: mtab_path_(path),
get_device_info_callback_(base::Bind(&GetDeviceInfo)),
mtab_watcher_task_runner_(
base::CreateSequencedTaskRunner({base::ThreadPool(), base::MayBlock(),
base::TaskPriority::BEST_EFFORT})) {}
StorageMonitorLinux::~StorageMonitorLinux() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
mtab_watcher_task_runner_->DeleteSoon(FROM_HERE, mtab_watcher_.release());
}
void StorageMonitorLinux::Init() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!mtab_path_.empty());
base::PostTaskAndReplyWithResult(
mtab_watcher_task_runner_.get(), FROM_HERE,
base::Bind(&CreateMtabWatcherLinuxOnMtabWatcherTaskRunner, mtab_path_,
base::SequencedTaskRunnerHandle::Get(),
base::Bind(&StorageMonitorLinux::UpdateMtab,
weak_ptr_factory_.GetWeakPtr())),
base::Bind(&StorageMonitorLinux::OnMtabWatcherCreated,
weak_ptr_factory_.GetWeakPtr()));
}
bool StorageMonitorLinux::GetStorageInfoForPath(
const base::FilePath& path,
StorageInfo* device_info) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(device_info);
if (!path.IsAbsolute())
return false;
base::FilePath current = path;
while (!base::Contains(mount_info_map_, current) &&
current != current.DirName())
current = current.DirName();
auto mount_info = mount_info_map_.find(current);
if (mount_info == mount_info_map_.end())
return false;
*device_info = mount_info->second.storage_info;
return true;
}
void StorageMonitorLinux::SetGetDeviceInfoCallbackForTest(
const GetDeviceInfoCallback& get_device_info_callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
get_device_info_callback_ = get_device_info_callback;
}
void StorageMonitorLinux::EjectDevice(
const std::string& device_id,
base::Callback<void(EjectStatus)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
StorageInfo::Type type;
if (!StorageInfo::CrackDeviceId(device_id, &type, nullptr)) {
callback.Run(EJECT_FAILURE);
return;
}
DCHECK_NE(type, StorageInfo::MTP_OR_PTP);
// Find the mount point for the given device ID.
base::FilePath path;
base::FilePath device;
for (auto mount_info = mount_info_map_.begin();
mount_info != mount_info_map_.end(); ++mount_info) {
if (mount_info->second.storage_info.device_id() == device_id) {
path = mount_info->first;
device = mount_info->second.mount_device;
mount_info_map_.erase(mount_info);
break;
}
}
if (path.empty()) {
callback.Run(EJECT_NO_SUCH_DEVICE);
return;
}
receiver()->ProcessDetach(device_id);
base::PostTaskAndReplyWithResult(
FROM_HERE,
{base::ThreadPool(), base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::Bind(&EjectPathOnBlockingTaskRunner, path, device), callback);
}
void StorageMonitorLinux::OnMtabWatcherCreated(MtabWatcherLinux* watcher) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
mtab_watcher_.reset(watcher);
}
void StorageMonitorLinux::UpdateMtab(const MountPointDeviceMap& new_mtab) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Check existing mtab entries for unaccounted mount points.
// These mount points must have been removed in the new mtab.
std::list<base::FilePath> mount_points_to_erase;
std::list<base::FilePath> multiple_mounted_devices_needing_reattachment;
for (MountMap::const_iterator old_iter = mount_info_map_.begin();
old_iter != mount_info_map_.end(); ++old_iter) {
const base::FilePath& mount_point = old_iter->first;
const base::FilePath& mount_device = old_iter->second.mount_device;
auto new_iter = new_mtab.find(mount_point);
// |mount_point| not in |new_mtab| or |mount_device| is no longer mounted at
// |mount_point|.
if (new_iter == new_mtab.end() || (new_iter->second != mount_device)) {
auto priority = mount_priority_map_.find(mount_device);
DCHECK(priority != mount_priority_map_.end());
ReferencedMountPoint::const_iterator has_priority =
priority->second.find(mount_point);
if (StorageInfo::IsRemovableDevice(
old_iter->second.storage_info.device_id())) {
DCHECK(has_priority != priority->second.end());
if (has_priority->second) {
receiver()->ProcessDetach(old_iter->second.storage_info.device_id());
}
if (priority->second.size() > 1)
multiple_mounted_devices_needing_reattachment.push_back(mount_device);
}
priority->second.erase(mount_point);
if (priority->second.empty())
mount_priority_map_.erase(mount_device);
mount_points_to_erase.push_back(mount_point);
}
}
// Erase the |mount_info_map_| entries afterwards. Erasing in the loop above
// using the iterator is slightly more efficient, but more tricky, since
// calling std::map::erase() on an iterator invalidates it.
for (std::list<base::FilePath>::const_iterator it =
mount_points_to_erase.begin();
it != mount_points_to_erase.end();
++it) {
mount_info_map_.erase(*it);
}
// For any multiply mounted device where the mount that we had notified
// got detached, send a notification of attachment for one of the other
// mount points.
for (std::list<base::FilePath>::const_iterator it =
multiple_mounted_devices_needing_reattachment.begin();
it != multiple_mounted_devices_needing_reattachment.end();
++it) {
auto first_mount_point_info = mount_priority_map_.find(*it)->second.begin();
const base::FilePath& mount_point = first_mount_point_info->first;
first_mount_point_info->second = true;
const StorageInfo& mount_info =
mount_info_map_.find(mount_point)->second.storage_info;
DCHECK(StorageInfo::IsRemovableDevice(mount_info.device_id()));
receiver()->ProcessAttach(mount_info);
}
// Check new mtab entries against existing ones.
scoped_refptr<base::SequencedTaskRunner> mounting_task_runner =
base::CreateSequencedTaskRunner({base::ThreadPool(), base::MayBlock(),
base::TaskPriority::BEST_EFFORT});
for (auto new_iter = new_mtab.begin(); new_iter != new_mtab.end();
++new_iter) {
const base::FilePath& mount_point = new_iter->first;
const base::FilePath& mount_device = new_iter->second;
auto old_iter = mount_info_map_.find(mount_point);
if (old_iter == mount_info_map_.end() ||
old_iter->second.mount_device != mount_device) {
// New mount point found or an existing mount point found with a new
// device.
if (IsDeviceAlreadyMounted(mount_device)) {
HandleDeviceMountedMultipleTimes(mount_device, mount_point);
} else {
base::PostTaskAndReplyWithResult(
mounting_task_runner.get(), FROM_HERE,
base::Bind(get_device_info_callback_, mount_device, mount_point),
base::Bind(&StorageMonitorLinux::AddNewMount,
weak_ptr_factory_.GetWeakPtr(), mount_device));
}
}
}
// Note: Relies on scheduled tasks on the |mounting_task_runner| being
// sequential. This block needs to follow the for loop, so that the DoNothing
// call on the |mounting_task_runner| happens after the scheduled metadata
// retrievals, meaning that the reply callback will then happen after all the
// AddNewMount calls.
if (!IsInitialized()) {
mounting_task_runner->PostTaskAndReply(
FROM_HERE, base::DoNothing(),
base::Bind(&StorageMonitorLinux::MarkInitialized,
weak_ptr_factory_.GetWeakPtr()));
}
}
bool StorageMonitorLinux::IsDeviceAlreadyMounted(
const base::FilePath& mount_device) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return base::Contains(mount_priority_map_, mount_device);
}
void StorageMonitorLinux::HandleDeviceMountedMultipleTimes(
const base::FilePath& mount_device,
const base::FilePath& mount_point) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto priority = mount_priority_map_.find(mount_device);
DCHECK(priority != mount_priority_map_.end());
const base::FilePath& other_mount_point = priority->second.begin()->first;
priority->second[mount_point] = false;
mount_info_map_[mount_point] =
mount_info_map_.find(other_mount_point)->second;
}
void StorageMonitorLinux::AddNewMount(
const base::FilePath& mount_device,
std::unique_ptr<StorageInfo> storage_info) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!storage_info)
return;
DCHECK(!storage_info->device_id().empty());
bool removable = StorageInfo::IsRemovableDevice(storage_info->device_id());
const base::FilePath mount_point(storage_info->location());
MountPointInfo mount_point_info;
mount_point_info.mount_device = mount_device;
mount_point_info.storage_info = *storage_info;
mount_info_map_[mount_point] = mount_point_info;
mount_priority_map_[mount_device][mount_point] = removable;
receiver()->ProcessAttach(*storage_info);
}
StorageMonitor* StorageMonitor::CreateInternal() {
const base::FilePath kDefaultMtabPath("/etc/mtab");
return new StorageMonitorLinux(kDefaultMtabPath);
}
} // namespace storage_monitor