| // Copyright 2024 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 "quiche/quic/tools/interactive_cli.h" |
| |
| #include <termios.h> |
| #include <unistd.h> |
| |
| #include <algorithm> |
| #include <cerrno> |
| #include <cstring> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/status/status.h" |
| #include "absl/strings/ascii.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/str_split.h" |
| #include "absl/strings/string_view.h" |
| #include "quiche/quic/core/io/quic_event_loop.h" |
| #include "quiche/quic/core/io/socket.h" |
| #include "quiche/common/platform/api/quiche_logging.h" |
| #include "quiche/common/quiche_callbacks.h" |
| |
| namespace quic { |
| namespace { |
| // Writes into stdout. |
| void Write(absl::string_view data) { |
| int written = write(STDOUT_FILENO, data.data(), data.size()); |
| QUICHE_DCHECK_EQ(written, data.size()); |
| } |
| } // namespace |
| |
| InteractiveCli::InteractiveCli(QuicEventLoop* event_loop, |
| LineCallback line_callback) |
| : event_loop_(event_loop), line_callback_(std::move(line_callback)) { |
| if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) { |
| QUICHE_LOG(FATAL) << "Both stdin and stdout must be a TTY"; |
| } |
| |
| [[maybe_unused]] bool success = |
| event_loop_->RegisterSocket(STDIN_FILENO, kSocketEventReadable, this); |
| QUICHE_LOG_IF(FATAL, !success) |
| << "Failed to register stdin with the event loop"; |
| |
| // Store old termios so that we can recover it when exiting. |
| ::termios config; |
| tcgetattr(STDIN_FILENO, &config); |
| old_termios_ = std::make_unique<char[]>(sizeof(config)); |
| memcpy(old_termios_.get(), &config, sizeof(config)); |
| |
| // Disable input buffering on the terminal. |
| config.c_lflag &= ~(ICANON | ECHO | ECHONL); |
| config.c_cc[VMIN] = 0; |
| config.c_cc[VTIME] = 0; |
| tcsetattr(STDIN_FILENO, TCSANOW, &config); |
| |
| RestoreCurrentInputLine(); |
| } |
| |
| InteractiveCli::~InteractiveCli() { |
| if (old_termios_ != nullptr) { |
| tcsetattr(STDIN_FILENO, TCSANOW, |
| reinterpret_cast<termios*>(old_termios_.get())); |
| } |
| [[maybe_unused]] bool success = event_loop_->UnregisterSocket(STDIN_FILENO); |
| QUICHE_LOG_IF(ERROR, !success) << "Failed to unregister stdin"; |
| } |
| |
| void InteractiveCli::ResetLine() { |
| constexpr absl::string_view kReset = "\033[G\033[K"; |
| Write(kReset); |
| } |
| |
| void InteractiveCli::RestoreCurrentInputLine() { |
| Write(absl::StrCat(prompt_, current_input_line_)); |
| } |
| |
| void InteractiveCli::PrintLine(absl::string_view line) { |
| ResetLine(); |
| Write(absl::StrCat("\n\033[1A", absl::StripTrailingAsciiWhitespace(line), |
| "\n")); |
| RestoreCurrentInputLine(); |
| } |
| |
| void InteractiveCli::OnSocketEvent(QuicEventLoop* event_loop, SocketFd fd, |
| QuicSocketEventMask events) { |
| QUICHE_DCHECK(events == kSocketEventReadable); |
| |
| std::string all_input; |
| for (;;) { |
| char buffer[1024]; |
| ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer)); |
| // Since we set both VMIN and VTIME to zero, read() will return immediately |
| // if there is nothing to read; see termios(3) for details. |
| if (bytes_read <= 0) { |
| if (bytes_read == 0) { |
| break; |
| } |
| QUICHE_LOG(FATAL) << "Failed to read from stdin, errno: " << errno; |
| return; |
| } |
| all_input.append(buffer, bytes_read); |
| } |
| |
| if (!event_loop_->SupportsEdgeTriggered()) { |
| (void)event_loop_->RearmSocket(STDIN_FILENO, kSocketEventReadable); |
| } |
| |
| std::vector<absl::string_view> lines = absl::StrSplit(all_input, '\n'); |
| if (lines.empty()) { |
| return; |
| } |
| if (lines.size() == 1) { |
| // Usual case: there are no newlines. |
| absl::StrAppend(¤t_input_line_, lines.front()); |
| } else { |
| // There could two (if user hit ENTER) or more (if user pastes things into |
| // the terminal) lines; process all but the last one immediately. |
| line_callback_(absl::StrCat(current_input_line_, lines.front())); |
| current_input_line_.clear(); |
| |
| for (int i = 1; i < lines.size() - 1; ++i) { |
| line_callback_(lines[i]); |
| } |
| current_input_line_ = std::string(lines.back()); |
| } |
| |
| // Handle backspace. |
| while (current_input_line_.size() >= 2 && |
| current_input_line_.back() == '\x7f') { |
| current_input_line_.resize(current_input_line_.size() - 2); |
| } |
| // "Remove" escape sequences (it does not fully remove them, but gives the |
| // user enough indication that those won't work). |
| current_input_line_.erase( |
| std::remove_if(current_input_line_.begin(), current_input_line_.end(), |
| [](char c) { return absl::ascii_iscntrl(c); }), |
| current_input_line_.end()); |
| |
| ResetLine(); |
| RestoreCurrentInputLine(); |
| } |
| |
| } // namespace quic |