crash-reporter: Attach Chrome logs to crash reports.

When Chrome crashes, gather the last 20 lines of each of the
latest two log files in /var/log/chrome and
/home/chronos/user/log. Attach them as gzipped metadata
using a "chrome.txt" key name and rename the GPU error state
key to "i915_error_state.log.xz".

(Re-landing after privacy review; originally reviewed at
https://chromium-review.googlesource.com/216427. Changes
since previous iteration include attaching the Chrome logs
and the GPU state separately and redirecting stderr to
/dev/null when listing user Chrome log files to avoid log
spam in the not-logged-in case.)

BUG=chromium:405732
TEST=triggered crashes and verified that logs were included
     in crash reports: b7c4d7f2bee32e2f, 6292f090468fcf28
CQ-DEPEND=CL:242492

Change-Id: I1642a8971f1373726e5b0e3977dbfdbcc2aa6667
Reviewed-on: https://chromium-review.googlesource.com/242457
Commit-Queue: Dan Erat <derat@chromium.org>
Trybot-Ready: Dan Erat <derat@chromium.org>
Tested-by: Dan Erat <derat@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
diff --git a/crash_reporter/chrome_collector.cc b/crash_reporter/chrome_collector.cc
index 7138407..8dd56fb 100644
--- a/crash_reporter/chrome_collector.cc
+++ b/crash_reporter/chrome_collector.cc
@@ -20,16 +20,23 @@
 #include <chromeos/process.h>
 #include <chromeos/syslog_logging.h>
 
-const char kDefaultMinidumpName[] = "upload_file_minidump";
-const char kTarPath[] = "/bin/tar";
-// From //net/crash/collector/collector.h
-const int kDefaultMaxUploadBytes = 1024 * 1024;
-
 using base::FilePath;
 using base::StringPrintf;
 
 namespace {
 
+const char kDefaultMinidumpName[] = "upload_file_minidump";
+
+// Path to the gzip binary.
+const char kGzipPath[] = "/bin/gzip";
+
+// Filenames for logs attached to crash reports. Also used as metadata keys.
+const char kChromeLogFilename[] = "chrome.txt";
+const char kGpuStateFilename[] = "i915_error_state.log.xz";
+
+// From //net/crash/collector/collector.h
+const int kDefaultMaxUploadBytes = 1024 * 1024;
+
 // Extract a string delimited by the given character, from the given offset
 // into a source string. Returns false if the string is zero-sized or no
 // delimiter was found.
@@ -42,8 +49,25 @@
   return true;
 }
 
