/* -*- 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 "mozilla/dom/SerialPort.h" #include "SerialLogging.h" #include "SerialPortPumps.h" #include "SerialPortStreamAlgorithms.h" #include "mozilla/EventDispatcher.h" #include "mozilla/EventListenerManager.h" #include "mozilla/ScopeExit.h" #include "mozilla/dom/DOMException.h" #include "mozilla/dom/DOMExceptionBinding.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/Promise-inl.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/ReadableStream.h" #include "mozilla/dom/Serial.h" #include "mozilla/dom/SerialPortBinding.h" #include "mozilla/dom/SerialPortChild.h" #include "mozilla/dom/SerialPortIPCTypes.h" #include "mozilla/dom/ToJSValue.h" #include "mozilla/dom/UnderlyingSourceCallbackHelpers.h" #include "mozilla/dom/WorkerCommon.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/WritableStream.h" #include "mozilla/ipc/DataPipe.h" namespace mozilla::dom { NS_IMPL_CYCLE_COLLECTION_CLASS(SerialPort) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(SerialPort, DOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSerial) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReadable) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWritable) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChild) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOpenPromise) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mClosePromise) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(SerialPort, DOMEventTargetHelper) tmp->Shutdown(); NS_IMPL_CYCLE_COLLECTION_UNLINK(mSerial) NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadable) NS_IMPL_CYCLE_COLLECTION_UNLINK(mWritable) NS_IMPL_CYCLE_COLLECTION_UNLINK(mChild) NS_IMPL_CYCLE_COLLECTION_UNLINK(mOpenPromise) NS_IMPL_CYCLE_COLLECTION_UNLINK(mClosePromise) NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_ADDREF_INHERITED(SerialPort, DOMEventTargetHelper) NS_IMPL_RELEASE_INHERITED(SerialPort, DOMEventTargetHelper) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SerialPort) NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) SerialPort::SerialPort(const IPCSerialPortInfo& aInfo, Serial* aSerial) : DOMEventTargetHelper(aSerial->GetOwnerGlobal()), mSerial(aSerial), mInfo(aInfo) { nsPIDOMWindowInner* window = aSerial->GetOwnerWindow(); if (window) { if (Document* doc = window->GetExtantDoc()) { // Disallow putting this page in the bfcache to ensure that // when we navigate away the OS resources associated with this // SerialPort get properly cleaned up. doc->DisallowBFCaching(); } } MOZ_LOG( gWebSerialLog, LogLevel::Info, ("SerialPort[%p] created for port '%s' (%s)", this, NS_ConvertUTF16toUTF8(mInfo.id()).get(), window ? "window" : "worker")); } void SerialPort::UpdateWorkerRef() { if (NS_IsMainThread()) { return; } bool needsRef = false; if (!mHasShutdown && mForgottenState == ForgottenState::NotForgotten) { EventListenerManager* elm = GetExistingListenerManager(); bool hasListeners = elm && (elm->HasListenersFor(u"connect"_ns) || elm->HasListenersFor(u"disconnect"_ns)); needsRef = mIsOpen || hasListeners; } if (needsRef && !mWorkerRef) { WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); if (workerPrivate) { RefPtr self = this; mWorkerRef = StrongWorkerRef::Create(workerPrivate, "SerialPort", [self]() { self->Shutdown(); }); } } else if (!needsRef && mWorkerRef) { mWorkerRef = nullptr; } } void SerialPort::EventListenerAdded(nsAtom* aType) { DOMEventTargetHelper::EventListenerAdded(aType); UpdateWorkerRef(); } void SerialPort::EventListenerRemoved(nsAtom* aType) { DOMEventTargetHelper::EventListenerRemoved(aType); UpdateWorkerRef(); } SerialPort::~SerialPort() { MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("SerialPort[%p] destroyed for port '%s'", this, NS_ConvertUTF16toUTF8(mInfo.id()).get())); MOZ_ASSERT(mHasShutdown); } void SerialPort::Shutdown() { if (mHasShutdown) { return; } MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("SerialPort[%p] shutting down port '%s'", this, NS_ConvertUTF16toUTF8(mInfo.id()).get())); mHasShutdown = true; if (mIsOpen) { mIsOpen = false; // Don't have to wait for this RefPtr ignoredPromise = CloseStreams(); } if (mOpenPromise) { mOpenPromise->MaybeRejectWithAbortError("Port was shut down"); mOpenPromise = nullptr; } if (mClosePromise) { mClosePromise->MaybeRejectWithNetworkError("Port was shut down"); mClosePromise = nullptr; } if (mChild) { mChild->Shutdown(); mChild = nullptr; } mWorkerRef = nullptr; } void SerialPort::DisconnectFromOwner() { Shutdown(); DOMEventTargetHelper::DisconnectFromOwner(); } JSObject* SerialPort::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return SerialPort_Binding::Wrap(aCx, this, aGivenProto); } void SerialPort::GetEventTargetParent(EventChainPreVisitor& aVisitor) { aVisitor.mCanHandle = true; aVisitor.SetParentTarget(mSerial, false); } already_AddRefed SerialPort::Open(const SerialOptions& aOptions, ErrorResult& aRv) { nsIGlobalObject* global = GetOwnerGlobal(); if (!global) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } // https://wicg.github.io/serial/#dom-serialport-open // 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::Info, ("SerialPort[%p]::Open called for port '%s' with baudRate=%u, " "dataBits=%u, stopBits=%u, parity=%u, bufferSize=%u, flowControl=%u", this, NS_ConvertUTF16toUTF8(mInfo.id()).get(), aOptions.mBaudRate, aOptions.mDataBits, aOptions.mStopBits, static_cast(aOptions.mParity), aOptions.mBufferSize, static_cast(aOptions.mFlowControl))); // Step 2: If this.[[state]] is not "closed", reject promise with an // InvalidStateError DOMException and return promise. if (mForgottenState != ForgottenState::NotForgotten) { promise->MaybeRejectWithInvalidStateError("Port has been forgotten"); return promise.forget(); } if (mIsOpen) { promise->MaybeRejectWithInvalidStateError("Port is already open"); return promise.forget(); } if (mOpenPromise) { promise->MaybeRejectWithInvalidStateError("Port is being opened"); return promise.forget(); } // Step 3: If options["baudRate"] is 0, reject promise with a // TypeError and return promise. if (aOptions.mBaudRate == 0) { promise->MaybeRejectWithTypeError("Invalid baud rate"); return promise.forget(); } // Step 4: If options["dataBits"] is not 7 or 8, reject promise with a // TypeError and return promise. if (aOptions.mDataBits != 7 && aOptions.mDataBits != 8) { promise->MaybeRejectWithTypeError("Data bits must be 7 or 8"); return promise.forget(); } // Step 5: If options["stopBits"] is not 1 or 2, reject promise with a // TypeError and return promise. if (aOptions.mStopBits != 1 && aOptions.mStopBits != 2) { promise->MaybeRejectWithTypeError("Stop bits must be 1 or 2"); return promise.forget(); } // Step 6: If options["bufferSize"] is 0, reject promise with a TypeError // and return promise. if (aOptions.mBufferSize == 0) { promise->MaybeRejectWithTypeError("Invalid buffer size"); return promise.forget(); } // Step 7: Optionally, if options["bufferSize"] is larger than the // implementation is able to support, reject promise with a TypeError. if (aOptions.mBufferSize > kMaxSerialBufferSize) { promise->MaybeRejectWithTypeError( "Requested buffer size exceeds the maximum supported size"); return promise.forget(); } if (!mChild) { promise->MaybeRejectWithNotSupportedError("Port actor not available"); return promise.forget(); } // Step 8: Set this.[[state]] to "opening". mOpenPromise = promise; IPCSerialOptions options{aOptions.mBaudRate, aOptions.mDataBits, aOptions.mStopBits, aOptions.mParity, aOptions.mBufferSize, aOptions.mFlowControl}; // Step 9 (in parallel): Request the operating system to open the serial port // with the given connection parameters. The PSerialPort actor was created // eagerly when the port was granted, so we can send Open directly. RefPtr child = mChild; RefPtr self = this; child->SendOpen(options)->Then( GetCurrentSerialEventTarget(), __func__, [self, bufferSize = options.bufferSize()](nsresult aResult) { if (self->mHasShutdown) { return; } if (NS_SUCCEEDED(aResult)) { MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPort[%p] opened successfully for port '%s'", self.get(), NS_ConvertUTF16toUTF8(self->mInfo.id()).get())); // Step 9.3: Set this.[[state]] to "opened". self->mIsOpen = true; self->UpdateWorkerRef(); self->NotifySharingStateChanged(true); // Step 9.4: Set this.[[bufferSize]]. self->mBufferSize = bufferSize; self->mPipeCapacity = std::max(bufferSize, kMinSerialPortPumpSize); // Streams are created lazily by GetReadable()/GetWritable(). // Step 9.5: Resolve promise with undefined. self->mOpenPromise->MaybeResolveWithUndefined(); self->mOpenPromise = nullptr; } else { // Step 9.2: Reject promise with a NetworkError. MOZ_LOG(gWebSerialLog, LogLevel::Error, ("SerialPort[%p] failed to open port '%s': error 0x%08x", self.get(), NS_ConvertUTF16toUTF8(self->mInfo.id()).get(), static_cast(aResult))); self->mOpenPromise->MaybeRejectWithNetworkError( "Failed to open port"); self->mOpenPromise = nullptr; } }, [self](mozilla::ipc::ResponseRejectReason aReason) { if (self->mHasShutdown) { return; } MOZ_LOG(gWebSerialLog, LogLevel::Error, ("SerialPort[%p] failed to open port '%s': IPC error " "(reason: %d)", self.get(), NS_ConvertUTF16toUTF8(self->mInfo.id()).get(), static_cast(aReason))); self->mOpenPromise->MaybeRejectWithNetworkError( "Failed to open port: IPC communication error"); self->mOpenPromise = nullptr; }); // Step 10: Return promise. return promise.forget(); } already_AddRefed SerialPort::SetSignals( const SerialOutputSignals& aSignals, ErrorResult& aRv) { nsIGlobalObject* global = GetOwnerGlobal(); if (!global) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } RefPtr promise = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("SerialPort[%p]::SetSignals called for port '%s'", this, NS_ConvertUTF16toUTF8(mInfo.id()).get())); if (mForgottenState != ForgottenState::NotForgotten) { promise->MaybeRejectWithInvalidStateError("Port has been forgotten"); return promise.forget(); } if (!mIsOpen) { promise->MaybeRejectWithInvalidStateError("Port is not open"); return promise.forget(); } if (!aSignals.mDataTerminalReady.WasPassed() && !aSignals.mRequestToSend.WasPassed() && !aSignals.mBreak.WasPassed()) { promise->MaybeRejectWithTypeError( "At least one signal must be specified in setSignals()"); return promise.forget(); } if (!mChild) { promise->MaybeRejectWithInvalidStateError("Port not initialized"); return promise.forget(); } IPCSerialOutputSignals signals{ aSignals.mDataTerminalReady.WasPassed() ? Some(aSignals.mDataTerminalReady.Value()) : Nothing(), aSignals.mRequestToSend.WasPassed() ? Some(aSignals.mRequestToSend.Value()) : Nothing(), aSignals.mBreak.WasPassed() ? Some(aSignals.mBreak.Value()) : Nothing()}; RefPtr child = mChild; nsISerialEventTarget* actorTarget = child->GetActorEventTarget(); if (!actorTarget) { promise->MaybeRejectWithNetworkError("Actor not available"); return promise.forget(); } InvokeAsync(actorTarget, "SerialPort::SendSetSignals", [child = std::move(child), signals = std::move(signals)]() { return child->SendSetSignals(signals); }) ->Then( GetCurrentSerialEventTarget(), __func__, [promise](nsresult aResult) { if (NS_SUCCEEDED(aResult)) { promise->MaybeResolveWithUndefined(); } else { promise->MaybeRejectWithNetworkError( nsPrintfCString("Failed to set signals: 0x%08x", static_cast(aResult))); } }, [promise](mozilla::ipc::ResponseRejectReason) { promise->MaybeRejectWithNetworkError( "Failed to set signals: IPC communication error"); }); return promise.forget(); } already_AddRefed SerialPort::GetSignals(ErrorResult& aRv) { nsIGlobalObject* global = GetOwnerGlobal(); if (!global) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } RefPtr promise = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("SerialPort[%p]::GetSignals called for port '%s'", this, NS_ConvertUTF16toUTF8(mInfo.id()).get())); if (mForgottenState != ForgottenState::NotForgotten) { promise->MaybeRejectWithInvalidStateError("Port has been forgotten"); return promise.forget(); } if (!mIsOpen) { promise->MaybeRejectWithInvalidStateError("Port is not open"); return promise.forget(); } if (!mChild) { promise->MaybeRejectWithInvalidStateError("Port not initialized"); return promise.forget(); } RefPtr child = mChild; nsISerialEventTarget* actorTarget = child->GetActorEventTarget(); if (!actorTarget) { promise->MaybeRejectWithNetworkError("Actor not available"); return promise.forget(); } InvokeAsync(actorTarget, "SerialPort::SendGetSignals", [child]() { return child->SendGetSignals(); }) ->Then( GetCurrentSerialEventTarget(), __func__, [promise]( const std::tuple& aResult) { nsresult rv = std::get<0>(aResult); if (NS_SUCCEEDED(rv)) { const IPCSerialInputSignals& ipcSignals = std::get<1>(aResult); SerialInputSignals result; result.mDataCarrierDetect = ipcSignals.dataCarrierDetect(); result.mClearToSend = ipcSignals.clearToSend(); result.mRingIndicator = ipcSignals.ringIndicator(); result.mDataSetReady = ipcSignals.dataSetReady(); promise->MaybeResolve(result); } else { promise->MaybeRejectWithNetworkError(nsPrintfCString( "Failed to get signals: 0x%08x", static_cast(rv))); } }, [promise](mozilla::ipc::ResponseRejectReason) { promise->MaybeRejectWithNetworkError( "Failed to get signals: IPC communication error"); }); return promise.forget(); } // https://wicg.github.io/serial/#dom-serialport-close already_AddRefed SerialPort::Close(ErrorResult& aRv) { nsIGlobalObject* global = GetOwnerGlobal(); if (!global) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } // 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::Info, ("SerialPort[%p]::Close called for port '%s'", this, NS_ConvertUTF16toUTF8(mInfo.id()).get())); // Step 2: If this.[[state]] is not "opened", reject promise with an // InvalidStateError DOMException and return promise. if (mForgottenState != ForgottenState::NotForgotten) { promise->MaybeRejectWithInvalidStateError("Port has been forgotten"); return promise.forget(); } if (!mIsOpen) { promise->MaybeRejectWithInvalidStateError("Port is not open"); return promise.forget(); } if (mClosePromise) { promise->MaybeRejectWithInvalidStateError("Port is being closed"); return promise.forget(); } // Steps 3-8: Cancel the readable stream (step 3), abort the writable // stream (step 4), and let combinedPromise be the result of getting a // promise to wait for all (steps 5-8). Our pendingClosePromise is // effectively resolved once both streams are nulled (step 6), so the // combined promise just waits for the cancel and abort to finish. RefPtr combinedPromise = CloseStreams(); if (!combinedPromise) { combinedPromise = Promise::CreateResolvedWithUndefined(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } } // Step 9: Set this.[[state]] to "closing". mClosePromise = promise; // Step 10: Upon fulfillment of combinedPromise, run the following steps // in parallel: request the operating system close the serial port. // Step 10 (rejection): Upon rejection of combinedPromise with reason r, // reject promise with r. combinedPromise->AddCallbacksWithCycleCollectedArgs( [](JSContext*, JS::Handle, ErrorResult&, SerialPort* aSelf) { aSelf->CloseAfterStreamsClosed(); }, [](JSContext*, JS::Handle aReason, ErrorResult&, SerialPort* aSelf) { if (aSelf->mHasShutdown) { return; } aSelf->mIsOpen = false; aSelf->UpdateWorkerRef(); aSelf->NotifySharingStateChanged(false); if (RefPtr closePromise = aSelf->mClosePromise.forget()) { closePromise->MaybeReject(aReason); } }, RefPtr(this)); // Step 11: Return promise. return promise.forget(); } already_AddRefed SerialPort::Forget(ErrorResult& aRv) { nsIGlobalObject* global = GetOwnerGlobal(); if (!global) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } RefPtr promise = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPort[%p]::Forget called for port '%s'", this, NS_ConvertUTF16toUTF8(mInfo.id()).get())); mForgottenState = ForgottenState::Forgetting; if (mSerial) { RefPtr serial = mSerial; serial->ForgetPort(mInfo.id()); } if (mIsOpen) { mIsOpen = false; // Don't have to wait for this RefPtr ignoredPromise = CloseStreams(); } UpdateWorkerRef(); NotifySharingStateChanged(false); if (mChild) { RefPtr child = mChild; nsISerialEventTarget* actorTarget = child->GetActorEventTarget(); if (!actorTarget) { mForgottenState = ForgottenState::Forgotten; promise->MaybeResolveWithUndefined(); return promise.forget(); } RefPtr self = this; InvokeAsync(actorTarget, "SerialPort::SendForget", [child = std::move(child)]() { return child->SendClose(); }) ->Then( GetCurrentSerialEventTarget(), __func__, [promise, self](nsresult aResult) { self->mForgottenState = ForgottenState::Forgotten; promise->MaybeResolveWithUndefined(); }, [promise, self](mozilla::ipc::ResponseRejectReason aReason) { self->mForgottenState = ForgottenState::Forgotten; promise->MaybeResolveWithUndefined(); }); } else { mForgottenState = ForgottenState::Forgotten; promise->MaybeResolveWithUndefined(); } return promise.forget(); } void SerialPort::MarkForgotten() { if (mForgottenState != ForgottenState::NotForgotten) { return; } MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPort[%p]::MarkForgotten for port '%s'", this, NS_ConvertUTF16toUTF8(mInfo.id()).get())); mForgottenState = ForgottenState::Forgotten; if (mIsOpen) { mIsOpen = false; // Don't have to wait for this RefPtr ignoredPromise = CloseStreams(); } UpdateWorkerRef(); NotifySharingStateChanged(false); } void SerialPort::GetInfo(SerialPortInfo& aRetVal, ErrorResult& aRv) { if (mInfo.usbVendorId().isSome()) { aRetVal.mUsbVendorId.Construct(mInfo.usbVendorId().value()); } if (mInfo.usbProductId().isSome()) { aRetVal.mUsbProductId.Construct(mInfo.usbProductId().value()); } if (mInfo.bluetoothServiceClassId().isSome()) { OwningStringOrUnsignedLong uuid; uuid.SetAsString() = mInfo.bluetoothServiceClassId().value(); aRetVal.mBluetoothServiceClassId.Construct(uuid); } } ReadableStream* SerialPort::GetReadable() { if (!mIsOpen) { return nullptr; } // Per spec, readable becomes null after reader.cancel(). Detect the // closed state and clear the reference so a fresh stream is created. if (mReadable && mReadable->State() == ReadableStream::ReaderState::Closed) { mReadable = nullptr; } if (!mReadable) { return CreateReadableStream(); } return mReadable; } WritableStream* SerialPort::GetWritable() { if (!mIsOpen) { return nullptr; } // Per spec, writable becomes null after writer.close() or writer.abort(). // Detect any non-writable state and clear the reference so a fresh // stream is created. if (mWritable && mWritable->State() != WritableStream::WriterState::Writable) { mWritable = nullptr; } if (!mWritable) { return CreateWritableStream(); } return mWritable; } void SerialPort::NotifySharingStateChanged(bool aConnected) { if (!mChild) { return; } RefPtr child = mChild; nsISerialEventTarget* actorTarget = child->GetActorEventTarget(); if (actorTarget) { actorTarget->Dispatch(NS_NewRunnableFunction( "SerialPort::SendUpdateSharingState", [child, aConnected]() { child->SendUpdateSharingState(aConnected); })); } } void SerialPort::OnActorDestroyed() { if (mHasShutdown) { return; } MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPort[%p]::OnActorDestroyed for port '%s'", this, NS_ConvertUTF16toUTF8(mInfo.id()).get())); // Clear the child reference first since the actor is already destroyed. // This prevents MarkForgotten/NotifySharingStateChanged from trying to // use the dead actor. mChild = nullptr; // Mark the port as forgotten (closes streams, updates worker ref). MarkForgotten(); // Remove from Serial's port list so it no longer appears in getPorts(). if (mSerial) { RefPtr serial = mSerial; serial->ForgetPort(mInfo.id()); } } void SerialPort::NotifyConnected() { MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPort[%p] connected for port '%s'", this, NS_ConvertUTF16toUTF8(mInfo.id()).get())); mPhysicallyPresent = true; auto event = MakeRefPtr(this, nullptr, nullptr); event->InitEvent(u"connect"_ns, true, false); event->SetTrusted(true); DispatchTrustedEvent(event); } void SerialPort::NotifyDisconnected() { MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPort[%p] disconnected for port '%s'", this, NS_ConvertUTF16toUTF8(mInfo.id()).get())); mIsOpen = false; mPhysicallyPresent = false; // Don't have to wait for this RefPtr ignoredPromise = CloseStreams(); UpdateWorkerRef(); NotifySharingStateChanged(false); auto event = MakeRefPtr(this, nullptr, nullptr); event->InitEvent(u"disconnect"_ns, true, false); event->SetTrusted(true); DispatchTrustedEvent(event); } // Thin subclass to access the protected SetUpByteNative. class SerialByteReadableStream final : public ReadableStream { public: explicit SerialByteReadableStream(nsIGlobalObject* aGlobal) : ReadableStream(aGlobal, HoldDropJSObjectsCaller::Implicit) {} void SetUp(JSContext* aCx, UnderlyingSourceAlgorithmsWrapper& aAlgorithms, Maybe aHighWaterMark, ErrorResult& aRv) { SetUpByteNative(aCx, aAlgorithms, aHighWaterMark, aRv); } }; ReadableStream* SerialPort::CreateReadableStream() { MOZ_ASSERT(mIsOpen); MOZ_ASSERT(!mReadable); // Create a DataPipe pair locally. The child keeps the receiver (for the // ReadableStream) and sends the sender to the parent (for the read pump). RefPtr sender; RefPtr receiver; nsresult rv = mozilla::ipc::NewDataPipe(mPipeCapacity, getter_AddRefs(sender), getter_AddRefs(receiver)); if (NS_FAILED(rv)) { return nullptr; } // Send the sender endpoint to the parent so it can start the read pump. if (mChild) { RefPtr child = mChild; nsISerialEventTarget* actorTarget = child->GetActorEventTarget(); if (actorTarget) { actorTarget->Dispatch(NS_NewRunnableFunction( "SerialPort::AttachReadPipe", [child, sender = std::move(sender)]() { child->SendAttachReadPipe(sender); })); } } AutoJSAPI jsapi; if (!jsapi.Init(GetOwnerGlobal())) { return nullptr; } JSContext* cx = jsapi.cx(); ErrorResult erv; nsCOMPtr readInput = receiver.get(); auto readableStream = MakeRefPtr(GetOwnerGlobal()); RefPtr readAlgorithms = MakeRefPtr(cx, readInput, readableStream, this); // Use a zero high water mark: the DataPipe itself provides buffering // (sized to mBufferSize), so the stream shouldn't eagerly pull before a // reader is acquired. readableStream->SetUp(cx, *readAlgorithms, Some(0.0), erv); if (erv.Failed()) { return nullptr; } mReadable = readableStream; MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPort[%p]::CreateReadableStream created readable=%p", this, mReadable.get())); return mReadable; } WritableStream* SerialPort::CreateWritableStream() { MOZ_ASSERT(mIsOpen); MOZ_ASSERT(!mWritable); // Create a DataPipe pair locally. The child keeps the sender (for the // WritableStream) and sends the receiver to the parent (for the write pump). RefPtr sender; RefPtr receiver; nsresult rv = mozilla::ipc::NewDataPipe(mPipeCapacity, getter_AddRefs(sender), getter_AddRefs(receiver)); if (NS_FAILED(rv)) { return nullptr; } // Send the receiver endpoint to the parent so it can start the write pump. if (mChild) { RefPtr child = mChild; nsISerialEventTarget* actorTarget = child->GetActorEventTarget(); if (actorTarget) { actorTarget->Dispatch( NS_NewRunnableFunction("SerialPort::AttachWritePipe", [child, receiver = std::move(receiver)]() { child->SendAttachWritePipe(receiver); })); } } AutoJSAPI jsapi; if (!jsapi.Init(GetOwnerGlobal())) { return nullptr; } JSContext* cx = jsapi.cx(); ErrorResult erv; nsCOMPtr writeOutput = sender.get(); RefPtr writeAlgorithms = MakeRefPtr( GetOwnerGlobal(), writeOutput, this); mWritable = WritableStream::CreateNative( cx, *GetOwnerGlobal(), *writeAlgorithms, Some(static_cast(mBufferSize)), nullptr, erv); if (erv.Failed()) { return nullptr; } MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPort[%p]::CreateWritableStream created writable=%p", this, mWritable.get())); return mWritable; } void SerialPort::CloseAfterStreamsClosed() { if (mHasShutdown) { return; } nsresult rv = NS_OK; // Every synchronous exit path marks the port as closed. auto markClosed = MakeScopeExit([&]() { SettleClosePromise(rv); }); RefPtr child = mChild; if (!child) { return; } nsISerialEventTarget* actorTarget = child->GetActorEventTarget(); if (!actorTarget) { // Make SettleClosePromise reject the promise with an error. rv = NS_ERROR_FAILURE; return; } // Async path: SettleClosePromise handles cleanup in the callbacks. markClosed.release(); RefPtr self = this; nsCOMPtr owningThread = GetCurrentSerialEventTarget(); InvokeAsync(actorTarget, "SerialPort::SendClose", [child]() { return child->SendClose(); }) ->Then( owningThread, "SerialPort::Close::SendClose", [self](nsresult aResult) { self->SettleClosePromise(aResult); }, [self](mozilla::ipc::ResponseRejectReason aReason) { self->SettleClosePromise(NS_ERROR_DOM_NETWORK_ERR); }); } void SerialPort::SettleClosePromise(nsresult aResult) { if (mHasShutdown) { return; } // Set this.[[state]] to "closed". mIsOpen = false; UpdateWorkerRef(); NotifySharingStateChanged(false); if (RefPtr closePromise = mClosePromise.forget()) { if (NS_SUCCEEDED(aResult)) { closePromise->MaybeResolveWithUndefined(); } else { closePromise->MaybeRejectWithNetworkError( "Failed to close port: IPC communication error"); } } } already_AddRefed SerialPort::CloseStreams() { nsIGlobalObject* global = GetOwnerGlobal(); if (!global) { return nullptr; } if (!mReadable && !mWritable) { return nullptr; } MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPort[%p]::CloseStreams closing streams " "(readable=%p, writable=%p)", this, mReadable.get(), mWritable.get())); AutoJSAPI jsapi; if (!jsapi.Init(global)) { return nullptr; } JSContext* cx = jsapi.cx(); // Cancel the readable and abort the writable, collecting their promises. nsTArray> streamPromises; RefPtr exception = DOMException::Create(NS_ERROR_DOM_NETWORK_ERR, "Port has been closed"_ns); JS::Rooted errorVal(cx); bool hasError = ToJSValue(cx, exception, &errorVal); if (mReadable && hasError) { IgnoredErrorResult rv; RefPtr readable = mReadable; if (RefPtr cancelPromise = readable->CancelNative(cx, errorVal, rv)) { streamPromises.AppendElement(std::move(cancelPromise)); } } if (mWritable && hasError) { IgnoredErrorResult rv; RefPtr writable = mWritable; if (RefPtr abortPromise = writable->AbortNative(cx, errorVal, rv)) { streamPromises.AppendElement(std::move(abortPromise)); } } if (streamPromises.IsEmpty()) { mReadable = nullptr; mWritable = nullptr; return nullptr; } IgnoredErrorResult rv; RefPtr combined = Promise::All(cx, streamPromises, rv); if (!combined) { mReadable = nullptr; mWritable = nullptr; return nullptr; } // Hold mReadable and mWritable until the cancel/abort promises settle. // Otherwise the streams may be cycle collected before finishing and those // promises may never resolve. combined->AddCallbacksWithCycleCollectedArgs( [](JSContext*, JS::Handle, ErrorResult&, SerialPort* aSelf) { aSelf->mReadable = nullptr; aSelf->mWritable = nullptr; }, [](JSContext*, JS::Handle, ErrorResult&, SerialPort* aSelf) { aSelf->mReadable = nullptr; aSelf->mWritable = nullptr; }, RefPtr(this)); return combined.forget(); } } // namespace mozilla::dom