1 // Copyright 2024 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "quiche/quic/tools/interactive_cli.h"
6
7 #include <termios.h>
8 #include <unistd.h>
9
10 #include <algorithm>
11 #include <cerrno>
12 #include <cstring>
13 #include <memory>
14 #include <string>
15 #include <utility>
16 #include <vector>
17
18 #include "absl/status/status.h"
19 #include "absl/strings/ascii.h"
20 #include "absl/strings/str_cat.h"
21 #include "absl/strings/str_split.h"
22 #include "absl/strings/string_view.h"
23 #include "quiche/quic/core/io/quic_event_loop.h"
24 #include "quiche/quic/core/io/socket.h"
25 #include "quiche/common/platform/api/quiche_logging.h"
26 #include "quiche/common/quiche_callbacks.h"
27
28 namespace quic {
29 namespace {
30 // Writes into stdout.
Write(absl::string_view data)31 void Write(absl::string_view data) {
32 int written = write(STDOUT_FILENO, data.data(), data.size());
33 QUICHE_DCHECK_EQ(written, data.size());
34 }
35 } // namespace
36
InteractiveCli(QuicEventLoop * event_loop,LineCallback line_callback)37 InteractiveCli::InteractiveCli(QuicEventLoop* event_loop,
38 LineCallback line_callback)
39 : event_loop_(event_loop), line_callback_(std::move(line_callback)) {
40 if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) {
41 QUICHE_LOG(FATAL) << "Both stdin and stdout must be a TTY";
42 }
43
44 [[maybe_unused]] bool success =
45 event_loop_->RegisterSocket(STDIN_FILENO, kSocketEventReadable, this);
46 QUICHE_LOG_IF(FATAL, !success)
47 << "Failed to register stdin with the event loop";
48
49 // Store old termios so that we can recover it when exiting.
50 ::termios config;
51 tcgetattr(STDIN_FILENO, &config);
52 old_termios_ = std::make_unique<char[]>(sizeof(config));
53 memcpy(old_termios_.get(), &config, sizeof(config));
54
55 // Disable input buffering on the terminal.
56 config.c_lflag &= ~(ICANON | ECHO | ECHONL);
57 config.c_cc[VMIN] = 0;
58 config.c_cc[VTIME] = 0;
59 tcsetattr(STDIN_FILENO, TCSANOW, &config);
60
61 RestoreCurrentInputLine();
62 }
63
~InteractiveCli()64 InteractiveCli::~InteractiveCli() {
65 if (old_termios_ != nullptr) {
66 tcsetattr(STDIN_FILENO, TCSANOW,
67 reinterpret_cast<termios*>(old_termios_.get()));
68 }
69 [[maybe_unused]] bool success = event_loop_->UnregisterSocket(STDIN_FILENO);
70 QUICHE_LOG_IF(ERROR, !success) << "Failed to unregister stdin";
71 }
72
ResetLine()73 void InteractiveCli::ResetLine() {
74 constexpr absl::string_view kReset = "\033[G\033[K";
75 Write(kReset);
76 }
77
RestoreCurrentInputLine()78 void InteractiveCli::RestoreCurrentInputLine() {
79 Write(absl::StrCat(prompt_, current_input_line_));
80 }
81
PrintLine(absl::string_view line)82 void InteractiveCli::PrintLine(absl::string_view line) {
83 ResetLine();
84 Write(absl::StrCat("\n\033[1A", absl::StripTrailingAsciiWhitespace(line),
85 "\n"));
86 RestoreCurrentInputLine();
87 }
88
OnSocketEvent(QuicEventLoop * event_loop,SocketFd fd,QuicSocketEventMask events)89 void InteractiveCli::OnSocketEvent(QuicEventLoop* event_loop, SocketFd fd,
90 QuicSocketEventMask events) {
91 QUICHE_DCHECK(events == kSocketEventReadable);
92
93 std::string all_input;
94 for (;;) {
95 char buffer[1024];
96 ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer));
97 // Since we set both VMIN and VTIME to zero, read() will return immediately
98 // if there is nothing to read; see termios(3) for details.
99 if (bytes_read <= 0) {
100 if (bytes_read == 0) {
101 break;
102 }
103 QUICHE_LOG(FATAL) << "Failed to read from stdin, errno: " << errno;
104 return;
105 }
106 all_input.append(buffer, bytes_read);
107 }
108
109 if (!event_loop_->SupportsEdgeTriggered()) {
110 (void)event_loop_->RearmSocket(STDIN_FILENO, kSocketEventReadable);
111 }
112
113 std::vector<absl::string_view> lines = absl::StrSplit(all_input, '\n');
114 if (lines.empty()) {
115 return;
116 }
117 if (lines.size() == 1) {
118 // Usual case: there are no newlines.
119 absl::StrAppend(¤t_input_line_, lines.front());
120 } else {
121 // There could two (if user hit ENTER) or more (if user pastes things into
122 // the terminal) lines; process all but the last one immediately.
123 line_callback_(absl::StrCat(current_input_line_, lines.front()));
124 current_input_line_.clear();
125
126 for (int i = 1; i < lines.size() - 1; ++i) {
127 line_callback_(lines[i]);
128 }
129 current_input_line_ = std::string(lines.back());
130 }
131
132 // Handle backspace.
133 while (current_input_line_.size() >= 2 &&
134 current_input_line_.back() == '\x7f') {
135 current_input_line_.resize(current_input_line_.size() - 2);
136 }
137 // "Remove" escape sequences (it does not fully remove them, but gives the
138 // user enough indication that those won't work).
139 current_input_line_.erase(
140 std::remove_if(current_input_line_.begin(), current_input_line_.end(),
141 [](char c) { return absl::ascii_iscntrl(c); }),
142 current_input_line_.end());
143
144 ResetLine();
145 RestoreCurrentInputLine();
146 }
147
148 } // namespace quic
149