chromium/services/device/serial/serial_io_handler_win.cc

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "services/device/serial/serial_io_handler_win.h"

#include <windows.h>

#include <memory>
#include <utility>

#include "base/functional/bind.h"
#include "base/sequence_checker.h"
#include "base/task/current_thread.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "components/device_event_log/device_event_log.h"

namespace device {

namespace {

int BitrateToSpeedConstant(int bitrate) {
#define BITRATE_TO_SPEED_CASE(x) \
  case x:                        \
    return CBR_##x;
  switch (bitrate) {
    BITRATE_TO_SPEED_CASE(110);
    BITRATE_TO_SPEED_CASE(300);
    BITRATE_TO_SPEED_CASE(600);
    BITRATE_TO_SPEED_CASE(1200);
    BITRATE_TO_SPEED_CASE(2400);
    BITRATE_TO_SPEED_CASE(4800);
    BITRATE_TO_SPEED_CASE(9600);
    BITRATE_TO_SPEED_CASE(14400);
    BITRATE_TO_SPEED_CASE(19200);
    BITRATE_TO_SPEED_CASE(38400);
    BITRATE_TO_SPEED_CASE(57600);
    BITRATE_TO_SPEED_CASE(115200);
    BITRATE_TO_SPEED_CASE(128000);
    BITRATE_TO_SPEED_CASE(256000);
    default:
      // If the bitrate doesn't match that of one of the standard
      // index constants, it may be provided as-is to the DCB
      // structure, according to MSDN.
      return bitrate;
  }
#undef BITRATE_TO_SPEED_CASE
}

int DataBitsEnumToConstant(mojom::SerialDataBits data_bits) {
  switch (data_bits) {
    case mojom::SerialDataBits::SEVEN:
      return 7;
    case mojom::SerialDataBits::EIGHT:
    default:
      return 8;
  }
}

int ParityBitEnumToConstant(mojom::SerialParityBit parity_bit) {
  switch (parity_bit) {
    case mojom::SerialParityBit::EVEN:
      return EVENPARITY;
    case mojom::SerialParityBit::ODD:
      return ODDPARITY;
    case mojom::SerialParityBit::NO_PARITY:
    default:
      return NOPARITY;
  }
}

int StopBitsEnumToConstant(mojom::SerialStopBits stop_bits) {
  switch (stop_bits) {
    case mojom::SerialStopBits::TWO:
      return TWOSTOPBITS;
    case mojom::SerialStopBits::ONE:
    default:
      return ONESTOPBIT;
  }
}

int SpeedConstantToBitrate(int speed) {
#define SPEED_TO_BITRATE_CASE(x) \
  case CBR_##x:                  \
    return x;
  switch (speed) {
    SPEED_TO_BITRATE_CASE(110);
    SPEED_TO_BITRATE_CASE(300);
    SPEED_TO_BITRATE_CASE(600);
    SPEED_TO_BITRATE_CASE(1200);
    SPEED_TO_BITRATE_CASE(2400);
    SPEED_TO_BITRATE_CASE(4800);
    SPEED_TO_BITRATE_CASE(9600);
    SPEED_TO_BITRATE_CASE(14400);
    SPEED_TO_BITRATE_CASE(19200);
    SPEED_TO_BITRATE_CASE(38400);
    SPEED_TO_BITRATE_CASE(57600);
    SPEED_TO_BITRATE_CASE(115200);
    SPEED_TO_BITRATE_CASE(128000);
    SPEED_TO_BITRATE_CASE(256000);
    default:
      // If it's not one of the standard index constants,
      // it should be an integral baud rate, according to
      // MSDN.
      return speed;
  }
#undef SPEED_TO_BITRATE_CASE
}

mojom::SerialDataBits DataBitsConstantToEnum(int data_bits) {
  switch (data_bits) {
    case 7:
      return mojom::SerialDataBits::SEVEN;
    case 8:
    default:
      return mojom::SerialDataBits::EIGHT;
  }
}

mojom::SerialParityBit ParityBitConstantToEnum(int parity_bit) {
  switch (parity_bit) {
    case EVENPARITY:
      return mojom::SerialParityBit::EVEN;
    case ODDPARITY:
      return mojom::SerialParityBit::ODD;
    case NOPARITY:
    default:
      return mojom::SerialParityBit::NO_PARITY;
  }
}

mojom::SerialStopBits StopBitsConstantToEnum(int stop_bits) {
  switch (stop_bits) {
    case TWOSTOPBITS:
      return mojom::SerialStopBits::TWO;
    case ONESTOPBIT:
    default:
      return mojom::SerialStopBits::ONE;
  }
}

}  // namespace

// static
scoped_refptr<SerialIoHandler> SerialIoHandler::Create(
    const base::FilePath& port,
    scoped_refptr<base::SingleThreadTaskRunner> ui_thread_task_runner) {
  return new SerialIoHandlerWin(port, std::move(ui_thread_task_runner));
}

bool SerialIoHandlerWin::PostOpen() {
  DCHECK(!read_context_);
  DCHECK(!write_context_);

  base::CurrentIOThread::Get()->RegisterIOHandler(file().GetPlatformFile(),
                                                  this);

  read_context_ = std::make_unique<base::MessagePumpForIO::IOContext>();
  write_context_ = std::make_unique<base::MessagePumpForIO::IOContext>();

  // Based on the MSDN documentation setting both ReadIntervalTimeout and
  // ReadTotalTimeoutMultiplier to MAXDWORD should cause ReadFile() to return
  // immediately if there is data in the buffer or when a byte arrives while
  // waiting.
  //
  // ReadTotalTimeoutConstant is set to a value low enough to ensure that the
  // timeout case is exercised frequently but high enough to avoid unnecessary
  // wakeups as it is not possible to have ReadFile() return immediately when a
  // byte is received without specifying a timeout.
  //
  // https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-commtimeouts#remarks
  COMMTIMEOUTS timeouts = {0};
  timeouts.ReadIntervalTimeout = MAXDWORD;
  timeouts.ReadTotalTimeoutMultiplier = MAXDWORD;
  timeouts.ReadTotalTimeoutConstant = base::Minutes(5).InMilliseconds();
  if (!::SetCommTimeouts(file().GetPlatformFile(), &timeouts)) {
    SERIAL_PLOG(DEBUG) << "Failed to set serial timeouts";
    return false;
  }

  return true;
}

void SerialIoHandlerWin::ReadImpl() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(IsReadPending());

