I looked into passing the SerialPlatformService into the SerialManagerParent, but this is a bit awkward because the constructor can't return an error. But I did cache the SerialPlatformService in SerialManagerParent::Init() and removed several now unneeded null checks elsewhere. Differential Revision: https://phabricator.services.mozilla.com/D304158
480 lines
17 KiB
C++
480 lines
17 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 "mozilla/dom/SerialManagerParent.h"
|
|
|
|
#include "SerialLogging.h"
|
|
#include "SerialPermissionRequest.h"
|
|
#include "TestSerialPlatformService.h"
|
|
#include "mozilla/Services.h"
|
|
#include "mozilla/StaticPrefs_dom.h"
|
|
#include "mozilla/dom/BrowsingContext.h"
|
|
#include "mozilla/dom/CanonicalBrowsingContext.h"
|
|
#include "mozilla/dom/PSerialPort.h"
|
|
#include "mozilla/dom/Serial.h"
|
|
#include "mozilla/dom/SerialPlatformService.h"
|
|
#include "mozilla/dom/SerialPortParent.h"
|
|
#include "mozilla/dom/WindowGlobalParent.h"
|
|
#include "mozilla/ipc/Endpoint.h"
|
|
#include "nsContentUtils.h"
|
|
#include "nsIObserverService.h"
|
|
#include "nsIScriptError.h"
|
|
#include "nsThreadUtils.h"
|
|
|
|
namespace mozilla::dom {
|
|
|
|
NS_IMPL_ISUPPORTS(SerialDeviceChangeProxy, nsIObserver)
|
|
|
|
SerialDeviceChangeProxy::SerialDeviceChangeProxy(
|
|
uint64_t aBrowserId, RefPtr<SerialPlatformService> aPlatformService)
|
|
: mBrowserId(aBrowserId), mPlatformService(std::move(aPlatformService)) {
|
|
MOZ_ASSERT(mPlatformService);
|
|
}
|
|
|
|
SerialDeviceChangeProxy::~SerialDeviceChangeProxy() = default;
|
|
|
|
void SerialDeviceChangeProxy::AddPortActor(SerialPortParent* aActor) {
|
|
MutexAutoLock lock(mMutex);
|
|
mPortActors.AppendElement(aActor);
|
|
}
|
|
|
|
void SerialDeviceChangeProxy::RemovePortActor(SerialPortParent* aActor) {
|
|
MutexAutoLock lock(mMutex);
|
|
mPortActors.RemoveElement(aActor);
|
|
}
|
|
|
|
nsTArray<RefPtr<SerialPortParent>> SerialDeviceChangeProxy::ActorsById(
|
|
const nsAString& aPortId) {
|
|
nsTArray<RefPtr<SerialPortParent>> actors;
|
|
MutexAutoLock lock(mMutex);
|
|
for (const auto& actor : mPortActors) {
|
|
if (actor->PortIdMatches(aPortId)) {
|
|
actors.AppendElement(actor);
|
|
}
|
|
}
|
|
return actors;
|
|
}
|
|
|
|
void SerialDeviceChangeProxy::RevokeAllPorts() {
|
|
nsTArray<RefPtr<SerialPortParent>> actors;
|
|
{
|
|
MutexAutoLock lock(mMutex);
|
|
actors.SwapElements(mPortActors);
|
|
}
|
|
|
|
if (actors.IsEmpty()) {
|
|
return;
|
|
}
|
|
|
|
mPlatformService->IOThread()->Dispatch(
|
|
NS_NewRunnableFunction("SerialDeviceChangeProxy::RevokeAllPorts",
|
|
[actors = std::move(actors)]() {
|
|
for (const auto& actor : actors) {
|
|
if (actor->CanSend()) {
|
|
actor->Close();
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
void SerialDeviceChangeProxy::OnPortConnected(
|
|
const IPCSerialPortInfo& aPortInfo) {
|
|
auto actors = ActorsById(aPortInfo.id());
|
|
if (!actors.IsEmpty()) {
|
|
mPlatformService->IOThread()->Dispatch(
|
|
NS_NewRunnableFunction("SerialDeviceChangeProxy::OnPortDisconnected",
|
|
[actors = std::move(actors)]() {
|
|
for (const auto& actor : actors) {
|
|
actor->NotifyConnected();
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
|
|
void SerialDeviceChangeProxy::OnPortDisconnected(const nsAString& aPortId) {
|
|
auto actors = ActorsById(aPortId);
|
|
if (!actors.IsEmpty()) {
|
|
mPlatformService->IOThread()->Dispatch(
|
|
NS_NewRunnableFunction("SerialDeviceChangeProxy::OnPortDisconnected",
|
|
[actors = std::move(actors)]() {
|
|
for (const auto& actor : actors) {
|
|
actor->NotifyDisconnected();
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
SerialDeviceChangeProxy::Observe(nsISupports* aSubject, const char* aTopic,
|
|
const char16_t* aData) {
|
|
if (strcmp(aTopic, "serial-permission-revoked") == 0 && aSubject) {
|
|
AssertIsOnMainThread();
|
|
nsCOMPtr<BrowsingContext> revokedBC = do_QueryInterface(aSubject);
|
|
if (!revokedBC) {
|
|
return NS_OK;
|
|
}
|
|
uint64_t revokedBrowserId = revokedBC->GetBrowserId();
|
|
|
|
if (mBrowserId != revokedBrowserId) {
|
|
return NS_OK;
|
|
}
|
|
|
|
RevokeAllPorts();
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
SerialManagerParent::SerialManagerParent() { AssertIsOnMainThread(); }
|
|
|
|
SerialManagerParent::~SerialManagerParent() {
|
|
AssertIsOnMainThread();
|
|
MOZ_ASSERT(!mProxy, "Proxy should have been cleared");
|
|
}
|
|
|
|
void SerialManagerParent::Init(uint64_t aBrowserId) {
|
|
AssertIsOnMainThread();
|
|
MOZ_ASSERT(CanSend(), "Actor should have already been initialized");
|
|
mBrowserId = aBrowserId;
|
|
|
|
RefPtr<SerialPlatformService> platformService =
|
|
SerialPlatformService::GetInstance();
|
|
if (!platformService) {
|
|
// If the SerialPlatformService is null, nothing is going to work
|
|
// (and we don't try to create it again later)
|
|
// We already log something in GetInstance(), so just exit here
|
|
// after tearing down the newly created actor.
|
|
(void)PSerialManagerParent::Send__delete__(this);
|
|
return;
|
|
}
|
|
mPlatformService = platformService;
|
|
mProxy = MakeRefPtr<SerialDeviceChangeProxy>(mBrowserId, mPlatformService);
|
|
mPlatformService->AddDeviceChangeObserver(mProxy);
|
|
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
|
|
if (obs) {
|
|
obs->AddObserver(mProxy, "serial-permission-revoked", false);
|
|
}
|
|
}
|
|
|
|
// https://wicg.github.io/serial/#dfn-matches-the-filter
|
|
// A port matches a filter if for each present member of the filter,
|
|
// the port has a matching value.
|
|
// Note that bluetoothServiceClassId UUIDs have already been canonicalized by
|
|
// the content process.
|
|
static void ApplyPortFilters(nsTArray<IPCSerialPortInfo>& aPorts,
|
|
const nsTArray<IPCSerialPortFilter>& aFilters) {
|
|
if (aFilters.IsEmpty()) {
|
|
return;
|
|
}
|
|
|
|
aPorts.RemoveElementsBy([&](const IPCSerialPortInfo& port) {
|
|
for (const auto& filter : aFilters) {
|
|
// Step 1: If filter.usbVendorId is present and port's USB vendor ID
|
|
// does not match, this filter fails.
|
|
bool vendorMatches =
|
|
filter.usbVendorId().isNothing() ||
|
|
(port.usbVendorId() &&
|
|
port.usbVendorId().value() == filter.usbVendorId().value());
|
|
|
|
// Step 2: If filter.usbProductId is present and port's USB product ID
|
|
// does not match, this filter fails.
|
|
bool productMatches =
|
|
filter.usbProductId().isNothing() ||
|
|
(port.usbProductId() &&
|
|
port.usbProductId().value() == filter.usbProductId().value());
|
|
|
|
// Step 3: If filter.bluetoothServiceClassId is present and port's
|
|
// Bluetooth service class ID does not match, this filter fails.
|
|
bool bluetoothMatches = true;
|
|
if (filter.bluetoothServiceClassId().isSome()) {
|
|
if (port.bluetoothServiceClassId().isNothing()) {
|
|
bluetoothMatches = false;
|
|
} else {
|
|
bluetoothMatches = port.bluetoothServiceClassId().value() ==
|
|
filter.bluetoothServiceClassId().value();
|
|
}
|
|
}
|
|
|
|
if (vendorMatches && productMatches && bluetoothMatches) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
mozilla::ipc::Endpoint<PSerialPortChild>
|
|
SerialManagerParent::CreateAndBindPortActor(const nsAString& aPortId) {
|
|
AssertIsOnMainThread();
|
|
|
|
mozilla::ipc::Endpoint<PSerialPortParent> parentEndpoint;
|
|
mozilla::ipc::Endpoint<PSerialPortChild> childEndpoint;
|
|
if (NS_WARN_IF(NS_FAILED(
|
|
PSerialPort::CreateEndpoints(&parentEndpoint, &childEndpoint)))) {
|
|
return {};
|
|
}
|
|
|
|
RefPtr<SerialDeviceChangeProxy> proxy = mProxy;
|
|
if (!proxy) {
|
|
return {};
|
|
}
|
|
|
|
mPlatformService->IOThread()->Dispatch(NS_NewRunnableFunction(
|
|
"SerialPortParent::Bind",
|
|
[portId = nsString(aPortId), browserId = mBrowserId, proxy = proxy,
|
|
endpoint = std::move(parentEndpoint)]() mutable {
|
|
auto actor = MakeRefPtr<SerialPortParent>(portId, browserId, proxy);
|
|
if (!endpoint.Bind(actor)) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Error,
|
|
("SerialPortParent::Bind failed"));
|
|
return;
|
|
}
|
|
proxy->AddPortActor(actor);
|
|
}));
|
|
|
|
return childEndpoint;
|
|
}
|
|
|
|
namespace {
|
|
struct EnumeratePortsResult {
|
|
SerialPortList mPorts;
|
|
bool mLikelyAccessDenied = false;
|
|
};
|
|
using EnumeratePortsPromise = MozPromise<EnumeratePortsResult, nsresult, true>;
|
|
} // namespace
|
|
|
|
mozilla::ipc::IPCResult SerialManagerParent::RecvRequestPort(
|
|
nsTArray<IPCSerialPortFilter>&& aFilters, bool aAutoselect,
|
|
RequestPortResolver&& aResolver) {
|
|
AssertIsOnMainThread();
|
|
|
|
auto rejectInternal = MakeScopeExit([&aResolver]() {
|
|
IPCRequestPortResult result;
|
|
result.reason() = RequestPortReason::InternalError;
|
|
aResolver(std::make_tuple(std::move(result),
|
|
mozilla::ipc::Endpoint<PSerialPortChild>()));
|
|
});
|
|
|
|
if (!StaticPrefs::dom_webserial_enabled()) {
|
|
return IPC_OK();
|
|
}
|
|
|
|
if (aAutoselect && !StaticPrefs::dom_webserial_testing_enabled()) {
|
|
return IPC_OK();
|
|
}
|
|
|
|
if (mChooserRequestInFlight) {
|
|
// Only one chooser at a time per PSerialManager.
|
|
return IPC_OK();
|
|
}
|
|
|
|
// Below this point we either succeed (or are returning a different error)
|
|
rejectInternal.release();
|
|
|
|
nsCString unusedFailureReason;
|
|
for (const auto& filter : aFilters) {
|
|
if (!Serial::ValidatePortFilter(
|
|
filter.usbVendorId().isSome(), filter.usbProductId().isSome(),
|
|
filter.bluetoothServiceClassId().isSome(), unusedFailureReason) ||
|
|
(filter.bluetoothServiceClassId().isSome() &&
|
|
!Serial::IsValidBluetoothUUID(
|
|
filter.bluetoothServiceClassId().value()))) {
|
|
return IPC_FAIL(this, "invalid filter");
|
|
}
|
|
}
|
|
|
|
// Claim the chooser slot synchronously so a second RecvRequestPort
|
|
// arriving on the main thread while we're enumerating on the IO thread
|
|
// is rejected.
|
|
mChooserRequestInFlight = true;
|
|
|
|
// Enumerate on the IO thread; only primitive/thread-safe captures cross
|
|
// thread boundaries. Once the enumeration is done we hop back to the main
|
|
// thread to construct the SerialPermissionRequest (which holds main-
|
|
// thread-only Element/Principal references).
|
|
nsCOMPtr<nsISerialEventTarget> ioThread = mPlatformService->IOThread();
|
|
|
|
InvokeAsync(ioThread, __func__,
|
|
[service = RefPtr{mPlatformService}] {
|
|
EnumeratePortsResult enumerated;
|
|
nsresult rv = service->EnumeratePorts(
|
|
enumerated.mPorts, &enumerated.mLikelyAccessDenied);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return EnumeratePortsPromise::CreateAndReject(rv, __func__);
|
|
}
|
|
return EnumeratePortsPromise::CreateAndResolve(
|
|
std::move(enumerated), __func__);
|
|
})
|
|
->Then(
|
|
GetCurrentSerialEventTarget(), __func__,
|
|
[self = RefPtr{this}, filters = std::move(aFilters), aAutoselect,
|
|
resolver = std::move(aResolver)](
|
|
EnumeratePortsPromise::ResolveOrRejectValue&& aValue) mutable {
|
|
if (aValue.IsReject()) {
|
|
self->mChooserRequestInFlight = false;
|
|
IPCRequestPortResult result;
|
|
result.reason() = RequestPortReason::InternalError;
|
|
resolver(
|
|
std::make_tuple(std::move(result),
|
|
mozilla::ipc::Endpoint<PSerialPortChild>()));
|
|
return;
|
|
}
|
|
EnumeratePortsResult enumerated = std::move(aValue.ResolveValue());
|
|
if (enumerated.mLikelyAccessDenied) {
|
|
uint64_t innerWindowId =
|
|
static_cast<WindowGlobalParent*>(self->Manager())
|
|
->InnerWindowId();
|
|
nsContentUtils::ReportToConsoleByWindowID(
|
|
u"WebSerial: No serial ports could be accessed. On "
|
|
u"Linux this may mean the current user does not have "
|
|
u"permission to access serial devices (for example, "
|
|
u"is not in the \"dialout\" group), or the browser is "
|
|
u"running in a Snap or Flatpak sandbox without "
|
|
u"serial port access."_ns,
|
|
nsIScriptError::warningFlag, "WebSerial"_ns, innerWindowId);
|
|
}
|
|
ApplyPortFilters(enumerated.mPorts, filters);
|
|
self->StartChooserRequest(aAutoselect, std::move(enumerated.mPorts),
|
|
std::move(resolver));
|
|
});
|
|
|
|
return IPC_OK();
|
|
}
|
|
|
|
void SerialManagerParent::StartChooserRequest(
|
|
bool aAutoselect, nsTArray<IPCSerialPortInfo>&& aPorts,
|
|
RequestPortResolver&& aResolver) {
|
|
AssertIsOnMainThread();
|
|
// should have been set by RecvRequestPort()
|
|
MOZ_ASSERT(mChooserRequestInFlight);
|
|
|
|
auto rejectInternal = MakeScopeExit([this, &aResolver]() {
|
|
mChooserRequestInFlight = false;
|
|
IPCRequestPortResult result;
|
|
result.reason() = RequestPortReason::InternalError;
|
|
aResolver(std::make_tuple(std::move(result),
|
|
mozilla::ipc::Endpoint<PSerialPortChild>()));
|
|
});
|
|
|
|
if (!CanSend()) {
|
|
return;
|
|
}
|
|
|
|
auto request = MakeRefPtr<SerialPermissionRequest>(
|
|
static_cast<WindowGlobalParent*>(Manager()), aAutoselect,
|
|
std::move(aPorts));
|
|
rejectInternal.release();
|
|
|
|
request->Run()->Then(
|
|
GetMainThreadSerialEventTarget(), __func__,
|
|
[self = RefPtr{this}, resolver = std::move(aResolver)](
|
|
SerialChooserPromise::ResolveOrRejectValue&& aValue) mutable {
|
|
self->mChooserRequestInFlight = false;
|
|
|
|
IPCRequestPortResult result;
|
|
mozilla::ipc::Endpoint<PSerialPortChild> childEndpoint;
|
|
|
|
if (aValue.IsResolve()) {
|
|
const IPCSerialPortInfo& port = aValue.ResolveValue();
|
|
childEndpoint = self->CreateAndBindPortActor(port.id());
|
|
if (childEndpoint.IsValid()) {
|
|
result.reason() = RequestPortReason::Granted;
|
|
result.port() = Some(port);
|
|
} else {
|
|
result.reason() = RequestPortReason::InternalError;
|
|
}
|
|
} else {
|
|
result.reason() = aValue.RejectValue();
|
|
}
|
|
|
|
resolver(std::make_tuple(result, std::move(childEndpoint)));
|
|
});
|
|
}
|
|
|
|
template <typename TWork, typename TResolver>
|
|
mozilla::ipc::IPCResult SerialManagerParent::DispatchTestOperation(
|
|
const char* aName, TWork&& aWork, TResolver&& aResolver) {
|
|
AssertIsOnMainThread();
|
|
if (!StaticPrefs::dom_webserial_testing_enabled()) {
|
|
return IPC_FAIL(this, "Testing not enabled");
|
|
}
|
|
|
|
RefPtr<TestSerialPlatformService> testService =
|
|
mPlatformService->AsTestService();
|
|
if (!testService) {
|
|
aResolver(NS_ERROR_FAILURE);
|
|
return IPC_OK();
|
|
}
|
|
|
|
mPlatformService->IOThread()->Dispatch(
|
|
NS_NewRunnableFunction(aName, [testService, aWork, aResolver]() {
|
|
aWork(testService);
|
|
NS_DispatchToMainThread(NS_NewRunnableFunction(
|
|
"SerialManagerParent::DispatchTestOperation::Resolve",
|
|
[aResolver]() { aResolver(NS_OK); }));
|
|
}));
|
|
|
|
return IPC_OK();
|
|
}
|
|
|
|
mozilla::ipc::IPCResult SerialManagerParent::RecvSimulateDeviceConnection(
|
|
const nsString& aDeviceId, const nsString& aDevicePath, uint16_t aVendorId,
|
|
uint16_t aProductId, SimulateDeviceConnectionResolver&& aResolver) {
|
|
return DispatchTestOperation(
|
|
"SerialManagerParent::SimulateDeviceConnection",
|
|
[deviceId = nsString(aDeviceId), devicePath = nsString(aDevicePath),
|
|
aVendorId, aProductId](TestSerialPlatformService* testService) {
|
|
testService->SimulateDeviceConnection(deviceId, devicePath, aVendorId,
|
|
aProductId);
|
|
},
|
|
std::move(aResolver));
|
|
}
|
|
|
|
mozilla::ipc::IPCResult SerialManagerParent::RecvSimulateDeviceDisconnection(
|
|
const nsString& aDeviceId,
|
|
SimulateDeviceDisconnectionResolver&& aResolver) {
|
|
return DispatchTestOperation(
|
|
"SerialManagerParent::SimulateDeviceDisconnection",
|
|
[deviceId = nsString(aDeviceId)](TestSerialPlatformService* testService) {
|
|
testService->SimulateDeviceDisconnection(deviceId);
|
|
},
|
|
std::move(aResolver));
|
|
}
|
|
|
|
mozilla::ipc::IPCResult SerialManagerParent::RecvRemoveAllMockDevices(
|
|
RemoveAllMockDevicesResolver&& aResolver) {
|
|
return DispatchTestOperation(
|
|
"SerialManagerParent::RemoveAllMockDevices",
|
|
[](TestSerialPlatformService* testService) {
|
|
testService->RemoveAllMockDevices();
|
|
},
|
|
std::move(aResolver));
|
|
}
|
|
|
|
mozilla::ipc::IPCResult SerialManagerParent::RecvResetToDefaultMockDevices(
|
|
ResetToDefaultMockDevicesResolver&& aResolver) {
|
|
return DispatchTestOperation(
|
|
"SerialManagerParent::ResetToDefaultMockDevices",
|
|
[](TestSerialPlatformService* testService) {
|
|
testService->ResetToDefaultMockDevices();
|
|
},
|
|
std::move(aResolver));
|
|
}
|
|
|
|
void SerialManagerParent::ActorDestroy(ActorDestroyReason aWhy) {
|
|
AssertIsOnMainThread();
|
|
|
|
RefPtr<SerialDeviceChangeProxy> proxy = mProxy.forget();
|
|
if (proxy) {
|
|
proxy->RevokeAllPorts();
|
|
mPlatformService->RemoveDeviceChangeObserver(proxy);
|
|
|
|
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
|
|
if (obs) {
|
|
obs->RemoveObserver(proxy, "serial-permission-revoked");
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace mozilla::dom
|