Add screencapture feature to bugreporting
Dumpstate does not collect screenshots of the physical displays other
than the primary one. Add logic to collect these because it is important
for Automotive use case.
Further, Android bugreports does not include screenshot of the primary
display in the bugreport (its api allows capturing it seperately). The
API in automotive combines all displays in a zip file for automotive.
Bug: 133368541
Test: Manual
Change-Id: I412fb55e2d0bd734b264014857ad80e4ec04870e
diff --git a/car-bugreportd/Android.bp b/car-bugreportd/Android.bp
index e026d0b..36420f0 100644
--- a/car-bugreportd/Android.bp
+++ b/car-bugreportd/Android.bp
@@ -29,7 +29,11 @@
],
shared_libs: [
"libbase",
- "liblog",
"libcutils",
+ "libgui",
+ "libhwui",
+ "liblog",
+ "libui",
+ "libziparchive",
],
}
diff --git a/car-bugreportd/car-bugreportd.rc b/car-bugreportd/car-bugreportd.rc
index 4c405e1..0935a60 100644
--- a/car-bugreportd/car-bugreportd.rc
+++ b/car-bugreportd/car-bugreportd.rc
@@ -1,6 +1,7 @@
service car-bugreportd /system/bin/car-bugreportd
socket car_br_progress_socket stream 0660 shell log
socket car_br_output_socket stream 0660 shell log
+ socket car_br_extra_output_socket stream 0660 shell log
class core
user shell
group log
diff --git a/car-bugreportd/main.cpp b/car-bugreportd/main.cpp
index 055d35a..f2ef87c 100644
--- a/car-bugreportd/main.cpp
+++ b/car-bugreportd/main.cpp
@@ -27,25 +27,34 @@
#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
@@ -55,6 +64,13 @@
// 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) {
@@ -96,6 +112,66 @@
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) {
@@ -182,6 +258,7 @@
line.append(1, c);
}
}
+ *out_bytes_written += bytes_read;
}
s.reset();
// Process final line, in case it didn't finish with newline.
@@ -194,6 +271,168 @@
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(int 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 (%d) as %s", id, 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) {
@@ -204,10 +443,16 @@
} // namespace
int main(void) {
- ALOGE("Starting bugreport collecting service");
+ 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");
@@ -231,6 +476,14 @@
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();
@@ -239,6 +492,8 @@
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");