crash: Send UMA metric if crash_sender fails to rm

BUG=chromium:1060019
TEST=unit tests

Change-Id: I4091a1d5ae8ca09c3f9ff361a109a051bc961c3e
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/2310887
Tested-by: Miriam Zimmerman <mutexlox@chromium.org>
Commit-Queue: Miriam Zimmerman <mutexlox@chromium.org>
Reviewed-by: Ian Barkley-Yeung <iby@chromium.org>
diff --git a/crash-reporter/crash_sender_util.cc b/crash-reporter/crash_sender_util.cc
index c63384e..c0f0d8b 100644
--- a/crash-reporter/crash_sender_util.cc
+++ b/crash-reporter/crash_sender_util.cc
@@ -85,6 +85,11 @@
 constexpr char kTestModeSuccessful[] =
     "Test Mode: Logging success and exiting instead of actually uploading";
 
+// UMA metrics to track crash removal attempts and failures.
+constexpr char kUMAFailedCrashRemoval[] = "Crash.Sender.FailedCrashRemoval";
+constexpr char kUMAAttemptedCrashRemoval[] =
+    "Crash.Sender.AttemptedCrashRemoval";
+
 // Returns true if the given report kind is known.
 // TODO(satorux): Move collector constants to a common file.
 bool IsKnownKind(const std::string& kind) {
@@ -306,41 +311,6 @@
             });
 }
 
-void RemoveReportFiles(const base::FilePath& meta_file, bool delete_crashes) {
-  if (meta_file.Extension() != ".meta") {
-    LOG(ERROR) << "Not a meta file: " << meta_file.value();
-    return;
-  }
-
-  const std::string pattern =
-      meta_file.BaseName().RemoveExtension().value() + ".*";
-
-  bool add_uploaded_file = !delete_crashes;
-  if (delete_crashes) {
-    base::FileEnumerator iter(meta_file.DirName(), false /* recursive */,
-                              base::FileEnumerator::FILES, pattern);
-    for (base::FilePath file = iter.Next(); !file.empty(); file = iter.Next()) {
-      if (!base::DeleteFile(file, false /* recursive */)) {
-        PLOG(WARNING) << "Failed to remove " << file.value();
-        // We may have failed to remove the file due to incorrect selinux config
-        // on the directory. However, we may still be able to add files to it,
-        // so mark the crash as uploaded to prevent uploading it again.
-        // See https://crbug.com/1060019.
-        if (file.Extension() == ".meta") {
-          add_uploaded_file = true;
-        }
-      }
-    }
-  }
-  if (add_uploaded_file) {
-    base::File f(meta_file.ReplaceExtension(kAlreadyUploadedExt),
-                 base::File::FLAG_CREATE | base::File::FLAG_WRITE);
-    if (!f.IsValid()) {
-      LOG(ERROR) << "Failed to mark crash as uploaded";
-    }
-  }
-}
-
 std::vector<base::FilePath> GetMetaFiles(const base::FilePath& crash_dir) {
   std::vector<base::FilePath> meta_files;
   if (!base::DirectoryExists(crash_dir)) {
@@ -483,7 +453,7 @@
       current_bytes += previous_send.size();
     } else {
       if (!base::DeleteFile(file, false /* recursive */))
-        PLOG(WARNING) << "Failed to remove " << file.value();
+        PLOG(WARNING) << "Failed to remove old report " << file.value();
     }
   }
   LOG(INFO) << "Current send rate: " << current_rate << " sends and "
@@ -1077,6 +1047,48 @@
   return form_data;
 }
 
