xref: /aosp_15_r20/external/cronet/net/third_party/quiche/src/quiche/quic/tools/interactive_cli.cc (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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(&current_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