  ClearPendingError();
  if (!IsReadPending())
    return;

  if (!ReadFile(file().GetPlatformFile(), pending_read_buffer().data(),
                pending_read_buffer().size(), nullptr,
                &read_context_->overlapped) &&
      GetLastError() != ERROR_IO_PENDING) {
    OnIOCompleted(read_context_.get(), 0, GetLastError());
  }
}

void SerialIoHandlerWin::WriteImpl() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(IsWritePending());

  if (!WriteFile(file().GetPlatformFile(), pending_write_buffer().data(),
                 pending_write_buffer().size(), nullptr,
                 &write_context_->overlapped) &&
      GetLastError() != ERROR_IO_PENDING) {
    OnIOCompleted(write_context_.get(), 0, GetLastError());
  }
}

void SerialIoHandlerWin::CancelReadImpl() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(file().IsValid());

  if (!PurgeComm(file().GetPlatformFile(), PURGE_RXABORT))
    SERIAL_PLOG(DEBUG) << "RX abort failed";
}

void SerialIoHandlerWin::CancelWriteImpl() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(file().IsValid());
  if (!PurgeComm(file().GetPlatformFile(), PURGE_TXABORT))
    SERIAL_PLOG(DEBUG) << "TX abort failed";
}

bool SerialIoHandlerWin::ConfigurePortImpl() {
  DCB config = {0};
  config.DCBlength = sizeof(config);
  if (!GetCommState(file().GetPlatformFile(), &config)) {
    SERIAL_PLOG(DEBUG) << "Failed to get serial port info";
    return false;
  }

  // Set up some sane default options that are not configurable.
  config.fBinary = TRUE;
  config.fParity = TRUE;
  config.fAbortOnError = FALSE;
  config.fOutxDsrFlow = FALSE;
  config.fDtrControl = DTR_CONTROL_ENABLE;
  config.fDsrSensitivity = FALSE;
  config.fOutX = FALSE;
  config.fInX = FALSE;

  DCHECK(options().bitrate);
  config.BaudRate = BitrateToSpeedConstant(options().bitrate);

  DCHECK(options().data_bits != mojom::SerialDataBits::NONE);
  config.ByteSize = DataBitsEnumToConstant(options().data_bits);

  DCHECK(options().parity_bit != mojom::SerialParityBit::NONE);
  config.Parity = ParityBitEnumToConstant(options().parity_bit);

  DCHECK(options().stop_bits != mojom::SerialStopBits::NONE);
  config.StopBits = StopBitsEnumToConstant(options().stop_bits);

  DCHECK(options().has_cts_flow_control);
  if (options().cts_flow_control) {
    config.fOutxCtsFlow = TRUE;
    config.fRtsControl = RTS_CONTROL_HANDSHAKE;
  } else {
    config.fOutxCtsFlow = FALSE;
    config.fRtsControl = RTS_CONTROL_ENABLE;
  }

  if (!SetCommState(file().GetPlatformFile(), &config)) {
    SERIAL_PLOG(DEBUG) << "Failed to set serial port info";
    return false;
  }
  return true;
}

