/* 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/Serial.h" #include "Navigator.h" #include "SerialLogging.h" #include "mozilla/Preferences.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/dom/BrowsingContext.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/FeaturePolicyUtils.h" #include "mozilla/dom/PSerialPort.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/SerialBinding.h" #include "mozilla/dom/SerialManagerChild.h" #include "mozilla/dom/SerialPort.h" #include "mozilla/dom/SerialPortChild.h" #include "mozilla/dom/WindowContext.h" #include "mozilla/dom/WindowGlobalChild.h" #include "mozilla/dom/WorkerCommon.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/WorkerRef.h" #include "nsContentUtils.h" #include "nsPIDOMWindow.h" #include "nsThreadUtils.h" namespace mozilla::dom { // Forward declaration for use in RequestPort below; the implementation lives // further down with the other Bluetooth UUID helpers. static bool ResolveBluetoothServiceUUID(const OwningStringOrUnsignedLong& aName, nsAutoString& aResult); static Serial* FindWindowSerialForWorkerPrivate(WorkerPrivate* aWorkerPrivate) { AssertIsOnMainThread(); MOZ_ASSERT(aWorkerPrivate); nsPIDOMWindowInner* inner = aWorkerPrivate->GetAncestorWindow(); if (!inner) { return nullptr; } return inner->Navigator()->GetExistingSerial(); } using TestingIpcPromise = MozPromise; // Runs aSendFn(child) on the main thread, where |child| is the // SerialManagerChild owned by aMainThreadSerial. template static RefPtr InvokeTestingSendOnMainThread( Serial* aMainThreadSerial, const SendFn&& aSendFn) { AssertIsOnMainThread(); if (!aMainThreadSerial) { return TestingIpcPromise::CreateAndReject( mozilla::ipc::ResponseRejectReason::ChannelClosed, __func__); } SerialManagerChild* child = aMainThreadSerial->GetOrCreateManagerChild(); if (!child) { return TestingIpcPromise::CreateAndReject( mozilla::ipc::ResponseRejectReason::ChannelClosed, __func__); } return aSendFn(child); } // Common implementation for the four test-only methods. SerialManagerChild // is a main-thread-only IPC actor, so when called from a worker, the Send* // call has to be bounced to the main thread (via the window's Serial), // and the resulting JS Promise resolved back on the worker. // // aSendFn is a callable taking SerialManagerChild* and returning // RefPtr (i.e. the result of one of the Send* methods). template static already_AddRefed RunTestingIpc( Serial* aSerial, ErrorResult& aRv, const nsLiteralCString& aIpcErrorMessage, SendFn&& aSendFn) { if (!StaticPrefs::dom_webserial_testing_enabled()) { aRv.ThrowNotSupportedError("Testing is not enabled"); return nullptr; } nsIGlobalObject* global = aSerial->GetRelevantGlobal(); if (!global) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } RefPtr promise = Promise::Create(global, aRv); if (aRv.Failed()) { return nullptr; } if (NS_IsMainThread()) { InvokeTestingSendOnMainThread(aSerial, std::forward(aSendFn)) ->Then( GetMainThreadSerialEventTarget(), __func__, [promise](nsresult) { promise->MaybeResolveWithUndefined(); }, [promise, aIpcErrorMessage](mozilla::ipc::ResponseRejectReason) { promise->MaybeRejectWithAbortError(aIpcErrorMessage); }); return promise.forget(); } WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); if (!workerPrivate) { promise->MaybeRejectWithNotSupportedError("Worker context not available"); return promise.forget(); } RefPtr strongRef = StrongWorkerRef::Create(workerPrivate, "Serial::RunTestingIpc"); if (!strongRef) { promise->MaybeRejectWithAbortError("Worker is shutting down"); return promise.forget(); } auto tsRef = MakeRefPtr(strongRef); InvokeAsync(GetMainThreadSerialEventTarget(), __func__, [tsRef, sendFn = std::forward(aSendFn)]() { Serial* windowSerial = FindWindowSerialForWorkerPrivate(tsRef->Private()); return InvokeTestingSendOnMainThread(windowSerial, std::move(sendFn)); }) ->Then( GetCurrentSerialEventTarget(), __func__, [promise](nsresult) { promise->MaybeResolveWithUndefined(); }, [promise, aIpcErrorMessage](mozilla::ipc::ResponseRejectReason) { promise->MaybeRejectWithAbortError(aIpcErrorMessage); }); return promise.forget(); } NS_IMPL_CYCLE_COLLECTION_WEAK_PTR_INHERITED(Serial, DOMEventTargetHelper, mPorts) NS_IMPL_ADDREF_INHERITED(Serial, DOMEventTargetHelper) NS_IMPL_RELEASE_INHERITED(Serial, DOMEventTargetHelper) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Serial) NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) LazyLogModule gWebSerialLog("WebSerial"); Serial::Serial(nsPIDOMWindowInner* aWindow) : DOMEventTargetHelper(aWindow) { MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("Serial[%p] created for window", this)); AssertIsOnMainThread(); } Serial::Serial(nsIGlobalObject* aGlobal) : DOMEventTargetHelper(aGlobal) { MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("Serial[%p] created for global", this)); MOZ_ASSERT(!NS_IsMainThread()); } Serial::~Serial() { MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("Serial[%p] destroyed", this)); MOZ_ASSERT(mHasShutdown); } void Serial::Shutdown() { if (mHasShutdown) { return; } MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("Serial[%p] shutting down", this)); mHasShutdown = true; mManagerChild = nullptr; for (const auto& port : mPorts) { port->Shutdown(); } mPorts.Clear(); } void Serial::DisconnectFromOwner() { Shutdown(); DOMEventTargetHelper::DisconnectFromOwner(); } JSObject* Serial::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return Serial_Binding::Wrap(aCx, this, aGivenProto); } SerialManagerChild* Serial::GetOrCreateManagerChild() { if (mManagerChild) { return mManagerChild; } AssertIsOnMainThread(); nsPIDOMWindowInner* window = GetOwnerWindow(); if (!window) { return nullptr; } WindowGlobalChild* wgc = window->GetWindowGlobalChild(); if (!wgc) { return nullptr; } auto child = MakeRefPtr(this); if (!wgc->SendPSerialManagerConstructor(child)) { return nullptr; } mManagerChild = child; return mManagerChild; } // Returns whether the security check was passed. If this method returns // false, the promise has been rejected. static bool PortSecurityCheck(Promise& aPromise, nsIGlobalObject* aGlobal, const nsCString& aFunctionName) { if (nsPIDOMWindowInner* window = aGlobal->GetAsInnerWindow()) { Document* doc = window->GetExtantDoc(); if (!doc) { aPromise.MaybeRejectWithSecurityError( aFunctionName + "() is not allowed without a document"_ns); return false; } // web-platform-tests seem to indicate this is necessary, but the spec does // not. spec issue: https://github.com/WICG/serial/issues/223 if (doc->NodePrincipal()->GetIsNullPrincipal()) { aPromise.MaybeRejectWithSecurityError( aFunctionName + "() is not allowed for opaque origins"_ns); return false; } if (!FeaturePolicyUtils::IsFeatureAllowed(doc, u"serial"_ns)) { nsAutoString message; message.AssignLiteral("WebSerial access request was denied: "); message.Append(NS_ConvertUTF8toUTF16(aFunctionName)); message.AppendLiteral("() is not allowed in this context"); nsContentUtils::ReportToConsoleNonLocalized( message, nsIScriptError::errorFlag, "Security"_ns, doc); aPromise.MaybeRejectWithSecurityError( aFunctionName + "() is not allowed in this context"_ns); return false; } return true; } WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); if (!workerPrivate) { aPromise.MaybeRejectWithSecurityError( aFunctionName + "() is not allowed without a window or worker"_ns); return false; } if (!workerPrivate->SerialAllowed()) { aPromise.MaybeRejectWithSecurityError( aFunctionName + "() is not allowed in this context"_ns); return false; } return true; } bool Serial::ValidatePortFilter(bool aHasUsbVendorId, bool aHasUsbProductId, bool aHasBluetoothServiceClassId, nsACString& aFailureReason) { // https://wicg.github.io/serial/#ref-for-dom-serialportrequestoptions-filters-1 if (aHasBluetoothServiceClassId) { if (aHasUsbVendorId || aHasUsbProductId) { aFailureReason = "A filter cannot specify both bluetoothServiceClassId and " "usbVendorId or usbProductId."_ns; return false; } } else { if (!aHasUsbVendorId) { if (!aHasUsbProductId) { aFailureReason = "A filter must provide a property to filter by."_ns; } else { aFailureReason = "A filter containing a usbProductId must also specify a usbVendorId."_ns; } return false; } } return true; } already_AddRefed Serial::RequestPort( const SerialPortRequestOptions& aOptions, ErrorResult& aRv) { AssertIsOnMainThread(); // RequestPort() doesn't work in workers, so we can skip straight to the // window. nsPIDOMWindowInner* window = GetOwnerWindow(); if (!window) { MOZ_LOG(gWebSerialLog, LogLevel::Error, ("Serial[%p]::RequestPort failed: no window available", this)); return nullptr; } // https://wicg.github.io/serial/#dom-serial-requestport // Step 1: Let promise be a new promise. RefPtr promise = Promise::Create(window->AsGlobal(), aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } MOZ_LOG( gWebSerialLog, LogLevel::Info, ("Serial[%p]::RequestPort called (filters: %s, allowedBluetoothUUIDs: " "%s)", this, aOptions.mFilters.WasPassed() ? "provided" : "none", aOptions.mAllowedBluetoothServiceClassIds.WasPassed() ? "provided" : "none")); // Step 2: If this's relevant global object's associated Document is not // allowed to use the "serial" feature, reject with a SecurityError. if (!PortSecurityCheck(*promise, window->AsGlobal(), "requestPort"_ns)) { MOZ_LOG(gWebSerialLog, LogLevel::Warning, ("Serial[%p]::RequestPort failed security check", this)); return promise.forget(); } // Step 3: If the relevant global object does not have transient activation, // reject with a SecurityError. WindowContext* context = window->GetWindowContext(); if (!context) { MOZ_LOG( gWebSerialLog, LogLevel::Error, ("Serial[%p]::RequestPort failed: no window context available", this)); promise->MaybeRejectWithNotSupportedError("No window context available"); return promise.forget(); } if (!context->HasValidTransientUserGestureActivation()) { MOZ_LOG(gWebSerialLog, LogLevel::Warning, ("Serial[%p]::RequestPort failed: no user activation", this)); promise->MaybeRejectWithSecurityError( "requestPort() requires user activation"); return promise.forget(); } // Step 4: If options["filters"] is present, validate each filter. if (aOptions.mFilters.WasPassed()) { MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("Serial[%p]::RequestPort validating %zu filters", this, aOptions.mFilters.Value().Length())); nsAutoCString validatePortFailureReason; for (const auto& filter : aOptions.mFilters.Value()) { if (!ValidatePortFilter(filter.mUsbVendorId.WasPassed(), filter.mUsbProductId.WasPassed(), filter.mBluetoothServiceClassId.WasPassed(), validatePortFailureReason)) { promise->MaybeRejectWithTypeError(validatePortFailureReason); MOZ_LOG(gWebSerialLog, LogLevel::Warning, ("Serial[%p]::RequestPort failed filter validation", this)); return promise.forget(); } } } // Step 5 (in parallel): ask the parent process to enumerate, prompt the // user, and record the granted port id. SerialManagerChild* child = GetOrCreateManagerChild(); if (!child) { promise->MaybeRejectWithNotSupportedError("Request failed"); return promise.forget(); } nsTArray ipcFilters; if (aOptions.mFilters.WasPassed()) { for (const auto& filter : aOptions.mFilters.Value()) { IPCSerialPortFilter ipcFilter; if (filter.mUsbVendorId.WasPassed()) { ipcFilter.usbVendorId() = Some(filter.mUsbVendorId.Value()); } if (filter.mUsbProductId.WasPassed()) { ipcFilter.usbProductId() = Some(filter.mUsbProductId.Value()); } if (filter.mBluetoothServiceClassId.WasPassed()) { nsAutoString uuid; if (!ResolveBluetoothServiceUUID( filter.mBluetoothServiceClassId.Value(), uuid)) { promise->MaybeRejectWithTypeError( "Invalid bluetoothServiceClassId in port filter"); return promise.forget(); } ipcFilter.bluetoothServiceClassId() = Some(uuid); } ipcFilters.AppendElement(std::move(ipcFilter)); } } bool autoselect = StaticPrefs::dom_webserial_testing_enabled() && mAutoselectPorts; child->SendRequestPort(ipcFilters, autoselect) ->Then( GetMainThreadSerialEventTarget(), __func__, [promise, self = RefPtr{this}]( std::tuple>&& aTuple) { IPCRequestPortResult& result = std::get<0>(aTuple); mozilla::ipc::Endpoint endpoint = std::move(std::get<1>(aTuple)); switch (result.reason()) { case RequestPortReason::Granted: { if (result.port().isNothing()) { promise->MaybeRejectWithNotSupportedError( "Granted port info missing"); return; } RefPtr port = self->GetOrCreatePort( result.port().value(), std::move(endpoint)); if (!port) { promise->MaybeRejectWithNotSupportedError( "Failed to create port actor"); return; } port->MarkPhysicallyPresent(); promise->MaybeResolve(port); return; } case RequestPortReason::UserCancelled: case RequestPortReason::AddonDenied: promise->MaybeRejectWithNotFoundError("No port selected"); return; case RequestPortReason::InternalError: promise->MaybeRejectWithNotSupportedError("Request failed"); return; case RequestPortReason::EndGuard_: MOZ_ASSERT_UNREACHABLE("Bad RequestPortReason"); promise->MaybeRejectWithNotSupportedError("Request failed"); return; } promise->MaybeRejectWithNotSupportedError("Request failed"); }, [promise](mozilla::ipc::ResponseRejectReason) { promise->MaybeRejectWithNotSupportedError("Request failed"); }); // Step 6: Return promise. return promise.forget(); } already_AddRefed Serial::GetPorts(ErrorResult& aRv) { nsIGlobalObject* global = GetRelevantGlobal(); if (!global) { aRv.ThrowInvalidStateError("No global object available"); return nullptr; } // https://wicg.github.io/serial/#dom-serial-getports // Step 1: Let promise be a new promise. RefPtr promise = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("Serial[%p]::GetPorts called", this)); // Step 2: If this's relevant global object's associated Document is not // allowed to use the "serial" feature, reject with a SecurityError. if (!PortSecurityCheck(*promise, global, "getPorts"_ns)) { MOZ_LOG(gWebSerialLog, LogLevel::Warning, ("Serial[%p]::GetPorts failed security check", this)); return promise.forget(); } // Step 3 (in parallel): Get the sequence of available serial ports the user // has granted access to, then queue a task to resolve the promise. if (NS_IsMainThread()) { nsTArray> result; for (const auto& port : mPorts) { if (!port->IsForgotten() && port->PhysicallyPresent()) { result.AppendElement(port); } } MOZ_LOG( gWebSerialLog, LogLevel::Info, ("Serial[%p]::GetPorts returning %zu ports", this, result.Length())); // Queue a task to resolve the promise per spec step 3.3 NS_DispatchToCurrentThread(NS_NewRunnableFunction( "Serial::GetPorts resolve", [promise = RefPtr{promise}, result = std::move(result)]() mutable { promise->MaybeResolve(result); })); // Step 4: Return promise. return promise.forget(); } // Worker path: collect known port IDs, dispatch to main thread to get // new grants and clone their actors, then dispatch back to create // SerialPort objects for any new ports. MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("Serial[%p]::GetPorts called from worker, dispatching to main " "thread", this)); WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); if (!workerPrivate) { MOZ_LOG(gWebSerialLog, LogLevel::Error, ("Serial[%p]::GetPorts failed: no worker private", this)); promise->MaybeRejectWithNotSupportedError("Worker context not available"); return promise.forget(); } nsTArray knownPortIds; for (const auto& port : mPorts) { knownPortIds.AppendElement(port->Id()); } RefPtr strongRef = StrongWorkerRef::Create(workerPrivate, "Serial::GetPorts"); if (!strongRef) { promise->MaybeRejectWithAbortError("Worker is shutting down"); return promise.forget(); } auto tsRef = MakeRefPtr(strongRef); struct NewPortData { IPCSerialPortInfo mInfo; mozilla::ipc::Endpoint mEndpoint; }; struct GetPortsData { nsTArray mNewPorts; nsTArray mIdsToForget; }; using GetPortsPromise = MozPromise; // InvokeAsync dispatches the lambda to the main thread and returns a // MozPromise. No non-threadsafe RefPtrs cross thread boundaries. InvokeAsync( GetMainThreadSerialEventTarget(), __func__, [tsRef, knownPortIds = std::move(knownPortIds)]() { Serial* windowSerial = FindWindowSerialForWorkerPrivate(tsRef->Private()); GetPortsData getPortsData; if (windowSerial) { for (const auto& port : windowSerial->mPorts) { if (port->IsForgotten()) { continue; } bool alreadyKnown = false; for (const auto& id : knownPortIds) { if (id == port->Id()) { alreadyKnown = true; break; } } if (alreadyKnown) { continue; } RefPtr child = port->GetChild(); if (!child || !child->CanSend()) { continue; } mozilla::ipc::Endpoint parentEp; mozilla::ipc::Endpoint childEp; if (NS_FAILED(PSerialPort::CreateEndpoints(&parentEp, &childEp))) { continue; } child->SendClone(std::move(parentEp)); NewPortData data; data.mInfo = port->GetPortInfo(); data.mEndpoint = std::move(childEp); getPortsData.mNewPorts.AppendElement(std::move(data)); } // Determine which ports the worker knows about that have been // forgotten or removed on the main thread. for (const auto& id : knownPortIds) { bool stillActive = false; for (const auto& port : windowSerial->mPorts) { if (!port->IsForgotten() && port->Id() == id) { stillActive = true; break; } } if (!stillActive) { getPortsData.mIdsToForget.AppendElement(id); } } } else { // Window Serial is gone; forget all known ports. getPortsData.mIdsToForget = knownPortIds.Clone(); } return GetPortsPromise::CreateAndResolve(std::move(getPortsData), __func__); }) ->Then( GetCurrentSerialEventTarget(), __func__, // Capture tsRef here to avoid a race with the worker losing the // workerref. [self = RefPtr{this}, tsRef, promise](GetPortsData&& aData) { for (auto& data : aData.mNewPorts) { RefPtr port = MakeRefPtr(data.mInfo, self); auto actor = MakeRefPtr(); if (data.mEndpoint.Bind(actor)) { actor->SetPort(port); port->SetChild(actor); } self->mPorts.AppendElement(std::move(port)); } for (const auto& id : aData.mIdsToForget) { self->ForgetPort(id); } nsTArray> result; for (const auto& port : self->mPorts) { if (!port->IsForgotten() && port->PhysicallyPresent()) { result.AppendElement(port); } } promise->MaybeResolve(result); }, [promise](nsresult aRv) { promise->MaybeRejectWithNotSupportedError( "Failed to get ports from main thread"); }); return promise.forget(); } // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothuuid-canonicaluuid // Replaces the top 32 bits of 00000000-0000-1000-8000-00805f9b34fb with the // alias. E.g. canonicalUUID(0xDEADBEEF) => // "deadbeef-0000-1000-8000-00805f9b34fb" static nsAutoString BluetoothCanonicalUUID(uint32_t aAlias) { nsAutoString result; result.AppendPrintf("%08x-0000-1000-8000-00805f9b34fb", aAlias); return result; } // A valid UUID is a lowercase string matching // /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ // https://webbluetoothcg.github.io/web-bluetooth/#valid-uuid bool Serial::IsValidBluetoothUUID(const nsAString& aString) { // 8-4-4-4-12 = 32 hex chars + 4 dashes = 36 chars if (aString.Length() != 36) { return false; } const char16_t* data = aString.BeginReading(); for (uint32_t i = 0; i < 36; i++) { if (i == 8 || i == 13 || i == 18 || i == 23) { if (data[i] != '-') { return false; } } else if (!((data[i] >= '0' && data[i] <= '9') || (data[i] >= 'a' && data[i] <= 'f'))) { return false; } } return true; } // Implements ResolveUUIDName(name, GATT assigned services) from the // Web Bluetooth spec, used by BluetoothUUID.getService(). // https://webbluetoothcg.github.io/web-bluetooth/#resolveuuidname // Returns true on success and sets aResult; returns false on error (step 4). static bool ResolveBluetoothServiceUUID(const OwningStringOrUnsignedLong& aName, nsAutoString& aResult) { // Step 1: If name is an unsigned long, return canonicalUUID(name). if (aName.IsUnsignedLong()) { aResult = BluetoothCanonicalUUID(aName.GetAsUnsignedLong()); return true; } const nsString& name = aName.GetAsString(); // Step 2: If name is a valid UUID, return name. if (Serial::IsValidBluetoothUUID(name)) { aResult = name; return true; } // Step 3: If name is a valid name and maps to a UUID in GATT assigned // services, return canonicalUUID(alias). // We do not currently support GATT assigned service name lookup (bug 2013908) // Step 4: Otherwise, throw a TypeError. return false; } void Serial::ForgetAllPorts() { if (mHasShutdown) { return; } nsTArray> portsToForget; for (const auto& port : mPorts) { if (!port->IsForgotten()) { portsToForget.AppendElement(port); } } MOZ_LOG(gWebSerialLog, LogLevel::Info, ("Serial[%p]::ForgetAllPorts forgetting %zu ports", this, portsToForget.Length())); for (const RefPtr& port : portsToForget) { RefPtr strongPort = port; IgnoredErrorResult rv; RefPtr promise = strongPort->Forget(rv); } } RefPtr Serial::GetOrCreatePort( const IPCSerialPortInfo& aInfo, mozilla::ipc::Endpoint&& aEndpoint) { // Look for an existing port with the same ID. If present, drop the // newly-minted endpoint; letting it fall out of scope closes its // channel, which will destroy the orphan SerialPortParent on the // other side. for (const auto& existing : mPorts) { if (existing->Id() == aInfo.id() && !existing->IsForgotten()) { return existing; } } if (!aEndpoint.IsValid()) { return nullptr; } auto actor = MakeRefPtr(); if (!aEndpoint.Bind(actor)) { return nullptr; } RefPtr port = MakeRefPtr(aInfo, this); actor->SetPort(port); port->SetChild(actor); mPorts.AppendElement(port); return port; } void Serial::ForgetPort(const nsAString& aPortId) { for (const auto& port : mPorts) { if (port->Id() == aPortId && !port->IsForgotten()) { RefPtr strongPort(port); strongPort->MarkForgotten(); } } mPorts.RemoveElementsBy([&aPortId](const RefPtr& port) { return port->Id() == aPortId; }); // If on a worker, also remove from the main thread's Serial. if (!NS_IsMainThread()) { WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); if (workerPrivate) { RefPtr strongRef = StrongWorkerRef::Create(workerPrivate, "Serial::ForgetPort"); if (strongRef) { auto tsRef = MakeRefPtr(strongRef); nsString portId(aPortId); NS_DispatchToMainThread(NS_NewRunnableFunction( "Serial::ForgetPort cross-context", [tsRef = std::move(tsRef), portId]() { RefPtr windowSerial = FindWindowSerialForWorkerPrivate(tsRef->Private()); if (windowSerial) { windowSerial->ForgetPort(portId); } })); } } } } already_AddRefed Serial::SimulateDeviceConnection( const nsAString& aDeviceId, const nsAString& aDevicePath, uint16_t aVendorId, uint16_t aProductId, ErrorResult& aRv) { return RunTestingIpc( this, aRv, nsLiteralCString("SimulateDeviceConnection IPC error"), [deviceId = nsString(aDeviceId), devicePath = nsString(aDevicePath), aVendorId, aProductId](SerialManagerChild* aChild) { return aChild->SendSimulateDeviceConnection(deviceId, devicePath, aVendorId, aProductId); }); } already_AddRefed Serial::SimulateDeviceDisconnection( const nsAString& aDeviceId, ErrorResult& aRv) { return RunTestingIpc( this, aRv, nsLiteralCString("SimulateDeviceDisconnection IPC error"), [deviceId = nsString(aDeviceId)](SerialManagerChild* aChild) { return aChild->SendSimulateDeviceDisconnection(deviceId); }); } already_AddRefed Serial::RemoveAllMockDevices(ErrorResult& aRv) { return RunTestingIpc(this, aRv, nsLiteralCString("RemoveAllMockDevices IPC error"), [](SerialManagerChild* aChild) { return aChild->SendRemoveAllMockDevices(); }); } already_AddRefed Serial::ResetToDefaultMockDevices(ErrorResult& aRv) { return RunTestingIpc(this, aRv, nsLiteralCString("ResetToDefaultMockDevices IPC error"), [](SerialManagerChild* aChild) { return aChild->SendResetToDefaultMockDevices(); }); } bool Serial::GetAutoselectPorts(ErrorResult& aRv) const { if (!StaticPrefs::dom_webserial_testing_enabled()) { aRv.ThrowNotSupportedError("Testing is not enabled"); return false; } return mAutoselectPorts; } void Serial::SetAutoselectPorts(bool aAutoselect, ErrorResult& aRv) { if (!StaticPrefs::dom_webserial_testing_enabled()) { aRv.ThrowNotSupportedError("Testing is not enabled"); return; } mAutoselectPorts = aAutoselect; } } // namespace mozilla::dom