Add support for optional cgroup attributes

Cgroup attributes like io.weight or io.bfq.weight only exist if the
corresponding I/O scheduler has been loaded. Hence add support for
ignoring failures to write to a cgroup attribute that does not exist.

Bug: 213617178
Test: This patch has been tested in combination with patch "Migrate the blkio controller to the cgroup v2 hierarchy".
Change-Id: I5debafabc4dd430a1b98ea343bf3740c764edcc3
Signed-off-by: Bart Van Assche <bvanassche@google.com>
diff --git a/libprocessgroup/Android.bp b/libprocessgroup/Android.bp
index c68552d..7b0e0d3 100644
--- a/libprocessgroup/Android.bp
+++ b/libprocessgroup/Android.bp
@@ -73,3 +73,29 @@
     ],
     min_sdk_version: "29",
 }
+
+cc_test {
+    name: "task_profiles_test",
+    host_supported: true,
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wexit-time-destructors",
+        "-Wno-unused-parameter",
+    ],
+    srcs: [
+        "task_profiles_test.cpp",
+    ],
+    header_libs: [
+        "libcutils_headers",
+        "libprocessgroup_headers",
+    ],
+    shared_libs: [
+        "libbase",
+        "libcgrouprc",
+        "libprocessgroup",
+    ],
+    static_libs: [
+        "libgmock",
+    ],
+}
diff --git a/libprocessgroup/profiles/task_profiles.proto b/libprocessgroup/profiles/task_profiles.proto
index 1de4395..2a09217 100644
--- a/libprocessgroup/profiles/task_profiles.proto
+++ b/libprocessgroup/profiles/task_profiles.proto
@@ -25,11 +25,12 @@
     repeated AggregateProfiles aggregateprofiles = 3 [json_name = "AggregateProfiles"];
 }
 
-// Next: 4
+// Next: 5
 message Attribute {
     string name = 1 [json_name = "Name"];
     string controller = 2 [json_name = "Controller"];
     string file = 3 [json_name = "File"];
+    string optional = 4 [json_name = "Optional"];
 }
 
 // Next: 3
diff --git a/libprocessgroup/task_profiles.cpp b/libprocessgroup/task_profiles.cpp
index dc7c368..4a2bf38 100644
--- a/libprocessgroup/task_profiles.cpp
+++ b/libprocessgroup/task_profiles.cpp
@@ -206,6 +206,14 @@
     }
 
     if (!WriteStringToFile(value_, path)) {
+        if (errno == ENOENT) {
+            if (optional_) {
+                return true;
+            } else {
+                LOG(ERROR) << "No such cgroup attribute: " << path;
+                return false;
+            }
+        }
         PLOG(ERROR) << "Failed to write '" << value_ << "' to " << path;
         return false;
     }
