Skip to content

Commit 3b937f5

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 5c13441 commit 3b937f5

8 files changed

Lines changed: 493 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: 34 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,36 @@ 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+
"@abseil-cpp//absl/strings",
492+
"@abseil-cpp//absl/strings:cord",
493+
"@fmt",
494+
],
495+
)
496+
497+
cf_cc_test(
498+
name = "monitor_test",
499+
srcs = ["monitor_test.cpp"],
500+
deps = [
501+
":monitor",
502+
"//cuttlefish/common/libs/fs",
503+
"//cuttlefish/result:result_matchers",
504+
"@abseil-cpp//absl/strings",
505+
],
506+
)
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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 "absl/strings/cord.h"
36+
#include "absl/strings/str_cat.h"
37+
#include "absl/strings/str_replace.h"
38+
#include "absl/strings/str_split.h"
39+
40+
#include "cuttlefish/common/libs/fs/shared_buf.h"
41+
#include "cuttlefish/common/libs/fs/shared_fd.h"
42+
#include "cuttlefish/host/commands/cvd/cli/command_request.h"
43+
#include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h"
44+
#include "cuttlefish/host/commands/cvd/cli/selector/selector.h"
45+
#include "cuttlefish/host/commands/cvd/cli/types.h"
46+
#include "cuttlefish/host/commands/cvd/cli/utils.h"
47+
#include "cuttlefish/host/commands/cvd/instances/instance_manager.h"
48+
#include "cuttlefish/result/result.h"
49+
50+
namespace cuttlefish {
51+
52+
namespace {
53+
54+
Result<std::vector<std::string>> GetLastNLines(SharedFD fd, size_t n) {
55+
off_t file_size = fd->LSeek(0, SEEK_END);
56+
CF_EXPECT(file_size != -1, "Failed to seek to end of file");
57+
58+
absl::Cord accumulated_data;
59+
off_t offset = file_size;
60+
size_t newline_count = 0;
61+
62+
while (offset > 0 && newline_count < n + 1) {
63+
static constexpr off_t kChunkSize = 4096;
64+
size_t to_read = std::min(kChunkSize, offset);
65+
offset -= to_read;
66+
fd->LSeek(offset, SEEK_SET);
67+
68+
std::string chunk(to_read, '\0');
69+
ssize_t bytes_read = ReadExact(fd, &chunk);
70+
CF_EXPECTF(bytes_read == static_cast<ssize_t>(to_read), "Read failed: '{}'",
71+
fd->StrError());
72+
73+
newline_count += std::count(chunk.begin(), chunk.end(), '\n');
74+
accumulated_data.Prepend(std::move(chunk));
75+
}
76+
77+
std::vector<std::string> all_lines =
78+
absl::StrSplit(std::string(accumulated_data), '\n');
79+
80+
// Handle trailing newline
81+
if (!all_lines.empty() && all_lines.back().empty()) {
82+
all_lines.pop_back();
83+
}
84+
85+
std::vector<std::string> result;
86+
size_t start_idx = all_lines.size() > n ? all_lines.size() - n : 0;
87+
for (size_t i = start_idx; i < all_lines.size(); ++i) {
88+
result.push_back(all_lines[i]);
89+
}
90+
91+
return result;
92+
}
93+
94+
} // namespace
95+
96+
LogMonitorDisplay::LogMonitorDisplay(size_t width)
97+
: width_(width), total_lines_drawn_(0) {}
98+
99+
void LogMonitorDisplay::DrawFile(SharedFD fd, const std::string& title) {
100+
std::vector<std::string> lines;
101+
if (fd->IsOpen()) {
102+
Result<std::vector<std::string>> lines_result = GetLastNLines(fd, 10);
103+
if (lines_result.ok()) {
104+
lines = *lines_result;
105+
} else {
106+
lines.push_back(absl::StrCat("Failed to read ", title, ":"));
107+
std::string error_str =
108+
lines_result.error().FormatForEnv(/*color=*/false);
109+
for (const auto& el : absl::StrSplit(error_str, '\n')) {
110+
if (!el.empty()) {
111+
lines.push_back(std::string(el));
112+
}
113+
}
114+
}
115+
} else {
116+
lines.push_back(absl::StrCat("Failed to read ", title, ": File not open"));
117+
lines.push_back(absl::StrCat("Error: ", fd->StrError()));
118+
}
119+
120+
while (lines.size() < 10) {
121+
lines.push_back("");
122+
}
123+
124+
DrawBorderedText(lines, title);
125+
}
126+
127+
std::string LogMonitorDisplay::Finalize() {
128+
CHECK_GE(width_, 2);
129+
ss_ << "+" << std::string(width_ - 2, '-') << "+\n";
130+
total_lines_drawn_++;
131+
return ss_.str();
132+
}
133+
134+
int LogMonitorDisplay::TotalLinesDrawn() const { return total_lines_drawn_; }
135+
136+
void LogMonitorDisplay::DrawBorderedText(const std::vector<std::string>& lines,
137+
const std::string& title) {
138+
std::string top_border = absl::StrCat("+--", title, " ");
139+
CHECK_GE(width_, 2);
140+
top_border.resize(width_ - 1, '-');
141+
ss_ << top_border << "+\n";
142+
143+
for (std::string line : lines) {
144+
absl::StrReplaceAll({{"\t", " "}, {"\r", ""}, {"\n", ""}}, &line);
145+
146+
line.resize(width_ - 2, ' ');
147+
ss_ << "|" << line << "|\n";
148+
}
149+
total_lines_drawn_ += 1 + lines.size();
150+
}
151+
152+
namespace {
153+
154+
constexpr char kSummaryHelpText[] =
155+
"Monitor device logs (launcher, kernel, and logcat) in real-time.";
156+
constexpr char kDetailedHelpText[] =
157+
R"(monitor: Monitors a particular device by displaying the last 10 lines of its logs.
158+
It requires an interactive terminal and will continuously update the display every 50ms.
159+
160+
It displays:
161+
- launcher.log
162+
- kernel.log
163+
- logcat
164+
165+
Usage:
166+
cvd [selector options] monitor
167+
)";
168+
169+
constexpr char kMonitorCmd[] = "monitor";
170+
171+
void ClearLastNLines(int n) {
172+
if (n > 0) {
173+
// Move cursor up N lines and clear to end of screen
174+
std::cout << "\033[" << n << "A\033[J" << std::flush;
175+
}
176+
}
177+
178+
class CvdMonitorCommandHandler : public CvdCommandHandler {
179+
public:
180+
CvdMonitorCommandHandler(InstanceManager& instance_manager)
181+
: instance_manager_{instance_manager} {}
182+
183+
Result<void> Handle(const CommandRequest& request) override {
184+
CF_EXPECT(CanHandle(request));
185+
186+
CF_EXPECT(isatty(0),
187+
"The monitor command requires an interactive terminal.");
188+
189+
auto [instance, unused] =
190+
CF_EXPECT(selector::SelectInstance(instance_manager_, request),
191+
"Unable to select an instance");
192+
193+
std::string kernel_log = instance.instance_dir() + "/logs/kernel.log";
194+
std::string launcher_log = instance.instance_dir() + "/logs/launcher.log";
195+
std::string logcat = instance.instance_dir() + "/logs/logcat";
196+
197+
SharedFD kernel_fd;
198+
SharedFD launcher_fd;
199+
SharedFD logcat_fd;
200+
201+
while (true) {
202+
if (!kernel_fd->IsOpen()) {
203+
kernel_fd = SharedFD::Open(kernel_log, O_RDONLY);
204+
}
205+
if (!launcher_fd->IsOpen()) {
206+
launcher_fd = SharedFD::Open(launcher_log, O_RDONLY);
207+
}
208+
if (!logcat_fd->IsOpen()) {
209+
logcat_fd = SharedFD::Open(logcat, O_RDONLY);
210+
}
211+
212+
Result<TerminalSize> term_size_result = GetTerminalSize();
213+
int width = 79; // Default fallback width (80 - 1)
214+
if (term_size_result.ok()) {
215+
width = term_size_result->columns - 1;
216+
}
217+
LogMonitorDisplay display(width);
218+
219+
display.DrawFile(launcher_fd, "launcher.log");
220+
display.DrawFile(kernel_fd, "kernel.log");
221+
display.DrawFile(logcat_fd, "logcat");
222+
223+
std::cout << display.Finalize() << std::flush;
224+
225+
// Wait a bit before clearing and redrawing
226+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
227+
ClearLastNLines(display.TotalLinesDrawn());
228+
}
229+
230+
return {};
231+
}
232+
233+
cvd_common::Args CmdList() const override { return {kMonitorCmd}; }
234+
235+
Result<std::string> SummaryHelp() const override { return kSummaryHelpText; }
236+
237+
bool RequiresDeviceExists() const override { return true; }
238+
239+
Result<std::string> DetailedHelp(
240+
const CommandRequest& request) const override {
241+
return kDetailedHelpText;
242+
}
243+
244+
private:
245+
InstanceManager& instance_manager_;
246+
};
247+
248+
} // namespace
249+
250+
std::unique_ptr<CvdCommandHandler> NewCvdMonitorCommandHandler(
251+
InstanceManager& instance_manager) {
252+
return std::unique_ptr<CvdCommandHandler>(
253+
new CvdMonitorCommandHandler(instance_manager));
254+
}
255+
256+
} // 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(size_t 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+
size_t 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)