/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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 // Including initguid.h needs to come before including devpkey.h, so // disable clang-format here. // clang-format off #include #include // clang-format on #include #include #include "SerialLogging.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(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(friendlyName), friendlyNameSize, nullptr, 0); if (!(succeeded && *friendlyName)) { deviceDescriptionPropKey = DEVPKEY_Device_FriendlyName; succeeded = SetupDiGetDevicePropertyW( deviceInfoSet, &deviceInfo, &deviceDescriptionPropKey, &unusedPropertyType, reinterpret_cast(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(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) { 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(aOptions.parity()), static_cast(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}; // MAXDWORD + MAXDWORD + ReadTotalTimeoutConstant is a special combination: // ReadFile returns immediately with available data, or waits up to // ReadTotalTimeoutConstant ms if no data is in the buffer. timeouts.ReadIntervalTimeout = MAXDWORD; timeouts.ReadTotalTimeoutMultiplier = MAXDWORD; timeouts.ReadTotalTimeoutConstant = 5; // 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_ATTRIBUTE_NORMAL, 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(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::ReadImpl(const nsString& aPortId, Span aBuf, uint32_t& aBytesRead) { mIOCapability.AssertOnCurrentThread(); HANDLE handle = FindPortHandle(aPortId); if (handle == INVALID_HANDLE_VALUE) { MOZ_LOG(gWebSerialLog, LogLevel::Error, ("Win32SerialPlatformService[%p]::Read port '%s' not found", this, NS_ConvertUTF16toUTF8(aPortId).get())); return NS_ERROR_NOT_AVAILABLE; } DWORD errors; COMSTAT comStat; if (!ClearCommError(handle, &errors, &comStat)) { DWORD error = GetLastError(); MOZ_LOG( gWebSerialLog, LogLevel::Error, ("Win32SerialPlatformService[%p]::Read ClearCommError failed for port " "'%s': 0x%08lx", this, NS_ConvertUTF16toUTF8(aPortId).get(), error)); return NS_ERROR_FAILURE; } if (errors & (CE_FRAME | CE_RXPARITY | CE_OVERRUN | CE_RXOVER)) { // These are non-fatal per the spec (FramingError, ParityError, // BufferOverrunError). ClearCommError already cleared them; log and // continue reading. MOZ_LOG(gWebSerialLog, LogLevel::Warning, ("Win32SerialPlatformService[%p]::Read non-fatal comm errors on " "port '%s': 0x%08lx (cleared, continuing)", this, NS_ConvertUTF16toUTF8(aPortId).get(), errors)); } if (comStat.cbInQue == 0) { return NS_OK; } DWORD bytesToRead = std::min({comStat.cbInQue, static_cast(aBuf.Length())}); DWORD bytesRead = 0; if (!ReadFile(handle, aBuf.Elements(), bytesToRead, &bytesRead, nullptr)) { DWORD error = GetLastError(); MOZ_LOG( gWebSerialLog, LogLevel::Error, ("Win32SerialPlatformService[%p]::Read ReadFile failed for port '%s': " "0x%08lx", this, NS_ConvertUTF16toUTF8(aPortId).get(), error)); return NS_ERROR_FAILURE; } aBytesRead = bytesRead; if (bytesRead > 0) { MOZ_LOG( gWebSerialLog, LogLevel::Debug, ("Win32SerialPlatformService[%p]::Read read %lu bytes from port '%s'", this, bytesRead, NS_ConvertUTF16toUTF8(aPortId).get())); } return NS_OK; } nsresult Win32SerialPlatformService::WriteImpl(const nsString& aPortId, Span 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(aData.Length()); while (remaining > 0) { DWORD bytesWritten = 0; if (!WriteFile(handle, buffer + totalWritten, remaining, &bytesWritten, nullptr)) { DWORD error = GetLastError(); 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; } 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::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(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 service = SerialPlatformService::GetInstance(); if (!service) { return ERROR_SUCCESS; } auto* winService = static_cast(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(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(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); } } // namespace mozilla::dom