/* * Copyright (C) 2019 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. */ #define LOG_TAG "car-bugreportd" #include <android-base/errors.h> #include <android-base/file.h> #include <android-base/logging.h> #include <android-base/macros.h> #include <android-base/properties.h> #include <android-base/stringprintf.h> #include <android-base/strings.h> #include <android-base/unique_fd.h> #include <cutils/sockets.h> #include <errno.h> #include <fcntl.h> #include <ftw.h> #include <gui/SurfaceComposerClient.h> #include <log/log_main.h> #include <private/android_filesystem_config.h> #include <stdio.h> #include <stdlib.h> #include <sys/prctl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/wait.h> #include <time.h> #include <unistd.h> #include <ziparchive/zip_writer.h> #include <chrono> #include <string> #include <vector> namespace { // Directory used for keeping temporary files constexpr const char* kTempDirectory = "/data/user_de/0/com.android.shell/temp_bugreport_files"; // Socket to write the progress information. constexpr const char* kCarBrProgressSocket = "car_br_progress_socket"; // Socket to write the zipped bugreport file. constexpr const char* kCarBrOutputSocket = "car_br_output_socket"; // Socket to write the extra bugreport zip file. This zip file contains data that does not exist // in bugreport file generated by dumpstate. constexpr const char* kCarBrExtraOutputSocket = "car_br_extra_output_socket"; // The prefix used by bugreportz protocol to indicate bugreport finished successfully. constexpr const char* kOkPrefix = "OK:"; // Number of connect attempts to dumpstate socket constexpr const int kMaxDumpstateConnectAttempts = 20; // Wait time between connect attempts constexpr const int kWaitTimeBetweenConnectAttemptsInSec = 1; // Wait time for dumpstate. No timeout in dumpstate is longer than 60 seconds. Choose // a value that is twice longer. constexpr const int kDumpstateTimeoutInSec = 120; // The prefix for screenshot filename in the generated zip file. constexpr const char* kScreenshotPrefix = "/screenshot"; using android::OK; using android::PhysicalDisplayId; using android::status_t; using android::SurfaceComposerClient; // Returns a valid socket descriptor or -1 on failure. int openSocket(const char* service) { int s = android_get_control_socket(service); if (s < 0) { ALOGE("android_get_control_socket(%s): %s", service, strerror(errno)); return -1; } fcntl(s, F_SETFD, FD_CLOEXEC); if (listen(s, 4) < 0) { ALOGE("listen(control socket): %s", strerror(errno)); return -1; } struct sockaddr addr; socklen_t alen = sizeof(addr); int fd = accept(s, &addr, &alen); if (fd < 0) { ALOGE("accept(control socket): %s", strerror(errno)); return -1; } return fd; } // Processes the given dumpstate progress protocol |line| and updates // |out_last_nonempty_line| when |line| is non-empty, and |out_zip_path| when // the bugreport is finished. void processLine(const std::string& line, std::string* out_zip_path, std::string* out_last_nonempty_line) { // The protocol is documented in frameworks/native/cmds/bugreportz/readme.md if (line.empty()) { return; } *out_last_nonempty_line = line; if (line.find(kOkPrefix) != 0) { return; } *out_zip_path = line.substr(strlen(kOkPrefix)); return; } // Sends the contents of the zip fileto |outfd|. // Returns true if success void zipFilesToFd(const std::vector<std::string>& extra_files, int outfd) { // pass fclose as Deleter to close the file when unique_ptr is destroyed. std::unique_ptr<FILE, decltype(fclose)*> outfile = {fdopen(outfd, "wb"), fclose}; if (outfile == nullptr) { ALOGE("Failed to open output descriptor"); return; } auto writer = std::make_unique<ZipWriter>(outfile.get()); int error = 0; for (const auto& filepath : extra_files) { const auto name = android::base::Basename(filepath); error = writer->StartEntry(name.c_str(), 0); if (error) { ALOGE("Failed to start entry %s", writer->ErrorCodeString(error)); return; } android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(filepath.c_str(), O_RDONLY))); if (fd == -1) { return; } while (1) { char buffer[65536]; ssize_t bytes_read = TEMP_FAILURE_RETRY(read(fd, buffer, sizeof(buffer))); if (bytes_read == 0) { break; } if (bytes_read == -1) { if (errno == EAGAIN) { ALOGE("timed out while reading %s", name.c_str()); } else { ALOGE("read terminated abnormally (%s)", strerror(errno)); } // fail immediately return; } error = writer->WriteBytes(buffer, bytes_read); if (error) { ALOGE("WriteBytes() failed %s", ZipWriter::ErrorCodeString(error)); // fail immediately return; } } error = writer->FinishEntry(); if (error) { ALOGE("failed to finish entry %s", writer->ErrorCodeString(error)); continue; } } error = writer->Finish(); if (error) { ALOGE("failed to finish zip writer %s", writer->ErrorCodeString(error)); } } int copyTo(int fd_in, int fd_out, void* buffer, size_t buffer_len) { ssize_t bytes_read = TEMP_FAILURE_RETRY(read(fd_in, buffer, buffer_len)); if (bytes_read == 0) { return 0; } if (bytes_read == -1) { // EAGAIN really means time out, so make that clear. if (errno == EAGAIN) { ALOGE("read timed out"); } else { ALOGE("read terminated abnormally (%s)", strerror(errno)); } return -1; } // copy all bytes to the output socket if (!android::base::WriteFully(fd_out, buffer, bytes_read)) { ALOGE("write failed"); return -1; } return bytes_read; } bool copyFile(const std::string& zip_path, int output_socket) { android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(zip_path.c_str(), O_RDONLY))); if (fd == -1) { return false; } while (1) { char buffer[65536]; int bytes_copied = copyTo(fd, output_socket, buffer, sizeof(buffer)); if (bytes_copied == 0) { break; } if (bytes_copied == -1) { return false; } } return true; } // Triggers a bugreport and waits until it is all collected. // returns false if error, true if success bool doBugreport(int progress_socket, size_t* out_bytes_written, std::string* zip_path) { // Socket will not be available until service starts. android::base::unique_fd s; for (int i = 0; i < kMaxDumpstateConnectAttempts; i++) { s.reset(socket_local_client("dumpstate", ANDROID_SOCKET_NAMESPACE_RESERVED, SOCK_STREAM)); if (s != -1) break; sleep(kWaitTimeBetweenConnectAttemptsInSec); } if (s == -1) { ALOGE("failed to connect to dumpstatez service"); return false; } // Set a timeout so that if nothing is read by the timeout, stop reading and quit struct timeval tv = { .tv_sec = kDumpstateTimeoutInSec, .tv_usec = 0, }; if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) != 0) { ALOGW("Cannot set socket timeout (%s)", strerror(errno)); } std::string line; std::string last_nonempty_line; while (true) { char buffer[65536]; ssize_t bytes_read = copyTo(s, progress_socket, buffer, sizeof(buffer)); if (bytes_read == 0) { break; } if (bytes_read == -1) { return false; } // Process the buffer line by line. this is needed for the filename. for (int i = 0; i < bytes_read; i++) { char c = buffer[i]; if (c == '\n') { processLine(line, zip_path, &last_nonempty_line); line.clear(); } else { line.append(1, c); } } *out_bytes_written += bytes_read; } s.reset(); // Process final line, in case it didn't finish with newline. processLine(line, zip_path, &last_nonempty_line); // if doBugReport finished successfully, zip path should be set. if (zip_path->empty()) { ALOGE("no zip file path was found in bugreportz progress data"); return false; } return true; } bool waitpid_with_timeout(pid_t pid, int timeout_secs, int* status) { sigset_t child_mask, old_mask; sigemptyset(&child_mask); sigaddset(&child_mask, SIGCHLD); if (sigprocmask(SIG_BLOCK, &child_mask, &old_mask) == -1) { ALOGE("*** sigprocmask failed: %s\n", strerror(errno)); return false; } timespec ts = {.tv_sec = timeout_secs, .tv_nsec = 0}; int ret = TEMP_FAILURE_RETRY(sigtimedwait(&child_mask, nullptr, &ts)); int saved_errno = errno; // Set the signals back the way they were. if (sigprocmask(SIG_SETMASK, &old_mask, nullptr) == -1) { ALOGE("*** sigprocmask failed: %s\n", strerror(errno)); if (ret == 0) { return false; } } if (ret == -1) { errno = saved_errno; if (errno == EAGAIN) { errno = ETIMEDOUT; } else { ALOGE("*** sigtimedwait failed: %s\n", strerror(errno)); } return false; } pid_t child_pid = waitpid(pid, status, WNOHANG); if (child_pid != pid) { if (child_pid != -1) { ALOGE("*** Waiting for pid %d, got pid %d instead\n", pid, child_pid); } else { ALOGE("*** waitpid failed: %s\n", strerror(errno)); } return false; } return true; } // Runs the given command. Kills the command if it does not finish by timeout. int runCommand(int timeout_secs, const char* file, std::vector<const char*> args) { pid_t pid = fork(); // handle error case if (pid < 0) { ALOGE("fork failed %s", strerror(errno)); return pid; } // handle child case if (pid == 0) { /* make sure the child dies when parent dies */ prctl(PR_SET_PDEATHSIG, SIGKILL); /* just ignore SIGPIPE, will go down with parent's */ struct sigaction sigact; memset(&sigact, 0, sizeof(sigact)); sigact.sa_handler = SIG_IGN; sigaction(SIGPIPE, &sigact, nullptr); execvp(file, (char**)args.data()); // execvp's result will be handled after waitpid_with_timeout() below, but // if it failed, it's safer to exit dumpstate. ALOGE("execvp on command %s failed (error: %s)", file, strerror(errno)); _exit(EXIT_FAILURE); } // handle parent case int status; bool ret = waitpid_with_timeout(pid, timeout_secs, &status); if (!ret) { if (errno == ETIMEDOUT) { ALOGE("command %s timed out (killing pid %d)", file, pid); } else { ALOGE("command %s: Error (killing pid %d)\n", file, pid); } kill(pid, SIGTERM); if (!waitpid_with_timeout(pid, 5, nullptr)) { kill(pid, SIGKILL); if (!waitpid_with_timeout(pid, 5, nullptr)) { ALOGE("could not kill command '%s' (pid %d) even with SIGKILL.\n", file, pid); } } return -1; } if (WIFSIGNALED(status)) { ALOGE("command '%s' failed: killed by signal %d\n", file, WTERMSIG(status)); } else if (WIFEXITED(status) && WEXITSTATUS(status) > 0) { status = WEXITSTATUS(status); ALOGE("command '%s' failed: exit code %d\n", file, status); } return status; } void takeScreenshotForDisplayId(PhysicalDisplayId id, const char* tmp_dir, std::vector<std::string>* extra_files) { std::string id_as_string = std::to_string(id); std::string filename = std::string(tmp_dir) + kScreenshotPrefix + id_as_string + ".png"; std::vector<const char*> args { "-p", "-d", id_as_string.c_str(), filename.c_str(), nullptr }; ALOGI("capturing screen for display (%s) as %s", id_as_string.c_str(), filename.c_str()); int status = runCommand(10, "/system/bin/screencap", args); if (status == 0) { LOG(INFO) << "Screenshot saved for display:" << id_as_string; } // add the file regardless of the exit status of the screencap util. extra_files->push_back(filename); LOG(ERROR) << "Failed to take screenshot for display:" << id_as_string; } void takeScreenshot(const char* tmp_dir, std::vector<std::string>* extra_files) { // Now send the screencaptures std::vector<PhysicalDisplayId> ids = SurfaceComposerClient::getPhysicalDisplayIds(); for (PhysicalDisplayId display_id : ids) { takeScreenshotForDisplayId(display_id, tmp_dir, extra_files); } } bool recursiveRemoveDir(const std::string& path) { auto callback = [](const char* child, const struct stat*, int file_type, struct FTW*) -> int { if (file_type == FTW_DP) { if (rmdir(child) == -1) { ALOGE("rmdir(%s): %s", child, strerror(errno)); return -1; } } else if (file_type == FTW_F) { if (unlink(child) == -1) { ALOGE("unlink(%s): %s", child, strerror(errno)); return -1; } } return 0; }; // do a file tree walk with a sufficiently large depth. return nftw(path.c_str(), callback, 128, FTW_DEPTH) == 0; } status_t createTempDir(const char* dir) { struct stat sb; if (TEMP_FAILURE_RETRY(stat(dir, &sb)) == 0) { if (!recursiveRemoveDir(dir)) { return -errno; } } else if (errno != ENOENT) { ALOGE("Failed to stat %s ", dir); return -errno; } if (TEMP_FAILURE_RETRY(mkdir(dir, 0700)) == -1) { ALOGE("Failed to mkdir %s", dir); return -errno; } return OK; } // Removes bugreport void cleanupBugreportFile(const std::string& zip_path) { if (unlink(zip_path.c_str()) != 0) { ALOGE("Could not unlink %s (%s)", zip_path.c_str(), strerror(errno)); } } } // namespace int main(void) { ALOGI("Starting bugreport collecting service"); auto t0 = std::chrono::steady_clock::now(); std::vector<std::string> extra_files; if (createTempDir(kTempDirectory) == OK) { // take screenshots of the physical displays as early as possible takeScreenshot(kTempDirectory, &extra_files); } // Start the dumpstatez service. android::base::SetProperty("ctl.start", "car-dumpstatez"); size_t bytes_written = 0; std::string zip_path; int progress_socket = openSocket(kCarBrProgressSocket); if (progress_socket < 0) { // early out. in this case we will not print the final message, but that is ok. android::base::SetProperty("ctl.stop", "car-dumpstatez"); return EXIT_FAILURE; } bool ret_val = doBugreport(progress_socket, &bytes_written, &zip_path); close(progress_socket); int output_socket = openSocket(kCarBrOutputSocket); if (output_socket != -1 && ret_val) { ret_val = copyFile(zip_path, output_socket); } if (output_socket != -1) { close(output_socket); } int extra_output_socket = openSocket(kCarBrExtraOutputSocket); if (extra_output_socket != -1 && ret_val) { zipFilesToFd(extra_files, extra_output_socket); } if (extra_output_socket != -1) { close(extra_output_socket); } auto delta = std::chrono::duration_cast<std::chrono::duration<double>>( std::chrono::steady_clock::now() - t0) .count(); std::string result = ret_val ? "success" : "failed"; ALOGI("bugreport %s in %.02fs, %zu bytes written", result.c_str(), delta, bytes_written); cleanupBugreportFile(zip_path); recursiveRemoveDir(kTempDirectory); // No matter how doBugreport() finished, let's try to explicitly stop // car-dumpstatez in case it stalled. android::base::SetProperty("ctl.stop", "car-dumpstatez"); return ret_val ? EXIT_SUCCESS : EXIT_FAILURE; }