I tested this on my Linux machine and verified that the message shows up if I remove my permission to access /dev/ttyS0, etc. Differential Revision: https://phabricator.services.mozilla.com/D303917
892 lines
31 KiB
C++
892 lines
31 KiB
C++
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "Win32SerialPlatformService.h"
|
|
|
|
#include <cfgmgr32.h>
|
|
// Including initguid.h needs to come before including devpkey.h, so
|
|
// disable clang-format here.
|
|
|
|
// clang-format off
|
|
#include <initguid.h>
|
|
#include <devpkey.h>
|
|
// clang-format on
|
|
|
|
#include <ntddser.h>
|
|
#include <setupapi.h>
|
|
|
|
#include "SerialLogging.h"
|
|
#include "mozilla/AsyncPlatformPipes.h"
|
|
#include "mozilla/ScopeExit.h"
|
|
#include "nsString.h"
|
|
#include "nsThreadUtils.h"
|
|
|
|
namespace mozilla::dom {
|
|
|
|
namespace {
|
|
constexpr size_t kPropertyBufferSize = 256;
|
|
constexpr unsigned int kDeviceChangeDelayMs = 200;
|
|
constexpr wchar_t kDevicePathPrefix[] = L"\\\\.\\";
|
|
} // namespace
|
|
|
|
Win32SerialPlatformService::Win32SerialPlatformService()
|
|
: mIOCapability(IOThread()) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
("Win32SerialPlatformService[%p] created", this));
|
|
MOZ_ALWAYS_SUCCEEDS(NS_CreateBackgroundTaskQueue(
|
|
"SerialMonitorQueue", getter_AddRefs(mMonitorThread)));
|
|
}
|
|
|
|
nsresult Win32SerialPlatformService::Init() {
|
|
return StartMonitoringDeviceChanges();
|
|
}
|
|
|
|
void Win32SerialPlatformService::Shutdown() {
|
|
AssertIsOnMainThread();
|
|
if (IsShutdown()) {
|
|
return;
|
|
}
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
("Win32SerialPlatformService[%p]::Shutdown", this));
|
|
StopMonitoringDeviceChanges();
|
|
mMonitorThread = nullptr;
|
|
SerialPlatformService::Shutdown();
|
|
}
|
|
|
|
Win32SerialPlatformService::~Win32SerialPlatformService() {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
("Win32SerialPlatformService[%p] destroyed (closing %u ports)", this,
|
|
mOpenPorts.Count()));
|
|
for (auto iter = mOpenPorts.Iter(); !iter.Done(); iter.Next()) {
|
|
HANDLE handle = iter.Data();
|
|
if (handle != INVALID_HANDLE_VALUE) {
|
|
CloseHandle(handle);
|
|
}
|
|
}
|
|
mOpenPorts.Clear();
|
|
}
|
|
|
|
namespace {
|
|
nsresult EnumeratePortsWin32(SerialPortList& aPorts) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService::EnumeratePorts"));
|
|
aPorts.Clear();
|
|
|
|
HDEVINFO deviceInfoSet =
|
|
SetupDiGetClassDevs(&GUID_DEVINTERFACE_COMPORT, nullptr, nullptr,
|
|
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
|
|
|
|
if (deviceInfoSet == INVALID_HANDLE_VALUE) {
|
|
DWORD error = GetLastError();
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService::EnumeratePorts "
|
|
"SetupDiGetClassDevs failed: 0x%08lx",
|
|
error));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
auto cleanupDeviceInfoSet =
|
|
MakeScopeExit([&]() { SetupDiDestroyDeviceInfoList(deviceInfoSet); });
|
|
|
|
SP_DEVINFO_DATA deviceInfo;
|
|
deviceInfo.cbSize = sizeof(SP_DEVINFO_DATA);
|
|
|
|
for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &deviceInfo); ++i) {
|
|
HKEY hKey = SetupDiOpenDevRegKey(deviceInfoSet, &deviceInfo,
|
|
DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ);
|
|
|
|
if (hKey == INVALID_HANDLE_VALUE) {
|
|
continue;
|
|
}
|
|
wchar_t portName[kPropertyBufferSize];
|
|
{
|
|
auto cleanupRegKey = MakeScopeExit([&]() { RegCloseKey(hKey); });
|
|
|
|
DWORD portNameSize = sizeof(portName);
|
|
LONG result =
|
|
RegGetValueW(hKey, nullptr, L"PortName", RRF_RT_REG_SZ, nullptr,
|
|
reinterpret_cast<LPBYTE>(portName), &portNameSize);
|
|
if (result != ERROR_SUCCESS) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
wchar_t friendlyName[kPropertyBufferSize] = {0};
|
|
DWORD friendlyNameSize = sizeof(friendlyName);
|
|
// The bus reported device description is usually more descriptive than
|
|
// the friendly name (for LEGO Spike, Flipper Zero, etc.)
|
|
auto deviceDescriptionPropKey = DEVPKEY_Device_BusReportedDeviceDesc;
|
|
DEVPROPTYPE unusedPropertyType;
|
|
BOOL succeeded = SetupDiGetDevicePropertyW(
|
|
deviceInfoSet, &deviceInfo, &deviceDescriptionPropKey,
|
|
&unusedPropertyType, reinterpret_cast<PBYTE>(friendlyName),
|
|
friendlyNameSize, nullptr, 0);
|
|
if (!(succeeded && *friendlyName)) {
|
|
deviceDescriptionPropKey = DEVPKEY_Device_FriendlyName;
|
|
succeeded = SetupDiGetDevicePropertyW(
|
|
deviceInfoSet, &deviceInfo, &deviceDescriptionPropKey,
|
|
&unusedPropertyType, reinterpret_cast<PBYTE>(friendlyName),
|
|
friendlyNameSize, nullptr, 0);
|
|
}
|
|
if (!(succeeded && *friendlyName)) {
|
|
wcscpy_s(friendlyName, portName);
|
|
}
|
|
// SPDRP_HARDWAREID is of type REG_MULTI_SZ. We just are interested
|
|
// in the first one, and since each string is null-terminated, we'll
|
|
// just treat it as a regular string.
|
|
wchar_t hardwareId[kPropertyBufferSize] = {0};
|
|
SetupDiGetDeviceRegistryPropertyW(
|
|
deviceInfoSet, &deviceInfo, SPDRP_HARDWAREID, nullptr,
|
|
reinterpret_cast<PBYTE>(hardwareId), sizeof(hardwareId), nullptr);
|
|
|
|
IPCSerialPortInfo info;
|
|
info.id() = nsString(portName);
|
|
info.friendlyName() = nsString(friendlyName);
|
|
|
|
{
|
|
nsString path(kDevicePathPrefix);
|
|
path.Append(portName);
|
|
info.path() = std::move(path);
|
|
}
|
|
|
|
uint16_t vendorId = 0;
|
|
uint16_t productId = 0;
|
|
wchar_t* vidLocation = wcsstr(hardwareId, L"VID_");
|
|
if (vidLocation) {
|
|
swscanf_s(vidLocation + 4, L"%4hx", &vendorId);
|
|
}
|
|
wchar_t* pidLocation = wcsstr(hardwareId, L"PID_");
|
|
if (pidLocation) {
|
|
swscanf_s(pidLocation + 4, L"%4hx", &productId);
|
|
}
|
|
|
|
if (vendorId != 0 && productId != 0) {
|
|
info.usbVendorId() = Some(vendorId);
|
|
info.usbProductId() = Some(productId);
|
|
}
|
|
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService::EnumeratePorts found port '%s' (%s) "
|
|
"VID:0x%04x PID:0x%04x",
|
|
NS_ConvertUTF16toUTF8(info.id()).get(),
|
|
NS_ConvertUTF16toUTF8(info.friendlyName()).get(), vendorId,
|
|
productId));
|
|
aPorts.AppendElement(info);
|
|
}
|
|
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
("Win32SerialPlatformService::EnumeratePorts found %zu ports",
|
|
aPorts.Length()));
|
|
return NS_OK;
|
|
}
|
|
} // namespace
|
|
|
|
nsresult Win32SerialPlatformService::EnumeratePortsImpl(
|
|
SerialPortList& aPorts, bool* aLikelyAccessDenied) {
|
|
return EnumeratePortsWin32(aPorts);
|
|
}
|
|
|
|
HANDLE Win32SerialPlatformService::FindPortHandle(const nsString& aPortId) {
|
|
mIOCapability.AssertOnCurrentThread();
|
|
return mOpenPorts.MaybeGet(aPortId).valueOr(INVALID_HANDLE_VALUE);
|
|
}
|
|
|
|
nsresult Win32SerialPlatformService::ConfigurePort(
|
|
HANDLE aHandle, const IPCSerialOptions& aOptions) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::ConfigurePort (baudRate=%u, "
|
|
"dataBits=%u, stopBits=%u, parity=%u, flowControl=%u)",
|
|
this, aOptions.baudRate(), aOptions.dataBits(), aOptions.stopBits(),
|
|
static_cast<unsigned>(aOptions.parity()),
|
|
static_cast<unsigned>(aOptions.flowControl())));
|
|
mIOCapability.AssertOnCurrentThread();
|
|
|
|
DCB dcb = {0};
|
|
dcb.DCBlength = sizeof(DCB);
|
|
|
|
if (!GetCommState(aHandle, &dcb)) {
|
|
DWORD error = GetLastError();
|
|
MOZ_LOG(
|
|
gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::ConfigurePort GetCommState failed: "
|
|
"0x%08lx",
|
|
this, error));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
// These options are not configurable
|
|
dcb.fBinary = TRUE;
|
|
dcb.fParity = TRUE;
|
|
dcb.fAbortOnError = FALSE;
|
|
dcb.fOutxDsrFlow = FALSE;
|
|
dcb.fDtrControl = DTR_CONTROL_ENABLE;
|
|
dcb.fDsrSensitivity = FALSE;
|
|
dcb.fOutX = FALSE;
|
|
dcb.fInX = FALSE;
|
|
|
|
dcb.BaudRate = aOptions.baudRate();
|
|
|
|
switch (aOptions.dataBits()) {
|
|
case 7:
|
|
dcb.ByteSize = 7;
|
|
break;
|
|
case 8:
|
|
dcb.ByteSize = 8;
|
|
break;
|
|
default:
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
switch (aOptions.stopBits()) {
|
|
case 1:
|
|
dcb.StopBits = ONESTOPBIT;
|
|
break;
|
|
case 2:
|
|
dcb.StopBits = TWOSTOPBITS;
|
|
break;
|
|
default:
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
switch (aOptions.parity()) {
|
|
case ParityType::None:
|
|
dcb.Parity = NOPARITY;
|
|
break;
|
|
case ParityType::Even:
|
|
dcb.Parity = EVENPARITY;
|
|
break;
|
|
case ParityType::Odd:
|
|
dcb.Parity = ODDPARITY;
|
|
break;
|
|
default:
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
switch (aOptions.flowControl()) {
|
|
case FlowControlType::None:
|
|
dcb.fOutxCtsFlow = FALSE;
|
|
dcb.fRtsControl = RTS_CONTROL_ENABLE;
|
|
break;
|
|
case FlowControlType::Hardware:
|
|
dcb.fOutxCtsFlow = TRUE;
|
|
dcb.fRtsControl = RTS_CONTROL_HANDSHAKE;
|
|
break;
|
|
default:
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
if (!SetCommState(aHandle, &dcb)) {
|
|
DWORD error = GetLastError();
|
|
MOZ_LOG(
|
|
gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::ConfigurePort SetCommState failed: "
|
|
"0x%08lx",
|
|
this, error));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
COMMTIMEOUTS timeouts = {0};
|
|
// When both ReadIntervalTimeout and ReadTotalTimeoutMultiplier are MAXDWORD,
|
|
// ReadFile returns immediately with buffered data, or waits up to
|
|
// ReadTotalTimeoutConstant ms for the first byte. We use a very large
|
|
// constant (MAXDWORD-1 ~= 49 days) so the overlapped ReadFile effectively
|
|
// waits forever for data, ensuring bytesRead > 0 on every completion.
|
|
// PlatformPipeReader treats 0-byte reads as EOF, so we must never time out
|
|
// with 0 bytes. Using MAXDWORD for all three would mean "return immediately
|
|
// even with 0 bytes" which we must avoid.
|
|
timeouts.ReadIntervalTimeout = MAXDWORD;
|
|
timeouts.ReadTotalTimeoutMultiplier = MAXDWORD;
|
|
timeouts.ReadTotalTimeoutConstant = MAXDWORD - 1;
|
|
// Write timeout as a safety net.
|
|
timeouts.WriteTotalTimeoutMultiplier = 0;
|
|
timeouts.WriteTotalTimeoutConstant = 5000;
|
|
|
|
if (!SetCommTimeouts(aHandle, &timeouts)) {
|
|
DWORD error = GetLastError();
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::ConfigurePort SetCommTimeouts "
|
|
"failed: 0x%08lx",
|
|
this, error));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::ConfigurePort succeeded", this));
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult Win32SerialPlatformService::OpenImpl(
|
|
const nsString& aPortId, const IPCSerialOptions& aOptions) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
("Win32SerialPlatformService[%p]::Open port '%s'", this,
|
|
NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
mIOCapability.AssertOnCurrentThread();
|
|
|
|
// Validate portId format: must be "COM" followed by one or more digits.
|
|
// This prevents a compromised content process from using a crafted portId
|
|
// to open arbitrary devices in the \\.\ namespace (e.g. PhysicalDrive0).
|
|
if (aPortId.Length() < 4 || !StringBeginsWith(aPortId, u"COM"_ns)) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::Open rejected invalid portId "
|
|
"'%s': bad prefix",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
for (uint32_t i = 3; i < aPortId.Length(); i++) {
|
|
if (!iswdigit(aPortId.CharAt(i))) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::Open rejected invalid portId "
|
|
"'%s': non-digit character",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
}
|
|
|
|
if (mOpenPorts.Contains(aPortId)) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Warning,
|
|
("Win32SerialPlatformService[%p]::Open port '%s' already open",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_ERROR_FILE_IS_LOCKED;
|
|
}
|
|
|
|
nsString devicePath(kDevicePathPrefix);
|
|
devicePath.Append(aPortId);
|
|
|
|
HANDLE handle =
|
|
CreateFileW(devicePath.get(), GENERIC_READ | GENERIC_WRITE, 0, nullptr,
|
|
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);
|
|
|
|
if (handle == INVALID_HANDLE_VALUE) {
|
|
DWORD error = GetLastError();
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::Open CreateFileW failed for port "
|
|
"'%s' at path '%s': 0x%08lx",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get(),
|
|
NS_ConvertUTF16toUTF8(devicePath).get(), error));
|
|
if (error == ERROR_ACCESS_DENIED) {
|
|
return NS_ERROR_FILE_ACCESS_DENIED;
|
|
}
|
|
return NS_ERROR_NOT_AVAILABLE;
|
|
}
|
|
|
|
nsresult rv = ConfigurePort(handle, aOptions);
|
|
if (NS_FAILED(rv)) {
|
|
MOZ_LOG(
|
|
gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::Open ConfigurePort failed for port "
|
|
"'%s': 0x%08x",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get(),
|
|
static_cast<uint32_t>(rv)));
|
|
CloseHandle(handle);
|
|
return rv;
|
|
}
|
|
|
|
PurgeComm(handle, PURGE_RXCLEAR | PURGE_TXCLEAR);
|
|
|
|
mOpenPorts.InsertOrUpdate(aPortId, handle);
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
("Win32SerialPlatformService[%p]::Open succeeded for port '%s'", this,
|
|
NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult Win32SerialPlatformService::CloseImpl(const nsString& aPortId) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
("Win32SerialPlatformService[%p]::Close port '%s'", this,
|
|
NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
mIOCapability.AssertOnCurrentThread();
|
|
|
|
HANDLE handle = FindPortHandle(aPortId);
|
|
if (handle == INVALID_HANDLE_VALUE) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Warning,
|
|
("Win32SerialPlatformService[%p]::Close port '%s' not found", this,
|
|
NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_ERROR_NOT_AVAILABLE;
|
|
}
|
|
|
|
mOpenPorts.Remove(aPortId);
|
|
CloseHandle(handle);
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
("Win32SerialPlatformService[%p]::Close succeeded for port '%s'",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult Win32SerialPlatformService::WriteImpl(const nsString& aPortId,
|
|
Span<const uint8_t> aData) {
|
|
mIOCapability.AssertOnCurrentThread();
|
|
HANDLE handle = FindPortHandle(aPortId);
|
|
if (handle == INVALID_HANDLE_VALUE) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::Write port '%s' not found", this,
|
|
NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_ERROR_NOT_AVAILABLE;
|
|
}
|
|
|
|
if (aData.IsEmpty()) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Verbose,
|
|
("Win32SerialPlatformService[%p]::Write empty data for port '%s'",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_OK;
|
|
}
|
|
|
|
MOZ_LOG(
|
|
gWebSerialLog, LogLevel::Verbose,
|
|
("Win32SerialPlatformService[%p]::Write writing %zu bytes to port '%s'",
|
|
this, aData.Length(), NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
|
|
DWORD totalWritten = 0;
|
|
const uint8_t* buffer = aData.Elements();
|
|
DWORD remaining = static_cast<DWORD>(aData.Length());
|
|
|
|
auto event = UniqueFileHandle(CreateEvent(nullptr, TRUE, FALSE, nullptr));
|
|
if (!event) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
OVERLAPPED ov = {};
|
|
// Setting the low-order bit of hEvent prevents the I/O completion from
|
|
// being queued to the IOCP. This is necessary because PlatformPipeReader
|
|
// may have registered a DuplicateHandle of this port with the IOCP, and
|
|
// completions from our local OVERLAPPED would corrupt the IOCP handler
|
|
// lookup. GetOverlappedResult still works via the event.
|
|
HANDLE rawEvent = event.get();
|
|
ov.hEvent =
|
|
reinterpret_cast<HANDLE>(reinterpret_cast<uintptr_t>(rawEvent) | 1);
|
|
|
|
while (remaining > 0) {
|
|
ResetEvent(event.get());
|
|
|
|
if (!WriteFile(handle, buffer + totalWritten, remaining, nullptr, &ov)) {
|
|
DWORD error = GetLastError();
|
|
if (error != ERROR_IO_PENDING) {
|
|
MOZ_LOG(
|
|
gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::Write WriteFile failed for port "
|
|
"'%s': 0x%08lx",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get(), error));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
}
|
|
DWORD bytesWritten = 0;
|
|
if (!GetOverlappedResult(handle, &ov, &bytesWritten, TRUE)) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::Write GetOverlappedResult "
|
|
"failed for port '%s': 0x%08lx",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get(), GetLastError()));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
if (bytesWritten == 0) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::Write WriteFile returned 0 for "
|
|
"port '%s'",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
totalWritten += bytesWritten;
|
|
remaining -= bytesWritten;
|
|
|
|
if (remaining > 0) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Verbose,
|
|
("Win32SerialPlatformService[%p]::Write partial write for port "
|
|
"'%s': %lu bytes, %lu remaining",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get(), bytesWritten,
|
|
remaining));
|
|
}
|
|
}
|
|
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::Write wrote %lu bytes to port '%s'",
|
|
this, totalWritten, NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult Win32SerialPlatformService::DrainImpl(const nsString& aPortId) {
|
|
mIOCapability.AssertOnCurrentThread();
|
|
HANDLE handle = FindPortHandle(aPortId);
|
|
if (handle == INVALID_HANDLE_VALUE) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::Drain port not found: %s", this,
|
|
NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_ERROR_NOT_AVAILABLE;
|
|
}
|
|
|
|
MOZ_LOG(
|
|
gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::Drain draining transmit buffers for "
|
|
"port '%s'",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
|
|
if (!FlushFileBuffers(handle)) {
|
|
DWORD error = GetLastError();
|
|
MOZ_LOG(
|
|
gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::Drain FlushFileBuffers failed for "
|
|
"port '%s': error=%lu",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get(), error));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::Drain successfully drained buffers "
|
|
"for port '%s'",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult Win32SerialPlatformService::FlushImpl(const nsString& aPortId,
|
|
bool aReceive) {
|
|
mIOCapability.AssertOnCurrentThread();
|
|
HANDLE handle = FindPortHandle(aPortId);
|
|
if (handle == INVALID_HANDLE_VALUE) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::Flush port not found: %s", this,
|
|
NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_ERROR_NOT_AVAILABLE;
|
|
}
|
|
|
|
DWORD flags = aReceive ? PURGE_RXCLEAR : PURGE_TXCLEAR;
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::Flush discarding %s buffers "
|
|
"for port '%s'",
|
|
this, aReceive ? "receive" : "transmit",
|
|
NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
|
|
if (!PurgeComm(handle, flags)) {
|
|
DWORD error = GetLastError();
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::Flush PurgeComm failed for port "
|
|
"'%s': error=%lu",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get(), error));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::Flush successfully flushed %s "
|
|
"buffers for port '%s'",
|
|
this, aReceive ? "receive" : "transmit",
|
|
NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult Win32SerialPlatformService::SetSignalsImpl(
|
|
const nsString& aPortId, const IPCSerialOutputSignals& aSignals) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::SetSignals for port '%s' (DTR=%s, "
|
|
"RTS=%s, Break=%s)",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get(),
|
|
aSignals.dataTerminalReady().isSome()
|
|
? (aSignals.dataTerminalReady().value() ? "true" : "false")
|
|
: "unset",
|
|
aSignals.requestToSend().isSome()
|
|
? (aSignals.requestToSend().value() ? "true" : "false")
|
|
: "unset",
|
|
aSignals.breakSignal().isSome()
|
|
? (aSignals.breakSignal().value() ? "true" : "false")
|
|
: "unset"));
|
|
mIOCapability.AssertOnCurrentThread();
|
|
|
|
HANDLE handle = FindPortHandle(aPortId);
|
|
if (handle == INVALID_HANDLE_VALUE) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::SetSignals port '%s' not found",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_ERROR_NOT_AVAILABLE;
|
|
}
|
|
|
|
if (aSignals.dataTerminalReady().isSome()) {
|
|
if (!EscapeCommFunction(
|
|
handle, aSignals.dataTerminalReady().value() ? SETDTR : CLRDTR)) {
|
|
DWORD error = GetLastError();
|
|
MOZ_LOG(
|
|
gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::SetSignals EscapeCommFunction DTR "
|
|
"failed for port '%s': 0x%08lx",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get(), error));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
}
|
|
|
|
if (aSignals.requestToSend().isSome()) {
|
|
if (!EscapeCommFunction(
|
|
handle, aSignals.requestToSend().value() ? SETRTS : CLRRTS)) {
|
|
DWORD error = GetLastError();
|
|
MOZ_LOG(
|
|
gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::SetSignals EscapeCommFunction RTS "
|
|
"failed for port '%s': 0x%08lx",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get(), error));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
}
|
|
|
|
if (aSignals.breakSignal().isSome()) {
|
|
if (!EscapeCommFunction(
|
|
handle, aSignals.breakSignal().value() ? SETBREAK : CLRBREAK)) {
|
|
DWORD error = GetLastError();
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::SetSignals EscapeCommFunction "
|
|
"Break failed for port '%s': 0x%08lx",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get(), error));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
}
|
|
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::SetSignals succeeded for port '%s'",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult Win32SerialPlatformService::GetSignalsImpl(
|
|
const nsString& aPortId, IPCSerialInputSignals& aSignals) {
|
|
mIOCapability.AssertOnCurrentThread();
|
|
HANDLE handle = FindPortHandle(aPortId);
|
|
if (handle == INVALID_HANDLE_VALUE) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::GetSignals port '%s' not found",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_ERROR_NOT_AVAILABLE;
|
|
}
|
|
|
|
DWORD status = 0;
|
|
if (!GetCommModemStatus(handle, &status)) {
|
|
DWORD error = GetLastError();
|
|
MOZ_LOG(
|
|
gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::GetSignals GetCommModemStatus failed "
|
|
"for port '%s': 0x%08lx",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get(), error));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
aSignals = IPCSerialInputSignals{
|
|
(status & MS_RLSD_ON) != 0, // dataCarrierDetect (DCD)
|
|
(status & MS_CTS_ON) != 0, // clearToSend (CTS)
|
|
(status & MS_RING_ON) != 0, // ringIndicator (RI)
|
|
(status & MS_DSR_ON) != 0 // dataSetReady (DSR)
|
|
};
|
|
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::GetSignals for port '%s': DCD=%s, "
|
|
"CTS=%s, RI=%s, DSR=%s",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get(),
|
|
aSignals.dataCarrierDetect() ? "true" : "false",
|
|
aSignals.clearToSend() ? "true" : "false",
|
|
aSignals.ringIndicator() ? "true" : "false",
|
|
aSignals.dataSetReady() ? "true" : "false"));
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult Win32SerialPlatformService::GetReadStreamImpl(
|
|
const nsString& aPortId, uint32_t aBufferSize,
|
|
nsIAsyncInputStream** aStream) {
|
|
mIOCapability.AssertOnCurrentThread();
|
|
HANDLE handle = FindPortHandle(aPortId);
|
|
if (handle == INVALID_HANDLE_VALUE) {
|
|
MOZ_LOG(
|
|
gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::GetReadStream port '%s' not found",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_ERROR_NOT_AVAILABLE;
|
|
}
|
|
UniqueFileHandle readHandle = DuplicateFileHandle(handle);
|
|
if (!readHandle) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::GetReadStream DuplicateHandle "
|
|
"failed for port '%s'",
|
|
this, NS_ConvertUTF16toUTF8(aPortId).get()));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
RefPtr<PlatformPipeReader> reader =
|
|
MakeRefPtr<PlatformPipeReader>(std::move(readHandle), aBufferSize);
|
|
reader.forget(aStream);
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult Win32SerialPlatformService::StartMonitoringDeviceChanges() {
|
|
if (mMonitoring) {
|
|
return NS_OK;
|
|
}
|
|
MOZ_LOG(
|
|
gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::StartMonitoringDeviceChanges", this));
|
|
|
|
CM_NOTIFY_FILTER filter = {};
|
|
filter.cbSize = sizeof(CM_NOTIFY_FILTER);
|
|
filter.FilterType = CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE;
|
|
filter.u.DeviceInterface.ClassGuid = GUID_DEVINTERFACE_COMPORT;
|
|
|
|
CONFIGRET cr = CM_Register_Notification(
|
|
&filter, this, DeviceNotificationCallback, &mDeviceNotification);
|
|
|
|
if (cr != CR_SUCCESS) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::StartMonitoringDeviceChanges "
|
|
"CM_Register_Notification failed: 0x%08lx",
|
|
this, cr));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
mMonitoring = true;
|
|
|
|
auto cachedPortList = mCachedPortList.Lock();
|
|
nsresult rv = EnumeratePortsWin32(*cachedPortList);
|
|
if (NS_FAILED(rv)) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::StartMonitoringDeviceChanges "
|
|
"EnumeratePorts failed: 0x%08x",
|
|
this, static_cast<unsigned>(rv)));
|
|
return rv;
|
|
}
|
|
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
("Win32SerialPlatformService[%p]::StartMonitoringDeviceChanges "
|
|
"succeeded, "
|
|
"monitoring %zu ports",
|
|
this, cachedPortList->Length()));
|
|
return NS_OK;
|
|
}
|
|
|
|
void Win32SerialPlatformService::StopMonitoringDeviceChanges() {
|
|
AssertIsOnMainThread();
|
|
if (!mMonitoring) {
|
|
return;
|
|
}
|
|
|
|
MOZ_LOG(
|
|
gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::StopMonitoringDeviceChanges", this));
|
|
|
|
if (mDeviceNotification) {
|
|
CM_Unregister_Notification(mDeviceNotification);
|
|
mDeviceNotification = nullptr;
|
|
}
|
|
|
|
mMonitoring = false;
|
|
}
|
|
|
|
DWORD CALLBACK Win32SerialPlatformService::DeviceNotificationCallback(
|
|
HCMNOTIFICATION hNotify, PVOID Context, CM_NOTIFY_ACTION Action,
|
|
PCM_NOTIFY_EVENT_DATA EventData, DWORD EventDataSize) {
|
|
RefPtr<SerialPlatformService> service = SerialPlatformService::GetInstance();
|
|
if (!service) {
|
|
return ERROR_SUCCESS;
|
|
}
|
|
auto* winService = static_cast<Win32SerialPlatformService*>(service.get());
|
|
|
|
if (!(Action == CM_NOTIFY_ACTION_DEVICEINTERFACEARRIVAL ||
|
|
Action == CM_NOTIFY_ACTION_DEVICEINTERFACEREMOVAL)) {
|
|
return ERROR_SUCCESS;
|
|
}
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::DeviceNotificationCallback "
|
|
"action=%d",
|
|
winService, Action));
|
|
|
|
// Only schedule a check if one isn't already pending
|
|
bool expected = false;
|
|
if (winService->mCheckPending.compareExchange(expected, true)) {
|
|
// Dispatch to the monitor thread (separate from the I/O queue so device
|
|
// change detection isn't blocked by hanging serial I/O calls).
|
|
nsresult rv = winService->mMonitorThread->DelayedDispatch(
|
|
NS_NewRunnableFunction(
|
|
"Win32SerialPlatformService::CheckForDeviceChanges",
|
|
[self = RefPtr{winService}]() {
|
|
self->mCheckPending = false;
|
|
self->CheckForDeviceChanges();
|
|
}),
|
|
kDeviceChangeDelayMs);
|
|
if (NS_FAILED(rv)) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::"
|
|
"DeviceNotificationCallback DelayedDispatch failed: 0x%08x",
|
|
winService, static_cast<uint32_t>(rv)));
|
|
winService->mCheckPending = false;
|
|
} else {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::"
|
|
"DeviceNotificationCallback scheduled CheckForDeviceChanges",
|
|
winService));
|
|
}
|
|
} else {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::"
|
|
"DeviceNotificationCallback check already pending, skipping",
|
|
winService));
|
|
}
|
|
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
void Win32SerialPlatformService::CheckForDeviceChanges() {
|
|
if (IsShutdown()) {
|
|
return;
|
|
}
|
|
MOZ_ASSERT(mMonitorThread->IsOnCurrentThread());
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
("Win32SerialPlatformService[%p]::CheckForDeviceChanges", this));
|
|
|
|
SerialPortList newPortList;
|
|
nsresult rv = EnumeratePortsWin32(newPortList);
|
|
if (NS_FAILED(rv)) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("Win32SerialPlatformService[%p]::CheckForDeviceChanges "
|
|
"EnumeratePorts failed: 0x%08x",
|
|
this, static_cast<unsigned>(rv)));
|
|
return;
|
|
}
|
|
|
|
auto cachedPortList = mCachedPortList.Lock();
|
|
|
|
// Find newly connected ports
|
|
for (const auto& newPort : newPortList) {
|
|
bool found = false;
|
|
for (const auto& oldPort : *cachedPortList) {
|
|
if (oldPort.id() == newPort.id()) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
("Win32SerialPlatformService[%p]::CheckForDeviceChanges port "
|
|
"connected: '%s'",
|
|
this, NS_ConvertUTF16toUTF8(newPort.id()).get()));
|
|
NotifyPortConnected(newPort);
|
|
}
|
|
}
|
|
|
|
// Find disconnected ports
|
|
for (const auto& oldPort : *cachedPortList) {
|
|
bool found = false;
|
|
for (const auto& newPort : newPortList) {
|
|
if (oldPort.id() == newPort.id()) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
("Win32SerialPlatformService[%p]::CheckForDeviceChanges port "
|
|
"disconnected: '%s'",
|
|
this, NS_ConvertUTF16toUTF8(oldPort.id()).get()));
|
|
// Since we're just using synchonous I/O for now, if the OS detects
|
|
// a device gets disconnected any pending writes will immediately
|
|
// fail, so we don't need to do any sort of cancelling here.
|
|
NotifyPortDisconnected(oldPort.id());
|
|
}
|
|
}
|
|
|
|
*cachedPortList = std::move(newPortList);
|
|
}
|
|
|
|
already_AddRefed<SerialPlatformService>
|
|
SerialPlatformService::GetInstanceImpl() {
|
|
return MakeAndAddRef<Win32SerialPlatformService>();
|
|
}
|
|
|
|
} // namespace mozilla::dom
|