blob: 54099541c5c1f733847c3832b93f7252275aac4a [file]
// Copyright 2023 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <base/files/file_util.h>
#include <base/logging.h>
#include <brillo/udev/udev_enumerate.h>
#include <re2/re2.h>
#include "arc/adbd/udev_monitor.h"
namespace adbd {
namespace {
constexpr char kUdev[] = "udev";
constexpr char kTypeCSubsystem[] = "typec";
// Regex to detect typec port partner events.
constexpr char kPartnerRegex[] = R"(port(\d+)-partner)";
// DbC enable / disable constants
constexpr char kDbcXmlPath[] = "/etc/arc/adbd.json";
constexpr char kEmptyDeviceId[] = "0000:00:00.0";
constexpr char kPciBusIdRegex[] = R"(\"pciBusDeviceId\": \"([^\"]*)\")";
constexpr char kDbcControlPath[] = "/sys/devices/pci0000:00/{PCI_BUS_ID}/dbc";
constexpr char kPciBusIdPlaceholderRegex[] = R"(\{PCI_BUS_ID\})";
constexpr char kDbcEnable[] = "enable";
constexpr char kDbcDisable[] = "disable";
// User space control to modify the USB Type-C role.
// Refer Documentation/ABI/testing/sysfs-class-usb_role.
constexpr char kTypecUsbRoleSysPath[] =
"/sys/class/typec/port{PORT}/usb-role-switch/role";
constexpr char kUsbRoleSysPath[] =
"/sys/class/usb_role/CON{PORT}-role-switch/role";
// Regex to replace PORT in USB role setting.
constexpr char kUsbRolePortRegex[] = R"(\{PORT\})";
constexpr char kUsbRoleHost[] = "host";
constexpr char kUsbRoleDevice[] = "device";
} // namespace
UdevMonitor::UdevMonitor() : udev_thread_("udev_monitor") {}
bool UdevMonitor::Init() {
num_typec_connections_ = 0;
// Extract the typec usb pci bus id from adbd.json, eg. 0000:00:0d.0 for brya
std::string adbd_json = std::string();
base::FilePath adb_json_path = base::FilePath(kDbcXmlPath);
if (!base::ReadFileToString(adb_json_path, &adbd_json)) {
PLOG(ERROR) << "Failed to read " << kDbcXmlPath;
}
usb_pci_bus_id_ = std::string(kEmptyDeviceId);
RE2::PartialMatch(adbd_json, kPciBusIdRegex, &usb_pci_bus_id_);
udev_ = brillo::Udev::Create();
if (!udev_) {
PLOG(ERROR) << "Failed to initialize udev object.";
return false;
}
// Enumerate existing devices and update usb role.
if (!UdevMonitor::Enumerate()) {
PLOG(ERROR) << "Failed to enumerate existing devices.";
return false;
}
// Set up udev monitor for typec usb events.
udev_monitor_ = udev_->CreateMonitorFromNetlink(kUdev);
if (!udev_monitor_) {
PLOG(ERROR) << "Failed to create udev monitor.";
return false;
}
if (!udev_monitor_->FilterAddMatchSubsystemDeviceType(kTypeCSubsystem,
nullptr)) {
PLOG(ERROR) << "Failed to add typec subsystem to udev monitor.";
return false;
}
if (!udev_monitor_->FilterAddMatchSubsystemDeviceType("tty", nullptr)) {
PLOG(ERROR) << "Failed to add typec subsystem to udev monitor.";
return false;
}
if (!udev_monitor_->EnableReceiving()) {
PLOG(ERROR) << "Failed to enable receiving for udev monitor.";
return false;
}
int fd = udev_monitor_->GetFileDescriptor();
if (fd == brillo::UdevMonitor::kInvalidFileDescriptor) {
PLOG(ERROR) << "Failed to get udev monitor fd.";
return false;
}
if (!udev_thread_.StartWithOptions(
base::Thread::Options(base::MessagePumpType::IO, 0))) {
PLOG(ERROR) << "Failed to start udev thread.";
return -1;
}
udev_thread_.task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&UdevMonitor::StartWatching, base::Unretained(this), fd));
VLOG(1) << "Udev monitor started";
return true;
}
bool UdevMonitor::Enumerate() {
DCHECK(udev_);
auto enumerate = udev_->CreateEnumerate();
if (!enumerate->AddMatchSubsystem(kTypeCSubsystem)) {
PLOG(ERROR) << "Failed to add typec subsystem to udev enumerate.";
return false;
}
enumerate->ScanDevices();
auto entry = enumerate->GetListEntry();
if (!entry) {
PLOG(WARNING) << "No existing typec devices.\n";
return true;
}
while (entry != nullptr) {
UdevMonitor::OnDeviceAdd(base::FilePath(std::string(entry->GetName())));
entry = entry->GetNext();
}
return true;
}
void UdevMonitor::StartWatching(int fd) {
udev_monitor_watcher_ = base::FileDescriptorWatcher::WatchReadable(
fd,
base::BindRepeating(&UdevMonitor::OnUdevEvent, base::Unretained(this)));
if (!udev_monitor_watcher_) {
PLOG(ERROR) << "Failed to start watcher for udev monitor fd.";
}
}
// Enable DbC and update usb role
void UdevMonitor::OnDeviceAdd(const base::FilePath& path) {
// Every cable event causes multiple adds, only watch for portx-partner
// events.
int port_num;
if (!RE2::FullMatch(path.BaseName().value(), kPartnerRegex, &port_num)) {
// Not a portx-partner event - ignore
return;
}
// Here we use a simple counting mechanism to enable dbc on the first and
// only the first portx-partner add event, and then disable it when the last
// portx-partner connection is removed. This should allow for multiple typec
// cables to be connected and only disable dbc when none are remaining. If
// this count gets into a bad state, there is a possibility that we do not
// correctly disable dbc (unnecessary polling will happen) until
// reboot or we will not enable dbc until a cable unplug / replug event.
num_typec_connections_++;
LOG(INFO) << "Typec connection detected at " << path.BaseName().value()
<< ". Total typec connections: " << num_typec_connections_;
// Enable DbC if this is the first typec connection
if (num_typec_connections_ == 1) {
LOG(INFO) << "First typec cable connected, enabling DbC.";
UpdateDbcState(kDbcEnable);
}
// Update role
UpdatePortRole(port_num, kUsbRoleHost);
}
// Disable dbc if usb cable unplugged
void UdevMonitor::OnDeviceRemove(const base::FilePath& path) {
// Only care about portx-partner remove events
int port_num;
if (!RE2::FullMatch(path.BaseName().value(), kPartnerRegex, &port_num)) {
return;
}
num_typec_connections_--;
LOG(INFO) << "Typec connection detected at " << path.BaseName().value()
<< ". Total typec connections: " << num_typec_connections_;
if (num_typec_connections_ < 0) {
num_typec_connections_ = 0;
}
// Disable DbC if no more typec connections
// No need to reset the usb mode since no cable is connected
if (num_typec_connections_ <= 0) {
LOG(INFO) << "No more typec connections, disabling DbC.";
UpdateDbcState(kDbcDisable);
}
}
// Update the DbC control file state with the given state (enable or disable)
void UdevMonitor::UpdateDbcState(const std::string& state) {
auto dbc_path_string = std::string(kDbcControlPath);
RE2::Replace(&dbc_path_string, kPciBusIdPlaceholderRegex, usb_pci_bus_id_);
base::FilePath dbc_control_path = base::FilePath(dbc_path_string);
if (state != kDbcEnable && state != kDbcDisable) {
PLOG(ERROR) << "Incorrect DbC state passed to UpdateDbcState. Should be "
<< kDbcEnable << " or " << kDbcDisable << " but " << state
<< " was given.";
return;
}
if (!base::PathExists(dbc_control_path)) {
PLOG(ERROR) << "DbC control file does not exist";
} else {
// Change DbC state
if (!base::WriteFile(dbc_control_path, state)) {
PLOG(ERROR) << "Failed to write '" << state << "' to dbc control file";
return;
}
VLOG(1) << "Successfully updated dbc to " << state << " for "
<< dbc_control_path;
}
}
// Update the USB port's role ("host" or "device")
void UdevMonitor::UpdatePortRole(int port_num, const std::string& role) {
if (role != kUsbRoleHost && role != kUsbRoleDevice) {
PLOG(ERROR) << "Incorrect USB role passed to UpdatePortRole. Should be "
<< kUsbRoleHost << " or " << kUsbRoleDevice << " but " << role
<< " was given.";
return;
}
// There are two possible paths to the typec usb role control file
// 1. /sys/class/typec/port{PORT}/usb-role-switch/role. This should exist
// in Linux 6.10+
auto typec_path_string = std::string(kTypecUsbRoleSysPath);
RE2::Replace(&typec_path_string, kUsbRolePortRegex, std::to_string(port_num));
base::FilePath typec_usb_role_path = base::FilePath(typec_path_string);
// 2. /sys/class/usb_role/CON{PORT}-role-switch/role. This should always
// exist but port_num may not match with CON{port_num}-role-switch so it is
// used only as a fallback case.
auto usb_role_path_string = std::string(kUsbRoleSysPath);
RE2::Replace(&usb_role_path_string, kUsbRolePortRegex,
std::to_string(port_num));
base::FilePath usb_role_path = base::FilePath(usb_role_path_string);
// First check the /sys/class/typec path
if (base::PathExists(typec_usb_role_path)) {
// update typec USB role on the /sys/class/typec/portX path
if (!base::WriteFile(typec_usb_role_path, role)) {
PLOG(ERROR) << "Failed to write typec usb role to "
<< typec_usb_role_path;
return;
} else {
VLOG(1) << "Successfully updated usb_role to " << role << " for "
<< typec_usb_role_path;
}
} else {
// Fallback to using /sys/class/usb_role
if (!base::PathExists(usb_role_path)) {
PLOG(ERROR) << "Usb role switch control files " << typec_usb_role_path
<< " and " << usb_role_path << " do not exist";
return;
} else {
// update USB role on the /sys/class/typec path
if (!base::WriteFile(usb_role_path, role)) {
PLOG(ERROR) << "Failed to write usb role";
return;
} else {
VLOG(1) << "Successfully updated usb_role to " << role << " for "
<< usb_role_path;
}
}
}
}
// Callback for subscribed udev events.
void UdevMonitor::OnUdevEvent() {
auto device = udev_monitor_->ReceiveDevice();
if (!device) {
LOG(ERROR) << "Udev receive device failed.";
return;
}
auto path = base::FilePath(device->GetSysPath());
if (path.empty()) {
LOG(ERROR) << "Failed to get device syspath.";
return;
}
auto action = std::string(device->GetAction());
if (action.empty()) {
LOG(ERROR) << "Failed to get device action.";
return;
}
auto subsystem = std::string(device->GetSubsystem());
if (subsystem.empty()) {
LOG(ERROR) << "Failed to get device subsystem";
return;
}
if (action == "add") {
UdevMonitor::OnDeviceAdd(path);
}
if (action == "remove") {
UdevMonitor::OnDeviceRemove(path);
}
}
} // namespace adbd