1054 lines
35 KiB
C++
1054 lines
35 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/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->GetRelevantGlobal()),
|
|
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 && mState != State::Forgetting &&
|
|
mState != State::Forgotten) {
|
|
EventListenerManager* elm = GetExistingListenerManager();
|
|
bool hasListeners = elm && (elm->HasListenersFor(u"connect"_ns) ||
|
|
elm->HasListenersFor(u"disconnect"_ns));
|
|
// The port effectively holds OS resources while opened, and while closing
|
|
// is in progress. Both states require the worker to remain alive so that
|
|
// close IPC callbacks can complete.
|
|
bool isActive = (mState == State::Opened) || (mState == State::Closing);
|
|
needsRef = isActive || hasListeners;
|
|
}
|
|
|
|
if (needsRef && !mWorkerRef) {
|
|
WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
|
|
if (workerPrivate) {
|
|
RefPtr<SerialPort> 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 (mState == State::Opened || mState == State::Closing) {
|
|
mState = State::Closed;
|
|
// Don't have to wait for this
|
|
RefPtr<Promise> ignoredPromise = CloseStreams(StreamCloseMode::Forced);
|
|
}
|
|
|
|
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<JSObject*> aGivenProto) {
|
|
return SerialPort_Binding::Wrap(aCx, this, aGivenProto);
|
|
}
|
|
|
|
void SerialPort::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
|
|
aVisitor.mCanHandle = true;
|
|
aVisitor.SetParentTarget(mSerial, false);
|
|
}
|
|
|
|
already_AddRefed<Promise> SerialPort::Open(const SerialOptions& aOptions,
|
|
ErrorResult& aRv) {
|
|
nsIGlobalObject* global = GetRelevantGlobal();
|
|
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 = 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<uint8_t>(aOptions.mParity), aOptions.mBufferSize,
|
|
static_cast<uint8_t>(aOptions.mFlowControl)));
|
|
|
|
// Step 2: If this.[[state]] is not "closed", reject promise with an
|
|
// InvalidStateError DOMException and return promise.
|
|
switch (mState) {
|
|
case State::Closed:
|
|
break;
|
|
case State::Opening:
|
|
promise->MaybeRejectWithInvalidStateError("Port is being opened");
|
|
return promise.forget();
|
|
case State::Opened:
|
|
promise->MaybeRejectWithInvalidStateError("Port is already open");
|
|
return promise.forget();
|
|
case State::Closing:
|
|
promise->MaybeRejectWithInvalidStateError("Port is being closed");
|
|
return promise.forget();
|
|
case State::Forgetting:
|
|
case State::Forgotten:
|
|
promise->MaybeRejectWithInvalidStateError("Port has been forgotten");
|
|
return promise.forget();
|
|
}
|
|
MOZ_ASSERT(mState == State::Closed);
|
|
|
|
// 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".
|
|
mState = State::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<SerialPortChild> child = mChild;
|
|
RefPtr<SerialPort> 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()));
|
|
if (self->mState == State::Opening) {
|
|
// Step 9.3: Set this.[[state]] to "opened".
|
|
self->mState = State::Opened;
|
|
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.
|
|
if (self->mOpenPromise) {
|
|
self->mOpenPromise->MaybeResolveWithUndefined();
|
|
}
|
|
} else if (self->mOpenPromise) {
|
|
// Forget()/MarkForgotten() moved us out of Opening while the IPC
|
|
// was in flight. The promise is rejected rather than resolved
|
|
// because the port is no longer usable.
|
|
self->mOpenPromise->MaybeRejectWithAbortError(
|
|
"Port was forgotten while opening");
|
|
}
|
|
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<uint32_t>(aResult)));
|
|
if (self->mState == State::Opening) {
|
|
self->mState = State::Closed;
|
|
}
|
|
if (self->mOpenPromise) {
|
|
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<int>(aReason)));
|
|
if (self->mState == State::Opening) {
|
|
self->mState = State::Closed;
|
|
}
|
|
if (self->mOpenPromise) {
|
|
self->mOpenPromise->MaybeRejectWithNetworkError(
|
|
"Failed to open port: IPC communication error");
|
|
self->mOpenPromise = nullptr;
|
|
}
|
|
});
|
|
|
|
// Step 10: Return promise.
|
|
return promise.forget();
|
|
}
|
|
|
|
already_AddRefed<Promise> SerialPort::SetSignals(
|
|
const SerialOutputSignals& aSignals, ErrorResult& aRv) {
|
|
nsIGlobalObject* global = GetRelevantGlobal();
|
|
if (!global) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<Promise> 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 (mState == State::Forgetting || mState == State::Forgotten) {
|
|
promise->MaybeRejectWithInvalidStateError("Port has been forgotten");
|
|
return promise.forget();
|
|
}
|
|
|
|
if (mState != State::Opened) {
|
|
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<SerialPortChild> 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<uint32_t>(aResult)));
|
|
}
|
|
},
|
|
[promise](mozilla::ipc::ResponseRejectReason) {
|
|
promise->MaybeRejectWithNetworkError(
|
|
"Failed to set signals: IPC communication error");
|
|
});
|
|
|
|
return promise.forget();
|
|
}
|
|
|
|
already_AddRefed<Promise> SerialPort::GetSignals(ErrorResult& aRv) {
|
|
nsIGlobalObject* global = GetRelevantGlobal();
|
|
if (!global) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<Promise> 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 (mState == State::Forgetting || mState == State::Forgotten) {
|
|
promise->MaybeRejectWithInvalidStateError("Port has been forgotten");
|
|
return promise.forget();
|
|
}
|
|
|
|
if (mState != State::Opened) {
|
|
promise->MaybeRejectWithInvalidStateError("Port is not open");
|
|
return promise.forget();
|
|
}
|
|
|
|
if (!mChild) {
|
|
promise->MaybeRejectWithInvalidStateError("Port not initialized");
|
|
return promise.forget();
|
|
}
|
|
|
|
RefPtr<SerialPortChild> 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<nsresult, IPCSerialInputSignals>& 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<uint32_t>(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<Promise> SerialPort::Close(ErrorResult& aRv) {
|
|
nsIGlobalObject* global = GetRelevantGlobal();
|
|
if (!global) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
|
|
// Step 1: Let promise be a new promise.
|
|
RefPtr<Promise> 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.
|
|
switch (mState) {
|
|
case State::Opened:
|
|
break;
|
|
case State::Closing:
|
|
promise->MaybeRejectWithInvalidStateError("Port is being closed");
|
|
return promise.forget();
|
|
case State::Forgetting:
|
|
case State::Forgotten:
|
|
promise->MaybeRejectWithInvalidStateError("Port has been forgotten");
|
|
return promise.forget();
|
|
case State::Closed:
|
|
case State::Opening:
|
|
promise->MaybeRejectWithInvalidStateError("Port is not open");
|
|
return promise.forget();
|
|
}
|
|
MOZ_ASSERT(mState == State::Opened);
|
|
|
|
// 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<Promise> combinedPromise = CloseStreams(StreamCloseMode::Graceful);
|
|
if (!combinedPromise) {
|
|
combinedPromise = Promise::CreateResolvedWithUndefined(global, aRv);
|
|
if (NS_WARN_IF(aRv.Failed())) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
// Step 9: Set this.[[state]] to "closing".
|
|
mState = State::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<JS::Value>, ErrorResult&, SerialPort* aSelf) {
|
|
aSelf->CloseAfterStreamsClosed();
|
|
},
|
|
[](JSContext*, JS::Handle<JS::Value> aReason, ErrorResult&,
|
|
SerialPort* aSelf) {
|
|
if (aSelf->mHasShutdown) {
|
|
return;
|
|
}
|
|
// Forget()/MarkForgotten() may have transitioned to a terminal state.
|
|
if (aSelf->mState == State::Closing) {
|
|
aSelf->mState = State::Closed;
|
|
}
|
|
aSelf->UpdateWorkerRef();
|
|
aSelf->NotifySharingStateChanged(false);
|
|
if (RefPtr<Promise> closePromise = aSelf->mClosePromise.forget()) {
|
|
closePromise->MaybeReject(aReason);
|
|
}
|
|
},
|
|
RefPtr(this));
|
|
|
|
// Step 11: Return promise.
|
|
return promise.forget();
|
|
}
|
|
|
|
already_AddRefed<Promise> SerialPort::Forget(ErrorResult& aRv) {
|
|
nsIGlobalObject* global = GetRelevantGlobal();
|
|
if (!global) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<Promise> 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()));
|
|
|
|
// Step 2.1: Set this.[[state]] to "forgetting".
|
|
const bool wasActive =
|
|
(mState == State::Opened) || (mState == State::Closing);
|
|
mState = State::Forgetting;
|
|
|
|
if (mSerial) {
|
|
RefPtr<Serial> serial = mSerial;
|
|
serial->ForgetPort(mInfo.id());
|
|
}
|
|
|
|
if (wasActive) {
|
|
// Don't have to wait for this
|
|
RefPtr<Promise> ignoredPromise = CloseStreams(StreamCloseMode::Graceful);
|
|
}
|
|
|
|
UpdateWorkerRef();
|
|
NotifySharingStateChanged(false);
|
|
|
|
if (mChild) {
|
|
RefPtr<SerialPortChild> child = mChild;
|
|
nsISerialEventTarget* actorTarget = child->GetActorEventTarget();
|
|
|
|
if (!actorTarget) {
|
|
// Step 2.3: Set this.[[state]] to "forgotten".
|
|
mState = State::Forgotten;
|
|
promise->MaybeResolveWithUndefined();
|
|
return promise.forget();
|
|
}
|
|
|
|
RefPtr<SerialPort> self = this;
|
|
InvokeAsync(actorTarget, "SerialPort::SendForget",
|
|
[child = std::move(child)]() { return child->SendClose(); })
|
|
->Then(
|
|
GetCurrentSerialEventTarget(), __func__,
|
|
[promise, self](nsresult aResult) {
|
|
// Step 2.3: Set this.[[state]] to "forgotten".
|
|
self->mState = State::Forgotten;
|
|
promise->MaybeResolveWithUndefined();
|
|
},
|
|
[promise, self](mozilla::ipc::ResponseRejectReason aReason) {
|
|
self->mState = State::Forgotten;
|
|
promise->MaybeResolveWithUndefined();
|
|
});
|
|
} else {
|
|
mState = State::Forgotten;
|
|
promise->MaybeResolveWithUndefined();
|
|
}
|
|
|
|
return promise.forget();
|
|
}
|
|
|
|
void SerialPort::MarkForgotten() {
|
|
if (mState == State::Forgetting || mState == State::Forgotten) {
|
|
return;
|
|
}
|
|
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
("SerialPort[%p]::MarkForgotten for port '%s'", this,
|
|
NS_ConvertUTF16toUTF8(mInfo.id()).get()));
|
|
|
|
const bool wasActive =
|
|
(mState == State::Opened) || (mState == State::Closing);
|
|
mState = State::Forgotten;
|
|
|
|
if (wasActive) {
|
|
// Don't have to wait for this
|
|
RefPtr<Promise> ignoredPromise = CloseStreams(StreamCloseMode::Graceful);
|
|
}
|
|
|
|
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 (mState != State::Opened) {
|
|
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 (mState != State::Opened) {
|
|
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<SerialPortChild> 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> 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<Event>(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()));
|
|
if (mState == State::Opened || mState == State::Closing) {
|
|
mState = State::Closed;
|
|
}
|
|
mPhysicallyPresent = false;
|
|
// Don't have to wait for this
|
|
RefPtr<Promise> ignoredPromise = CloseStreams(StreamCloseMode::Graceful);
|
|
UpdateWorkerRef();
|
|
NotifySharingStateChanged(false);
|
|
|
|
auto event = MakeRefPtr<Event>(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<double> aHighWaterMark, ErrorResult& aRv) {
|
|
SetUpByteNative(aCx, aAlgorithms, aHighWaterMark, aRv);
|
|
}
|
|
};
|
|
|
|
ReadableStream* SerialPort::CreateReadableStream() {
|
|
MOZ_ASSERT(mState == State::Opened);
|
|
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<mozilla::ipc::DataPipeSender> sender;
|
|
RefPtr<mozilla::ipc::DataPipeReceiver> 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<SerialPortChild> 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(GetRelevantGlobal())) {
|
|
return nullptr;
|
|
}
|
|
|
|
JSContext* cx = jsapi.cx();
|
|
ErrorResult erv;
|
|
|
|
nsCOMPtr<nsIAsyncInputStream> readInput = receiver.get();
|
|
auto readableStream =
|
|
MakeRefPtr<SerialByteReadableStream>(GetRelevantGlobal());
|
|
RefPtr readAlgorithms =
|
|
MakeRefPtr<SerialPortReadAlgorithms>(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(mState == State::Opened);
|
|
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<mozilla::ipc::DataPipeSender> sender;
|
|
RefPtr<mozilla::ipc::DataPipeReceiver> 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<SerialPortChild> 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(GetRelevantGlobal())) {
|
|
return nullptr;
|
|
}
|
|
|
|
JSContext* cx = jsapi.cx();
|
|
ErrorResult erv;
|
|
|
|
nsCOMPtr<nsIAsyncOutputStream> writeOutput = sender.get();
|
|
RefPtr writeAlgorithms = MakeRefPtr<SerialPortWriteAlgorithms>(
|
|
GetRelevantGlobal(), writeOutput, this);
|
|
mWritable = WritableStream::CreateNative(
|
|
cx, *GetRelevantGlobal(), *writeAlgorithms,
|
|
Some(static_cast<double>(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<SerialPortChild> 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<SerialPort> self = this;
|
|
nsCOMPtr<nsISerialEventTarget> 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", unless Forget()/MarkForgotten() raced
|
|
// ahead and moved us into a terminal state.
|
|
if (mState == State::Closing) {
|
|
mState = State::Closed;
|
|
}
|
|
UpdateWorkerRef();
|
|
NotifySharingStateChanged(false);
|
|
if (RefPtr<Promise> closePromise = mClosePromise.forget()) {
|
|
if (NS_SUCCEEDED(aResult)) {
|
|
closePromise->MaybeResolveWithUndefined();
|
|
} else {
|
|
closePromise->MaybeRejectWithNetworkError(
|
|
"Failed to close port: IPC communication error");
|
|
}
|
|
}
|
|
}
|
|
|
|
already_AddRefed<Promise> SerialPort::CloseStreams(StreamCloseMode aMode) {
|
|
nsIGlobalObject* global = GetRelevantGlobal();
|
|
if (!global) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (!mReadable && !mWritable) {
|
|
return nullptr;
|
|
}
|
|
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
("SerialPort[%p]::CloseStreams closing streams "
|
|
"(readable=%p, writable=%p, mode=%s)",
|
|
this, mReadable.get(), mWritable.get(),
|
|
aMode == StreamCloseMode::Forced ? "forced" : "graceful"));
|
|
|
|
AutoJSAPI jsapi;
|
|
if (!jsapi.Init(global)) {
|
|
return nullptr;
|
|
}
|
|
|
|
JSContext* cx = jsapi.cx();
|
|
|
|
RefPtr<DOMException> exception =
|
|
DOMException::Create(NS_ERROR_DOM_NETWORK_ERR, "Port has been closed"_ns);
|
|
JS::Rooted<JS::Value> errorVal(cx);
|
|
bool hasError = ToJSValue(cx, exception, &errorVal);
|
|
|
|
// Error the streams instead of aborting, so we don't dispatchevents
|
|
// or runauthor-visible algorithms. The OS resources are released by
|
|
// mChild->Shutdown() in Shutdown().
|
|
if (aMode == StreamCloseMode::Forced) {
|
|
if (mReadable && hasError) {
|
|
IgnoredErrorResult rv;
|
|
RefPtr readable = mReadable;
|
|
readable->ErrorNative(cx, errorVal, rv);
|
|
}
|
|
if (mWritable && hasError) {
|
|
IgnoredErrorResult rv;
|
|
RefPtr writable = mWritable;
|
|
writable->ErrorNative(cx, errorVal, rv);
|
|
}
|
|
mReadable = nullptr;
|
|
mWritable = nullptr;
|
|
return nullptr;
|
|
}
|
|
|
|
// Cancel the readable and abort the writable, collecting their promises.
|
|
nsTArray<RefPtr<Promise>> streamPromises;
|
|
|
|
if (mReadable && hasError) {
|
|
IgnoredErrorResult rv;
|
|
RefPtr readable = mReadable;
|
|
if (RefPtr<Promise> cancelPromise =
|
|
readable->CancelNative(cx, errorVal, rv)) {
|
|
streamPromises.AppendElement(std::move(cancelPromise));
|
|
}
|
|
}
|
|
|
|
if (mWritable && hasError) {
|
|
IgnoredErrorResult rv;
|
|
RefPtr writable = mWritable;
|
|
if (RefPtr<Promise> abortPromise =
|
|
writable->AbortNative(cx, errorVal, rv)) {
|
|
streamPromises.AppendElement(std::move(abortPromise));
|
|
}
|
|
}
|
|
|
|
if (streamPromises.IsEmpty()) {
|
|
mReadable = nullptr;
|
|
mWritable = nullptr;
|
|
return nullptr;
|
|
}
|
|
|
|
IgnoredErrorResult rv;
|
|
RefPtr<Promise> 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<JS::Value>, ErrorResult&, SerialPort* aSelf) {
|
|
aSelf->mReadable = nullptr;
|
|
aSelf->mWritable = nullptr;
|
|
},
|
|
[](JSContext*, JS::Handle<JS::Value>, ErrorResult&, SerialPort* aSelf) {
|
|
aSelf->mReadable = nullptr;
|
|
aSelf->mWritable = nullptr;
|
|
},
|
|
RefPtr(this));
|
|
|
|
return combined.forget();
|
|
}
|
|
|
|
} // namespace mozilla::dom
|