diff options
Diffstat (limited to 'libarttools/tools')
| -rw-r--r-- | libarttools/tools/art_exec.cc | 225 | ||||
| -rw-r--r-- | libarttools/tools/art_exec_test.cc | 262 | ||||
| -rw-r--r-- | libarttools/tools/cmdline_builder.h | 156 | ||||
| -rw-r--r-- | libarttools/tools/cmdline_builder_test.cc | 135 | ||||
| -rw-r--r-- | libarttools/tools/system_properties.h | 104 | ||||
| -rw-r--r-- | libarttools/tools/system_properties_test.cc | 97 | ||||
| -rw-r--r-- | libarttools/tools/tools.cc | 121 | ||||
| -rw-r--r-- | libarttools/tools/tools.h | 15 | ||||
| -rw-r--r-- | libarttools/tools/tools_test.cc | 93 |
9 files changed, 1202 insertions, 6 deletions
diff --git a/libarttools/tools/art_exec.cc b/libarttools/tools/art_exec.cc new file mode 100644 index 0000000000..1806ed4514 --- /dev/null +++ b/libarttools/tools/art_exec.cc @@ -0,0 +1,225 @@ +/* + * 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 <stdlib.h> +#include <sys/capability.h> +#include <sys/resource.h> +#include <unistd.h> + +#include <filesystem> +#include <iostream> +#include <iterator> +#include <optional> +#include <string> +#include <string_view> +#include <system_error> +#include <unordered_map> +#include <unordered_set> +#include <vector> + +#include "android-base/logging.h" +#include "android-base/parseint.h" +#include "android-base/result.h" +#include "android-base/strings.h" +#include "base/macros.h" +#include "base/scoped_cap.h" +#include "fmt/format.h" +#include "palette/palette.h" +#include "system/thread_defs.h" + +namespace { + +using ::android::base::ConsumePrefix; +using ::android::base::Join; +using ::android::base::ParseInt; +using ::android::base::Result; +using ::android::base::Split; + +using ::fmt::literals::operator""_format; // NOLINT + +constexpr const char* kUsage = + R"(A wrapper binary that configures the process and executes a command. + +By default, it closes all open file descriptors except stdin, stdout, and stderr. `--keep-fds` can +be passed to keep some more file descriptors open. + +Usage: art_exec [OPTIONS]... -- [COMMAND]... + +Supported options: + --help: Print this text. + --set-task-profile=PROFILES: Apply a set of task profiles (see + https://source.android.com/devices/tech/perf/cgroups). Requires root access. PROFILES can be a + comma-separated list of task profile names. + --set-priority=PRIORITY: Apply the process priority. Currently, the only supported value of + PRIORITY is "background". + --drop-capabilities: Drop all root capabilities. Note that this has effect only if `art_exec` runs + with some root capabilities but not as the root user. + --keep-fds=FILE_DESCRIPTORS: A semicolon-separated list of file descriptors to keep open. + --env=KEY=VALUE: Set an environment variable. This flag can be passed multiple times to set + multiple environment variables. +)"; + +constexpr int kErrorUsage = 100; +constexpr int kErrorOther = 101; + +struct Options { + int command_pos = -1; + std::vector<std::string> task_profiles; + std::optional<int> priority = std::nullopt; + bool drop_capabilities = false; + std::unordered_set<int> keep_fds{fileno(stdin), fileno(stdout), fileno(stderr)}; + std::unordered_map<std::string, std::string> envs; +}; + +[[noreturn]] void Usage(const std::string& error_msg) { + LOG(ERROR) << error_msg; + std::cerr << error_msg << "\n" << kUsage << "\n"; + exit(kErrorUsage); +} + +Options ParseOptions(int argc, char** argv) { + Options options; + for (int i = 1; i < argc; i++) { + std::string_view arg = argv[i]; + if (arg == "--help") { + std::cerr << kUsage << "\n"; + exit(0); + } else if (ConsumePrefix(&arg, "--set-task-profile=")) { + options.task_profiles = Split(std::string(arg), ","); + if (options.task_profiles.empty()) { + Usage("Empty task profile list"); + } + } else if (ConsumePrefix(&arg, "--set-priority=")) { + if (arg == "background") { + options.priority = ANDROID_PRIORITY_BACKGROUND; + } else { + Usage("Unknown priority " + std::string(arg)); + } + } else if (arg == "--drop-capabilities") { + options.drop_capabilities = true; + } else if (ConsumePrefix(&arg, "--keep-fds=")) { + for (const std::string& fd_str : Split(std::string(arg), ":")) { + int fd; + if (!ParseInt(fd_str, &fd)) { + Usage("Invalid fd " + fd_str); + } + options.keep_fds.insert(fd); + } + } else if (ConsumePrefix(&arg, "--env=")) { + size_t pos = arg.find('='); + if (pos == std::string_view::npos) { + Usage("Malformed environment variable. Must contain '='"); + } + options.envs[std::string(arg.substr(/*pos=*/0, /*n=*/pos))] = + std::string(arg.substr(pos + 1)); + } else if (arg == "--") { + if (i + 1 >= argc) { + Usage("Missing command after '--'"); + } + options.command_pos = i + 1; + return options; + } else { + Usage("Unknown option " + std::string(arg)); + } + } + Usage("Missing '--'"); +} + +Result<void> DropInheritableCaps() { + art::ScopedCap cap(cap_get_proc()); + if (cap.Get() == nullptr) { + return ErrnoErrorf("Failed to call cap_get_proc"); + } + if (cap_clear_flag(cap.Get(), CAP_INHERITABLE) != 0) { + return ErrnoErrorf("Failed to call cap_clear_flag"); + } + if (cap_set_proc(cap.Get()) != 0) { + return ErrnoErrorf("Failed to call cap_set_proc"); + } + return {}; +} + +Result<void> CloseFds(const std::unordered_set<int>& keep_fds) { + std::vector<int> open_fds; + std::error_code ec; + for (const std::filesystem::directory_entry& dir_entry : + std::filesystem::directory_iterator("/proc/self/fd", ec)) { + int fd; + if (!ParseInt(dir_entry.path().filename(), &fd)) { + return Errorf("Invalid entry in /proc/self/fd {}", dir_entry.path().filename()); + } + open_fds.push_back(fd); + } + if (ec) { + return Errorf("Failed to list open FDs: {}", ec.message()); + } + for (int fd : open_fds) { + if (keep_fds.find(fd) == keep_fds.end()) { + if (close(fd) != 0) { + Result<void> error = ErrnoErrorf("Failed to close FD {}", fd); + if (std::filesystem::exists("/proc/self/fd/{}"_format(fd))) { + return error; + } + } + } + } + return {}; +} + +} // namespace + +int main(int argc, char** argv) { + android::base::InitLogging(argv); + + Options options = ParseOptions(argc, argv); + + if (auto result = CloseFds(options.keep_fds); !result.ok()) { + LOG(ERROR) << "Failed to close open FDs: " << result.error(); + return kErrorOther; + } + + if (!options.task_profiles.empty()) { + if (int ret = PaletteSetTaskProfiles(/*tid=*/0, options.task_profiles); + ret != PALETTE_STATUS_OK) { + LOG(ERROR) << "Failed to set task profile: " << ret; + return kErrorOther; + } + } + + if (options.priority.has_value()) { + if (setpriority(PRIO_PROCESS, /*who=*/0, options.priority.value()) != 0) { + PLOG(ERROR) << "Failed to setpriority"; + return kErrorOther; + } + } + + if (options.drop_capabilities) { + if (auto result = DropInheritableCaps(); !result.ok()) { + LOG(ERROR) << "Failed to drop inheritable capabilities: " << result.error(); + return kErrorOther; + } + } + + for (const auto& [key, value] : options.envs) { + setenv(key.c_str(), value.c_str(), /*overwrite=*/1); + } + + execv(argv[options.command_pos], argv + options.command_pos); + + std::vector<const char*> command_args(argv + options.command_pos, argv + argc); + PLOG(FATAL) << "Failed to execute (" << Join(command_args, ' ') << ")"; + UNREACHABLE(); +} diff --git a/libarttools/tools/art_exec_test.cc b/libarttools/tools/art_exec_test.cc new file mode 100644 index 0000000000..9e8b0de3ad --- /dev/null +++ b/libarttools/tools/art_exec_test.cc @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2021 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 <sys/capability.h> +#include <sys/resource.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <csignal> +#include <filesystem> +#include <functional> +#include <string> +#include <utility> + +#include "android-base/file.h" +#include "android-base/logging.h" +#include "android-base/scopeguard.h" +#include "android-base/strings.h" +#include "base/common_art_test.h" +#include "base/file_utils.h" +#include "base/globals.h" +#include "base/macros.h" +#include "base/os.h" +#include "base/scoped_cap.h" +#include "exec_utils.h" +#include "fmt/format.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "system/thread_defs.h" + +#ifdef ART_TARGET_ANDROID +#include "android-modules-utils/sdk_level.h" +#endif + +namespace art { +namespace { + +using ::android::base::make_scope_guard; +using ::android::base::ScopeGuard; +using ::android::base::Split; +using ::testing::Contains; +using ::testing::ElementsAre; +using ::testing::HasSubstr; +using ::testing::Not; + +// clang-tidy incorrectly complaints about the using declaration while the user-defined literal is +// actually being used. +using ::fmt::literals::operator""_format; // NOLINT + +constexpr uid_t kRoot = 0; +constexpr uid_t kNobody = 9999; + +// This test executes a few Linux system commands such as "ls", which are linked against system +// libraries. In many ART gtests we set LD_LIBRARY_PATH to make the test binaries link to libraries +// from the ART module first, and if that setting is propagated to the system commands they may also +// try to link to those libraries instead of the system ones they are built against. This is +// particularly noticeable when 32-bit tests run on a 64-bit system. Hence we need to set +// LD_LIBRARY_PATH to an empty string here. +// TODO(b/247108425): Remove this when ART gtests no longer use LD_LIBRARY_PATH. +constexpr const char* kEmptyLdLibraryPath = "--env=LD_LIBRARY_PATH="; + +std::string GetArtBin(const std::string& name) { return "{}/bin/{}"_format(GetArtRoot(), name); } + +std::string GetBin(const std::string& name) { return "{}/bin/{}"_format(GetAndroidRoot(), name); } + +// Executes the command, waits for it to finish, and keeps it in a waitable state until the current +// scope exits. +std::pair<pid_t, ScopeGuard<std::function<void()>>> ScopedExecAndWait( + std::vector<std::string>& args) { + std::vector<char*> execv_args; + execv_args.reserve(args.size() + 1); + for (std::string& arg : args) { + execv_args.push_back(arg.data()); + } + execv_args.push_back(nullptr); + + pid_t pid = fork(); + if (pid == 0) { + execv(execv_args[0], execv_args.data()); + UNREACHABLE(); + } else if (pid > 0) { + siginfo_t info; + CHECK_EQ(TEMP_FAILURE_RETRY(waitid(P_PID, pid, &info, WEXITED | WNOWAIT)), 0); + CHECK_EQ(info.si_code, CLD_EXITED); + CHECK_EQ(info.si_status, 0); + std::function<void()> cleanup([=] { + siginfo_t info; + CHECK_EQ(TEMP_FAILURE_RETRY(waitid(P_PID, pid, &info, WEXITED)), 0); + }); + return std::make_pair(pid, make_scope_guard(std::move(cleanup))); + } else { + LOG(FATAL) << "Failed to call fork"; + UNREACHABLE(); + } +} + +// Grants the current process the given root capability. +void SetCap(cap_flag_t flag, cap_value_t value) { + ScopedCap cap(cap_get_proc()); + CHECK_NE(cap.Get(), nullptr); + cap_value_t caps[]{value}; + CHECK_EQ(cap_set_flag(cap.Get(), flag, /*ncap=*/1, caps, CAP_SET), 0); + CHECK_EQ(cap_set_proc(cap.Get()), 0); +} + +// Returns true if the given process has the given root capability. +bool GetCap(pid_t pid, cap_flag_t flag, cap_value_t value) { + ScopedCap cap(cap_get_pid(pid)); + CHECK_NE(cap.Get(), nullptr); + cap_flag_value_t flag_value; + CHECK_EQ(cap_get_flag(cap.Get(), value, flag, &flag_value), 0); + return flag_value == CAP_SET; +} + +class ArtExecTest : public testing::Test { + protected: + void SetUp() override { + testing::Test::SetUp(); + if (!kIsTargetAndroid) { + GTEST_SKIP() << "art_exec is for device only"; + } + if (getuid() != kRoot) { + GTEST_SKIP() << "art_exec requires root"; + } + art_exec_bin_ = GetArtBin("art_exec"); + } + + std::string art_exec_bin_; +}; + +TEST_F(ArtExecTest, Command) { + std::string error_msg; + int ret = ExecAndReturnCode({art_exec_bin_, "--", GetBin("sh"), "-c", "exit 123"}, &error_msg); + ASSERT_EQ(ret, 123) << error_msg; +} + +TEST_F(ArtExecTest, SetTaskProfiles) { +// The condition is always true because ArtExecTest is run on device only. +#ifdef ART_TARGET_ANDROID + if (!android::modules::sdklevel::IsAtLeastU()) { + GTEST_SKIP() << "This test depends on a libartpalette API that is only available on U+"; + } +#endif + + std::string filename = "/data/local/tmp/art-exec-test-XXXXXX"; + ScratchFile scratch_file(new File(mkstemp(filename.data()), filename, /*check_usage=*/false)); + ASSERT_GE(scratch_file.GetFd(), 0); + + std::vector<std::string> args{art_exec_bin_, + "--set-task-profile=ProcessCapacityHigh", + kEmptyLdLibraryPath, + "--", + GetBin("sh"), + "-c", + "cat /proc/self/cgroup > " + filename}; + auto [pid, scope_guard] = ScopedExecAndWait(args); + std::string cgroup; + ASSERT_TRUE(android::base::ReadFileToString(filename, &cgroup)); + EXPECT_THAT(cgroup, HasSubstr(":cpuset:/foreground\n")); +} + +TEST_F(ArtExecTest, SetPriority) { + std::vector<std::string> args{ + art_exec_bin_, "--set-priority=background", kEmptyLdLibraryPath, "--", GetBin("true")}; + auto [pid, scope_guard] = ScopedExecAndWait(args); + EXPECT_EQ(getpriority(PRIO_PROCESS, pid), ANDROID_PRIORITY_BACKGROUND); +} + +TEST_F(ArtExecTest, DropCapabilities) { + // Switch to a non-root user, but still keep the CAP_FOWNER capability available and inheritable. + // The order of the following calls matters. + CHECK_EQ(cap_setuid(kNobody), 0); + SetCap(CAP_INHERITABLE, CAP_FOWNER); + SetCap(CAP_EFFECTIVE, CAP_FOWNER); + ASSERT_EQ(cap_set_ambient(CAP_FOWNER, CAP_SET), 0); + + // Make sure the test is set up correctly (i.e., the child process should normally have the + // inherited root capability: CAP_FOWNER). + { + std::vector<std::string> args{art_exec_bin_, kEmptyLdLibraryPath, "--", GetBin("true")}; + auto [pid, scope_guard] = ScopedExecAndWait(args); + ASSERT_TRUE(GetCap(pid, CAP_EFFECTIVE, CAP_FOWNER)); + } + + { + std::vector<std::string> args{ + art_exec_bin_, "--drop-capabilities", kEmptyLdLibraryPath, "--", GetBin("true")}; + auto [pid, scope_guard] = ScopedExecAndWait(args); + EXPECT_FALSE(GetCap(pid, CAP_EFFECTIVE, CAP_FOWNER)); + } +} + +TEST_F(ArtExecTest, CloseFds) { + std::unique_ptr<File> file1(OS::OpenFileForReading("/dev/zero")); + std::unique_ptr<File> file2(OS::OpenFileForReading("/dev/zero")); + std::unique_ptr<File> file3(OS::OpenFileForReading("/dev/zero")); + ASSERT_NE(file1, nullptr); + ASSERT_NE(file2, nullptr); + ASSERT_NE(file3, nullptr); + + std::string filename = "/data/local/tmp/art-exec-test-XXXXXX"; + ScratchFile scratch_file(new File(mkstemp(filename.data()), filename, /*check_usage=*/false)); + ASSERT_GE(scratch_file.GetFd(), 0); + + std::vector<std::string> args{art_exec_bin_, + "--keep-fds={}:{}"_format(file3->Fd(), file2->Fd()), + kEmptyLdLibraryPath, + "--", + GetBin("sh"), + "-c", + "(" + "readlink /proc/self/fd/{} || echo;" + "readlink /proc/self/fd/{} || echo;" + "readlink /proc/self/fd/{} || echo;" + ") > {}"_format(file1->Fd(), file2->Fd(), file3->Fd(), filename)}; + + ScopedExecAndWait(args); + + std::string open_fds; + ASSERT_TRUE(android::base::ReadFileToString(filename, &open_fds)); + + // `file1` should be closed, while the other two should be open. There's a blank line at the end. + EXPECT_THAT(Split(open_fds, "\n"), ElementsAre(Not("/dev/zero"), "/dev/zero", "/dev/zero", "")); +} + +TEST_F(ArtExecTest, Env) { + std::string filename = "/data/local/tmp/art-exec-test-XXXXXX"; + ScratchFile scratch_file(new File(mkstemp(filename.data()), filename, /*check_usage=*/false)); + ASSERT_GE(scratch_file.GetFd(), 0); + + std::vector<std::string> args{art_exec_bin_, + "--env=FOO=BAR", + kEmptyLdLibraryPath, + "--", + GetBin("sh"), + "-c", + "env > " + filename}; + + ScopedExecAndWait(args); + + std::string envs; + ASSERT_TRUE(android::base::ReadFileToString(filename, &envs)); + + EXPECT_THAT(Split(envs, "\n"), Contains("FOO=BAR")); +} + +} // namespace +} // namespace art diff --git a/libarttools/tools/cmdline_builder.h b/libarttools/tools/cmdline_builder.h new file mode 100644 index 0000000000..fd11ee87ba --- /dev/null +++ b/libarttools/tools/cmdline_builder.h @@ -0,0 +1,156 @@ +/* + * 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. + */ + +#ifndef ART_LIBARTTOOLS_TOOLS_CMDLINE_BUILDER_H_ +#define ART_LIBARTTOOLS_TOOLS_CMDLINE_BUILDER_H_ + +#include <algorithm> +#include <iterator> +#include <string> +#include <string_view> +#include <vector> + +#include "android-base/stringprintf.h" + +namespace art { +namespace tools { + +namespace internal { + +constexpr bool ContainsOneFormatSpecifier(std::string_view format, char specifier) { + int count = 0; + size_t pos = 0; + while ((pos = format.find('%', pos)) != std::string_view::npos) { + if (pos == format.length() - 1) { + // Invalid trailing '%'. + return false; + } + if (format[pos + 1] == specifier) { + count++; + } else if (format[pos + 1] != '%') { + // "%%" is okay. Otherwise, it's a wrong specifier. + return false; + } + pos += 2; + } + return count == 1; +} + +} // namespace internal + +// A util class that builds cmdline arguments. +class CmdlineBuilder { + public: + // Returns all arguments. + const std::vector<std::string>& Get() const { return elements_; } + + // Adds an argument as-is. + CmdlineBuilder& Add(std::string_view arg) { + elements_.push_back(std::string(arg)); + return *this; + } + + // Same as above but adds a runtime argument. + CmdlineBuilder& AddRuntime(std::string_view arg) { return Add("--runtime-arg").Add(arg); } + + // Adds a string value formatted by the format string. + // + // Usage: Add("--flag=%s", "value") + CmdlineBuilder& Add(const char* arg_format, const std::string& value) + __attribute__((enable_if(internal::ContainsOneFormatSpecifier(arg_format, 's'), + "'arg' must be a string literal that contains '%s'"))) { + return Add(android::base::StringPrintf(arg_format, value.c_str())); + } + + // Same as above but adds a runtime argument. + CmdlineBuilder& AddRuntime(const char* arg_format, const std::string& value) + __attribute__((enable_if(internal::ContainsOneFormatSpecifier(arg_format, 's'), + "'arg' must be a string literal that contains '%s'"))) { + return AddRuntime(android::base::StringPrintf(arg_format, value.c_str())); + } + + // Adds an integer value formatted by the format string. + // + // Usage: Add("--flag=%d", 123) + CmdlineBuilder& Add(const char* arg_format, int value) + __attribute__((enable_if(internal::ContainsOneFormatSpecifier(arg_format, 'd'), + "'arg' must be a string literal that contains '%d'"))) { + return Add(android::base::StringPrintf(arg_format, value)); + } + + // Same as above but adds a runtime argument. + CmdlineBuilder& AddRuntime(const char* arg_format, int value) + __attribute__((enable_if(internal::ContainsOneFormatSpecifier(arg_format, 'd'), + "'arg' must be a string literal that contains '%d'"))) { + return AddRuntime(android::base::StringPrintf(arg_format, value)); + } + + // Adds a string value formatted by the format string if the value is non-empty. Does nothing + // otherwise. + // + // Usage: AddIfNonEmpty("--flag=%s", "value") + CmdlineBuilder& AddIfNonEmpty(const char* arg_format, const std::string& value) + __attribute__((enable_if(internal::ContainsOneFormatSpecifier(arg_format, 's'), + "'arg' must be a string literal that contains '%s'"))) { + if (!value.empty()) { + Add(android::base::StringPrintf(arg_format, value.c_str())); + } + return *this; + } + + // Same as above but adds a runtime argument. + CmdlineBuilder& AddRuntimeIfNonEmpty(const char* arg_format, const std::string& value) + __attribute__((enable_if(internal::ContainsOneFormatSpecifier(arg_format, 's'), + "'arg' must be a string literal that contains '%s'"))) { + if (!value.empty()) { + AddRuntime(android::base::StringPrintf(arg_format, value.c_str())); + } + return *this; + } + + // Adds an argument as-is if the boolean value is true. Does nothing otherwise. + CmdlineBuilder& AddIf(bool value, std::string_view arg) { + if (value) { + Add(arg); + } + return *this; + } + + // Same as above but adds a runtime argument. + CmdlineBuilder& AddRuntimeIf(bool value, std::string_view arg) { + if (value) { + AddRuntime(arg); + } + return *this; + } + + // Concatenates this builder with another. Returns the concatenated result and nullifies the input + // builder. + CmdlineBuilder& Concat(CmdlineBuilder&& other) { + elements_.reserve(elements_.size() + other.elements_.size()); + std::move(other.elements_.begin(), other.elements_.end(), std::back_inserter(elements_)); + other.elements_.clear(); + return *this; + } + + private: + std::vector<std::string> elements_; +}; + +} // namespace tools +} // namespace art + +#endif // ART_LIBARTTOOLS_TOOLS_CMDLINE_BUILDER_H_ diff --git a/libarttools/tools/cmdline_builder_test.cc b/libarttools/tools/cmdline_builder_test.cc new file mode 100644 index 0000000000..55518607db --- /dev/null +++ b/libarttools/tools/cmdline_builder_test.cc @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2021 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 "cmdline_builder.h" + +#include <utility> + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace art { +namespace tools { +namespace { + +using ::testing::ElementsAre; +using ::testing::IsEmpty; + +class CmdlineBuilderTest : public testing::Test { + protected: + CmdlineBuilder args_; +}; + +TEST_F(CmdlineBuilderTest, ContainsOneFormatSpecifier) { + EXPECT_TRUE(internal::ContainsOneFormatSpecifier("--flag=%s", 's')); + EXPECT_TRUE(internal::ContainsOneFormatSpecifier("--flag=[%s]", 's')); + EXPECT_TRUE(internal::ContainsOneFormatSpecifier("--flag=%s%%", 's')); + EXPECT_TRUE(internal::ContainsOneFormatSpecifier("--flag=[%s%%]", 's')); + EXPECT_TRUE(internal::ContainsOneFormatSpecifier("--flag=%%%s", 's')); + EXPECT_FALSE(internal::ContainsOneFormatSpecifier("--flag=", 's')); + EXPECT_FALSE(internal::ContainsOneFormatSpecifier("--flag=%s%s", 's')); + EXPECT_FALSE(internal::ContainsOneFormatSpecifier("--flag=%s%", 's')); + EXPECT_FALSE(internal::ContainsOneFormatSpecifier("--flag=%d", 's')); + EXPECT_FALSE(internal::ContainsOneFormatSpecifier("--flag=%s%d", 's')); + EXPECT_FALSE(internal::ContainsOneFormatSpecifier("--flag=%%s", 's')); +} + +TEST_F(CmdlineBuilderTest, Add) { + args_.Add("--flag"); + EXPECT_THAT(args_.Get(), ElementsAre("--flag")); +} + +TEST_F(CmdlineBuilderTest, AddRuntime) { + args_.AddRuntime("--flag"); + EXPECT_THAT(args_.Get(), ElementsAre("--runtime-arg", "--flag")); +} + +TEST_F(CmdlineBuilderTest, AddString) { + args_.Add("--flag=[%s]", "foo"); + EXPECT_THAT(args_.Get(), ElementsAre("--flag=[foo]")); +} + +TEST_F(CmdlineBuilderTest, AddRuntimeString) { + args_.AddRuntime("--flag=[%s]", "foo"); + EXPECT_THAT(args_.Get(), ElementsAre("--runtime-arg", "--flag=[foo]")); +} + +TEST_F(CmdlineBuilderTest, AddInt) { + args_.Add("--flag=[%d]", 123); + EXPECT_THAT(args_.Get(), ElementsAre("--flag=[123]")); +} + +TEST_F(CmdlineBuilderTest, AddRuntimeInt) { + args_.AddRuntime("--flag=[%d]", 123); + EXPECT_THAT(args_.Get(), ElementsAre("--runtime-arg", "--flag=[123]")); +} + +TEST_F(CmdlineBuilderTest, AddIfNonEmpty) { + args_.AddIfNonEmpty("--flag=[%s]", "foo"); + EXPECT_THAT(args_.Get(), ElementsAre("--flag=[foo]")); +} + +TEST_F(CmdlineBuilderTest, AddIfNonEmptyEmpty) { + args_.AddIfNonEmpty("--flag=[%s]", ""); + EXPECT_THAT(args_.Get(), IsEmpty()); +} + +TEST_F(CmdlineBuilderTest, AddRuntimeIfNonEmpty) { + args_.AddRuntimeIfNonEmpty("--flag=[%s]", "foo"); + EXPECT_THAT(args_.Get(), ElementsAre("--runtime-arg", "--flag=[foo]")); +} + +TEST_F(CmdlineBuilderTest, AddRuntimeIfNonEmptyEmpty) { + args_.AddRuntimeIfNonEmpty("--flag=[%s]", ""); + EXPECT_THAT(args_.Get(), IsEmpty()); +} + +TEST_F(CmdlineBuilderTest, AddIfTrue) { + args_.AddIf(true, "--flag"); + EXPECT_THAT(args_.Get(), ElementsAre("--flag")); +} + +TEST_F(CmdlineBuilderTest, AddIfFalse) { + args_.AddIf(false, "--flag"); + EXPECT_THAT(args_.Get(), IsEmpty()); +} + +TEST_F(CmdlineBuilderTest, AddRuntimeIfTrue) { + args_.AddRuntimeIf(true, "--flag"); + EXPECT_THAT(args_.Get(), ElementsAre("--runtime-arg", "--flag")); +} + +TEST_F(CmdlineBuilderTest, AddRuntimeIfFalse) { + args_.AddRuntimeIf(false, "--flag"); + EXPECT_THAT(args_.Get(), IsEmpty()); +} + +TEST_F(CmdlineBuilderTest, Concat) { + args_.Add("--flag1"); + args_.Add("--flag2"); + + CmdlineBuilder other; + other.Add("--flag3"); + other.Add("--flag4"); + + args_.Concat(std::move(other)); + EXPECT_THAT(args_.Get(), ElementsAre("--flag1", "--flag2", "--flag3", "--flag4")); + EXPECT_THAT(other.Get(), IsEmpty()); +} + +} // namespace +} // namespace tools +} // namespace art diff --git a/libarttools/tools/system_properties.h b/libarttools/tools/system_properties.h new file mode 100644 index 0000000000..06b7bcb340 --- /dev/null +++ b/libarttools/tools/system_properties.h @@ -0,0 +1,104 @@ +/* + * 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. + */ + +#ifndef ART_LIBARTTOOLS_TOOLS_SYSTEM_PROPERTIES_H_ +#define ART_LIBARTTOOLS_TOOLS_SYSTEM_PROPERTIES_H_ + +#include <string> + +#include "android-base/parsebool.h" +#include "android-base/properties.h" + +namespace art { +namespace tools { + +// A class for getting system properties with fallback lookup support. Different from +// android::base::GetProperty, this class is mockable. +class SystemProperties { + public: + virtual ~SystemProperties() = default; + + // Returns the current value of the system property `key`, or `default_value` if the property + // doesn't have a value. + std::string Get(const std::string& key, const std::string& default_value) const { + std::string value = GetProperty(key); + if (!value.empty()) { + return value; + } + return default_value; + } + + // Same as above, but allows specifying one or more fallback keys. The last argument is a string + // default value that will be used if none of the given keys has a value. + // + // Usage: + // + // Look up for "key_1", then "key_2", then "key_3". If none of them has a value, return "default": + // Get("key_1", "key_2", "key_3", /*default_value=*/"default") + template <typename... Args> + std::string Get(const std::string& key, const std::string& fallback_key, Args... args) const { + return Get(key, Get(fallback_key, args...)); + } + + // Returns the current value of the system property `key` with zero or more fallback keys, or an + // empty string if none of the given keys has a value. + // + // Usage: + // + // Look up for "key_1". If it doesn't have a value, return an empty string: + // GetOrEmpty("key_1") + // + // Look up for "key_1", then "key_2", then "key_3". If none of them has a value, return an empty + // string: + // GetOrEmpty("key_1", "key_2", "key_3") + template <typename... Args> + std::string GetOrEmpty(const std::string& key, Args... fallback_keys) const { + return Get(key, fallback_keys..., /*default_value=*/""); + } + + // Returns the current value of the boolean system property `key`, or `default_value` if the + // property doesn't have a value. See `android::base::ParseBool` for how the value is parsed. + bool GetBool(const std::string& key, bool default_value) const { + android::base::ParseBoolResult result = android::base::ParseBool(GetProperty(key)); + if (result != android::base::ParseBoolResult::kError) { + return result == android::base::ParseBoolResult::kTrue; + } + return default_value; + } + + // Same as above, but allows specifying one or more fallback keys. The last argument is a bool + // default value that will be used if none of the given keys has a value. + // + // Usage: + // + // Look up for "key_1", then "key_2", then "key_3". If none of them has a value, return true: + // Get("key_1", "key_2", "key_3", /*default_value=*/true) + template <typename... Args> + bool GetBool(const std::string& key, const std::string& fallback_key, Args... args) const { + return GetBool(key, GetBool(fallback_key, args...)); + } + + protected: + // The single source of truth of system properties. Can be mocked in unit tests. + virtual std::string GetProperty(const std::string& key) const { + return android::base::GetProperty(key, /*default_value=*/""); + } +}; + +} // namespace tools +} // namespace art + +#endif // ART_LIBARTTOOLS_TOOLS_SYSTEM_PROPERTIES_H_ diff --git a/libarttools/tools/system_properties_test.cc b/libarttools/tools/system_properties_test.cc new file mode 100644 index 0000000000..80300f0343 --- /dev/null +++ b/libarttools/tools/system_properties_test.cc @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2021 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 "system_properties.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace art { +namespace tools { +namespace { + +using ::testing::Return; + +class MockSystemProperties : public SystemProperties { + public: + MOCK_METHOD(std::string, GetProperty, (const std::string& key), (const, override)); +}; + +class SystemPropertiesTest : public testing::Test { + protected: + MockSystemProperties system_properties_; +}; + +TEST_F(SystemPropertiesTest, Get) { + EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return("value_1")); + EXPECT_EQ(system_properties_.Get("key_1", /*default_value=*/"default"), "value_1"); +} + +TEST_F(SystemPropertiesTest, GetWithFallback) { + EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return("")); + EXPECT_CALL(system_properties_, GetProperty("key_2")).WillOnce(Return("value_2")); + EXPECT_CALL(system_properties_, GetProperty("key_3")).WillOnce(Return("value_3")); + EXPECT_EQ(system_properties_.Get("key_1", "key_2", "key_3", /*default_value=*/"default"), + "value_2"); +} + +TEST_F(SystemPropertiesTest, GetDefault) { + EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return("")); + EXPECT_EQ(system_properties_.Get("key_1", /*default_value=*/"default"), "default"); +} + +TEST_F(SystemPropertiesTest, GetOrEmpty) { + EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return("value_1")); + EXPECT_EQ(system_properties_.GetOrEmpty("key_1"), "value_1"); +} + +TEST_F(SystemPropertiesTest, GetOrEmptyWithFallback) { + EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return("")); + EXPECT_CALL(system_properties_, GetProperty("key_2")).WillOnce(Return("value_2")); + EXPECT_CALL(system_properties_, GetProperty("key_3")).WillOnce(Return("value_3")); + EXPECT_EQ(system_properties_.GetOrEmpty("key_1", "key_2", "key_3"), "value_2"); +} + +TEST_F(SystemPropertiesTest, GetOrEmptyDefault) { + EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return("")); + EXPECT_EQ(system_properties_.GetOrEmpty("key_1"), ""); +} + +TEST_F(SystemPropertiesTest, GetBoolTrue) { + EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return("true")); + EXPECT_EQ(system_properties_.GetBool("key_1", /*default_value=*/false), true); +} + +TEST_F(SystemPropertiesTest, GetBoolFalse) { + EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return("false")); + EXPECT_EQ(system_properties_.GetBool("key_1", /*default_value=*/true), false); +} + +TEST_F(SystemPropertiesTest, GetBoolWithFallback) { + EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return("")); + EXPECT_CALL(system_properties_, GetProperty("key_2")).WillOnce(Return("true")); + EXPECT_CALL(system_properties_, GetProperty("key_3")).WillOnce(Return("false")); + EXPECT_EQ(system_properties_.GetBool("key_1", "key_2", "key_3", /*default_value=*/false), true); +} + +TEST_F(SystemPropertiesTest, GetBoolDefault) { + EXPECT_CALL(system_properties_, GetProperty("key_1")).WillOnce(Return("")); + EXPECT_EQ(system_properties_.GetBool("key_1", /*default_value=*/true), true); +} + +} // namespace +} // namespace tools +} // namespace art diff --git a/libarttools/tools/tools.cc b/libarttools/tools/tools.cc index a3a91e81e2..3d5301ad7f 100644 --- a/libarttools/tools/tools.cc +++ b/libarttools/tools/tools.cc @@ -16,12 +16,129 @@ #include "tools.h" +#include <errno.h> +#include <fnmatch.h> + +#include <algorithm> +#include <filesystem> +#include <functional> +#include <string> +#include <string_view> +#include <system_error> +#include <vector> + +#include "android-base/logging.h" +#include "fmt/format.h" + namespace art { namespace tools { -std::string getMsg() { - return "hello world!"; +namespace { + +using ::std::placeholders::_1; + +using ::fmt::literals::operator""_format; // NOLINT + +// Returns true if `path_prefix` matches `pattern` or can be a prefix of a path that matches +// `pattern` (i.e., `path_prefix` represents a directory that may contain a file whose path matches +// `pattern`). +bool PartialMatch(const std::filesystem::path& pattern, const std::filesystem::path& path_prefix) { + for (std::filesystem::path::const_iterator pattern_it = pattern.begin(), + path_prefix_it = path_prefix.begin(); + ; // NOLINT + pattern_it++, path_prefix_it++) { + if (path_prefix_it == path_prefix.end()) { + return true; + } + if (pattern_it == pattern.end()) { + return false; + } + if (*pattern_it == "**") { + return true; + } + if (fnmatch(pattern_it->c_str(), path_prefix_it->c_str(), /*flags=*/0) != 0) { + return false; + } + } } +bool FullMatchRecursive(const std::filesystem::path& pattern, + std::filesystem::path::const_iterator pattern_it, + const std::filesystem::path& path, + std::filesystem::path::const_iterator path_it, + bool double_asterisk_visited = false) { + if (pattern_it == pattern.end() && path_it == path.end()) { + return true; + } + if (pattern_it == pattern.end()) { + return false; + } + if (*pattern_it == "**") { + DCHECK(!double_asterisk_visited); + std::filesystem::path::const_iterator next_pattern_it = pattern_it; + return FullMatchRecursive( + pattern, ++next_pattern_it, path, path_it, /*double_asterisk_visited=*/true) || + (path_it != path.end() && FullMatchRecursive(pattern, pattern_it, path, ++path_it)); + } + if (path_it == path.end()) { + return false; + } + if (fnmatch(pattern_it->c_str(), path_it->c_str(), /*flags=*/0) != 0) { + return false; + } + return FullMatchRecursive(pattern, ++pattern_it, path, ++path_it); } + +// Returns true if `path` fully matches `pattern`. +bool FullMatch(const std::filesystem::path& pattern, const std::filesystem::path& path) { + return FullMatchRecursive(pattern, pattern.begin(), path, path.begin()); } + +void MatchGlobRecursive(const std::vector<std::filesystem::path>& patterns, + const std::filesystem::path& root_dir, + /*out*/ std::vector<std::string>* results) { + std::error_code ec; + for (auto it = std::filesystem::recursive_directory_iterator( + root_dir, std::filesystem::directory_options::skip_permission_denied, ec); + !ec && it != std::filesystem::end(it); + it.increment(ec)) { + const std::filesystem::directory_entry& entry = *it; + if (std::none_of(patterns.begin(), patterns.end(), std::bind(PartialMatch, _1, entry.path()))) { + // Avoid unnecessary I/O and SELinux denials. + it.disable_recursion_pending(); + continue; + } + std::error_code ec2; + if (entry.is_regular_file(ec2) && + std::any_of(patterns.begin(), patterns.end(), std::bind(FullMatch, _1, entry.path()))) { + results->push_back(entry.path()); + } + if (ec2) { + // It's expected that we don't have permission to stat some dirs/files, and we don't care + // about them. + if (ec2.value() != EACCES) { + LOG(ERROR) << "Unable to lstat '{}': {}"_format(entry.path().string(), ec2.message()); + } + continue; + } + } + if (ec) { + LOG(ERROR) << "Unable to walk through '{}': {}"_format(root_dir.string(), ec.message()); + } +} + +} // namespace + +std::vector<std::string> Glob(const std::vector<std::string>& patterns, std::string_view root_dir) { + std::vector<std::filesystem::path> parsed_patterns; + parsed_patterns.reserve(patterns.size()); + for (std::string_view pattern : patterns) { + parsed_patterns.emplace_back(pattern); + } + std::vector<std::string> results; + MatchGlobRecursive(parsed_patterns, root_dir, &results); + return results; +} + +} // namespace tools +} // namespace art diff --git a/libarttools/tools/tools.h b/libarttools/tools/tools.h index 8231f5f74a..c2bcee77a6 100644 --- a/libarttools/tools/tools.h +++ b/libarttools/tools/tools.h @@ -18,11 +18,24 @@ #define ART_LIBARTTOOLS_TOOLS_TOOLS_H_ #include <string> +#include <string_view> +#include <vector> namespace art { namespace tools { -std::string getMsg(); +// Searches in a filesystem, starting from `root_dir`. Returns all regular files (i.e., excluding +// directories, symlinks, etc.) that match at least one pattern in `patterns`. Each pattern is an +// absolute path that contains zero or more wildcards. The scan does not follow symlinks to +// directories. +// +// Supported wildcards are: +// - Those documented in glob(7) +// - '**': Matches zero or more path elements. This is only recognised by itself as a path segment. +// +// For simplicity and efficiency, at most one '**' is allowed. +std::vector<std::string> Glob(const std::vector<std::string>& patterns, + std::string_view root_dir = "/"); } // namespace tools } // namespace art diff --git a/libarttools/tools/tools_test.cc b/libarttools/tools/tools_test.cc index 6eaa8f60bb..2f61181c73 100644 --- a/libarttools/tools/tools_test.cc +++ b/libarttools/tools/tools_test.cc @@ -15,14 +15,101 @@ */ #include "tools.h" + +#include <algorithm> +#include <filesystem> +#include <iterator> + +#include "android-base/file.h" +#include "base/common_art_test.h" +#include "gmock/gmock.h" #include "gtest/gtest.h" namespace art { +namespace tools { +namespace { + +using ::android::base::WriteStringToFile; +using ::testing::UnorderedElementsAre; + +void CreateFile(const std::string& filename) { + std::filesystem::path path(filename); + std::filesystem::create_directories(path.parent_path()); + ASSERT_TRUE(WriteStringToFile(/*content=*/"", filename)); +} + +class ArtToolsTest : public CommonArtTest { + protected: + void SetUp() override { + CommonArtTest::SetUp(); + scratch_dir_ = std::make_unique<ScratchDir>(); + scratch_path_ = scratch_dir_->GetPath(); + // Remove the trailing '/'; + scratch_path_.resize(scratch_path_.length() - 1); + } + + void TearDown() override { + scratch_dir_.reset(); + CommonArtTest::TearDown(); + } + + std::unique_ptr<ScratchDir> scratch_dir_; + std::string scratch_path_; +}; + +TEST_F(ArtToolsTest, Glob) { + CreateFile(scratch_path_ + "/abc/def/000.txt"); + CreateFile(scratch_path_ + "/abc/def/ghi/123.txt"); + CreateFile(scratch_path_ + "/abc/def/ghi/456.txt"); + CreateFile(scratch_path_ + "/abc/def/ghi/456.pdf"); + CreateFile(scratch_path_ + "/abc/def/ghi/jkl/456.txt"); + CreateFile(scratch_path_ + "/789.txt"); + CreateFile(scratch_path_ + "/abc/789.txt"); + CreateFile(scratch_path_ + "/abc/aaa/789.txt"); + CreateFile(scratch_path_ + "/abc/aaa/bbb/789.txt"); + CreateFile(scratch_path_ + "/abc/mno/123.txt"); + CreateFile(scratch_path_ + "/abc/aaa/mno/123.txt"); + CreateFile(scratch_path_ + "/abc/aaa/bbb/mno/123.txt"); + CreateFile(scratch_path_ + "/abc/aaa/bbb/mno/ccc/123.txt"); + CreateFile(scratch_path_ + "/pqr/123.txt"); + CreateFile(scratch_path_ + "/abc/pqr/123.txt"); + CreateFile(scratch_path_ + "/abc/aaa/pqr/123.txt"); + CreateFile(scratch_path_ + "/abc/aaa/bbb/pqr/123.txt"); + CreateFile(scratch_path_ + "/abc/aaa/bbb/pqr/ccc/123.txt"); + CreateFile(scratch_path_ + "/abc/aaa/bbb/pqr/ccc/ddd/123.txt"); + + // This symlink will cause infinite recursion. It should not be followed. + std::filesystem::create_directory_symlink(scratch_path_ + "/abc/aaa/bbb/pqr", + scratch_path_ + "/abc/aaa/bbb/pqr/lnk"); + + // This is a directory. It should not be included in the results. + std::filesystem::create_directory(scratch_path_ + "/abc/def/ghi/000.txt"); -class ArtToolsTest : public testing::Test {}; + std::vector<std::string> patterns = { + scratch_path_ + "/abc/def/000.txt", + scratch_path_ + "/abc/def/ghi/*.txt", + scratch_path_ + "/abc/**/789.txt", + scratch_path_ + "/abc/**/mno/*.txt", + scratch_path_ + "/abc/**/pqr/**", + }; -TEST_F(ArtToolsTest, Hello) { - EXPECT_EQ("hello world!", art::tools::getMsg()); + EXPECT_THAT(Glob(patterns, scratch_path_), + UnorderedElementsAre(scratch_path_ + "/abc/def/000.txt", + scratch_path_ + "/abc/def/ghi/123.txt", + scratch_path_ + "/abc/def/ghi/456.txt", + scratch_path_ + "/abc/789.txt", + scratch_path_ + "/abc/aaa/789.txt", + scratch_path_ + "/abc/aaa/bbb/789.txt", + scratch_path_ + "/abc/mno/123.txt", + scratch_path_ + "/abc/aaa/mno/123.txt", + scratch_path_ + "/abc/aaa/bbb/mno/123.txt", + scratch_path_ + "/abc/pqr/123.txt", + scratch_path_ + "/abc/aaa/pqr/123.txt", + scratch_path_ + "/abc/aaa/bbb/pqr/123.txt", + scratch_path_ + "/abc/aaa/bbb/pqr/ccc/123.txt", + scratch_path_ + "/abc/aaa/bbb/pqr/ccc/ddd/123.txt")); } +} // namespace +} // namespace tools } // namespace art |