+void Sender::RemoveReportFiles(const base::FilePath& meta_file,
+                               bool delete_crashes) {
+  if (meta_file.Extension() != ".meta") {
+    LOG(ERROR) << "Not a meta file: " << meta_file.value();
+    return;
+  }
+
+  const std::string pattern =
+      meta_file.BaseName().RemoveExtension().value() + ".*";
+
+  bool add_uploaded_file = !delete_crashes;
+  if (delete_crashes) {
+    if (!metrics_lib_->SendCrosEventToUMA(kUMAAttemptedCrashRemoval)) {
+      LOG(WARNING) << "Failed to record crash removal attempt in UMA";
+    }
+    base::FileEnumerator iter(meta_file.DirName(), false /* recursive */,
+                              base::FileEnumerator::FILES, pattern);
+    for (base::FilePath file = iter.Next(); !file.empty(); file = iter.Next()) {
+      if (!base::DeleteFile(file, false /* recursive */)) {
+        PLOG(WARNING) << "Failed to remove " << file.value();
+        // We may have failed to remove the file due to incorrect selinux config
+        // on the directory. However, we may still be able to add files to it,
+        // so mark the crash as uploaded to prevent uploading it again.
+        // See https://crbug.com/1060019.
+        if (file.Extension() == ".meta") {
+          if (!metrics_lib_->SendCrosEventToUMA(kUMAFailedCrashRemoval)) {
+            LOG(WARNING) << "Further, couldn't record UMA event for failure";
+          }
+          add_uploaded_file = true;
+        }
+      }
+    }
+  }
+  if (add_uploaded_file) {
+    base::File f(meta_file.ReplaceExtension(kAlreadyUploadedExt),
+                 base::File::FLAG_CREATE | base::File::FLAG_WRITE);
+    if (!f.IsValid()) {
+      LOG(ERROR) << "Failed to mark crash as uploaded";
+    }
+  }
+}
+
 std::unique_ptr<base::Value> Sender::CreateJsonEntity(
     const std::string& report_id,
     const std::string& product_name,
diff --git a/crash-reporter/crash_sender_util.h b/crash-reporter/crash_sender_util.h
index 0f330c3..81bdb70 100644
--- a/crash-reporter/crash_sender_util.h
+++ b/crash-reporter/crash_sender_util.h
@@ -20,6 +20,7 @@
 #include <brillo/http/http_form_data.h>
 #include <brillo/key_value_store.h>
 #include <brillo/osrelease_reader.h>
+#include <gtest/gtest_prod.h>  // for FRIEND_TEST
 #include <metrics/metrics_library.h>
 #include <session_manager/dbus-proxies.h>
 #include <shill/dbus-proxies.h>
@@ -126,10 +127,6 @@
 // is at the front of the vector.
 void SortReports(std::vector<MetaFile>* reports);
 
-// Removes report files associated with the given meta file.
-// More specifically, if "foo.meta" is given, "foo.*" will be removed.
-void RemoveReportFiles(const base::FilePath& meta_file, bool delete_crashes);
-
 // Returns the list of meta data files (files with ".meta" suffix), sorted by
 // the timestamp in the old-to-new order.
 std::vector<base::FilePath> GetMetaFiles(const base::FilePath& crash_dir);
@@ -312,6 +309,12 @@
 
  private:
   friend class IsNetworkOnlineTest;
+  FRIEND_TEST(CrashSenderUtilTest, RemoveReportFiles);
+  FRIEND_TEST(CrashSenderUtilTest, FailRemoveReportFilesSendsMetric);
+
+  // Removes report files associated with the given meta file.
+  // More specifically, if "foo.meta" is given, "foo.*" will be removed.
+  void RemoveReportFiles(const base::FilePath& meta_file, bool delete_crashes);
 
   // Creates a JSON entity with the required fields for uploads.log file.
   std::unique_ptr<base::Value> CreateJsonEntity(const std::string& report_id,
diff --git a/crash-reporter/crash_sender_util_test.cc b/crash-reporter/crash_sender_util_test.cc
index 0993d3b..2f42bf8 100644
--- a/crash-reporter/crash_sender_util_test.cc
+++ b/crash-reporter/crash_sender_util_test.cc
@@ -1006,6 +1006,11 @@
 }
 
 TEST_F(CrashSenderUtilTest, RemoveReportFiles) {
+  Sender::Options options;
+  Sender sender(std::move(metrics_lib_),
+                std::make_unique<test_util::AdvancingClock>(), options);
+  ASSERT_TRUE(sender.Init());
+
   const base::FilePath crash_directory =
       paths::Get(paths::kSystemCrashDirectory);
   ASSERT_TRUE(base::CreateDirectory(crash_directory));
@@ -1024,7 +1029,7 @@
   EXPECT_FALSE(base::PathExists(foo_uploaded));
 
   // This should create the alreadyuploaded file.
-  RemoveReportFiles(foo_meta, false);
+  sender.RemoveReportFiles(foo_meta, false);
   std::string contents;
   ASSERT_TRUE(base::ReadFileToString(foo_meta, &contents));
   EXPECT_TRUE(base::PathExists(foo_uploaded));
@@ -1033,7 +1038,7 @@
   base::File::Info uploaded_info;
   ASSERT_TRUE(base::GetFileInfo(foo_uploaded, &uploaded_info));
   // This should not update the alreadyuploaded file, since it already exists
-  RemoveReportFiles(foo_meta, false);
+  sender.RemoveReportFiles(foo_meta, false);
   ASSERT_TRUE(base::ReadFileToString(foo_meta, &contents));
   EXPECT_TRUE(base::PathExists(foo_uploaded));
   base::File::Info new_uploaded_info;
@@ -1042,9 +1047,9 @@
   EXPECT_EQ(new_uploaded_info.size, uploaded_info.size);
 
   // This should remove foo.*.
-  RemoveReportFiles(foo_meta, true);
+  sender.RemoveReportFiles(foo_meta, true);
   // This should do nothing because the suffix is not ".meta".
-  RemoveReportFiles(bar_log, true);
+  sender.RemoveReportFiles(bar_log, true);
 
   // Check what files were removed.
   EXPECT_FALSE(base::PathExists(foo_meta));
@@ -1053,6 +1058,38 @@
   EXPECT_TRUE(base::PathExists(bar_log));
 }
 
+TEST_F(CrashSenderUtilTest, FailRemoveReportFilesSendsMetric) {
+  Sender::Options options;
+  EXPECT_CALL(*metrics_lib_,
+              SendCrosEventToUMA("Crash.Sender.AttemptedCrashRemoval"))
+      .WillOnce(Return(true));
+  EXPECT_CALL(*metrics_lib_,
+              SendCrosEventToUMA("Crash.Sender.FailedCrashRemoval"))
+      .WillOnce(Return(true));
+
+  Sender sender(std::move(metrics_lib_),
+                std::make_unique<test_util::AdvancingClock>(), options);
+  ASSERT_TRUE(sender.Init());
+
+  const base::FilePath crash_directory =
+      paths::Get(paths::kSystemCrashDirectory);
+  ASSERT_TRUE(base::CreateDirectory(crash_directory));
+
+  const base::FilePath foo_meta = crash_directory.Append("foo.meta");
+  ASSERT_TRUE(test_util::CreateFile(foo_meta, ""));
+  const base::FilePath foo_log = crash_directory.Append("foo.log");
+  ASSERT_TRUE(test_util::CreateFile(foo_log, ""));
+
+  // chmod the file so RemoveReportFiles fails
+  ASSERT_EQ(chmod(crash_directory.value().c_str(), 0500), 0);
+
+  sender.RemoveReportFiles(foo_meta, true);
+
+  // Clean up after ourselves
+  EXPECT_EQ(chmod(crash_directory.value().c_str(), 0700), 0);
+  EXPECT_TRUE(base::DeleteFile(crash_directory, true));
+}
+
 TEST_F(CrashSenderUtilTest, GetMetaFiles) {
   const base::FilePath crash_directory =
       paths::Get(paths::kSystemCrashDirectory);
diff --git a/metrics/metrics_library.cc b/metrics/metrics_library.cc
index 755401c..f3f7236 100644
--- a/metrics/metrics_library.cc
+++ b/metrics/metrics_library.cc
@@ -76,12 +76,14 @@
     "Crash.Collector.CollectionCount",          // 27
     "Cryptohome.DoubleMountRequest",            // 28
     "SessionManager.SafeModeEnabled",           // 29
+    "Crash.Sender.FailedCrashRemoval",          // 30
+    "Crash.Sender.AttemptedCrashRemoval",       // 31
 };
 
 // Update this to be last entry + 1 when you add new entries to the end. Checks
 // that no one tries to remove entries from the middle or misnumbers during a
 // merge conflict.
-static_assert(base::size(kCrosEventNames) == 30,
+static_assert(base::size(kCrosEventNames) == 32,
               "CrosEvent enums not lining up properly");
 
 }  // namespace