blob: 681820acde875138ea39d2600d24ee8f79eb6afd [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/serial/serial_port.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "third_party/blink/renderer/bindings/core/v8/script_function.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_serial_input_signals.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_serial_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_serial_output_signals.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_serial_port_info.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/streams/readable_stream.h"
#include "third_party/blink/renderer/core/streams/writable_stream.h"
#include "third_party/blink/renderer/modules/event_target_modules_names.h"
#include "third_party/blink/renderer/modules/serial/serial.h"
#include "third_party/blink/renderer/modules/serial/serial_port_underlying_sink.h"
#include "third_party/blink/renderer/modules/serial/serial_port_underlying_source.h"
#include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h"
namespace blink {
namespace {
using device::mojom::SerialReceiveError;
using device::mojom::SerialSendError;
const char kResourcesExhaustedReadBuffer[] =
"Resources exhausted allocating read buffer.";
const char kResourcesExhaustedWriteBuffer[] =
"Resources exhausted allocation write buffer.";
const char kNoSignals[] =
"Signals dictionary must contain at least one member.";
const char kPortClosed[] = "The port is closed.";
const char kOpenError[] = "Failed to open serial port.";
const char kDeviceLostError[] = "The device has been lost.";
const char kSystemError[] = "An unknown system error has occurred.";
const int kMaxBufferSize = 16 * 1024 * 1024; /* 16 MiB */
bool SendErrorIsFatal(SerialSendError error) {
switch (error) {
case SerialSendError::NONE:
NOTREACHED();
return false;
case SerialSendError::SYSTEM_ERROR:
return false;
case SerialSendError::DISCONNECTED:
return true;
}
}
DOMException* DOMExceptionFromSendError(SerialSendError error) {
switch (error) {
case SerialSendError::NONE:
NOTREACHED();
return nullptr;
case SerialSendError::DISCONNECTED:
return MakeGarbageCollected<DOMException>(DOMExceptionCode::kNetworkError,
kDeviceLostError);
case SerialSendError::SYSTEM_ERROR:
return MakeGarbageCollected<DOMException>(DOMExceptionCode::kUnknownError,
kSystemError);
}
}
bool ReceiveErrorIsFatal(SerialReceiveError error) {
switch (error) {
case SerialReceiveError::NONE:
NOTREACHED();
return false;
case SerialReceiveError::BREAK:
case SerialReceiveError::FRAME_ERROR:
case SerialReceiveError::OVERRUN:
case SerialReceiveError::BUFFER_OVERFLOW:
case SerialReceiveError::PARITY_ERROR:
case SerialReceiveError::SYSTEM_ERROR:
return false;
case SerialReceiveError::DISCONNECTED:
case SerialReceiveError::DEVICE_LOST:
return true;
}
}
DOMException* DOMExceptionFromReceiveError(SerialReceiveError error) {
switch (error) {
case SerialReceiveError::NONE:
NOTREACHED();
return nullptr;
case SerialReceiveError::DISCONNECTED:
case SerialReceiveError::DEVICE_LOST:
return MakeGarbageCollected<DOMException>(DOMExceptionCode::kNetworkError,
kDeviceLostError);
case SerialReceiveError::BREAK:
return MakeGarbageCollected<DOMException>(DOMExceptionCode::kBreakError);
case SerialReceiveError::FRAME_ERROR:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kFramingError);
case SerialReceiveError::OVERRUN:
case SerialReceiveError::BUFFER_OVERFLOW:
return MakeGarbageCollected<DOMException>(
DOMExceptionCode::kBufferOverrunError);
case SerialReceiveError::PARITY_ERROR:
return MakeGarbageCollected<DOMException>(DOMExceptionCode::kParityError);
case SerialReceiveError::SYSTEM_ERROR:
return MakeGarbageCollected<DOMException>(DOMExceptionCode::kUnknownError,
kSystemError);
}
}
// A ScriptFunction that calls ContinueClose() on the provided SerialPort.
class ContinueCloseFunction : public ScriptFunction {
public:
static v8::Local<v8::Function> Create(ScriptState* script_state,
SerialPort* port) {
auto* self =
MakeGarbageCollected<ContinueCloseFunction>(script_state, port);
return self->BindToV8Function();
}
ContinueCloseFunction(ScriptState* script_state, SerialPort* port)
: ScriptFunction(script_state), port_(port) {}
ScriptValue Call(ScriptValue) override {
return port_->ContinueClose(GetScriptState()).AsScriptValue();
}
void Trace(Visitor* visitor) const override {
visitor->Trace(port_);
ScriptFunction::Trace(visitor);
}
private:
Member<SerialPort> port_;
};
// A ScriptFunction that calls AbortClose() on the provided SerialPort.
class AbortCloseFunction : public ScriptFunction {
public:
static v8::Local<v8::Function> Create(ScriptState* script_state,
SerialPort* port) {
auto* self = MakeGarbageCollected<AbortCloseFunction>(script_state, port);
return self->BindToV8Function();
}
AbortCloseFunction(ScriptState* script_state, SerialPort* port)
: ScriptFunction(script_state), port_(port) {}
ScriptValue Call(ScriptValue) override {
port_->AbortClose();
return ScriptValue();
}
void Trace(Visitor* visitor) const override {
visitor->Trace(port_);
ScriptFunction::Trace(visitor);
}
private:
Member<SerialPort> port_;
};
} // namespace
SerialPort::SerialPort(Serial* parent, mojom::blink::SerialPortInfoPtr info)
: info_(std::move(info)),
parent_(parent),
port_(parent->GetExecutionContext()),
client_receiver_(this, parent->GetExecutionContext()) {}
SerialPort::~SerialPort() = default;
SerialPortInfo* SerialPort::getInfo() {
auto* info = MakeGarbageCollected<SerialPortInfo>();
if (info_->has_usb_vendor_id)
info->setUsbVendorId(info_->usb_vendor_id);
if (info_->has_usb_product_id)
info->setUsbProductId(info_->usb_product_id);
return info;
}
ScriptPromise SerialPort::open(ScriptState* script_state,
const SerialOptions* options,
ExceptionState& exception_state) {
if (open_resolver_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"A call to open() is already in progress.");
return ScriptPromise();
}
if (port_.is_bound()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
"The port is already open.");
return ScriptPromise();
}
auto mojo_options = device::mojom::blink::SerialConnectionOptions::New();
if (options->baudRate() == 0) {
exception_state.ThrowTypeError(
"Requested baud rate must be greater than zero.");
return ScriptPromise();
}
mojo_options->bitrate = options->baudRate();
switch (options->dataBits()) {
case 7:
mojo_options->data_bits = device::mojom::blink::SerialDataBits::SEVEN;
break;
case 8:
mojo_options->data_bits = device::mojom::blink::SerialDataBits::EIGHT;
break;
default:
exception_state.ThrowTypeError(
"Requested number of data bits must be 7 or 8.");
return ScriptPromise();
}
if (options->parity() == "none") {
mojo_options->parity_bit = device::mojom::blink::SerialParityBit::NO_PARITY;
} else if (options->parity() == "even") {
mojo_options->parity_bit = device::mojom::blink::SerialParityBit::EVEN;
} else if (options->parity() == "odd") {
mojo_options->parity_bit = device::mojom::blink::SerialParityBit::ODD;
} else {
NOTREACHED();
}
switch (options->stopBits()) {
case 1:
mojo_options->stop_bits = device::mojom::blink::SerialStopBits::ONE;
break;
case 2:
mojo_options->stop_bits = device::mojom::blink::SerialStopBits::TWO;
break;
default:
exception_state.ThrowTypeError(
"Requested number of stop bits must be 1 or 2.");
return ScriptPromise();
}
if (options->bufferSize() == 0) {
exception_state.ThrowTypeError(String::Format(
"Requested buffer size (%d bytes) must be greater than zero.",
options->bufferSize()));
return ScriptPromise();
}
if (options->bufferSize() > kMaxBufferSize) {
exception_state.ThrowTypeError(
String::Format("Requested buffer size (%d bytes) is greater than "
"the maximum allowed (%d bytes).",
options->bufferSize(), kMaxBufferSize));
return ScriptPromise();
}
buffer_size_ = options->bufferSize();
mojo_options->has_cts_flow_control = true;
mojo_options->cts_flow_control = options->flowControl() == "hardware";
mojo::PendingRemote<device::mojom::blink::SerialPortClient> client;
open_resolver_ = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
auto callback = WTF::Bind(&SerialPort::OnOpen, WrapPersistent(this),
client.InitWithNewPipeAndPassReceiver());
parent_->OpenPort(info_->token, std::move(mojo_options), std::move(client),
std::move(callback));
return open_resolver_->Promise();
}
ReadableStream* SerialPort::readable(ScriptState* script_state,
ExceptionState& exception_state) {
if (readable_)
return readable_;
if (!port_.is_bound() || open_resolver_ || closing_ || read_fatal_)
return nullptr;
mojo::ScopedDataPipeProducerHandle producer;
mojo::ScopedDataPipeConsumerHandle consumer;
if (!CreateDataPipe(&producer, &consumer)) {
exception_state.ThrowDOMException(DOMExceptionCode::kQuotaExceededError,
kResourcesExhaustedReadBuffer);
return nullptr;
}
port_->StartReading(std::move(producer));
DCHECK(!underlying_source_);
underlying_source_ = MakeGarbageCollected<SerialPortUnderlyingSource>(
script_state, this, std::move(consumer));
// Ideally the stream would report the number of bytes that can be read from
// the underlying Mojo data pipe. As an approximation the high water mark is
// set to 0 so that data remains in the pipe rather than being queued in the
// stream and thus adding an extra layer of buffering.
readable_ = ReadableStream::CreateWithCountQueueingStrategy(
script_state, underlying_source_, /*high_water_mark=*/0);
return readable_;
}
WritableStream* SerialPort::writable(ScriptState* script_state,
ExceptionState& exception_state) {
if (writable_)
return writable_;
if (!port_.is_bound() || open_resolver_ || closing_ || write_fatal_)
return nullptr;
mojo::ScopedDataPipeProducerHandle producer;
mojo::ScopedDataPipeConsumerHandle consumer;
if (!CreateDataPipe(&producer, &consumer)) {
exception_state.ThrowDOMException(DOMExceptionCode::kQuotaExceededError,
kResourcesExhaustedWriteBuffer);
return nullptr;
}
port_->StartWriting(std::move(consumer));
DCHECK(!underlying_sink_);
underlying_sink_ =
MakeGarbageCollected<SerialPortUnderlyingSink>(this, std::move(producer));
// Ideally the stream would report the number of bytes that could be written
// to the underlying Mojo data pipe. As an approximation the high water mark
// is set to 1 so that the stream appears ready but producers observing
// backpressure won't queue additional chunks in the stream and thus add an
// extra layer of buffering.
writable_ = WritableStream::CreateWithCountQueueingStrategy(
script_state, underlying_sink_, /*high_water_mark=*/1);
return writable_;
}
ScriptPromise SerialPort::getSignals(ScriptState* script_state,
ExceptionState& exception_state) {
if (!port_.is_bound()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kPortClosed);
return ScriptPromise();
}
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
signal_resolvers_.insert(resolver);
port_->GetControlSignals(WTF::Bind(&SerialPort::OnGetSignals,
WrapPersistent(this),
WrapPersistent(resolver)));
return resolver->Promise();
}
ScriptPromise SerialPort::setSignals(ScriptState* script_state,
const SerialOutputSignals* signals,
ExceptionState& exception_state) {
if (!port_.is_bound()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kPortClosed);
return ScriptPromise();
}
if (!signals->hasDataTerminalReady() && !signals->hasRequestToSend() &&
!signals->hasBrk()) {
exception_state.ThrowTypeError(kNoSignals);
return ScriptPromise();
}
auto mojo_signals = device::mojom::blink::SerialHostControlSignals::New();
if (signals->hasDataTerminalReady()) {
mojo_signals->has_dtr = true;
mojo_signals->dtr = signals->dataTerminalReady();
}
if (signals->hasRequestToSend()) {
mojo_signals->has_rts = true;
mojo_signals->rts = signals->requestToSend();
}
if (signals->hasBrk()) {
mojo_signals->has_brk = true;
mojo_signals->brk = signals->brk();
}
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
signal_resolvers_.insert(resolver);
port_->SetControlSignals(
std::move(mojo_signals),
WTF::Bind(&SerialPort::OnSetSignals, WrapPersistent(this),
WrapPersistent(resolver)));
return resolver->Promise();
}
ScriptPromise SerialPort::close(ScriptState* script_state,
ExceptionState& exception_state) {
if (!port_.is_bound()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
"The port is already closed.");
return ScriptPromise();
}
if (closing_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"A call to close() is already in progress.");
return ScriptPromise();
}
closing_ = true;
HeapVector<ScriptPromise> promises;
if (readable_) {
promises.push_back(readable_->cancel(script_state, exception_state));
if (exception_state.HadException()) {
closing_ = false;
return ScriptPromise();
}
}
if (writable_) {
auto* reason = MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError, kPortClosed);
promises.push_back(writable_->abort(script_state,
ScriptValue::From(script_state, reason),
exception_state));
if (exception_state.HadException()) {
closing_ = false;
return ScriptPromise();
}
}
return ScriptPromise::All(script_state, promises)
.Then(ContinueCloseFunction::Create(script_state, this),
AbortCloseFunction::Create(script_state, this));
}
ScriptPromise SerialPort::ContinueClose(ScriptState* script_state) {
DCHECK(closing_);
DCHECK(!close_resolver_);
if (!port_.is_bound())
return ScriptPromise::CastUndefined(script_state);
close_resolver_ = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
// readable.cancel() and writable.abort() can resolve before |readable_| or
// |writable_| are set to null if the streams were already erroring. Wait
// until UnderlyingSourceClosed() and UnderlyingSinkClosed() have been called
// before continuing.
if (!readable_ && !writable_) {
StreamsClosed();
}
return close_resolver_->Promise();
}
void SerialPort::AbortClose() {
DCHECK(closing_);
closing_ = false;
}
void SerialPort::StreamsClosed() {
DCHECK(!readable_);
DCHECK(!writable_);
port_->Close(WTF::Bind(&SerialPort::OnClose, WrapPersistent(this)));
}
void SerialPort::Flush(
device::mojom::blink::SerialPortFlushMode mode,
device::mojom::blink::SerialPort::FlushCallback callback) {
DCHECK(port_.is_bound());
port_->Flush(mode, std::move(callback));
}
void SerialPort::Drain(
device::mojom::blink::SerialPort::DrainCallback callback) {
DCHECK(port_.is_bound());
port_->Drain(std::move(callback));
}
void SerialPort::UnderlyingSourceClosed() {
DCHECK(readable_);
readable_ = nullptr;
underlying_source_ = nullptr;
if (close_resolver_ && !writable_) {
StreamsClosed();
}
}
void SerialPort::UnderlyingSinkClosed() {
DCHECK(writable_);
writable_ = nullptr;
underlying_sink_ = nullptr;
if (close_resolver_ && !readable_) {
StreamsClosed();
}
}
void SerialPort::ContextDestroyed() {
// Release connection-related resources as quickly as possible.
port_.reset();
}
void SerialPort::Trace(Visitor* visitor) const {
visitor->Trace(parent_);
visitor->Trace(port_);
visitor->Trace(client_receiver_);
visitor->Trace(readable_);
visitor->Trace(underlying_source_);
visitor->Trace(writable_);
visitor->Trace(underlying_sink_);
visitor->Trace(open_resolver_);
visitor->Trace(signal_resolvers_);
visitor->Trace(close_resolver_);
ScriptWrappable::Trace(visitor);
}
bool SerialPort::HasPendingActivity() const {
// There is no need to check if the execution context has been destroyed, this
// is handled by the common tracing logic.
//
// This object should be considered active as long as it is open so that any
// chain of streams originating from this port are not closed prematurely.
return port_.is_bound();
}
ExecutionContext* SerialPort::GetExecutionContext() const {
return parent_->GetExecutionContext();
}
const AtomicString& SerialPort::InterfaceName() const {
return event_target_names::kSerialPort;
}
DispatchEventResult SerialPort::DispatchEventInternal(Event& event) {
event.SetTarget(this);
// Events fired on a SerialPort instance bubble to the parent Serial instance.
event.SetEventPhase(Event::kCapturingPhase);
event.SetCurrentTarget(parent_);
parent_->FireEventListeners(event);
if (event.PropagationStopped())
goto doneDispatching;
event.SetEventPhase(Event::kAtTarget);
event.SetCurrentTarget(this);
FireEventListeners(event);
if (event.PropagationStopped() || !event.bubbles())
goto doneDispatching;
event.SetEventPhase(Event::kBubblingPhase);
event.SetCurrentTarget(parent_);
parent_->FireEventListeners(event);
doneDispatching:
event.SetCurrentTarget(nullptr);
event.SetEventPhase(Event::kNone);
return EventTarget::GetDispatchEventResult(event);
}
void SerialPort::OnReadError(device::mojom::blink::SerialReceiveError error) {
if (ReceiveErrorIsFatal(error))
read_fatal_ = true;
if (underlying_source_)
underlying_source_->SignalErrorOnClose(DOMExceptionFromReceiveError(error));
}
void SerialPort::OnSendError(device::mojom::blink::SerialSendError error) {
if (SendErrorIsFatal(error))
write_fatal_ = true;
if (underlying_sink_)
underlying_sink_->SignalErrorOnClose(DOMExceptionFromSendError(error));
}
bool SerialPort::CreateDataPipe(mojo::ScopedDataPipeProducerHandle* producer,
mojo::ScopedDataPipeConsumerHandle* consumer) {
MojoCreateDataPipeOptions options;
options.struct_size = sizeof(MojoCreateDataPipeOptions);
options.flags = MOJO_CREATE_DATA_PIPE_FLAG_NONE;
options.element_num_bytes = 1;
options.capacity_num_bytes = buffer_size_;
MojoResult result = mojo::CreateDataPipe(&options, *producer, *consumer);
if (result == MOJO_RESULT_OK)
return true;
DCHECK_EQ(result, MOJO_RESULT_RESOURCE_EXHAUSTED);
return false;
}
void SerialPort::OnConnectionError() {
closing_ = false;
read_fatal_ = false;
write_fatal_ = false;
port_.reset();
client_receiver_.reset();
// Move fields since rejecting a Promise can execute script.
ScriptPromiseResolver* open_resolver = open_resolver_;
open_resolver_ = nullptr;
HeapHashSet<Member<ScriptPromiseResolver>> signal_resolvers;
signal_resolvers_.swap(signal_resolvers);
SerialPortUnderlyingSource* underlying_source = underlying_source_;
underlying_source_ = nullptr;
SerialPortUnderlyingSink* underlying_sink = underlying_sink_;
underlying_sink_ = nullptr;
ScriptPromiseResolver* close_resolver = close_resolver_;
close_resolver_ = nullptr;
if (open_resolver) {
open_resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNetworkError, kOpenError));
}
for (ScriptPromiseResolver* resolver : signal_resolvers) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNetworkError, kDeviceLostError));
}
if (underlying_source) {
underlying_source->SignalErrorOnClose(
DOMExceptionFromReceiveError(SerialReceiveError::DISCONNECTED));
}
if (underlying_sink) {
underlying_sink->SignalErrorOnClose(
DOMExceptionFromSendError(SerialSendError::DISCONNECTED));
}
if (close_resolver)
close_resolver->Resolve();
}
void SerialPort::OnOpen(
mojo::PendingReceiver<device::mojom::blink::SerialPortClient>
client_receiver,
mojo::PendingRemote<device::mojom::blink::SerialPort> port) {
ScriptState* script_state = open_resolver_->GetScriptState();
if (!script_state->ContextIsValid())
return;
if (!port) {
ScriptPromiseResolver* resolver = open_resolver_;
open_resolver_ = nullptr;
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNetworkError, kOpenError));
return;
}
port_.Bind(std::move(port),
GetExecutionContext()->GetTaskRunner(TaskType::kMiscPlatformAPI));
port_.set_disconnect_handler(
WTF::Bind(&SerialPort::OnConnectionError, WrapWeakPersistent(this)));
client_receiver_.Bind(
std::move(client_receiver),
GetExecutionContext()->GetTaskRunner(TaskType::kMiscPlatformAPI));
open_resolver_->Resolve();
open_resolver_ = nullptr;
}
void SerialPort::OnGetSignals(
ScriptPromiseResolver* resolver,
device::mojom::blink::SerialPortControlSignalsPtr mojo_signals) {
DCHECK(signal_resolvers_.Contains(resolver));
signal_resolvers_.erase(resolver);
if (!mojo_signals) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNetworkError, "Failed to get control signals."));
return;
}
auto* signals = MakeGarbageCollected<SerialInputSignals>();
signals->setDataCarrierDetect(mojo_signals->dcd);
signals->setClearToSend(mojo_signals->cts);
signals->setRingIndicator(mojo_signals->ri);
signals->setDataSetReady(mojo_signals->dsr);
resolver->Resolve(signals);
}
void SerialPort::OnSetSignals(ScriptPromiseResolver* resolver, bool success) {
DCHECK(signal_resolvers_.Contains(resolver));
signal_resolvers_.erase(resolver);
if (!success) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNetworkError, "Failed to set control signals."));
return;
}
resolver->Resolve();
}
void SerialPort::OnClose() {
DCHECK(close_resolver_);
closing_ = false;
read_fatal_ = false;
write_fatal_ = false;
port_.reset();
client_receiver_.reset();
ScriptPromiseResolver* close_resolver = close_resolver_;
close_resolver_ = nullptr;
close_resolver->Resolve();
}
} // namespace blink