Skip to content

Commit afbe0a9

Browse files
committed
Add CvdMonitor command to monitor device logs.
This command repeatedly clears and prints the last 10 lines of launcher.log, kernel.log, and logcat in a bordered ASCII box. It includes unit tests for the display logic. Assisted-by: Jetski Bug: b/510094996
1 parent fbe4349 commit afbe0a9

8 files changed

Lines changed: 469 additions & 1 deletion

File tree

base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ cf_cc_library(
9191
"//cuttlefish/host/commands/cvd/cli/commands:login",
9292
"//cuttlefish/host/commands/cvd/cli/commands:power_btn",
9393
"//cuttlefish/host/commands/cvd/cli/commands:powerwash",
94+
"//cuttlefish/host/commands/cvd/cli/commands:monitor",
9495
"//cuttlefish/host/commands/cvd/cli/commands:remove",
9596
"//cuttlefish/host/commands/cvd/cli/commands:reset",
9697
"//cuttlefish/host/commands/cvd/cli/commands:restart",

base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("//cuttlefish/bazel:rules.bzl", "cf_cc_library")
1+
load("//cuttlefish/bazel:rules.bzl", "cf_cc_library", "cf_cc_test")
22

33
package(
44
default_visibility = ["//:android_cuttlefish"],
@@ -259,6 +259,29 @@ cf_cc_library(
259259
],
260260
)
261261

262+
cf_cc_library(
263+
name = "monitor",
264+
srcs = ["monitor.cpp"],
265+
hdrs = ["monitor.h"],
266+
deps = [
267+
"//cuttlefish/common/libs/fs",
268+
"//cuttlefish/common/libs/utils:flag_parser",
269+
"//cuttlefish/host/commands/cvd/cli:command_request",
270+
"//cuttlefish/host/commands/cvd/cli:types",
271+
"//cuttlefish/host/commands/cvd/cli:utils",
272+
"//cuttlefish/host/commands/cvd/cli/commands:command_handler",
273+
"//cuttlefish/host/commands/cvd/cli/selector",
274+
"//cuttlefish/host/commands/cvd/instances",
275+
"//cuttlefish/host/commands/cvd/instances:instance_manager",
276+
"//cuttlefish/result",
277+
"//libbase",
278+
"@fmt",
279+
],
280+
)
281+
282+
283+
284+
262285
cf_cc_library(
263286
name = "screen_recording",
264287
srcs = ["screen_recording.cpp"],
@@ -453,3 +476,18 @@ cf_cc_library(
453476
"@jsoncpp",
454477
],
455478
)
479+
480+
cf_cc_test(
481+
name = "monitor_test",
482+
srcs = ["monitor_test.cpp"],
483+
deps = [
484+
":monitor",
485+
"//cuttlefish/common/libs/fs",
486+
"//cuttlefish/result:result_matchers",
487+
"@abseil-cpp//absl/strings",
488+
],
489+
)
490+
491+
492+
493+
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
/*
2+
* Copyright (C) 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "cuttlefish/host/commands/cvd/cli/commands/monitor.h"
18+
19+
#include <fcntl.h>
20+
#include <sys/types.h>
21+
#include <unistd.h>
22+
23+
#include <algorithm>
24+
#include <chrono>
25+
#include <cstddef>
26+
#include <cstdio>
27+
#include <cstring>
28+
#include <iostream>
29+
#include <memory>
30+
#include <sstream>
31+
#include <string>
32+
#include <thread>
33+
#include <vector>
34+
35+
#include "cuttlefish/common/libs/fs/shared_fd.h"
36+
#include "cuttlefish/host/commands/cvd/cli/command_request.h"
37+
#include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h"
38+
#include "cuttlefish/host/commands/cvd/cli/selector/selector.h"
39+
#include "cuttlefish/host/commands/cvd/cli/types.h"
40+
#include "cuttlefish/host/commands/cvd/cli/utils.h"
41+
#include "cuttlefish/host/commands/cvd/instances/instance_manager.h"
42+
#include "cuttlefish/result/result.h"
43+
44+
namespace cuttlefish {
45+
46+
namespace {
47+
48+
Result<std::vector<std::string>> GetLastNLines(SharedFD fd, int n) {
49+
off_t file_size = fd->LSeek(0, SEEK_END);
50+
CF_EXPECT(file_size != -1, "Failed to seek to end of file");
51+
52+
std::vector<std::string> lines;
53+
std::string current_line = "";
54+
off_t offset = file_size;
55+
const size_t kChunkSize = 4096;
56+
57+
while (offset > 0 && lines.size() < static_cast<size_t>(n)) {
58+
size_t to_read = std::min(static_cast<off_t>(kChunkSize), offset);
59+
offset -= to_read;
60+
fd->LSeek(offset, SEEK_SET);
61+
62+
std::string chunk(to_read, '\0');
63+
ssize_t bytes_read = fd->Read(chunk.data(), to_read);
64+
CF_EXPECT(bytes_read == static_cast<ssize_t>(to_read), "Read failed");
65+
66+
for (ssize_t i = to_read - 1; i >= 0; --i) {
67+
char c = chunk[i];
68+
if (c == '\n') {
69+
if (offset + i == file_size - 1) {
70+
continue;
71+
}
72+
std::reverse(current_line.begin(), current_line.end());
73+
lines.insert(lines.begin(), current_line);
74+
current_line.clear();
75+
if (lines.size() >= static_cast<size_t>(n)) {
76+
break;
77+
}
78+
} else {
79+
current_line.push_back(c);
80+
}
81+
}
82+
}
83+
84+
if (lines.size() < static_cast<size_t>(n) && !current_line.empty()) {
85+
std::reverse(current_line.begin(), current_line.end());
86+
lines.insert(lines.begin(), current_line);
87+
}
88+
89+
return lines;
90+
}
91+
92+
} // namespace
93+
94+
LogMonitorDisplay::LogMonitorDisplay(int width)
95+
: width_(width), total_lines_drawn_(0) {}
96+
97+
void LogMonitorDisplay::DrawFile(SharedFD fd, const std::string& title) {
98+
std::vector<std::string> lines;
99+
if (!fd->IsOpen()) {
100+
lines.push_back("Failed to read " + title + ": File not open");
101+
} else {
102+
Result<std::vector<std::string>> lines_result = GetLastNLines(fd, 10);
103+
if (!lines_result.ok()) {
104+
lines.push_back("Failed to read " + title + ": " +
105+
lines_result.error().Message());
106+
} else {
107+
lines = *lines_result;
108+
}
109+
}
110+
111+
while (lines.size() < 10) {
112+
lines.push_back("");
113+
}
114+
115+
DrawBorderedText(lines, title);
116+
total_lines_drawn_ += 11;
117+
}
118+
119+
std::string LogMonitorDisplay::Finalize() {
120+
std::string bottom_border = "+";
121+
if (width_ > 2) {
122+
bottom_border += std::string(width_ - 2, '-');
123+
bottom_border += "+";
124+
}
125+
ss_ << bottom_border << std::endl;
126+
total_lines_drawn_++;
127+
return ss_.str();
128+
}
129+
130+
int LogMonitorDisplay::TotalLinesDrawn() const { return total_lines_drawn_; }
131+
132+
void LogMonitorDisplay::DrawBorderedText(const std::vector<std::string>& lines,
133+
const std::string& title) {
134+
// Top border
135+
std::string top_border = "+--" + title + " ";
136+
if (top_border.length() > static_cast<size_t>(width_)) {
137+
top_border = top_border.substr(0, width_);
138+
}
139+
ss_ << top_border;
140+
if (top_border.length() < static_cast<size_t>(width_)) {
141+
ss_ << std::string(width_ - top_border.length() - 1, '-');
142+
ss_ << "+";
143+
}
144+
ss_ << std::endl;
145+
146+
for (const auto& line : lines) {
147+
std::string processed_line = line;
148+
processed_line.erase(
149+
std::remove(processed_line.begin(), processed_line.end(), '\r'),
150+
processed_line.end());
151+
processed_line.erase(
152+
std::remove(processed_line.begin(), processed_line.end(), '\n'),
153+
processed_line.end());
154+
155+
std::string expanded_line = "";
156+
for (char c : processed_line) {
157+
if (c == '\t') {
158+
expanded_line += " ";
159+
} else {
160+
expanded_line.push_back(c);
161+
}
162+
}
163+
164+
std::string truncated_line = expanded_line;
165+
int content_width = width_ - 2;
166+
if (content_width < 0) {
167+
content_width = 0;
168+
}
169+
170+
if (truncated_line.length() > static_cast<size_t>(content_width)) {
171+
truncated_line = truncated_line.substr(0, content_width);
172+
}
173+
174+
std::string middle_line = "|";
175+
middle_line += truncated_line;
176+
if (middle_line.length() < static_cast<size_t>(width_ - 1)) {
177+
middle_line += std::string(width_ - 1 - middle_line.length(), ' ');
178+
}
179+
if (width_ > 1) {
180+
middle_line += "|";
181+
}
182+
ss_ << middle_line << std::endl;
183+
}
184+
}
185+
186+
namespace {
187+
188+
constexpr char kSummaryHelpText[] = "Monitor a particular device";
189+
constexpr char kDetailedHelpText[] =
190+
R"(monitor: Monitors a particular device.
191+
192+
Flags:
193+
None yet.
194+
)";
195+
196+
constexpr char kMonitorCmd[] = "monitor";
197+
198+
void ClearLastNLines(int n) {
199+
if (n > 0) {
200+
// Move cursor up N lines and clear to end of screen
201+
std::cout << "\033[" << n << "A\033[J" << std::flush;
202+
}
203+
}
204+
205+
class CvdMonitorCommandHandler : public CvdCommandHandler {
206+
public:
207+
CvdMonitorCommandHandler(InstanceManager& instance_manager)
208+
: instance_manager_{instance_manager} {}
209+
210+
Result<void> Handle(const CommandRequest& request) override {
211+
CF_EXPECT(CanHandle(request));
212+
213+
std::vector<std::string> subcmd_args = request.SubcommandArguments();
214+
// Parse flags if needed, none for now.
215+
216+
CF_EXPECT(isatty(0),
217+
"The monitor command requires an interactive terminal.");
218+
219+
auto [instance, unused] =
220+
CF_EXPECT(selector::SelectInstance(instance_manager_, request),
221+
"Unable to select an instance");
222+
223+
std::string kernel_log = instance.instance_dir() + "/logs/kernel.log";
224+
std::string launcher_log = instance.instance_dir() + "/logs/launcher.log";
225+
std::string logcat = instance.instance_dir() + "/logs/logcat";
226+
227+
SharedFD kernel_fd;
228+
SharedFD launcher_fd;
229+
SharedFD logcat_fd;
230+
231+
while (true) {
232+
if (!kernel_fd->IsOpen()) {
233+
kernel_fd = SharedFD::Open(kernel_log, O_RDONLY);
234+
}
235+
if (!launcher_fd->IsOpen()) {
236+
launcher_fd = SharedFD::Open(launcher_log, O_RDONLY);
237+
}
238+
if (!logcat_fd->IsOpen()) {
239+
logcat_fd = SharedFD::Open(logcat, O_RDONLY);
240+
}
241+
242+
Result<TerminalSize> term_size_result = GetTerminalSize();
243+
int width = 79; // Default fallback width (80 - 1)
244+
if (term_size_result.ok()) {
245+
width = term_size_result->columns - 1;
246+
}
247+
LogMonitorDisplay display(width);
248+
249+
display.DrawFile(launcher_fd, "launcher.log");
250+
display.DrawFile(kernel_fd, "kernel.log");
251+
display.DrawFile(logcat_fd, "logcat");
252+
253+
std::cout << display.Finalize() << std::flush;
254+
255+
// Wait a bit before clearing and redrawing
256+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
257+
ClearLastNLines(display.TotalLinesDrawn());
258+
}
259+
260+
return {};
261+
}
262+
263+
cvd_common::Args CmdList() const override { return {kMonitorCmd}; }
264+
265+
Result<std::string> SummaryHelp() const override { return kSummaryHelpText; }
266+
267+
bool RequiresDeviceExists() const override { return true; }
268+
269+
Result<std::string> DetailedHelp(
270+
const CommandRequest& request) const override {
271+
return kDetailedHelpText;
272+
}
273+
274+
private:
275+
InstanceManager& instance_manager_;
276+
};
277+
278+
} // namespace
279+
280+
std::unique_ptr<CvdCommandHandler> NewCvdMonitorCommandHandler(
281+
InstanceManager& instance_manager) {
282+
return std::unique_ptr<CvdCommandHandler>(
283+
new CvdMonitorCommandHandler(instance_manager));
284+
}
285+
286+
} // namespace cuttlefish

0 commit comments

Comments
 (0)