SerialIoHandlerWin::SerialIoHandlerWin(
    const base::FilePath& port,
    scoped_refptr<base::SingleThreadTaskRunner> ui_thread_task_runner)
    : SerialIoHandler(port, std::move(ui_thread_task_runner)),
      base::MessagePumpForIO::IOHandler(FROM_HERE) {}

SerialIoHandlerWin::~SerialIoHandlerWin() = default;

void SerialIoHandlerWin::OnIOCompleted(
    base::MessagePumpForIO::IOContext* context,
    DWORD bytes_transferred,
    DWORD error) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (context == read_context_.get()) {
    if (read_canceled()) {
      ReadCompleted(bytes_transferred, read_cancel_reason());
    } else if (error == ERROR_SUCCESS || error == ERROR_OPERATION_ABORTED) {
      ReadCompleted(bytes_transferred, mojom::SerialReceiveError::NONE);
    } else if (error == ERROR_ACCESS_DENIED || error == ERROR_BAD_COMMAND ||
               error == ERROR_DEVICE_REMOVED) {
      ReadCompleted(0, mojom::SerialReceiveError::DEVICE_LOST);
    } else {
      SERIAL_LOG(DEBUG) << "Read failed: "
                        << logging::SystemErrorCodeToString(error);
      ReadCompleted(0, mojom::SerialReceiveError::SYSTEM_ERROR);
    }
  } else if (context == write_context_.get()) {
    DCHECK(IsWritePending());
    if (write_canceled()) {
      WriteCompleted(0, write_cancel_reason());
    } else if (error == ERROR_SUCCESS || error == ERROR_OPERATION_ABORTED) {
      WriteCompleted(bytes_transferred, mojom::SerialSendError::NONE);
    } else if (error == ERROR_GEN_FAILURE) {
      WriteCompleted(0, mojom::SerialSendError::DISCONNECTED);
    } else {
      SERIAL_LOG(DEBUG) << "Write failed: "
                        << logging::SystemErrorCodeToString(error);
      if (error == ERROR_GEN_FAILURE && IsReadPending()) {
        // For devices using drivers such as FTDI, CP2xxx, when device is
        // disconnected, the context is |read_context_| and the error is
        // ERROR_OPERATION_ABORTED.
        // However, for devices using CDC-ACM driver, when device is
        // disconnected, the context is |write_context_| and the error is
        // ERROR_GEN_FAILURE. In this situation, in addition to a write error
        // signal, also need to generate a read error signal
        // mojom::SerialOnReceiveError which will notify the app about the
        // disconnection.
        CancelRead(mojom::SerialReceiveError::SYSTEM_ERROR);
      }
      WriteCompleted(0, mojom::SerialSendError::SYSTEM_ERROR);
    }
  } else {
    NOTREACHED_IN_MIGRATION() << "Invalid IOContext";
  }
}