@@ -675,11 +683,12 @@
             } else if (action_name == "SetAttribute") {
                 std::string attr_name = params_val["Name"].asString();
                 std::string attr_value = params_val["Value"].asString();
+                bool optional = strcmp(params_val["Optional"].asString().c_str(), "true") == 0;
 
                 auto iter = attributes_.find(attr_name);
                 if (iter != attributes_.end()) {
-                    profile->Add(
-                            std::make_unique<SetAttributeAction>(iter->second.get(), attr_value));
+                    profile->Add(std::make_unique<SetAttributeAction>(iter->second.get(),
+                                                                      attr_value, optional));
                 } else {
                     LOG(WARNING) << "SetAttribute: unknown attribute: " << attr_name;
                 }
diff --git a/libprocessgroup/task_profiles.h b/libprocessgroup/task_profiles.h
index 9ee3781..4747511 100644
--- a/libprocessgroup/task_profiles.h
+++ b/libprocessgroup/task_profiles.h
@@ -102,8 +102,8 @@
 // Set attribute profile element
 class SetAttributeAction : public ProfileAction {
   public:
-    SetAttributeAction(const IProfileAttribute* attribute, const std::string& value)
-        : attribute_(attribute), value_(value) {}
+    SetAttributeAction(const IProfileAttribute* attribute, const std::string& value, bool optional)
+        : attribute_(attribute), value_(value), optional_(optional) {}
 
     const char* Name() const override { return "SetAttribute"; }
     bool ExecuteForProcess(uid_t uid, pid_t pid) const override;
@@ -112,6 +112,7 @@
   private:
     const IProfileAttribute* attribute_;
     std::string value_;
+    bool optional_;
 };
 
 // Set cgroup profile element
diff --git a/libprocessgroup/task_profiles_test.cpp b/libprocessgroup/task_profiles_test.cpp
new file mode 100644
index 0000000..09ac44c
--- /dev/null
+++ b/libprocessgroup/task_profiles_test.cpp
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+#include "task_profiles.h"
+#include <android-base/logging.h>
+#include <gtest/gtest.h>
+#include <mntent.h>
+#include <processgroup/processgroup.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <fstream>
+
+using ::android::base::ERROR;
+using ::android::base::LogFunction;
+using ::android::base::LogId;
+using ::android::base::LogSeverity;
+using ::android::base::SetLogger;
+using ::android::base::VERBOSE;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+namespace {
+
+bool IsCgroupV2Mounted() {
+    std::unique_ptr<FILE, int (*)(FILE*)> mnts(setmntent("/proc/mounts", "re"), endmntent);
+    if (!mnts) {
+        LOG(ERROR) << "Failed to open /proc/mounts";
+        return false;
+    }
+    struct mntent* mnt;
+    while ((mnt = getmntent(mnts.get()))) {
+        if (strcmp(mnt->mnt_fsname, "cgroup2") == 0) {
+            return true;
+        }
+    }
+    return false;
+}
+
+class ScopedLogCapturer {
+  public:
+    struct log_args {
+        LogId log_buffer_id;
+        LogSeverity severity;
+        std::string tag;
+        std::string file;
+        unsigned int line;
+        std::string message;
+    };
+
+    // Constructor. Installs a new logger and saves the currently active logger.
+    ScopedLogCapturer() {
+        saved_severity_ = SetMinimumLogSeverity(android::base::VERBOSE);
+        saved_logger_ = SetLogger([this](LogId log_buffer_id, LogSeverity severity, const char* tag,
+                                         const char* file, unsigned int line, const char* message) {
+            if (saved_logger_) {
+                saved_logger_(log_buffer_id, severity, tag, file, line, message);
+            }
+            log_.emplace_back(log_args{.log_buffer_id = log_buffer_id,
+                                       .severity = severity,
+                                       .tag = tag,
+                                       .file = file,
+                                       .line = line,
+                                       .message = message});
+        });
+    }
+    // Destructor. Restores the original logger and log level.
+    ~ScopedLogCapturer() {
+        SetLogger(std::move(saved_logger_));
+        SetMinimumLogSeverity(saved_severity_);
+    }
+    ScopedLogCapturer(const ScopedLogCapturer&) = delete;
+    ScopedLogCapturer& operator=(const ScopedLogCapturer&) = delete;
+    // Returns the logged lines.
+    const std::vector<log_args>& Log() const { return log_; }
+
+  private:
+    LogSeverity saved_severity_;
+    LogFunction saved_logger_;
+    std::vector<log_args> log_;
+};
+
+// cgroup attribute at the top level of the cgroup hierarchy.
+class ProfileAttributeMock : public IProfileAttribute {
+  public:
+    ProfileAttributeMock(const std::string& file_name) : file_name_(file_name) {}
+    ~ProfileAttributeMock() override = default;
+    void Reset(const CgroupController& controller, const std::string& file_name) override {
+        CHECK(false);
+    }
+    const CgroupController* controller() const override {
+        CHECK(false);
+        return {};
+    }
+    const std::string& file_name() const override { return file_name_; }
+    bool GetPathForTask(int tid, std::string* path) const override {
+#ifdef __ANDROID__
+        CHECK(CgroupGetControllerPath(CGROUPV2_CONTROLLER_NAME, path));
+        CHECK_GT(path->length(), 0);
+        if (path->rbegin()[0] != '/') {
+            *path += "/";
+        }
+#else
+        // Not Android.
+        *path = "/sys/fs/cgroup/";
+#endif
+        *path += file_name_;
+        return true;
+    };
+
+  private:
+    const std::string file_name_;
+};
+
+struct TestParam {
+    const char* attr_name;
+    const char* attr_value;
+    bool optional_attr;
+    bool result;
+    LogSeverity log_severity;
+    const char* log_prefix;
+    const char* log_suffix;
+};
+
+class SetAttributeFixture : public TestWithParam<TestParam> {
+  public:
+    ~SetAttributeFixture() = default;
+};
+
+TEST_P(SetAttributeFixture, SetAttribute) {
+    // Treehugger runs host tests inside a container without cgroupv2 support.
+    if (!IsCgroupV2Mounted()) {
+        GTEST_SKIP();
+        return;
+    }
+    const TestParam params = GetParam();
+    ScopedLogCapturer captured_log;
+    ProfileAttributeMock pa(params.attr_name);
+    SetAttributeAction a(&pa, params.attr_value, params.optional_attr);
+    EXPECT_EQ(a.ExecuteForProcess(getuid(), getpid()), params.result);
+    auto log = captured_log.Log();
+    if (params.log_prefix || params.log_suffix) {
+        ASSERT_EQ(log.size(), 1);
+        EXPECT_EQ(log[0].severity, params.log_severity);
+        if (params.log_prefix) {
+            EXPECT_EQ(log[0].message.find(params.log_prefix), 0);
+        }
+        if (params.log_suffix) {
+            EXPECT_NE(log[0].message.find(params.log_suffix), std::string::npos);
+        }
+    } else {
+        ASSERT_EQ(log.size(), 0);
+    }
+}
+
+// Test the four combinations of optional_attr {false, true} and cgroup attribute { does not exist,
+// exists }.
+INSTANTIATE_TEST_SUITE_P(
+        SetAttributeTestSuite, SetAttributeFixture,
+        Values(
+                // Test that attempting to write into a non-existing cgroup attribute fails and also
+                // that an error message is logged.
+                TestParam{.attr_name = "no-such-attribute",
+                          .attr_value = ".",
+                          .optional_attr = false,
+                          .result = false,
+                          .log_severity = ERROR,
+                          .log_prefix = "No such cgroup attribute"},
+                // Test that attempting to write into an optional non-existing cgroup attribute
+                // results in the return value 'true' and also that no messages are logged.
+                TestParam{.attr_name = "no-such-attribute",
+                          .attr_value = ".",
+                          .optional_attr = true,
+                          .result = true},
+                // Test that attempting to write an invalid value into an existing optional cgroup
+                // attribute fails and also that it causes an error
+                // message to be logged.
+                TestParam{.attr_name = "cgroup.procs",
+                          .attr_value = "-1",
+                          .optional_attr = true,
+                          .result = false,
+                          .log_severity = ERROR,
+                          .log_prefix = "Failed to write",
+                          .log_suffix = geteuid() == 0 ? "Invalid argument" : "Permission denied"},
+                // Test that attempting to write into an existing optional read-only cgroup
+                // attribute fails and also that it causes an error message to be logged.
+                TestParam{
+                        .attr_name = "cgroup.controllers",
+                        .attr_value = ".",
+                        .optional_attr = false,
+                        .result = false,
+                        .log_severity = ERROR,
+                        .log_prefix = "Failed to write",
+                        .log_suffix = geteuid() == 0 ? "Invalid argument" : "Permission denied"}));
+
+}  // namespace