|
| 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/bug.h" |
| 18 | + |
| 19 | +#include <fcntl.h> |
| 20 | +#include <algorithm> |
| 21 | +#include <cctype> |
| 22 | +#include <cstddef> |
| 23 | +#include <iostream> |
| 24 | +#include <memory> |
| 25 | +#include <optional> |
| 26 | +#include <string> |
| 27 | +#include <string_view> |
| 28 | +#include <utility> |
| 29 | +#include <vector> |
| 30 | + |
| 31 | +#include <fmt/format.h> |
| 32 | +#include <fmt/ranges.h> |
| 33 | + |
| 34 | +#include "cuttlefish/common/libs/fs/shared_fd.h" |
| 35 | +#include "cuttlefish/common/libs/utils/files.h" |
| 36 | +#include "cuttlefish/common/libs/utils/subprocess.h" |
| 37 | +#include "cuttlefish/common/libs/utils/subprocess_managed_stdio.h" |
| 38 | +#include "cuttlefish/common/libs/utils/users.h" |
| 39 | +#include "cuttlefish/host/commands/cvd/cli/command_request.h" |
| 40 | +#include "cuttlefish/host/commands/cvd/cli/commands/bugreport.h" |
| 41 | +#include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h" |
| 42 | +#include "cuttlefish/host/commands/cvd/cli/log_files.h" |
| 43 | +#include "cuttlefish/host/commands/cvd/cli/log_tail.h" |
| 44 | +#include "cuttlefish/host/commands/cvd/cli/types.h" |
| 45 | +#include "cuttlefish/host/commands/cvd/instances/instance_database.h" |
| 46 | +#include "cuttlefish/host/commands/cvd/instances/local_instance.h" |
| 47 | +#include "cuttlefish/host/commands/cvd/version/version.h" |
| 48 | +#include "cuttlefish/result/result.h" |
| 49 | + |
| 50 | +namespace cuttlefish { |
| 51 | +namespace { |
| 52 | + |
| 53 | +constexpr char kSummaryHelpText[] = "File an issue using go/bugged"; |
| 54 | +constexpr char kHelpMessage[] = R"( |
| 55 | +usage: cvd bug |
| 56 | +
|
| 57 | + `cvd bug` will invoke `bugged create` to file an issue against Cuttlefish. |
| 58 | + It requires `bugged` to be installed on the system. |
| 59 | + See go/bugged for more information. |
| 60 | +)"; |
| 61 | + |
| 62 | +Result<std::string> ParseBugId(std::string_view stdout_str) { |
| 63 | + const std::string_view prefix = "http://b/"; |
| 64 | + const size_t pos = stdout_str.find(prefix); |
| 65 | + CF_EXPECT(pos != std::string_view::npos, "Prefix not found"); |
| 66 | + |
| 67 | + const size_t start = pos + prefix.size(); |
| 68 | + size_t end = start; |
| 69 | + while (end < stdout_str.size() && std::isdigit(stdout_str[end])) { |
| 70 | + end++; |
| 71 | + } |
| 72 | + CF_EXPECT(end > start, "No digits found after prefix"); |
| 73 | + |
| 74 | + return std::string(stdout_str.substr(start, end - start)); |
| 75 | +} |
| 76 | + |
| 77 | +Result<LocalInstance> GetLatestLocalInstance( |
| 78 | + const InstanceDatabase& instance_db) { |
| 79 | + const std::vector<LocalInstanceGroup> groups = |
| 80 | + CF_EXPECT(instance_db.InstanceGroups()); |
| 81 | + CF_EXPECT(!groups.empty(), "No instance groups found."); |
| 82 | + |
| 83 | + const auto latest_group = std::max_element( |
| 84 | + groups.begin(), groups.end(), |
| 85 | + [](const LocalInstanceGroup& a, const LocalInstanceGroup& b) { |
| 86 | + return a.StartTime() < b.StartTime(); |
| 87 | + }); |
| 88 | + |
| 89 | + CF_EXPECT(!latest_group->Instances().empty(), |
| 90 | + "Latest instance group has no instances."); |
| 91 | + return latest_group->Instances().front(); |
| 92 | +} |
| 93 | + |
| 94 | +Result<std::string> GenerateIssueText() { |
| 95 | + const std::optional<std::string> previous_log_opt = |
| 96 | + CF_EXPECT(GetPreviousLogFile()); |
| 97 | + CF_EXPECT(previous_log_opt.has_value(), "No previous log file found."); |
| 98 | + const std::string previous_log = *previous_log_opt; |
| 99 | + |
| 100 | + const SharedFD fd = SharedFD::Open(previous_log, O_RDONLY); |
| 101 | + CF_EXPECTF(fd->IsOpen(), "Failed to open log file {}: {}", previous_log, |
| 102 | + fd->StrError()); |
| 103 | + |
| 104 | + const std::vector<std::string> lines = CF_EXPECTF( |
| 105 | + GetLastNLines(fd, 30), "Failed to read log file {}", previous_log); |
| 106 | + const std::string log_tail = |
| 107 | + fmt::format("```\n{}\n```\n", fmt::join(lines, "\n")); |
| 108 | + |
| 109 | + const std::string username = CF_EXPECT(CurrentUserName()); |
| 110 | + |
| 111 | + return fmt::format( |
| 112 | + "Cuttlefish bug report\n\n" |
| 113 | + "{}\n" |
| 114 | + "CVD Version:\n{}\n\n" |
| 115 | + "CC+=cloud-android-devs\n" |
| 116 | + "COMPONENT=162041\n" |
| 117 | + "HOTLIST+=1883485\n" |
| 118 | + "PRIORITY=P2\n" |
| 119 | + "REPORTER={}\n" |
| 120 | + "SEVERITY=S2\n" |
| 121 | + "STATUS=NEW\n" |
| 122 | + "TYPE=BUG\n", |
| 123 | + log_tail, GetVersionIds().ToPrettyString(), username); |
| 124 | +} |
| 125 | + |
| 126 | +Result<std::string> GetBuggedBinary() { |
| 127 | + CF_EXPECT(FileExists("/usr/bin/gcertstatus"), "Not a Googler desktop."); |
| 128 | + const int gcert_status = Execute({"/usr/bin/gcertstatus"}); |
| 129 | + CF_EXPECT(gcert_status == 0, "Please run gcert."); |
| 130 | + |
| 131 | + if (FileExists("/usr/bin/bugged")) { |
| 132 | + return "/usr/bin/bugged"; |
| 133 | + } |
| 134 | + return "/google/bin/releases/bugged/bugged"; |
| 135 | +} |
| 136 | + |
| 137 | +Result<void> ProduceBugreport(const InstanceDatabase& instance_db, |
| 138 | + const cvd_common::Envs& env) { |
| 139 | + const LocalInstance latest_instance = |
| 140 | + CF_EXPECT(GetLatestLocalInstance(instance_db)); |
| 141 | + const std::string android_host_out = latest_instance.host_artifacts_path(); |
| 142 | + const std::string home = latest_instance.home_directory(); |
| 143 | + const std::string log_dir = CvdUserLogDir(); |
| 144 | + |
| 145 | + CF_EXPECT(RunHostBugreportCommand(android_host_out, home, env, {}, log_dir)); |
| 146 | + return {}; |
| 147 | +} |
| 148 | + |
| 149 | +Result<std::string> FileIssue(const std::string& bugged_bin, |
| 150 | + const std::string& issue_text) { |
| 151 | + Command command(bugged_bin); |
| 152 | + command.AddParameter("create"); |
| 153 | + command.AddParameter("--format=MARKDOWN"); |
| 154 | + |
| 155 | + std::string stdout_str; |
| 156 | + const int exit_code = RunWithManagedStdio(std::move(command), &issue_text, |
| 157 | + &stdout_str, nullptr); |
| 158 | + CF_EXPECTF(exit_code == 0, "bugged exited with code {}", exit_code); |
| 159 | + |
| 160 | + const std::string bug_id = |
| 161 | + CF_EXPECTF(ParseBugId(stdout_str), |
| 162 | + "Failed to parse bug ID from bugged output: {}", stdout_str); |
| 163 | + return bug_id; |
| 164 | +} |
| 165 | + |
| 166 | +Result<void> AttachFile(const std::string& bugged_bin, |
| 167 | + const std::string& bug_id, |
| 168 | + const std::string& file_path) { |
| 169 | + if (FileExists(file_path)) { |
| 170 | + Command attach_cmd(bugged_bin); |
| 171 | + attach_cmd.AddParameter("attach"); |
| 172 | + attach_cmd.AddParameter(bug_id); |
| 173 | + attach_cmd.AddParameter(file_path); |
| 174 | + |
| 175 | + const int attach_status = |
| 176 | + RunWithManagedStdio(std::move(attach_cmd), nullptr, nullptr, nullptr); |
| 177 | + if (attach_status != 0) { |
| 178 | + std::cerr << "Failed to attach file " << file_path << " to bug " << bug_id |
| 179 | + << std::endl; |
| 180 | + } |
| 181 | + } else { |
| 182 | + std::cerr << "File " << file_path << " does not exist to attach." |
| 183 | + << std::endl; |
| 184 | + } |
| 185 | + return {}; |
| 186 | +} |
| 187 | + |
| 188 | +} // namespace |
| 189 | + |
| 190 | +CvdBugCommandHandler::CvdBugCommandHandler(const InstanceDatabase& instance_db) |
| 191 | + : instance_db_(instance_db) {} |
| 192 | + |
| 193 | +Result<void> CvdBugCommandHandler::Handle(const CommandRequest& request) { |
| 194 | + const std::string bugged_bin = CF_EXPECT(GetBuggedBinary()); |
| 195 | + const std::string issue_text = CF_EXPECT(GenerateIssueText()); |
| 196 | + CF_EXPECT(ProduceBugreport(instance_db_, request.Env())); |
| 197 | + const std::string bug_id = CF_EXPECT(FileIssue(bugged_bin, issue_text)); |
| 198 | + std::cout << "Created issue http://b/" << bug_id << std::endl; |
| 199 | + |
| 200 | + const std::string bugreport_zip = CvdUserLogDir() + "/host_bugreport.zip"; |
| 201 | + CF_EXPECT(AttachFile(bugged_bin, bug_id, bugreport_zip)); |
| 202 | + |
| 203 | + const std::optional<std::string> previous_log_opt = |
| 204 | + CF_EXPECT(GetPreviousLogFile()); |
| 205 | + if (previous_log_opt) { |
| 206 | + CF_EXPECT(AttachFile(bugged_bin, bug_id, *previous_log_opt)); |
| 207 | + } else { |
| 208 | + std::cerr << "No previous log file found to attach." << std::endl; |
| 209 | + } |
| 210 | + |
| 211 | + return {}; |
| 212 | +} |
| 213 | + |
| 214 | +std::vector<std::string> CvdBugCommandHandler::CmdList() const { |
| 215 | + return {"bug"}; |
| 216 | +} |
| 217 | + |
| 218 | +std::string CvdBugCommandHandler::SummaryHelp() const { |
| 219 | + return kSummaryHelpText; |
| 220 | +} |
| 221 | + |
| 222 | +Result<std::string> CvdBugCommandHandler::DetailedHelp(const CommandRequest&) { |
| 223 | + return kHelpMessage; |
| 224 | +} |
| 225 | + |
| 226 | +std::unique_ptr<CvdCommandHandler> NewCvdBugCommandHandler( |
| 227 | + const InstanceDatabase& instance_db) { |
| 228 | + return std::make_unique<CvdBugCommandHandler>(instance_db); |
| 229 | +} |
| 230 | + |
| 231 | +} // namespace cuttlefish |
0 commit comments