#include "tty_protocol_writer.h" #include #include #ifdef _WIN32 TtyProtocolWriter::TtyProtocolWriter(std::string path) : path_(std::move(path)) { throw std::runtime_error("tty output is supported only on Linux/POSIX"); } TtyProtocolWriter::~TtyProtocolWriter() = default; TtyProtocolWriter::TtyProtocolWriter(TtyProtocolWriter&& other) noexcept = default; TtyProtocolWriter& TtyProtocolWriter::operator=(TtyProtocolWriter&& other) noexcept = default; void TtyProtocolWriter::emit_packet_start() const {} void TtyProtocolWriter::emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg) const { (void) index; (void) ch1_avg; (void) ch2_avg; } const std::string& TtyProtocolWriter::path() const { return path_; } void TtyProtocolWriter::write_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) const { (void) word0; (void) word1; (void) word2; (void) word3; } void TtyProtocolWriter::close_fd() noexcept {} #else #include #include #include #include #include #include #include #include #include #include #include #include namespace { std::string io_error(const std::string& action, const std::string& path) { std::ostringstream out; out << action << " '" << path << "': " << std::strerror(errno); return out.str(); } void set_fd_raw(int fd) { struct termios tio {}; if (::tcgetattr(fd, &tio) != 0) { throw std::runtime_error(io_error("Cannot read tty attributes for", std::to_string(fd))); } ::cfmakeraw(&tio); if (::tcsetattr(fd, TCSANOW, &tio) != 0) { throw std::runtime_error(io_error("Cannot apply raw tty attributes to", std::to_string(fd))); } } bool is_character_device_path(const std::string& path) { struct stat st {}; if (::stat(path.c_str(), &st) != 0) { if (errno == ENOENT) { return false; } throw std::runtime_error(io_error("Cannot stat tty output", path)); } return S_ISCHR(st.st_mode); } std::optional read_link_target(const std::string& path) { std::array buf {}; const ssize_t len = ::readlink(path.c_str(), buf.data(), buf.size() - 1U); if (len < 0) { if (errno == EINVAL || errno == ENOENT) { return std::nullopt; } throw std::runtime_error(io_error("Cannot read symlink", path)); } buf[static_cast(len)] = '\0'; return std::string(buf.data()); } } // namespace TtyProtocolWriter::TtyProtocolWriter(std::string path) : path_(std::move(path)) { if (is_character_device_path(path_)) { fd_ = ::open(path_.c_str(), O_WRONLY | O_NOCTTY); if (fd_ < 0) { throw std::runtime_error(io_error("Cannot open tty output", path_)); } return; } std::array slave_name {}; if (::openpty(&fd_, &slave_fd_, slave_name.data(), nullptr, nullptr) != 0) { throw std::runtime_error(io_error("Cannot create PTY bridge for", path_)); } try { slave_path_ = slave_name.data(); set_fd_raw(slave_fd_); struct stat st {}; if (::lstat(path_.c_str(), &st) == 0) { if (!S_ISLNK(st.st_mode) && !S_ISREG(st.st_mode)) { throw std::runtime_error("Refusing to replace non-link path '" + path_ + "'"); } if (::unlink(path_.c_str()) != 0) { throw std::runtime_error(io_error("Cannot remove existing tty link", path_)); } } else if (errno != ENOENT) { throw std::runtime_error(io_error("Cannot inspect tty link path", path_)); } if (::symlink(slave_path_.c_str(), path_.c_str()) != 0) { throw std::runtime_error(io_error("Cannot create tty symlink", path_)); } } catch (...) { close_slave_fd(); close_fd(); throw; } owns_link_ = true; } TtyProtocolWriter::~TtyProtocolWriter() { remove_owned_link(); close_slave_fd(); close_fd(); } TtyProtocolWriter::TtyProtocolWriter(TtyProtocolWriter&& other) noexcept : path_(std::move(other.path_)), fd_(other.fd_), slave_fd_(other.slave_fd_), slave_path_(std::move(other.slave_path_)), owns_link_(other.owns_link_) { other.fd_ = -1; other.slave_fd_ = -1; other.owns_link_ = false; } TtyProtocolWriter& TtyProtocolWriter::operator=(TtyProtocolWriter&& other) noexcept { if (this != &other) { remove_owned_link(); close_slave_fd(); close_fd(); path_ = std::move(other.path_); fd_ = other.fd_; slave_fd_ = other.slave_fd_; slave_path_ = std::move(other.slave_path_); owns_link_ = other.owns_link_; other.fd_ = -1; other.slave_fd_ = -1; other.owns_link_ = false; } return *this; } void TtyProtocolWriter::emit_packet_start() const { write_frame(0x000A, 0xFFFF, 0xFFFF, 0xFFFF); } void TtyProtocolWriter::emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg) const { write_frame(0x000A, index, static_cast(ch1_avg), static_cast(ch2_avg)); } const std::string& TtyProtocolWriter::path() const { return path_; } void TtyProtocolWriter::write_frame(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) const { const uint16_t frame[4] = {word0, word1, word2, word3}; const std::uint8_t* bytes = reinterpret_cast(frame); std::size_t remaining = sizeof(frame); while (remaining != 0U) { const ssize_t written = ::write(fd_, bytes, remaining); if (written < 0) { if (errno == EINTR) { continue; } throw std::runtime_error(io_error("Cannot write tty frame to", path_)); } if (written == 0) { throw std::runtime_error("tty write returned 0 bytes for '" + path_ + "'"); } bytes += static_cast(written); remaining -= static_cast(written); } } void TtyProtocolWriter::close_fd() noexcept { if (fd_ >= 0) { ::close(fd_); fd_ = -1; } } void TtyProtocolWriter::close_slave_fd() noexcept { if (slave_fd_ >= 0) { ::close(slave_fd_); slave_fd_ = -1; } } void TtyProtocolWriter::remove_owned_link() noexcept { if (!owns_link_ || path_.empty()) { return; } try { const auto target = read_link_target(path_); if (target && (*target == slave_path_)) { ::unlink(path_.c_str()); } } catch (...) { } owns_link_ = false; } #endif