-bool GetDriErrorState(const chromeos::dbus::Proxy &proxy,
-                      const FilePath &error_state_path) {
+// Gets the GPU's error state from debugd and writes it to |error_state_path|.
+// Returns true on success.
+bool GetDriErrorState(const FilePath &error_state_path) {
+  chromeos::dbus::BusConnection dbus = chromeos::dbus::GetSystemBusConnection();
+  if (!dbus.HasConnection()) {
+    LOG(ERROR) << "Error connecting to system D-Bus";
+    return false;
+  }
+
+  chromeos::dbus::Proxy proxy(dbus,
+                              debugd::kDebugdServiceName,
+                              debugd::kDebugdServicePath,
+                              debugd::kDebugdInterface);
+  if (!proxy) {
+    LOG(ERROR) << "Error creating D-Bus proxy to interface "
+               << "'" << debugd::kDebugdServiceName << "'";
+    return false;
+  }
+
   chromeos::glib::ScopedError error;
   gchar *error_state = nullptr;
   if (!dbus_g_proxy_call(proxy.gproxy(), debugd::kGetLog,
@@ -88,44 +112,19 @@
   return true;
 }
 
-bool GetAdditionalLogs(const FilePath &log_path) {
-  chromeos::dbus::BusConnection dbus = chromeos::dbus::GetSystemBusConnection();
-  if (!dbus.HasConnection()) {
-    LOG(ERROR) << "Error connecting to system D-Bus";
-    return false;
+// Gzip-compresses |path|, removes the original file, and returns the path of
+// the new file. On failure, the original file is left alone and an empty path
+// is returned.
+FilePath GzipFile(const FilePath& path) {
+  chromeos::ProcessImpl proc;
+  proc.AddArg(kGzipPath);
+  proc.AddArg(path.value());
+  const int res = proc.Run();
+  if (res != 0) {
+    LOG(ERROR) << "Failed to gzip " << path.value();
+    return FilePath();
   }
-
-  chromeos::dbus::Proxy proxy(dbus,
-                              debugd::kDebugdServiceName,
-                              debugd::kDebugdServicePath,
-                              debugd::kDebugdInterface);
-  if (!proxy) {
-    LOG(ERROR) << "Error creating D-Bus proxy to interface "
-               << "'" << debugd::kDebugdServiceName << "'";
-    return false;
-  }
-
-  FilePath error_state_path =
-      log_path.DirName().Append("i915_error_state.log.xz");
-  if (!GetDriErrorState(proxy, error_state_path))
-    return false;
-
-  chromeos::ProcessImpl tar_process;
-  tar_process.AddArg(kTarPath);
-  tar_process.AddArg("cfJ");
-  tar_process.AddArg(log_path.value());
-  tar_process.AddStringOption("-C", log_path.DirName().value());
-  tar_process.AddArg(error_state_path.BaseName().value());
-  int res = tar_process.Run();
-
-  base::DeleteFile(error_state_path, false);
-
-  if (res || !base::PathExists(log_path)) {
-    LOG(ERROR) << "Could not tar file " << log_path.value();
-    return false;
-  }
-
-  return true;
+  return path.AddExtension(".gz");
 }
 
 }  // namespace
@@ -161,7 +160,6 @@
   std::string dump_basename = FormatDumpBasename(exe_name, time(nullptr), pid);
   FilePath meta_path = GetCrashPath(dir, dump_basename, "meta");
   FilePath minidump_path = GetCrashPath(dir, dump_basename, "dmp");
-  FilePath log_path = GetCrashPath(dir, dump_basename, "log.tar.xz");
 
   std::string data;
   if (!base::ReadFileToString(file_path, &data)) {
@@ -174,19 +172,31 @@
     return false;
   }
 
-  if (GetAdditionalLogs(log_path)) {
-    int64_t minidump_size = 0;
-    int64_t log_size = 0;
-    if (base::GetFileSize(minidump_path, &minidump_size) &&
-        base::GetFileSize(log_path, &log_size) &&
-        minidump_size > 0 && log_size > 0 &&
-        minidump_size + log_size < kDefaultMaxUploadBytes) {
-      AddCrashMetaData("log", log_path.value());
-    } else {
-      LOG(INFO) << "Skipping logs upload to prevent discarding minidump "
-          "because of report size limit < " << minidump_size + log_size;
-      base::DeleteFile(log_path, false);
+
+  int64_t report_size = 0;
+  base::GetFileSize(minidump_path, &report_size);
+
+  // Keyed by crash metadata key name.
+  const std::map<std::string, base::FilePath> additional_logs =
+      GetAdditionalLogs(dir, dump_basename, exe_name);
+  for (auto it : additional_logs) {
+    int64_t file_size = 0;
+    if (!base::GetFileSize(it.second, &file_size)) {
+      PLOG(WARNING) << "Unable to get size of " << it.second.value();
+      continue;
     }
+    if (report_size + file_size > kDefaultMaxUploadBytes) {
+      LOG(INFO) << "Skipping upload of " << it.second.value() << "("
+                << file_size << "B) because report size would exceed limit ("
+                << kDefaultMaxUploadBytes << "B)";
+      continue;
+    }
+    VLOG(1) << "Adding metadata: " << it.first << " -> " << it.second.value();
+    // Call AddCrashMetaUploadFile() rather than AddCrashMetaData() here. The
+    // former adds a prefix to the key name; without the prefix, only the key
+    // "logs" appears to be displayed on the crash server.
+    AddCrashMetaUploadFile(it.first, it.second.value());
+    report_size += file_size;
   }
 
   // We're done.
@@ -298,5 +308,31 @@
   return at == data.size();
 }
 
+std::map<std::string, base::FilePath> ChromeCollector::GetAdditionalLogs(
+    const FilePath &dir,
+    const std::string &basename,
+    const std::string &exe_name) {
+  std::map<std::string, base::FilePath> logs;
+
+  // Run the command specified by the config file to gather logs.
+  const FilePath chrome_log_path =
+      GetCrashPath(dir, basename, kChromeLogFilename);
+  if (GetLogContents(log_config_path_, exe_name, chrome_log_path)) {
+    const FilePath compressed_path = GzipFile(chrome_log_path);
+    if (!compressed_path.empty())
+      logs[kChromeLogFilename] = compressed_path;
+    else
+      base::DeleteFile(chrome_log_path, false /* recursive */);
+  }
+
+  // Now get the GPU state from debugd.
+  const FilePath dri_error_state_path =
+      GetCrashPath(dir, basename, kGpuStateFilename);
+  if (GetDriErrorState(dri_error_state_path))
+    logs[kGpuStateFilename] = dri_error_state_path;
+
+  return logs;
+}
+
 // static
 const char ChromeCollector::kSuccessMagic[] = "_sys_cr_finished";
diff --git a/crash_reporter/chrome_collector.h b/crash_reporter/chrome_collector.h
index d8602b9..566b5a8 100644
--- a/crash_reporter/chrome_collector.h
+++ b/crash_reporter/chrome_collector.h
@@ -48,6 +48,14 @@
                      const base::FilePath &minidump,
                      const std::string &basename);
 
+  // Writes additional logs for |exe_name| to files based on |basename| within
+  // |dir|. Crash report metadata key names and the corresponding file paths are
+  // returned.
+  std::map<std::string, base::FilePath> GetAdditionalLogs(
+      const base::FilePath &dir,
+      const std::string &basename,
+      const std::string &exe_name);
+
   FILE *output_file_ptr_;
 
   DISALLOW_COPY_AND_ASSIGN(ChromeCollector);
diff --git a/crash_reporter/crash_reporter_logs.conf b/crash_reporter/crash_reporter_logs.conf
index e4ad207..3440770 100644
--- a/crash_reporter/crash_reporter_logs.conf
+++ b/crash_reporter/crash_reporter_logs.conf
@@ -20,6 +20,10 @@
 # so it is handled in the same way as update_engine.
 cros_installer:cat $(ls -1tr /var/log/update_engine | tail -5 | sed s.^./var/log/update_engine/.) | tail -c 50000
 
+# Dump the last 20 lines of the last two files in Chrome's system and user log
+# directories.
+chrome:for f in $(ls -1rt /var/log/chrome/chrome_[0-9]* | tail -2) $(ls -1rt /home/chronos/u-*/log/chrome_[0-9]* 2>/dev/null | tail -2); do echo "===$f (tail)==="; tail -20 $f; echo EOF; done
+
 # The following rule is used for generating additional diagnostics when
 # collection of user crashes fails.  This output should not be too large
 # as it is stored in memory.  The output format specified for 'ps' is the