blob: 1d7583e71050853f13268cd90808278161d4cef6 [file] [log] [blame]
// Copyright 2017 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 "hammerd/usb_utils.h"
#include <fcntl.h>
#include <linux/usbdevice_fs.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <memory>
#include <string>
#include <base/files/file_enumerator.h>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
namespace hammerd {
namespace {
constexpr int kError = -1;
constexpr unsigned int kTimeoutMs = 1000; // Default timeout value.
} // namespace
const base::FilePath GetUsbSysfsPath(uint16_t bus, const std::string& port) {
return base::FilePath(base::StringPrintf("/sys/bus/usb/devices/%d-%s",
bus, port.c_str()));
}
static bool GetUsbDevicePath(uint16_t bus, const std::string& port,
base::FilePath* out) {
// Find the line in the uevent that starts with "DEVNAME=", and replace it
// with "/dev/".
const std::string devname_prefix = "DEVNAME=";
const base::FilePath uevent_path =
GetUsbSysfsPath(bus, port).Append("uevent");
std::string content;
if (!base::ReadFileToString(uevent_path, &content)) {
LOG(ERROR) << "Failed to read uevent.";
return false;
}
for (const auto& line : base::SplitStringPiece(content, "\n",
base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)) {
if (base::StartsWith(line, devname_prefix, base::CompareCase::SENSITIVE)) {
std::string path(line.data(), line.size());
*out = base::FilePath("/dev").Append(path.substr(devname_prefix.size()));
return true;
}
}
LOG(ERROR) << "Failed to get usbfs path.";
return false;
}
static bool ReadFileToInt(const base::FilePath& path, int* value) {
std::string str;
if (!base::ReadFileToString(path, &str)) {
return false;
}
base::TrimWhitespaceASCII(str, base::TRIM_ALL, &str);
return base::HexStringToInt(str, value);
}
static bool CheckFileIntValue(const base::FilePath& path, int value) {
int file_value;
return ReadFileToInt(path, &file_value) && (value == file_value);
}
UsbEndpoint::UsbEndpoint(uint16_t vendor_id, uint16_t product_id,
uint16_t bus, std::string port)
: vendor_id_(vendor_id), product_id_(product_id), bus_(bus), port_(port),
fd_(-1), iface_num_(-1), ep_num_(-1), chunk_len_(-1) {}
UsbEndpoint::~UsbEndpoint() {
Close();
}
bool UsbEndpoint::UsbSysfsExists() {
const base::FilePath usb_path = GetUsbSysfsPath(bus_, port_);
return base::DirectoryExists(usb_path);
}
UsbConnectStatus UsbEndpoint::Connect() {
if (IsConnected()) {
DLOG(INFO) << "Already initialized. Ignore.";
return UsbConnectStatus::kSuccess;
}
// Confirm the device has valid vendor/product ID.
// (b/70955082): Only return |kInvalidDevice| when the VID/PID files exist but
// aren't the expected values. This is to prevent mis-reporting an invalid
// device on AP suspend/resume, when the files may not yet be ready.
if (!UsbSysfsExists()) {
LOG(ERROR) << "USB sysfs does not exist.";
return UsbConnectStatus::kUsbPathEmpty;
}
const base::FilePath usb_path = GetUsbSysfsPath(bus_, port_);
int vendor_id, product_id;
if (!ReadFileToInt(usb_path.Append("idVendor"), &vendor_id) ||
!ReadFileToInt(usb_path.Append("idProduct"), &product_id)) {
LOG(ERROR) << "Failed to read VID and PID.";
return UsbConnectStatus::kUnknownError;
}
if (vendor_id_ != vendor_id || product_id_ != product_id) {
LOG(ERROR) << "Invalid VID and PID.";
return UsbConnectStatus::kInvalidDevice;
}
if (!base::ReadFileToString(usb_path.Append("configuration"),
&configuration_string_)) {
LOG(ERROR) << "Failed to read configuration file.";
return UsbConnectStatus::kUnknownError;
}
base::TrimWhitespaceASCII(configuration_string_, base::TRIM_ALL,
&configuration_string_);
// Find the interface matching class, subclass, and protocol and the endpoint
// number. The interface should only contain one pair of endpoints with the
// same endpoint number, one for IN and another for OUT.
// The endpoint address is composed of:
// - Bits 0..6: Endpoint Number
// - Bits 7: Direction 0 = Out, 1 = In
bool is_found = false;
base::FileEnumerator iface_paths(
usb_path, false,
base::FileEnumerator::FileType::DIRECTORIES,
base::StringPrintf("%d-%s:*", bus_, port_.c_str()));
for (base::FilePath iface_path = iface_paths.Next();
!iface_path.empty();
iface_path = iface_paths.Next()) {
if (CheckFileIntValue(iface_path.Append("bInterfaceClass"),
kUsbClassGoogleUpdate) &&
CheckFileIntValue(iface_path.Append("bInterfaceSubClass"),
kUsbSubclassGoogleUpdate) &&
CheckFileIntValue(iface_path.Append("bInterfaceProtocol"),
kUsbProtocolGoogleUpdate)) {
if (!ReadFileToInt(iface_path.Append("bInterfaceNumber"), &iface_num_)) {
LOG(ERROR) << "Failed to read interface number.";
return UsbConnectStatus::kUnknownError;
}
if (!CheckFileIntValue(iface_path.Append("bNumEndpoints"), 2)) {
LOG(ERROR) << "Interface should only have 2 Endpoints.";
return UsbConnectStatus::kUnknownError;
}
// Get endpoint number and chunk size. Two endpoints should have the same
// endpoint number, so just calculate it by the first one.
base::FilePath ep_path = base::FileEnumerator(
iface_path, false,
base::FileEnumerator::FileType::DIRECTORIES, "ep_*").Next();
if (!ReadFileToInt(ep_path.Append("bEndpointAddress"), &ep_num_) ||
!ReadFileToInt(ep_path.Append("wMaxPacketSize"), &chunk_len_)) {
LOG(ERROR) << "Failed to read endpoint address and chunk size.";
return UsbConnectStatus::kUnknownError;
}
ep_num_ = ep_num_ & 0x7f; // Bit mask of 0~6 bits.
DLOG(INFO) << "found interface " << iface_num_ << ", endpoint "
<< static_cast<int>(ep_num_) << ", chunk_len " << chunk_len_;
is_found = true;
break;
}
}
if (!is_found) {
LOG(ERROR) << "Failed to find a valid interface.";
return UsbConnectStatus::kUnknownError;
}
// Open the usbfs file, and claim the interface.
base::FilePath usbfs_path;
if (!GetUsbDevicePath(bus_, port_, &usbfs_path)) {
return UsbConnectStatus::kUnknownError;
}
fd_ = open(usbfs_path.value().c_str(), O_RDWR | O_CLOEXEC);
if (fd_ < 0) {
PLOG(ERROR) << "Failed to open usbfs file";
Close();
return UsbConnectStatus::kUnknownError;
}
if (ioctl(fd_, USBDEVFS_CLAIMINTERFACE, &iface_num_)) {
PLOG(ERROR) << "Failed to claim interface";
Close();
return UsbConnectStatus::kUnknownError;
}
LOG(INFO) << "USB endpoint is initialized successfully.";
return UsbConnectStatus::kSuccess;
}
// Release USB device.
void UsbEndpoint::Close() {
if (iface_num_ >= 0) {
ioctl(fd_, USBDEVFS_RELEASEINTERFACE, &iface_num_);
}
if (fd_ != -1) {
close(fd_);
}
configuration_string_ = "";
iface_num_ = -1;
fd_ = -1;
ep_num_ = -1;
chunk_len_ = -1;
}
bool UsbEndpoint::IsConnected() const {
return (fd_ != -1);
}
int UsbEndpoint::Transfer(const void* outbuf,
int outlen,
void* inbuf,
int inlen,
bool allow_less,
unsigned int timeout_ms) {
if (Send(outbuf, outlen, timeout_ms) != outlen) {
return kError;
}
if (inlen == 0) {
return 0;
}
return Receive(inbuf, inlen, allow_less, timeout_ms);
}
int UsbEndpoint::Send(const void* outbuf, int outlen, unsigned int timeout_ms) {
// BulkTransfer() does not modify the buffer while using kUsbEndpointOut
// direction mask.
int actual = BulkTransfer(
const_cast<void*>(outbuf), kUsbEndpointOut, outlen, timeout_ms);
if (actual != outlen) {
LOG(ERROR) << "Failed to send the complete data.";
}
return actual;
}
int UsbEndpoint::Receive(void* inbuf,
int inlen,
bool allow_less,
unsigned int timeout_ms) {
int actual = BulkTransfer(inbuf, kUsbEndpointIn, inlen, timeout_ms);
if ((actual != inlen) && !allow_less) {
LOG(ERROR) << "Failed to receive the complete data.";
return kError;
}
return actual;
}
int UsbEndpoint::BulkTransfer(void* buf,
int direction_mask,
int len,
unsigned int timeout_ms) {
if (timeout_ms == 0) {
timeout_ms = kTimeoutMs;
}
struct usbdevfs_bulktransfer bulk = {
.ep = static_cast<unsigned int>(ep_num_ | direction_mask),
.len = static_cast<unsigned int>(len),
.timeout = timeout_ms,
.data = buf};
return ioctl(fd_, USBDEVFS_BULK, &bulk);
}
} // namespace hammerd