// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/device/serial/serial_device_enumerator_win.h"
#include <windows.h> // Must be in front of other Windows header files.
#define INITGUID
#include <devguid.h>
#include <devpkey.h>
#include <ntddser.h>
#include <setupapi.h>
#include <stdint.h>
#include <algorithm>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include "base/containers/contains.h"
#include "base/metrics/histogram_functions.h"
#include "base/scoped_generic.h"
#include "base/scoped_observation.h"
#include "base/sequence_checker.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/win/registry.h"
#include "base/win/scoped_devinfo.h"
#include "components/device_event_log/device_event_log.h"
#include "third_party/re2/src/re2/re2.h"
namespace device {
namespace {
std::optional<std::string> GetProperty(HDEVINFO dev_info,
SP_DEVINFO_DATA* dev_info_data,
const DEVPROPKEY& property) {
// SetupDiGetDeviceProperty() makes an RPC which may block.
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
DEVPROPTYPE property_type;
DWORD required_size;
if (SetupDiGetDeviceProperty(dev_info, dev_info_data, &property,
&property_type, /*PropertyBuffer=*/nullptr,
/*PropertyBufferSize=*/0, &required_size,
/*Flags=*/0) ||
GetLastError() != ERROR_INSUFFICIENT_BUFFER ||
property_type != DEVPROP_TYPE_STRING) {
return std::nullopt;
}
std::wstring buffer;
if (!SetupDiGetDeviceProperty(
dev_info, dev_info_data, &property, &property_type,
reinterpret_cast<PBYTE>(base::WriteInto(&buffer, required_size)),
required_size, /*RequiredSize=*/nullptr, /*Flags=*/0)) {
return std::nullopt;
}
return base::WideToUTF8(buffer);
}
// Get the port name from the registry.
std::optional<std::string> GetPortName(HDEVINFO dev_info,
SP_DEVINFO_DATA* dev_info_data) {
HKEY key = SetupDiOpenDevRegKey(dev_info, dev_info_data, DICS_FLAG_GLOBAL, 0,
DIREG_DEV, KEY_READ);
if (key == INVALID_HANDLE_VALUE) {
SERIAL_PLOG(ERROR) << "Could not open device registry key";
return std::nullopt;
}
base::win::RegKey scoped_key(key);
std::wstring port_name;
LONG result = scoped_key.ReadValue(L"PortName", &port_name);
if (result != ERROR_SUCCESS) {
SERIAL_LOG(ERROR) << "Failed to read port name: "
<< logging::SystemErrorCodeToString(result);
return std::nullopt;
}
return base::SysWideToUTF8(port_name);
}
// Deduce the path for the device from the port name.
base::FilePath GetPath(std::string port_name) {
// For COM numbers less than 9, CreateFile is called with a string such as
// "COM1". For numbers greater than 9, a prefix of "\\.\" must be added.
if (port_name.length() > std::string_view("COM9").length()) {
return base::FilePath(LR"(\\.\)").AppendASCII(port_name);
}
return base::FilePath::FromUTF8Unsafe(port_name);
}
// Searches for the display name in the device's friendly name. Returns nullopt
// if the name does not match the expected pattern.
std::optional<std::string> GetDisplayName(const std::string& friendly_name) {
std::string display_name;
if (!RE2::PartialMatch(friendly_name, R"((.*) \(COM[0-9]+\))",
&display_name)) {
return std::nullopt;
}
return display_name;
}
// Searches for the vendor ID in the device's instance ID. Returns nullopt if
// the instance ID does not match the expected pattern.
std::optional<uint32_t> GetVendorID(const std::string& instance_id) {
std::string vendor_id_str;
if (!RE2::PartialMatch(instance_id, "VID_([0-9a-fA-F]+)", &vendor_id_str)) {
return std::nullopt;
}
uint32_t vendor_id;
if (!base::HexStringToUInt(vendor_id_str, &vendor_id)) {
return std::nullopt;
}
return vendor_id;
}
// Searches for the product ID in the device's instance ID. Returns nullopt if
// the instance ID does not match the expected pattern.
std::optional<uint32_t> GetProductID(const std::string& instance_id) {
std::string product_id_str;
if (!RE2::PartialMatch(instance_id, "PID_([0-9a-fA-F]+)", &product_id_str)) {
return std::nullopt;
}
uint32_t product_id;
if (!base::HexStringToUInt(product_id_str, &product_id)) {
return std::nullopt;
}
return product_id;
}
} // namespace
class SerialDeviceEnumeratorWin::UiThreadHelper
: public DeviceMonitorWin::Observer {
public:
UiThreadHelper(base::WeakPtr<SerialDeviceEnumeratorWin> enumerator,
scoped_refptr<base::SequencedTaskRunner> task_runner)
: enumerator_(std::move(enumerator)),
task_runner_(std::move(task_runner)) {
// Note that this uses GUID_DEVINTERFACE_COMPORT even though we use
// GUID_DEVINTERFACE_SERENUM_BUS_ENUMERATOR for enumeration because it
// doesn't seem to make a difference and ports which aren't enumerable by
// device interface don't generate WM_DEVICECHANGE events.
device_observation_.Observe(
DeviceMonitorWin::GetForDeviceInterface(GUID_DEVINTERFACE_COMPORT));
}
// Disallow copy and assignment.
UiThreadHelper(UiThreadHelper&) = delete;
UiThreadHelper& operator=(UiThreadHelper&) = delete;
virtual ~UiThreadHelper() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void OnDeviceAdded(const GUID& class_guid,
const std::wstring& device_path) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
task_runner_->PostTask(
FROM_HERE, base::BindOnce(&SerialDeviceEnumeratorWin::OnPathAdded,
enumerator_, device_path));
}
void OnDeviceRemoved(const GUID& class_guid,
const std::wstring& device_path) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
task_runner_->PostTask(
FROM_HERE, base::BindOnce(&SerialDeviceEnumeratorWin::OnPathRemoved,
enumerator_, device_path));
}
private:
SEQUENCE_CHECKER(sequence_checker_);
// Weak reference to the SerialDeviceEnumeratorWin that owns this object.
// Calls on |enumerator_| must be posted to |task_runner_|.
base::WeakPtr<SerialDeviceEnumeratorWin> enumerator_;
scoped_refptr<base::SequencedTaskRunner> task_runner_;
base::ScopedObservation<DeviceMonitorWin, DeviceMonitorWin::Observer>
device_observation_{this};
};
SerialDeviceEnumeratorWin::SerialDeviceEnumeratorWin(
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner) {
helper_ = base::SequenceBound<UiThreadHelper>(
std::move(ui_task_runner), weak_factory_.GetWeakPtr(),
base::SequencedTaskRunner::GetCurrentDefault());
DoInitialEnumeration();
}
SerialDeviceEnumeratorWin::~SerialDeviceEnumeratorWin() = default;
void SerialDeviceEnumeratorWin::OnPathAdded(const std::wstring& device_path) {
base::win::ScopedDevInfo dev_info(
SetupDiCreateDeviceInfoList(nullptr, nullptr));
if (!dev_info.is_valid())
return;
if (!SetupDiOpenDeviceInterface(dev_info.get(), device_path.c_str(), 0,
nullptr)) {
return;
}
SP_DEVINFO_DATA dev_info_data = {};
dev_info_data.cbSize = sizeof(dev_info_data);
if (!SetupDiEnumDeviceInfo(dev_info.get(), 0, &dev_info_data))
return;
EnumeratePort(dev_info.get(), &dev_info_data, /*check_port_name=*/false);
}
void SerialDeviceEnumeratorWin::OnPathRemoved(const std::wstring& device_path) {
base::win::ScopedDevInfo dev_info(
SetupDiCreateDeviceInfoList(nullptr, nullptr));
if (!dev_info.is_valid())
return;
if (!SetupDiOpenDeviceInterface(dev_info.get(), device_path.c_str(), 0,
nullptr)) {
return;
}
SP_DEVINFO_DATA dev_info_data = {};
dev_info_data.cbSize = sizeof(dev_info_data);
if (!SetupDiEnumDeviceInfo(dev_info.get(), 0, &dev_info_data))
return;
std::optional<std::string> port_name =
GetPortName(dev_info.get(), &dev_info_data);
if (!port_name)
return;
auto it = paths_.find(GetPath(*port_name));
if (it == paths_.end())
return;
base::UnguessableToken token = it->second;
paths_.erase(it);
RemovePort(token);
}
void SerialDeviceEnumeratorWin::DoInitialEnumeration() {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
// On Windows 10 and above most COM port drivers register using the COMPORT
// device interface class. Try to enumerate these first.
{
base::win::ScopedDevInfo dev_info;
dev_info.reset(SetupDiGetClassDevs(&GUID_DEVINTERFACE_COMPORT, nullptr, 0,
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE));
if (!dev_info.is_valid())
return;
SP_DEVINFO_DATA dev_info_data = {.cbSize = sizeof(dev_info_data)};
for (DWORD i = 0; SetupDiEnumDeviceInfo(dev_info.get(), i, &dev_info_data);
i++) {
EnumeratePort(dev_info.get(), &dev_info_data, /*check_port_name=*/false);
}
}
// To detect devices which don't register with GUID_DEVINTERFACE_COMPORT also
// enuerate all devices in the "Ports" and "Modems" device classes. These must
// be checked to see if the port name starts with "COM" because it also
// includes LPT ports.
constexpr const GUID* kDeviceClasses[] = {&GUID_DEVCLASS_MODEM,
&GUID_DEVCLASS_PORTS};
for (const GUID* guid : kDeviceClasses) {
base::win::ScopedDevInfo dev_info;
dev_info.reset(SetupDiGetClassDevs(guid, nullptr, 0, DIGCF_PRESENT));
if (!dev_info.is_valid())
return;
SP_DEVINFO_DATA dev_info_data = {.cbSize = sizeof(dev_info_data)};
for (DWORD i = 0; SetupDiEnumDeviceInfo(dev_info.get(), i, &dev_info_data);
i++) {
EnumeratePort(dev_info.get(), &dev_info_data, /*check_port_name=*/true);
}
}
}
void SerialDeviceEnumeratorWin::EnumeratePort(HDEVINFO dev_info,
SP_DEVINFO_DATA* dev_info_data,
bool check_port_name) {
std::optional<std::string> port_name = GetPortName(dev_info, dev_info_data);
if (!port_name)
return;
if (check_port_name && !base::StartsWith(*port_name, "COM"))
return;
// Check whether the currently enumerating port has been seen before since
// the method above will generate duplicate enumerations for some ports.
base::FilePath path = GetPath(*port_name);
if (base::Contains(paths_, path))
return;
std::optional<std::string> instance_id =
GetProperty(dev_info, dev_info_data, DEVPKEY_Device_InstanceId);
if (!instance_id)
return;
// Some versions of Windows pad this string with a variable number of NUL
// bytes for no discernible reason.
instance_id = std::string(base::TrimString(
*instance_id, std::string_view("\0", 1), base::TRIM_TRAILING));
base::UnguessableToken token = base::UnguessableToken::Create();
auto info = mojom::SerialPortInfo::New();
info->token = token;
info->path = path;
info->device_instance_id = *instance_id;
// TODO(crbug.com/40653536): While the "bus reported device
// description" is usually the USB product string this is still up to the
// individual serial driver and could be equal to the "friendly name". It
// would be more reliable to read the real USB strings here.
info->display_name = GetProperty(dev_info, dev_info_data,
DEVPKEY_Device_BusReportedDeviceDesc);
if (info->display_name) {
// This string is also sometimes padded with a variable number of NUL bytes
// for no discernible reason.
info->display_name = std::string(base::TrimString(
*info->display_name, std::string_view("\0", 1), base::TRIM_TRAILING));
} else {
// Fall back to the "friendly name" if no "bus reported device description"
// is available. This name will likely be the same for all devices using the
// same driver.
std::optional<std::string> friendly_name =
GetProperty(dev_info, dev_info_data, DEVPKEY_Device_FriendlyName);
if (!friendly_name)
return;
info->display_name = GetDisplayName(*friendly_name);
}
// The instance ID looks like "FTDIBUS\VID_0403+PID_6001+A703X87GA\0000".
std::optional<uint32_t> vendor_id = GetVendorID(*instance_id);
std::optional<uint32_t> product_id = GetProductID(*instance_id);
std::optional<std::string> vendor_id_str, product_id_str;
if (vendor_id) {
info->has_vendor_id = true;
info->vendor_id = *vendor_id;
vendor_id_str = base::StringPrintf("%04X", *vendor_id);
}
if (product_id) {
info->has_product_id = true;
info->product_id = *product_id;
product_id_str = base::StringPrintf("%04X", *product_id);
}
SERIAL_LOG(EVENT) << "Serial device added: path=" << info->path
<< " instance_id=" << info->device_instance_id
<< " vid=" << vendor_id_str.value_or("(none)")
<< " pid=" << product_id_str.value_or("(none)");
paths_.insert(std::make_pair(path, token));
AddPort(std::move(info));
}
} // namespace device