Skip to content

Commit 4fb17a8

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 d48ed1b commit 4fb17a8

8 files changed

Lines changed: 472 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
@@ -99,6 +99,7 @@ cf_cc_library(
9999
"//cuttlefish/host/commands/cvd/cli/commands:lint",
100100
"//cuttlefish/host/commands/cvd/cli/commands:login",
101101
"//cuttlefish/host/commands/cvd/cli/commands:logs",
102+
"//cuttlefish/host/commands/cvd/cli/commands:monitor",
102103
"//cuttlefish/host/commands/cvd/cli/commands:power_btn",
103104
"//cuttlefish/host/commands/cvd/cli/commands:powerwash",
104105
"//cuttlefish/host/commands/cvd/cli/commands:remove",

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

Lines changed: 32 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"],
@@ -471,3 +471,34 @@ cf_cc_library(
471471
"@abseil-cpp//absl/log",
472472
],
473473
)
474+
475+
cf_cc_library(
476+
name = "monitor",
477+
srcs = ["monitor.cpp"],
478+
hdrs = ["monitor.h"],
479+
deps = [
480+
"//cuttlefish/common/libs/fs",
481+
"//cuttlefish/common/libs/utils:flag_parser",
482+
"//cuttlefish/host/commands/cvd/cli:command_request",
483+
"//cuttlefish/host/commands/cvd/cli:types",
484+
"//cuttlefish/host/commands/cvd/cli:utils",
485+
"//cuttlefish/host/commands/cvd/cli/commands:command_handler",
486+
"//cuttlefish/host/commands/cvd/cli/selector",
487+
"//cuttlefish/host/commands/cvd/instances",
488+
"//cuttlefish/host/commands/cvd/instances:instance_manager",
489+
"//cuttlefish/result",
490+
"//libbase",
491+
"@fmt",
492+
],
493+
)
494+
495+
cf_cc_test(
496+
name = "monitor_test",
497+
srcs = ["monitor_test.cpp"],
498+
deps = [
499+
":monitor",
500+
"//cuttlefish/common/libs/fs",
501+
"//cuttlefish/result:result_matchers",
502+
"@abseil-cpp//absl/strings",
503+
],
504+
)
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
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_buf.h"
36+
#include "cuttlefish/common/libs/fs/shared_fd.h"
37+
#include "cuttlefish/host/commands/cvd/cli/command_request.h"
38+
#include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h"
39+
#include "cuttlefish/host/commands/cvd/cli/selector/selector.h"
40+
#include "cuttlefish/host/commands/cvd/cli/types.h"
41+
#include "cuttlefish/host/commands/cvd/cli/utils.h"
42+
#include "cuttlefish/host/commands/cvd/instances/instance_manager.h"
43+
#include "cuttlefish/result/result.h"
44+
45+
namespace cuttlefish {
46+
47+
namespace {
48+
49+
Result<std::vector<std::string>> GetLastNLines(SharedFD fd, int n) {
50+
off_t file_size = fd->LSeek(0, SEEK_END);
51+
CF_EXPECT(file_size != -1, "Failed to seek to end of file");
52+
53+
std::vector<std::string> lines;
54+
std::string current_line = "";
55+
off_t offset = file_size;
56+
const size_t kChunkSize = 4096;
57+
58+
while (offset > 0 && lines.size() < static_cast<size_t>(n)) {
59+
size_t to_read = std::min(static_cast<off_t>(kChunkSize), offset);
60+
offset -= to_read;
61+
fd->LSeek(offset, SEEK_SET);
62+
63+
std::string chunk(to_read, '\0');
64+
ssize_t bytes_read = ReadExact(fd, &chunk);
65+
CF_EXPECT(bytes_read == static_cast<ssize_t>(to_read), "Read failed");
66+
67+
for (ssize_t i = to_read - 1; i >= 0; --i) {
68+
char c = chunk[i];
69+
if (c == '\n') {
70+
if (offset + i == file_size - 1) {
71+
continue;
72+
}
73+
std::reverse(current_line.begin(), current_line.end());
74+
lines.insert(lines.begin(), current_line);
75+
current_line.clear();
76+
if (lines.size() >= static_cast<size_t>(n)) {
77+
break;
78+
}
79+
} else {
80+
current_line.push_back(c);
81+
}
82+
}
83+
}
84+
85+
if (lines.size() < static_cast<size_t>(n) && !current_line.empty()) {
86+
std::reverse(current_line.begin(), current_line.end());
87+
lines.insert(lines.begin(), current_line);
88+
}
89+
90+
return lines;
91+
}
92+
93+
} // namespace
94+
95+
LogMonitorDisplay::LogMonitorDisplay(int width)
96+
: width_(width), total_lines_drawn_(0) {}
97+
98+
void LogMonitorDisplay::DrawFile(SharedFD fd, const std::string& title) {
99+
std::vector<std::string> lines;
100+
if (!fd->IsOpen()) {
101+
lines.push_back("Failed to read " + title + ": File not open");
102+
} else {
103+
Result<std::vector<std::string>> lines_result = GetLastNLines(fd, 10);
104+
if (!lines_result.ok()) {
105+
lines.push_back("Failed to read " + title + ": " +
106+
lines_result.error().Message());
107+
} else {
108+
lines = *lines_result;
109+
}
110+
}
111+
112+
while (lines.size() < 10) {
113+
lines.push_back("");
114+
}
115+
116+
DrawBorderedText(lines, title);
117+
total_lines_drawn_ += 11;
118+
}
119+
120+
std::string LogMonitorDisplay::Finalize() {
121+
std::string bottom_border = "+";
122+
if (width_ > 2) {
123+
bottom_border += std::string(width_ - 2, '-');
124+
bottom_border += "+";
125+
}
126+
ss_ << bottom_border << std::endl;
127+
total_lines_drawn_++;
128+
return ss_.str();
129+
}
130+
131+
int LogMonitorDisplay::TotalLinesDrawn() const { return total_lines_drawn_; }
132+
133+
void LogMonitorDisplay::DrawBorderedText(const std::vector<std::string>& lines,
134+
const std::string& title) {
135+
// Top border
136+
std::string top_border = "+--" + title + " ";
137+
if (top_border.length() > static_cast<size_t>(width_)) {
138+
top_border = top_border.substr(0, width_);
139+
}
140+
ss_ << top_border;
141+
if (top_border.length() < static_cast<size_t>(width_)) {
142+
ss_ << std::string(width_ - top_border.length() - 1, '-');
143+
ss_ << "+";
144+
}
145+
ss_ << std::endl;
146+
147+
for (const auto& line : lines) {
148+
std::string processed_line = line;
149+
processed_line.erase(
150+
std::remove(processed_line.begin(), processed_line.end(), '\r'),
151+
processed_line.end());
152+
processed_line.erase(
153+
std::remove(processed_line.begin(), processed_line.end(), '\n'),
154+
processed_line.end());
155+
156+
std::string expanded_line = "";
157+
for (char c : processed_line) {
158+
if (c == '\t') {
159+
expanded_line += " ";
160+
} else {
161+
expanded_line.push_back(c);
162+
}
163+
}
164+
165+
std::string truncated_line = expanded_line;
166+
int content_width = width_ - 2;
167+
if (content_width < 0) {
168+
content_width = 0;
169+
}
170+
171+
if (truncated_line.length() > static_cast<size_t>(content_width)) {
172+
truncated_line = truncated_line.substr(0, content_width);
173+
}
174+
175+
std::string middle_line = "|";
176+
middle_line += truncated_line;
177+
if (middle_line.length() < static_cast<size_t>(width_ - 1)) {
178+
middle_line += std::string(width_ - 1 - middle_line.length(), ' ');
179+
}
180+
if (width_ > 1) {
181+
middle_line += "|";
182+
}
183+
ss_ << middle_line << std::endl;
184+
}
185+
}
186+
187+
namespace {
188+
189+
constexpr char kSummaryHelpText[] =
190+
"Monitor device logs (launcher, kernel, and logcat) in real-time.";
191+
constexpr char kDetailedHelpText[] =
192+
R"(monitor: Monitors a particular device by displaying the last 10 lines of its logs.
193+
It requires an interactive terminal and will continuously update the display every 50ms.
194+
195+
It displays:
196+
- launcher.log
197+
- kernel.log
198+
- logcat
199+
200+
Usage:
201+
cvd [selector options] monitor
202+
)";
203+
204+
constexpr char kMonitorCmd[] = "monitor";
205+
206+
void ClearLastNLines(int n) {
207+
if (n > 0) {
208+
// Move cursor up N lines and clear to end of screen
209+
std::cout << "\033[" << n << "A\033[J" << std::flush;
210+
}
211+
}
212+
213+
class CvdMonitorCommandHandler : public CvdCommandHandler {
214+
public:
215+
CvdMonitorCommandHandler(InstanceManager& instance_manager)
216+
: instance_manager_{instance_manager} {}
217+
218+
Result<void> Handle(const CommandRequest& request) override {
219+
CF_EXPECT(CanHandle(request));
220+
221+
std::vector<std::string> subcmd_args = request.SubcommandArguments();
222+
// Parse flags if needed, none for now.
223+
224+
CF_EXPECT(isatty(0),
225+
"The monitor command requires an interactive terminal.");
226+
227+
auto [instance, unused] =
228+
CF_EXPECT(selector::SelectInstance(instance_manager_, request),
229+
"Unable to select an instance");
230+
231+
std::string kernel_log = instance.instance_dir() + "/logs/kernel.log";
232+
std::string launcher_log = instance.instance_dir() + "/logs/launcher.log";
233+
std::string logcat = instance.instance_dir() + "/logs/logcat";
234+
235+
SharedFD kernel_fd;
236+
SharedFD launcher_fd;
237+
SharedFD logcat_fd;
238+
239+
while (true) {
240+
if (!kernel_fd->IsOpen()) {
241+
kernel_fd = SharedFD::Open(kernel_log, O_RDONLY);
242+
}
243+
if (!launcher_fd->IsOpen()) {
244+
launcher_fd = SharedFD::Open(launcher_log, O_RDONLY);
245+
}
246+
if (!logcat_fd->IsOpen()) {
247+
logcat_fd = SharedFD::Open(logcat, O_RDONLY);
248+
}
249+
250+
Result<TerminalSize> term_size_result = GetTerminalSize();
251+
int width = 79; // Default fallback width (80 - 1)
252+
if (term_size_result.ok()) {
253+
width = term_size_result->columns - 1;
254+
}
255+
LogMonitorDisplay display(width);
256+
257+
display.DrawFile(launcher_fd, "launcher.log");
258+
display.DrawFile(kernel_fd, "kernel.log");
259+
display.DrawFile(logcat_fd, "logcat");
260+
261+
std::cout << display.Finalize() << std::flush;
262+
263+
// Wait a bit before clearing and redrawing
264+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
265+
ClearLastNLines(display.TotalLinesDrawn());
266+
}
267+
268+
return {};
269+
}
270+
271+
cvd_common::Args CmdList() const override { return {kMonitorCmd}; }
272+
273+
Result<std::string> SummaryHelp() const override { return kSummaryHelpText; }
274+
275+
bool RequiresDeviceExists() const override { return true; }
276+
277+
Result<std::string> DetailedHelp(
278+
const CommandRequest& request) const override {
279+
return kDetailedHelpText;
280+
}
281+
282+
private:
283+
InstanceManager& instance_manager_;
284+
};
285+
286+
} // namespace
287+
288+
std::unique_ptr<CvdCommandHandler> NewCvdMonitorCommandHandler(
289+
InstanceManager& instance_manager) {
290+
return std::unique_ptr<CvdCommandHandler>(
291+
new CvdMonitorCommandHandler(instance_manager));
292+
}
293+
294+
} // namespace cuttlefish
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
#pragma once
18+
19+
#include <memory>
20+
#include <sstream>
21+
#include <string>
22+
#include <vector>
23+
24+
#include "cuttlefish/common/libs/fs/shared_fd.h"
25+
#include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h"
26+
#include "cuttlefish/host/commands/cvd/instances/instance_manager.h"
27+
28+
namespace cuttlefish {
29+
30+
class LogMonitorDisplay {
31+
public:
32+
LogMonitorDisplay(int width);
33+
34+
void DrawFile(SharedFD fd, const std::string& title);
35+
36+
std::string Finalize();
37+
38+
int TotalLinesDrawn() const;
39+
40+
private:
41+
void DrawBorderedText(const std::vector<std::string>& lines,
42+
const std::string& title);
43+
44+
int width_;
45+
std::stringstream ss_;
46+
int total_lines_drawn_;
47+
};
48+
49+
std::unique_ptr<CvdCommandHandler> NewCvdMonitorCommandHandler(
50+
InstanceManager& instance_manager);
51+
52+
} // namespace cuttlefish

0 commit comments

Comments
 (0)