void SerialIoHandlerWin::ClearPendingError() {
  DWORD errors;
  if (!ClearCommError(file().GetPlatformFile(), &errors, nullptr)) {
    SERIAL_PLOG(DEBUG) << "Failed to clear communication error";
    return;
  }

  if (errors & CE_BREAK) {
    ReadCompleted(0, mojom::SerialReceiveError::BREAK);
  } else if (errors & CE_FRAME) {
    ReadCompleted(0, mojom::SerialReceiveError::FRAME_ERROR);
  } else if (errors & CE_OVERRUN) {
    ReadCompleted(0, mojom::SerialReceiveError::OVERRUN);
  } else if (errors & CE_RXOVER) {
    ReadCompleted(0, mojom::SerialReceiveError::BUFFER_OVERFLOW);
  } else if (errors & CE_RXPARITY) {
    ReadCompleted(0, mojom::SerialReceiveError::PARITY_ERROR);
  } else if (errors != 0) {
    NOTIMPLEMENTED() << "Unexpected communication error: " << std::hex
                     << errors;
    ReadCompleted(0, mojom::SerialReceiveError::SYSTEM_ERROR);
  }
}

void SerialIoHandlerWin::Flush(mojom::SerialPortFlushMode mode) const {
  DWORD flags;
  switch (mode) {
    case mojom::SerialPortFlushMode::kReceiveAndTransmit:
      flags = PURGE_RXCLEAR | PURGE_TXCLEAR;
      break;
    case mojom::SerialPortFlushMode::kReceive:
      flags = PURGE_RXCLEAR;
      break;
    case mojom::SerialPortFlushMode::kTransmit:
      flags = PURGE_TXCLEAR;
      break;
  }

  if (!PurgeComm(file().GetPlatformFile(), flags))
    SERIAL_PLOG(DEBUG) << "Failed to flush serial port";
}

void SerialIoHandlerWin::Drain() {
  if (!FlushFileBuffers(file().GetPlatformFile()))
    SERIAL_PLOG(DEBUG) << "Failed to drain serial port";
}

mojom::SerialPortControlSignalsPtr SerialIoHandlerWin::GetControlSignals()
    const {
  DWORD status;
  if (!GetCommModemStatus(file().GetPlatformFile(), &status)) {
    SERIAL_PLOG(DEBUG) << "Failed to get port control signals";
    return mojom::SerialPortControlSignalsPtr();
  }

  auto signals = mojom::SerialPortControlSignals::New();
  signals->dcd = (status & MS_RLSD_ON) != 0;
  signals->cts = (status & MS_CTS_ON) != 0;
  signals->dsr = (status & MS_DSR_ON) != 0;
  signals->ri = (status & MS_RING_ON) != 0;
  return signals;
}

bool SerialIoHandlerWin::SetControlSignals(
    const mojom::SerialHostControlSignals& signals) {
  if (signals.has_dtr && !EscapeCommFunction(file().GetPlatformFile(),
                                             signals.dtr ? SETDTR : CLRDTR)) {
    SERIAL_PLOG(DEBUG) << "Failed to configure data-terminal-ready signal";
    return false;
  }
  if (signals.has_rts && !EscapeCommFunction(file().GetPlatformFile(),
                                             signals.rts ? SETRTS : CLRRTS)) {
    SERIAL_PLOG(DEBUG) << "Failed to configure request-to-send signal";
    return false;
  }
  if (signals.has_brk &&
      !EscapeCommFunction(file().GetPlatformFile(),
                          signals.brk ? SETBREAK : CLRBREAK)) {
    SERIAL_PLOG(DEBUG) << "Failed to configure break signal";
    return false;
  }

  return true;
}

mojom::SerialConnectionInfoPtr SerialIoHandlerWin::GetPortInfo() const {
  DCB config = {0};
  config.DCBlength = sizeof(config);
  if (!GetCommState(file().GetPlatformFile(), &config)) {
    SERIAL_PLOG(DEBUG) << "Failed to get serial port info";
    return mojom::SerialConnectionInfoPtr();
  }
  auto info = mojom::SerialConnectionInfo::New();
  info->bitrate = SpeedConstantToBitrate(config.BaudRate);
  info->data_bits = DataBitsConstantToEnum(config.ByteSize);
  info->parity_bit = ParityBitConstantToEnum(config.Parity);
  info->stop_bits = StopBitsConstantToEnum(config.StopBits);
  info->cts_flow_control = config.fOutxCtsFlow != 0;
  return info;
}

}  // namespace device