blob: 9654bd040ecacfa6ae5a978b29fdd00efaef1234 [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cros-disks/disk_monitor.h"
#include <inttypes.h>
#include <string.h>
#include <sys/mount.h>
#include <time.h>
#include <utility>
#include <base/bind.h>
#include <base/logging.h>
#include <base/stl_util.h>
#include <base/strings/string_piece.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <base/time/time.h>
#include <brillo/udev/udev.h>
#include <brillo/udev/udev_device.h>
#include <brillo/udev/udev_enumerate.h>
#include <brillo/udev/udev_monitor.h>
#include "cros-disks/device_ejector.h"
#include "cros-disks/quote.h"
#include "cros-disks/udev_device.h"
namespace cros_disks {
namespace {
const char kBlockSubsystem[] = "block";
const char kMmcSubsystem[] = "mmc";
const char kScsiSubsystem[] = "scsi";
const char kScsiDevice[] = "scsi_device";
const char kUdevAddAction[] = "add";
const char kUdevChangeAction[] = "change";
const char kUdevRemoveAction[] = "remove";
const char kPropertyDiskEjectRequest[] = "DISK_EJECT_REQUEST";
const char kPropertyDiskMediaChange[] = "DISK_MEDIA_CHANGE";
// Checks if the device is allowed to be used through cros-disks.
bool IsDeviceAllowed(const UdevDevice& device,
const std::set<std::string>& allowlist) {
if (device.IsIgnored())
return false;
if (base::Contains(allowlist, device.NativePath()))
return true;
if (device.IsLoopDevice())
return false;
if (device.IsMobileBroadbandDevice())
return false;
if (device.IsOnBootDevice())
return false;
return true;
}
// An EnumerateBlockDevices callback that appends a Disk object, created from
// |dev|, to |disks| if |dev| should not be ignored by cros-disks. Always
// returns true to continue the enumeration in EnumerateBlockDevices.
bool AppendDiskIfNotIgnored(const std::set<std::string>& allowlist,
std::vector<Disk>* disks,
std::unique_ptr<brillo::UdevDevice> dev) {
UdevDevice device(std::move(dev));
if (IsDeviceAllowed(device, allowlist))
disks->push_back(device.ToDisk());
return true; // Continue the enumeration.
}
// An EnumerateBlockDevices callback that checks if |dev| matches |path|. If
// it's a match, populates |device| and returns false to stop the enumeration in
// EnumerateBlockDevices.
bool MatchDiskByPath(const std::string& path,
std::unique_ptr<UdevDevice>* device,
std::unique_ptr<brillo::UdevDevice> dev) {
DCHECK(dev);
DCHECK(device);
const char* sys_path = dev->GetSysPath();
const char* dev_path = dev->GetDevicePath();
const char* dev_file = dev->GetDeviceNode();
bool match = (sys_path && path == sys_path) ||
(dev_path && path == dev_path) || (dev_file && path == dev_file);
if (!match)
return true; // Not a match. Continue the enumeration.
*device = std::make_unique<UdevDevice>(std::move(dev));
return false; // Match. Stop enumeration.
}
// Logs a device with its properties.
void LogUdevDevice(const brillo::UdevDevice& dev) {
if (!VLOG_IS_ON(1))
return;
// Some device events (eg USB drive removal) result in devnode being null.
// This is gracefully handled by quote() without crashing.
VLOG(1) << " node: " << quote(dev.GetDeviceNode());
VLOG(1) << " devtype: " << quote(dev.GetDeviceType());
VLOG(1) << " syspath: " << quote(dev.GetSysPath());
if (!VLOG_IS_ON(2))
return;
// Log all properties.
for (std::unique_ptr<brillo::UdevListEntry> prop =
dev.GetPropertiesListEntry();
prop; prop = prop->GetNext()) {
VLOG(2) << " " << prop->GetName() << ": " << quote(prop->GetValue());
}
}
void LogDevice(const UdevDevice& dev) {
if (!VLOG_IS_ON(1))
return;
VLOG(1) << "path: " << quote(dev.NativePath());
VLOG(1) << " attributes: virtual=" << dev.IsVirtual()
<< " loop=" << dev.IsLoopDevice() << " boot=" << dev.IsOnBootDevice()
<< " removable=" << dev.IsOnRemovableDevice()
<< " broadband=" << dev.IsMobileBroadbandDevice()
<< " automount=" << dev.IsAutoMountable()
<< " media=" << dev.IsMediaAvailable();
}
} // namespace
DiskMonitor::DiskMonitor() : udev_(brillo::Udev::Create()) {
CHECK(udev_) << "Failed to initialize udev";
udev_monitor_ = udev_->CreateMonitorFromNetlink("udev");
CHECK(udev_monitor_) << "Failed to create a udev monitor";
udev_monitor_->FilterAddMatchSubsystemDeviceType(kBlockSubsystem, nullptr);
udev_monitor_->FilterAddMatchSubsystemDeviceType(kMmcSubsystem, nullptr);
udev_monitor_->FilterAddMatchSubsystemDeviceType(kScsiSubsystem, kScsiDevice);
CHECK(udev_monitor_->EnableReceiving());
}
DiskMonitor::~DiskMonitor() = default;
int DiskMonitor::udev_monitor_fd() const {
return udev_monitor_->GetFileDescriptor();
}
bool DiskMonitor::Initialize() {
// Since there are no udev add events for the devices that already exist
// when the disk manager starts, emulate udev add events for these devices
// to correctly populate |disks_detected_|.
EnumerateBlockDevices(base::BindRepeating(
&DiskMonitor::EmulateAddBlockDeviceEvent, base::Unretained(this)));
return true;
}
bool DiskMonitor::EmulateAddBlockDeviceEvent(
std::unique_ptr<brillo::UdevDevice> dev) {
DeviceEventList events;
LOG(INFO) << "Emulating action 'add' on device " << quote(dev->GetSysName());
LogUdevDevice(*dev);
ProcessBlockDeviceEvents(std::move(dev), kUdevAddAction, &events);
return true; // Continue the enumeration.
}
std::vector<Disk> DiskMonitor::EnumerateDisks() const {
std::vector<Disk> disks;
EnumerateBlockDevices(base::BindRepeating(&AppendDiskIfNotIgnored, allowlist_,
base::Unretained(&disks)));
return disks;
}
void DiskMonitor::EnumerateBlockDevices(
base::RepeatingCallback<bool(std::unique_ptr<brillo::UdevDevice> dev)>
callback) const {
std::unique_ptr<brillo::UdevEnumerate> enumerate = udev_->CreateEnumerate();
enumerate->AddMatchSubsystem(kBlockSubsystem);
enumerate->ScanDevices();
for (std::unique_ptr<brillo::UdevListEntry> entry = enumerate->GetListEntry();
entry; entry = entry->GetNext()) {
std::unique_ptr<brillo::UdevDevice> dev =
udev_->CreateDeviceFromSysPath(entry->GetName());
if (!dev)
continue;
VLOG(1) << "Found device " << quote(dev->GetSysName());
LogUdevDevice(*dev);
bool continue_enumeration = callback.Run(std::move(dev));
if (!continue_enumeration)
break;
}
}
void DiskMonitor::ProcessBlockDeviceEvents(
std::unique_ptr<brillo::UdevDevice> dev,
const char* action,
DeviceEventList* events) {
brillo::UdevDevice* raw_dev = dev.get();
UdevDevice device(std::move(dev));
LogDevice(device);
if (!IsDeviceAllowed(device, allowlist_))
return;
bool disk_added = false;
bool disk_removed = false;
bool child_disk_removed = false;
if (strcmp(action, kUdevAddAction) == 0) {
disk_added = true;
} else if (strcmp(action, kUdevRemoveAction) == 0) {
disk_removed = true;
} else if (strcmp(action, kUdevChangeAction) == 0) {
// For removable devices like CD-ROM, an eject request event
// is treated as disk removal, while a media change event with
// media available is treated as disk insertion.
if (device.IsPropertyTrue(kPropertyDiskEjectRequest)) {
disk_removed = true;
} else if (device.IsPropertyTrue(kPropertyDiskMediaChange)) {
if (device.IsMediaAvailable()) {
disk_added = true;
} else {
child_disk_removed = true;
}
}
}
std::string device_path = device.NativePath();
if (disk_added) {
if (device.IsAutoMountable()) {
if (base::Contains(disks_detected_, device_path)) {
// Disk already exists, so remove it and then add it again.
LOG(INFO) << "Re-add block device " << quote(device_path);
events->push_back(DeviceEvent(DeviceEvent::kDiskRemoved, device_path));
} else {
disks_detected_[device_path] = {};
// Add the disk as a child of its parent if the parent is already
// added to |disks_detected_|.
std::unique_ptr<brillo::UdevDevice> parent = raw_dev->GetParent();
if (parent) {
std::string parent_device_path =
UdevDevice(std::move(parent)).NativePath();
if (base::Contains(disks_detected_, parent_device_path)) {
disks_detected_[parent_device_path].insert(device_path);
}
}
}
LOG(INFO) << "Add block device " << quote(device_path);
events->push_back(DeviceEvent(DeviceEvent::kDiskAdded, device_path));
}
} else if (disk_removed) {
disks_detected_.erase(device_path);
LOG(INFO) << "Remove block device " << quote(device_path);
events->push_back(DeviceEvent(DeviceEvent::kDiskRemoved, device_path));
} else if (child_disk_removed) {
bool no_child_disks_found = true;
if (base::Contains(disks_detected_, device_path)) {
auto& child_disks = disks_detected_[device_path];
no_child_disks_found = child_disks.empty();
for (const auto& child_disk : child_disks) {
LOG(INFO) << "Remove child device " << quote(child_disk);
events->push_back(DeviceEvent(DeviceEvent::kDiskRemoved, child_disk));
}
}
// When the device contains a full-disk partition, there are no child disks.
// Remove the device instead.
if (no_child_disks_found) {
LOG(INFO) << "Remove parent device " << quote(device_path);
events->push_back(DeviceEvent(DeviceEvent::kDiskRemoved, device_path));
}
}
}
void DiskMonitor::ProcessMmcOrScsiDeviceEvents(
std::unique_ptr<brillo::UdevDevice> dev,
const char* action,
DeviceEventList* events) {
UdevDevice device(std::move(dev));
LogDevice(device);
if (!IsDeviceAllowed(device, allowlist_))
return;
std::string device_path = device.NativePath();
if (strcmp(action, kUdevAddAction) == 0) {
if (base::Contains(devices_detected_, device_path)) {
LOG(INFO) << "Re-add device " << quote(device_path);
events->push_back(DeviceEvent(DeviceEvent::kDeviceScanned, device_path));
} else {
devices_detected_.insert(device_path);
LOG(INFO) << "Add device " << quote(device_path);
events->push_back(DeviceEvent(DeviceEvent::kDeviceAdded, device_path));
}
} else if (strcmp(action, kUdevRemoveAction) == 0) {
if (base::Contains(devices_detected_, device_path)) {
devices_detected_.erase(device_path);
LOG(INFO) << "Remove device " << quote(device_path);
events->push_back(DeviceEvent(DeviceEvent::kDeviceRemoved, device_path));
}
}
}
bool DiskMonitor::GetDeviceEvents(DeviceEventList* events) {
CHECK(events) << "Invalid device event list";
std::unique_ptr<brillo::UdevDevice> dev = udev_monitor_->ReceiveDevice();
if (!dev) {
LOG(WARNING) << "Ignore device event with no associated udev device.";
return false;
}
const char* sys_path = dev->GetSysPath();
const char* subsystem = dev->GetSubsystem();
const char* action = dev->GetAction();
LOG(INFO) << "Got action " << quote(action) << " on device "
<< quote(dev->GetSysName());
LogUdevDevice(*dev);
if (!sys_path || !subsystem || !action) {
return false;
}
// |udev_monitor_| only monitors block, mmc, and scsi device changes, so
// subsystem is either "block", "mmc", or "scsi".
if (strcmp(subsystem, kBlockSubsystem) == 0) {
ProcessBlockDeviceEvents(std::move(dev), action, events);
} else {
// strcmp(subsystem, kMmcSubsystem) == 0 ||
// strcmp(subsystem, kScsiSubsystem) == 0
ProcessMmcOrScsiDeviceEvents(std::move(dev), action, events);
}
return true;
}
bool DiskMonitor::GetDiskByDevicePath(const base::FilePath& device_path,
Disk* disk) const {
LOG(INFO) << "Get disk by path " << quote(device_path);
if (device_path.empty())
return false;
std::unique_ptr<UdevDevice> device;
EnumerateBlockDevices(base::BindRepeating(
&MatchDiskByPath, device_path.value(), base::Unretained(&device)));
if (!device)
return false;
LogDevice(*device);
if (!IsDeviceAllowed(*device, allowlist_))
return false;
if (disk)
*disk = device->ToDisk();
return true;
}
void DiskMonitor::AddDeviceToAllowlist(const base::FilePath& device) {
allowlist_.insert(device.value());
}
void DiskMonitor::RemoveDeviceFromAllowlist(const base::FilePath& device) {
allowlist_.erase(device.value());
}
} // namespace cros_disks