From 9d1a9e3a773170b3ebe3c3b6e9b8b2ddaa19c5c1 Mon Sep 17 00:00:00 2001 From: Sergio Andres Rodriguez Orama Date: Wed, 29 Apr 2026 16:15:05 -0400 Subject: [PATCH 1/7] Boilerplate for new subcommand: "logs" Bug: b/507588992 --- .../host/commands/cvd/cli/BUILD.bazel | 1 + .../commands/cvd/cli/commands/BUILD.bazel | 12 +++++ .../host/commands/cvd/cli/commands/logs.cpp | 48 +++++++++++++++++++ .../host/commands/cvd/cli/commands/logs.h | 11 +++++ .../host/commands/cvd/cli/request_context.cpp | 2 + 5 files changed, 74 insertions(+) create mode 100644 base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp create mode 100644 base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.h diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel index 70e88396071..c6582784e4d 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel @@ -89,6 +89,7 @@ cf_cc_library( "//cuttlefish/host/commands/cvd/cli/commands:host_tool_target", "//cuttlefish/host/commands/cvd/cli/commands:lint", "//cuttlefish/host/commands/cvd/cli/commands:login", + "//cuttlefish/host/commands/cvd/cli/commands:logs", "//cuttlefish/host/commands/cvd/cli/commands:power_btn", "//cuttlefish/host/commands/cvd/cli/commands:powerwash", "//cuttlefish/host/commands/cvd/cli/commands:remove", diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel index 09402875ab0..b0052000b48 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel @@ -453,3 +453,15 @@ cf_cc_library( "@jsoncpp", ], ) + +cf_cc_library( + name = "logs", + srcs = ["logs.cpp"], + hdrs = ["logs.h"], + deps = [ + "//cuttlefish/host/commands/cvd/cli:command_request", + "//cuttlefish/host/commands/cvd/cli:types", + "//cuttlefish/host/commands/cvd/cli/commands:command_handler", + "//cuttlefish/result", + ], +) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp new file mode 100644 index 00000000000..5eeea558880 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp @@ -0,0 +1,48 @@ +#include "cuttlefish/host/commands/cvd/cli/commands/logs.h" + +#include +#include +#include + +#include "cuttlefish/host/commands/cvd/cli/command_request.h" +#include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h" +#include "cuttlefish/host/commands/cvd/cli/types.h" +#include "cuttlefish/result/result.h" + +namespace cuttlefish { +namespace { + +constexpr char kSummaryHelpText[] = "Print device logs"; + +constexpr char kDetailedHelpText[] = R"( +usage: cvd [--group_name name [--instance_name name]] logs +)"; + +class CvdLogsHandler : public CvdCommandHandler { + public: + CvdLogsHandler() = default; + + Result Handle(const CommandRequest& request) override { + CF_EXPECT(CanHandle(request)); + std::cout << "hello, logs\n"; + return {}; + } + + cvd_common::Args CmdList() const override { return {"logs"}; } + + Result SummaryHelp() const override { return kSummaryHelpText; } + + bool RequiresDeviceExists() const override { return true; } + + Result DetailedHelp(const CommandRequest&) const override { + return kDetailedHelpText; + } +}; + +} // namespace + +std::unique_ptr NewCvdLogsHandler() { + return std::unique_ptr(new CvdLogsHandler()); +} + +} // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.h b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.h new file mode 100644 index 00000000000..75919c17717 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h" + +namespace cuttlefish { + +std::unique_ptr NewCvdLogsHandler(); + +} // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp index 79af99b2f09..5bb41fe092b 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp @@ -38,6 +38,7 @@ #include "cuttlefish/host/commands/cvd/cli/commands/lint.h" #include "cuttlefish/host/commands/cvd/cli/commands/load_configs.h" #include "cuttlefish/host/commands/cvd/cli/commands/login.h" +#include "cuttlefish/host/commands/cvd/cli/commands/logs.h" #include "cuttlefish/host/commands/cvd/cli/commands/power_btn.h" #include "cuttlefish/host/commands/cvd/cli/commands/powerwash.h" @@ -115,6 +116,7 @@ RequestContext::RequestContext(InstanceManager& instance_manager, request_handlers_.emplace_back(NewCvdStartCommandHandler(instance_manager)); request_handlers_.emplace_back(NewCvdStatusCommandHandler(instance_manager)); request_handlers_.emplace_back(NewCvdVersionHandler()); + request_handlers_.emplace_back(NewCvdLogsHandler()); } Result RequestContext::Handler( From 88596a25b6d4920be5b0b9ffb02e4716e7cb5480 Mon Sep 17 00:00:00 2001 From: Sergio Andres Rodriguez Orama Date: Wed, 29 Apr 2026 16:42:22 -0400 Subject: [PATCH 2/7] Fail if using invalid selector. Bug: b/507588992 --- .../commands/cvd/cli/commands/BUILD.bazel | 2 ++ .../host/commands/cvd/cli/commands/logs.cpp | 19 ++++++++++++++++--- .../host/commands/cvd/cli/commands/logs.h | 4 +++- .../host/commands/cvd/cli/request_context.cpp | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel index b0052000b48..32f2bca3181 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel @@ -462,6 +462,8 @@ cf_cc_library( "//cuttlefish/host/commands/cvd/cli:command_request", "//cuttlefish/host/commands/cvd/cli:types", "//cuttlefish/host/commands/cvd/cli/commands:command_handler", + "//cuttlefish/host/commands/cvd/cli/selector", + "//cuttlefish/host/commands/cvd/instances:instance_manager", "//cuttlefish/result", ], ) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp index 5eeea558880..4d75a70a4b6 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp @@ -6,7 +6,9 @@ #include "cuttlefish/host/commands/cvd/cli/command_request.h" #include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h" +#include "cuttlefish/host/commands/cvd/cli/selector/selector.h" #include "cuttlefish/host/commands/cvd/cli/types.h" +#include "cuttlefish/host/commands/cvd/instances/instance_manager.h" #include "cuttlefish/result/result.h" namespace cuttlefish { @@ -20,10 +22,16 @@ usage: cvd [--group_name name [--instance_name name]] logs class CvdLogsHandler : public CvdCommandHandler { public: - CvdLogsHandler() = default; + CvdLogsHandler(InstanceManager& instance_manager) + : instance_manager_(instance_manager) {} Result Handle(const CommandRequest& request) override { CF_EXPECT(CanHandle(request)); + + auto [instance, unused] = + CF_EXPECT(selector::SelectInstance(instance_manager_, request), + "Unable to select an instance"); + std::cout << "hello, logs\n"; return {}; } @@ -37,12 +45,17 @@ class CvdLogsHandler : public CvdCommandHandler { Result DetailedHelp(const CommandRequest&) const override { return kDetailedHelpText; } + + private: + InstanceManager& instance_manager_; }; } // namespace -std::unique_ptr NewCvdLogsHandler() { - return std::unique_ptr(new CvdLogsHandler()); +std::unique_ptr NewCvdLogsHandler( + InstanceManager& instance_manager) { + return std::unique_ptr( + new CvdLogsHandler(instance_manager)); } } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.h b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.h index 75919c17717..0518f45fe80 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.h +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.h @@ -3,9 +3,11 @@ #include #include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h" +#include "cuttlefish/host/commands/cvd/instances/instance_manager.h" namespace cuttlefish { -std::unique_ptr NewCvdLogsHandler(); +std::unique_ptr NewCvdLogsHandler( + InstanceManager& instance_manager); } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp index 5bb41fe092b..4c259c1bac9 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp @@ -116,7 +116,7 @@ RequestContext::RequestContext(InstanceManager& instance_manager, request_handlers_.emplace_back(NewCvdStartCommandHandler(instance_manager)); request_handlers_.emplace_back(NewCvdStatusCommandHandler(instance_manager)); request_handlers_.emplace_back(NewCvdVersionHandler()); - request_handlers_.emplace_back(NewCvdLogsHandler()); + request_handlers_.emplace_back(NewCvdLogsHandler(instance_manager)); } Result RequestContext::Handler( From 3e64a24f34487549f37d24dc4b46db3de6e68fc5 Mon Sep 17 00:00:00 2001 From: Sergio Andres Rodriguez Orama Date: Wed, 29 Apr 2026 17:21:38 -0400 Subject: [PATCH 3/7] List available log names. Bug: b/507588992 --- .../commands/cvd/cli/commands/BUILD.bazel | 3 ++ .../host/commands/cvd/cli/commands/logs.cpp | 30 +++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel index 32f2bca3181..ae886b15c17 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel @@ -459,11 +459,14 @@ cf_cc_library( srcs = ["logs.cpp"], hdrs = ["logs.h"], deps = [ + "//cuttlefish/common/libs/utils:files", "//cuttlefish/host/commands/cvd/cli:command_request", "//cuttlefish/host/commands/cvd/cli:types", "//cuttlefish/host/commands/cvd/cli/commands:command_handler", "//cuttlefish/host/commands/cvd/cli/selector", "//cuttlefish/host/commands/cvd/instances:instance_manager", "//cuttlefish/result", + "//libbase", + "@abseil-cpp//absl/log", ], ) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp index 4d75a70a4b6..e2b84cc7239 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp @@ -1,9 +1,14 @@ #include "cuttlefish/host/commands/cvd/cli/commands/logs.h" +#include + +#include #include #include #include +#include "absl/log/log.h" +#include "cuttlefish/common/libs/utils/files.h" #include "cuttlefish/host/commands/cvd/cli/command_request.h" #include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h" #include "cuttlefish/host/commands/cvd/cli/selector/selector.h" @@ -28,11 +33,32 @@ class CvdLogsHandler : public CvdCommandHandler { Result Handle(const CommandRequest& request) override { CF_EXPECT(CanHandle(request)); - auto [instance, unused] = + auto [instance, _] = CF_EXPECT(selector::SelectInstance(instance_manager_, request), "Unable to select an instance"); - std::cout << "hello, logs\n"; + std::string dir = instance.instance_dir(); + std::string logs_dir = dir + "/logs"; + if (!FileExists(logs_dir)) { + VLOG(0) << "Logs directory `" << logs_dir << "` does not exist."; + LOG(INFO) << "There are no logs files available"; + return {}; + } + CF_EXPECT(IsDirectory(logs_dir)); + + auto callback = [](const std::string& filename) -> Result { + constexpr int kMaxPadding = 30; + std::string basename = android::base::Basename(filename); + std::cout << basename; + std::cout << std::string( + std::max(int(kMaxPadding - basename.length()), 1), ' '); + std::cout << filename; + std::cout << std::endl; + return {}; + }; + + CF_EXPECT(WalkDirectory(logs_dir, callback)); + return {}; } From d27754f73c597c1ea6e73249537371fd5209d289 Mon Sep 17 00:00:00 2001 From: Sergio Andres Rodriguez Orama Date: Thu, 30 Apr 2026 13:30:04 -0400 Subject: [PATCH 4/7] Print given log name. Bug: b/507588992 --- .../commands/cvd/cli/commands/BUILD.bazel | 1 + .../host/commands/cvd/cli/commands/logs.cpp | 64 +++++++++++++++---- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel index ae886b15c17..7f6b9c60907 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel @@ -460,6 +460,7 @@ cf_cc_library( hdrs = ["logs.h"], deps = [ "//cuttlefish/common/libs/utils:files", + "//cuttlefish/common/libs/utils:flag_parser", "//cuttlefish/host/commands/cvd/cli:command_request", "//cuttlefish/host/commands/cvd/cli:types", "//cuttlefish/host/commands/cvd/cli/commands:command_handler", diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp index e2b84cc7239..fa58e92732f 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/logs.cpp @@ -1,14 +1,19 @@ #include "cuttlefish/host/commands/cvd/cli/commands/logs.h" #include +#include #include +#include +#include #include #include #include +#include #include "absl/log/log.h" #include "cuttlefish/common/libs/utils/files.h" +#include "cuttlefish/common/libs/utils/flag_parser.h" #include "cuttlefish/host/commands/cvd/cli/command_request.h" #include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h" #include "cuttlefish/host/commands/cvd/cli/selector/selector.h" @@ -22,9 +27,45 @@ namespace { constexpr char kSummaryHelpText[] = "Print device logs"; constexpr char kDetailedHelpText[] = R"( -usage: cvd [--group_name name [--instance_name name]] logs +usage: cvd [--group_name name [--instance_name name]] logs [-p|--print name] )"; +struct LogsCmdOptions { + std::string print_target; + + std::vector Flags() { + return { + GflagsCompatFlag("print", print_target) + .Alias({FlagAliasMode::kFlagConsumesFollowing, "-p"}), + }; + } +}; + +Result PrintLogsList(const std::string& dir) { + auto callback = [](const std::string& filename) -> Result { + std::string basename = android::base::Basename(filename); + std::cout << basename; + std::cout << " "; + if (isatty(STDOUT_FILENO)) { + constexpr int kMaxPadding = 30; + // Add more spaces for a clear two column view printing to a terminal. + std::cout << std::string( + std::max(int(kMaxPadding - basename.length()), 1), ' '); + } + std::cout << filename; + std::cout << std::endl; + return {}; + }; + CF_EXPECT(WalkDirectory(dir, callback)); + return {}; +} + +Result PrintLog(const std::string& filename) { + const char* exec_name = isatty(STDOUT_FILENO) ? "less" : "cat"; + execlp(exec_name, exec_name, filename.c_str(), nullptr); + return CF_ERR("execlp failed: " << strerror(errno)); +} + class CvdLogsHandler : public CvdCommandHandler { public: CvdLogsHandler(InstanceManager& instance_manager) @@ -37,6 +78,10 @@ class CvdLogsHandler : public CvdCommandHandler { CF_EXPECT(selector::SelectInstance(instance_manager_, request), "Unable to select an instance"); + LogsCmdOptions opts; + std::vector args = request.SubcommandArguments(); + CF_EXPECT(ConsumeFlags(opts.Flags(), args)); + std::string dir = instance.instance_dir(); std::string logs_dir = dir + "/logs"; if (!FileExists(logs_dir)) { @@ -46,19 +91,14 @@ class CvdLogsHandler : public CvdCommandHandler { } CF_EXPECT(IsDirectory(logs_dir)); - auto callback = [](const std::string& filename) -> Result { - constexpr int kMaxPadding = 30; - std::string basename = android::base::Basename(filename); - std::cout << basename; - std::cout << std::string( - std::max(int(kMaxPadding - basename.length()), 1), ' '); - std::cout << filename; - std::cout << std::endl; + if (opts.print_target.empty()) { + CF_EXPECT(PrintLogsList(logs_dir)); return {}; - }; - - CF_EXPECT(WalkDirectory(logs_dir, callback)); + } + std::string print_target = logs_dir + "/" + opts.print_target; + CF_EXPECT(FileExists(print_target)); + CF_EXPECT(PrintLog(print_target)); return {}; } From 7e1d2eccbc1d72ff92b555cc15a6adbdfedc9d31 Mon Sep 17 00:00:00 2001 From: Sergio Andres Rodriguez Orama Date: Thu, 30 Apr 2026 15:29:54 -0400 Subject: [PATCH 5/7] Split output between stdout and stderr Bug: b/507588992 --- e2etests/cvd/bugreport_tests/main_test.go | 2 +- e2etests/cvd/common/common.go | 50 ++++++++++--------- e2etests/cvd/cvd_powerwash_tests/main_test.go | 6 +-- e2etests/cvd/display_tests/main_test.go | 4 +- e2etests/cvd/env_tests/main_test.go | 2 +- .../cvd/graphics_detector_tests/main_test.go | 2 +- e2etests/cvd/media_tests/main_test.go | 4 +- e2etests/cvd/metrics_tests/main_test.go | 2 +- 8 files changed, 37 insertions(+), 35 deletions(-) diff --git a/e2etests/cvd/bugreport_tests/main_test.go b/e2etests/cvd/bugreport_tests/main_test.go index a1d9c5d02c0..5394d4ec2aa 100644 --- a/e2etests/cvd/bugreport_tests/main_test.go +++ b/e2etests/cvd/bugreport_tests/main_test.go @@ -39,7 +39,7 @@ func TestTakeBugreport(t *testing.T) { t.Fatal(err) } - if _, err := c.RunCmd(c.TargetBin(), "host_bugreport", "--output=/tmp/host_bugreport.zip", "--include_adb_bugreport=true"); err != nil { + if _, _, err := c.RunCmd(c.TargetBin(), "host_bugreport", "--output=/tmp/host_bugreport.zip", "--include_adb_bugreport=true"); err != nil { t.Fatal(err) } diff --git a/e2etests/cvd/common/common.go b/e2etests/cvd/common/common.go index 907175e2227..e3efe2c0e86 100644 --- a/e2etests/cvd/common/common.go +++ b/e2etests/cvd/common/common.go @@ -79,13 +79,15 @@ type TestContext struct { usePodcvd bool } -func runCmdWithContextEnv(ctx context.Context, command []string, envvars map[string]string) (string, error) { +func runCmdWithContextEnv(ctx context.Context, command []string, envvars map[string]string) (string, string, error) { cmd := exec.CommandContext(ctx, command[0], command[1:]...) - cmdOutputBuf := bytes.Buffer{} - cmdWriter := io.MultiWriter(&cmdOutputBuf, log.Writer()) - cmd.Stdout = cmdWriter - cmd.Stderr = cmdWriter + stdOutBuf := bytes.Buffer{} + stdErrBuf := bytes.Buffer{} + stdOutWriter := io.MultiWriter(&stdOutBuf, log.Writer()) + stdErrWriter := io.MultiWriter(&stdErrBuf, log.Writer()) + cmd.Stdout = stdOutWriter + cmd.Stderr = stdErrWriter envvarPairs := []string{} for k, v := range envvars { @@ -97,14 +99,14 @@ func runCmdWithContextEnv(ctx context.Context, command []string, envvars map[str log.Printf("Running `%s %s`\n", strings.Join(envvarPairs, " "), strings.Join(command, " ")) err := cmd.Run() if err != nil { - return "", fmt.Errorf("`%s` failed: %w", strings.Join(command, " "), err) + return "", "", fmt.Errorf("`%s` failed: %w", strings.Join(command, " "), err) } - return cmdOutputBuf.String(), nil + return stdOutBuf.String(), stdErrBuf.String(), nil } // Runs the given command with the given set of envvars overrided. -func (tc *TestContext) RunCmdWithEnv(command []string, envvars map[string]string) (string, error) { +func (tc *TestContext) RunCmdWithEnv(command []string, envvars map[string]string) (string, string, error) { return runCmdWithContextEnv(tc.context, command, envvars) } @@ -117,14 +119,14 @@ func (tc *TestContext) RunAdbWaitForDevice() error { "adb", "wait-for-device", } - if _, err := tc.RunCmd(adbCommand...); err != nil { + if _, _, err := tc.RunCmd(adbCommand...); err != nil { return fmt.Errorf("timed out waiting for Cuttlefish device to connect to adb: %w", err) } return nil } // Runs the given command with the existing envvars. -func (tc *TestContext) RunCmd(args ...string) (string, error) { +func (tc *TestContext) RunCmd(args ...string) (string, string, error) { command := []string{} command = append(command, args...) return tc.RunCmdWithEnv(command, map[string]string{}) @@ -174,7 +176,7 @@ func (tc *TestContext) CVDFetch(args FetchArgs) error { if credentialArg != "" { fetchCmd = append(fetchCmd, fmt.Sprintf("--credential_source=%s", credentialArg)) } - if _, err := tc.RunCmd(fetchCmd...); err != nil { + if _, _, err := tc.RunCmd(fetchCmd...); err != nil { log.Printf("Failed to fetch: %w", err) return err } @@ -202,7 +204,7 @@ func (tc *TestContext) CVDCreate(args CreateArgs) error { if len(args.Args) > 0 { createCmd = append(createCmd, args.Args...) } - if _, err := tc.RunCmdWithEnv(createCmd, tempdirEnv); err != nil { + if _, _, err := tc.RunCmdWithEnv(createCmd, tempdirEnv); err != nil { log.Printf("Failed to create instance(s): %w", err) return err } @@ -218,7 +220,7 @@ func (tc *TestContext) CVDStop() error { } stopCmd := []string{tc.TargetBin(), "stop"} - if _, err := tc.RunCmdWithEnv(stopCmd, tempdirEnv); err != nil { + if _, _, err := tc.RunCmdWithEnv(stopCmd, tempdirEnv); err != nil { log.Printf("Failed to stop instance(s): %w", err) return err } @@ -238,7 +240,7 @@ func (tc *TestContext) LaunchCVD(args CreateArgs) error { if len(args.Args) > 0 { createCmd = append(createCmd, args.Args...) } - if _, err := tc.RunCmdWithEnv(createCmd, tempdirEnv); err != nil { + if _, _, err := tc.RunCmdWithEnv(createCmd, tempdirEnv); err != nil { log.Printf("Failed to create instance(s): %w", err) return err } @@ -254,7 +256,7 @@ func (tc *TestContext) StopCVD() error { } stopCmd := []string{"bin/stop_cvd"} - if _, err := tc.RunCmdWithEnv(stopCmd, tempdirEnv); err != nil { + if _, _, err := tc.RunCmdWithEnv(stopCmd, tempdirEnv); err != nil { log.Printf("Failed to stop instance(s): %w", err) return err } @@ -269,7 +271,7 @@ func (tc *TestContext) CVDPowerwash() error { } createCmd := []string{tc.TargetBin(), "powerwash"} - if _, err := tc.RunCmdWithEnv(createCmd, tempdirEnv); err != nil { + if _, _, err := tc.RunCmdWithEnv(createCmd, tempdirEnv); err != nil { log.Printf("Failed to powerwash instance(s): %w", err) return err } @@ -304,7 +306,7 @@ func (tc *TestContext) CVDLoad(load LoadArgs) error { if credentialArg != "" { loadCmd = append(loadCmd, fmt.Sprintf("--credential_source=%s", credentialArg)) } - if _, err := tc.RunCmd(loadCmd...); err != nil { + if _, _, err := tc.RunCmd(loadCmd...); err != nil { log.Printf("Failed to perform `cvd load`: %w", err) return err } @@ -315,13 +317,13 @@ func (tc *TestContext) CVDLoad(load LoadArgs) error { } func (tc *TestContext) GetMetricsDir() (string, error) { - output, err := tc.RunCmd("cvd", "fleet") + stdOut, _, err := tc.RunCmd("cvd", "fleet") if err != nil { return "", fmt.Errorf("failed to run `cvd fleet`") } re := regexp.MustCompile(`"metrics_dir" : "(.*)",`) - matches := re.FindStringSubmatch(output) + matches := re.FindStringSubmatch(stdOut) if len(matches) != 2 { return "", fmt.Errorf("failed to find metrics directory") } @@ -344,7 +346,7 @@ func (tc *TestContext) SetUp(t *testing.T) { log.Printf("Initializing %s test...", tc.t.Name()) log.Printf("Cleaning up any pre-existing instances...") - if _, err := tc.RunCmd(tc.TargetBin(), "reset", "-y"); err != nil { + if _, _, err := tc.RunCmd(tc.TargetBin(), "reset", "-y"); err != nil { log.Printf("Failed to cleanup any pre-existing instances: %w", err) } log.Printf("Finished cleaning up any pre-existing instances!") @@ -426,7 +428,7 @@ func (tc *TestContext) TearDown() { matches, err := filepath.Glob(path.Join(tc.tempdir, pattern)) if err == nil { for _, file := range matches { - _, err := tc.RunCmd("cp", "--dereference", file, testoutdir) + _, _, err := tc.RunCmd("cp", "--dereference", file, testoutdir) if err != nil { log.Printf("failed to copy %s to %s: %w", file, testoutdir, err) } @@ -445,7 +447,7 @@ func (tc *TestContext) TearDown() { err := os.MkdirAll(outinstancedir, os.ModePerm) if err == nil { logdir := path.Join(instancedir, "logs") - _, err := runCmdWithContextEnv(context.TODO(), []string{"cp", "-r", "--dereference", logdir, outinstancedir}, map[string]string{}) + _, _, err := runCmdWithContextEnv(context.TODO(), []string{"cp", "-r", "--dereference", logdir, outinstancedir}, map[string]string{}) if err != nil { log.Printf("failed to copy %s to %s: %w", logdir, outinstancedir, err) } @@ -461,7 +463,7 @@ func (tc *TestContext) TearDown() { outmetricsdir := path.Join(testoutdir, "metrics_files") err := os.MkdirAll(outmetricsdir, os.ModePerm) if err == nil { - _, err := runCmdWithContextEnv(context.TODO(), []string{"cp", "-r", "--dereference", metricsdir, outmetricsdir}, map[string]string{}) + _, _, err := runCmdWithContextEnv(context.TODO(), []string{"cp", "-r", "--dereference", metricsdir, outmetricsdir}, map[string]string{}) if err != nil { log.Printf("failed to copy %s to %s: %w", metricsdir, outmetricsdir, err) } @@ -586,7 +588,7 @@ func RunXts(t *testing.T, cuttlefishArgs FetchAndCreateArgs, xtsArgs XtsArgs) { "--log-level-display=INFO", } xtsCommand = append(xtsCommand, xtsArgs.XtsArgs...) - if _, err := tc.RunCmd(xtsCommand...); err != nil { + if _, _, err := tc.RunCmd(xtsCommand...); err != nil { t.Fatalf("failed to fully run XTS: %w", err) } log.Printf("Finished running XTS!") diff --git a/e2etests/cvd/cvd_powerwash_tests/main_test.go b/e2etests/cvd/cvd_powerwash_tests/main_test.go index e5161ab00ed..c800b755698 100644 --- a/e2etests/cvd/cvd_powerwash_tests/main_test.go +++ b/e2etests/cvd/cvd_powerwash_tests/main_test.go @@ -53,11 +53,11 @@ func TestCvdPowerwash(t *testing.T) { } const tmpFile = "/data/local/tmp/foo" - if _, err := c.RunCmd("adb", "shell", "touch", tmpFile); err != nil { + if _, _, err := c.RunCmd("adb", "shell", "touch", tmpFile); err != nil { t.Fatalf("failed to create %s: %w", tmpFile, err) } - if _, err := c.RunCmd("adb", "shell", "stat", tmpFile); err != nil { + if _, _, err := c.RunCmd("adb", "shell", "stat", tmpFile); err != nil { t.Fatal("failed to verify %s created: %w", err) } @@ -65,7 +65,7 @@ func TestCvdPowerwash(t *testing.T) { t.Fatal(err) } - if _, err := c.RunCmd("adb", "shell", "stat", tmpFile); err == nil { + if _, _, err := c.RunCmd("adb", "shell", "stat", tmpFile); err == nil { t.Fatal("failed to powerwash, %s still exists") } }) diff --git a/e2etests/cvd/display_tests/main_test.go b/e2etests/cvd/display_tests/main_test.go index be15878fa4f..f56d065b6e9 100644 --- a/e2etests/cvd/display_tests/main_test.go +++ b/e2etests/cvd/display_tests/main_test.go @@ -35,7 +35,7 @@ func addDisplay(c e2etests.TestContext, t *testing.T) { t.Fatal(err) } - if _, err := c.RunCmd(c.TargetBin(), "display", "add", "--display=width=500,height=500"); err != nil { + if _, _, err := c.RunCmd(c.TargetBin(), "display", "add", "--display=width=500,height=500"); err != nil { t.Fatal(err) } } @@ -55,7 +55,7 @@ func listDisplays(c e2etests.TestContext, t *testing.T) { t.Fatal(err) } - if _, err := c.RunCmd(c.TargetBin(), "display", "list"); err != nil { + if _, _, err := c.RunCmd(c.TargetBin(), "display", "list"); err != nil { t.Fatal(err) } } diff --git a/e2etests/cvd/env_tests/main_test.go b/e2etests/cvd/env_tests/main_test.go index d304e256b90..aaf527b41b0 100644 --- a/e2etests/cvd/env_tests/main_test.go +++ b/e2etests/cvd/env_tests/main_test.go @@ -36,7 +36,7 @@ func TestListEnvServices(t *testing.T) { t.Fatal(err) } - if _, err := c.RunCmd(c.TargetBin(), "env", "ls"); err != nil { + if _, _, err := c.RunCmd(c.TargetBin(), "env", "ls"); err != nil { t.Fatal(err) } } diff --git a/e2etests/cvd/graphics_detector_tests/main_test.go b/e2etests/cvd/graphics_detector_tests/main_test.go index 5673ea9faed..e54a4f6d8f1 100644 --- a/e2etests/cvd/graphics_detector_tests/main_test.go +++ b/e2etests/cvd/graphics_detector_tests/main_test.go @@ -41,7 +41,7 @@ func TestLaunchingWithAutoEnablesGfxstream(t *testing.T) { t.Fatalf("failed to wait for Cuttlefish device to connect to adb: %w", err) } - output, err := c.RunCmd("adb", "shell", "getprop", "ro.hardware.egl") + output, _, err := c.RunCmd("adb", "shell", "getprop", "ro.hardware.egl") if err != nil { t.Fatalf("failed to get EGL sysprop: %w", err) } diff --git a/e2etests/cvd/media_tests/main_test.go b/e2etests/cvd/media_tests/main_test.go index b9a8bb12df4..74bb403a588 100644 --- a/e2etests/cvd/media_tests/main_test.go +++ b/e2etests/cvd/media_tests/main_test.go @@ -52,11 +52,11 @@ func TestEmulatedCameraV4l2Compliance(t *testing.T) { t.Fatalf("failed to wait for Cuttlefish device to connect to adb: %w", err) } - if _, err := c.RunCmd("adb", "shell", "su", "0", "v4l2-ctl", "--list-devices"); err != nil { + if _, _, err := c.RunCmd("adb", "shell", "su", "0", "v4l2-ctl", "--list-devices"); err != nil { t.Fatalf("v4l2-ctl --list-devices failed: %w", err) } - if _, err := c.RunCmd("adb", "shell", "su", "0", "v4l2-compliance", "-d1", "-s"); err != nil { + if _, _, err := c.RunCmd("adb", "shell", "su", "0", "v4l2-compliance", "-d1", "-s"); err != nil { t.Fatalf("v4l2-compliance failed: %w", err) } }) diff --git a/e2etests/cvd/metrics_tests/main_test.go b/e2etests/cvd/metrics_tests/main_test.go index 33395e462df..46d03ec3452 100644 --- a/e2etests/cvd/metrics_tests/main_test.go +++ b/e2etests/cvd/metrics_tests/main_test.go @@ -50,7 +50,7 @@ func TestMetrics(t *testing.T) { var metricsdir string err := func() error { - output, err := c.RunCmd(c.TargetBin(), "fleet") + output, _, err := c.RunCmd(c.TargetBin(), "fleet") if err != nil { return fmt.Errorf("failed to run `cvd fleet`") } From dad04533e3dfd286071052834a23a1dd6da4a195 Mon Sep 17 00:00:00 2001 From: Sergio Andres Rodriguez Orama Date: Thu, 30 Apr 2026 15:31:03 -0400 Subject: [PATCH 6/7] Add cvd logs e2e test. Bug: b/507588992 --- e2etests/cvd/logs_tests/BUILD.bazel | 18 ++++++ e2etests/cvd/logs_tests/main_test.go | 87 ++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 e2etests/cvd/logs_tests/BUILD.bazel create mode 100644 e2etests/cvd/logs_tests/main_test.go diff --git a/e2etests/cvd/logs_tests/BUILD.bazel b/e2etests/cvd/logs_tests/BUILD.bazel new file mode 100644 index 00000000000..1bf7c8b0653 --- /dev/null +++ b/e2etests/cvd/logs_tests/BUILD.bazel @@ -0,0 +1,18 @@ +load("@rules_go//go:def.bzl", "go_test") + +go_test( + name = "logs_tests", + size = "large", + srcs = ["main_test.go"], + data = ["//:debian_substitution_marker"], + env = {"LOCAL_DEBIAN_SUBSTITUTION_MARKER_FILE": "$(rlocationpath //:debian_substitution_marker)"}, + tags = [ + "exclusive", + "external", + "no-sandbox", + "supports-graceful-termination", + ], + deps = [ + "//cvd/common", + ], +) diff --git a/e2etests/cvd/logs_tests/main_test.go b/e2etests/cvd/logs_tests/main_test.go new file mode 100644 index 00000000000..4ad790f715f --- /dev/null +++ b/e2etests/cvd/logs_tests/main_test.go @@ -0,0 +1,87 @@ +// Copyright (C) 2026 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "slices" + "strings" + "testing" + + "github.com/google/android-cuttlefish/e2etests/cvd/common" +) + +func TestPrintLogs(t *testing.T) { + testcases := []struct { + branch string + target string + }{ + { + branch: "git_main", + target: "aosp_cf_x86_64_only_phone-trunk_staging-userdebug", + }, + } + c := e2etests.TestContext{} + for _, tc := range testcases { + t.Run("foo", func(t *testing.T) { + c.SetUp(t) + defer c.TearDown() + + if err := c.CVDFetch(e2etests.FetchArgs{ + DefaultBuildBranch: tc.branch, + DefaultBuildTarget: tc.target, + }); err != nil { + t.Fatal(err) + } + + if err := c.CVDCreate(e2etests.CreateArgs{}); err != nil { + t.Fatal(err) + } + + stdOut, _, err := c.RunCmd(c.TargetBin(), "logs") + if err != nil { + t.Fatal(err) + } + + listedNames := []string{} + lines := strings.Split(stdOut, "\n") + for _, line := range lines[:len(lines)-1] { + fields := strings.Fields(line) + if len(fields) != 2 { + t.Errorf("line %q does not match format ' '", line) + } + _, stderr, err := c.RunCmd("stat", fields[1]) + if err != nil { + t.Fatal(stderr) + } + listedNames = append(listedNames, fields[0]) + } + keyExpectedNames := []string{"logcat", "launcher.log", "kernel.log"} + for _, name := range keyExpectedNames { + if !slices.Contains(listedNames, name) { + t.Fatalf("missing log name: %q", name) + } + } + + stdOut, _, err = c.RunCmd(c.TargetBin(), "logs", "--print", "launcher.log") + if err != nil { + t.Fatal(err) + } + if len(stdOut) == 0 { + t.Fatalf("empty launcher.log") + } + + }) + } +} From c872b00626d3b8d20f36b379383393170499b029 Mon Sep 17 00:00:00 2001 From: Sergio Andres Rodriguez Orama Date: Tue, 5 May 2026 16:58:28 -0400 Subject: [PATCH 7/7] Adds file pager `less` tool as a cuttlefish-base depenency. - The pager is used by the cvd logs subcommand. Bug: b/507588992 --- base/debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/base/debian/control b/base/debian/control index 5f3271ab17e..dbbadc1cf06 100644 --- a/base/debian/control +++ b/base/debian/control @@ -43,6 +43,7 @@ Depends: adduser, grub-efi-amd64-bin [!arm64], iproute2, iptables, + less, libarchive-tools | bsdtar, libcap2-bin, libcurl4,