diff options
40 files changed, 3324 insertions, 109 deletions
diff --git a/artd/Android.bp b/artd/Android.bp index c0ffd0a6f4..69c7706a7a 100644 --- a/artd/Android.bp +++ b/artd/Android.bp @@ -27,6 +27,7 @@ cc_defaults { defaults: ["art_defaults"], srcs: [ "artd.cc", + "path_utils.cc", ], shared_libs: [ "libarttools", @@ -35,6 +36,7 @@ cc_defaults { ], static_libs: [ "artd-aidl-ndk", + "libc++fs", ], } @@ -45,6 +47,7 @@ art_cc_binary { "artd_main.cc", ], shared_libs: [ + "libart", "libartbase", ], apex_available: [ @@ -63,8 +66,12 @@ prebuilt_etc { art_cc_defaults { name: "art_artd_tests_defaults", defaults: ["artd_defaults"], + static_libs: [ + "libgmock", + ], srcs: [ "artd_test.cc", + "path_utils_test.cc", ], } diff --git a/artd/artd.cc b/artd/artd.cc index 27a609d7be..33fe8af873 100644 --- a/artd/artd.cc +++ b/artd/artd.cc @@ -16,16 +16,31 @@ #include "artd.h" +#include <stdlib.h> #include <unistd.h> +#include <cstdint> +#include <filesystem> +#include <memory> #include <string> +#include <system_error> +#include <utility> +#include <vector> #include "aidl/com/android/server/art/BnArtd.h" +#include "android-base/errors.h" #include "android-base/logging.h" #include "android-base/result.h" +#include "android-base/stringprintf.h" +#include "android-base/strings.h" #include "android/binder_auto_utils.h" #include "android/binder_manager.h" #include "android/binder_process.h" +#include "base/array_ref.h" +#include "base/file_utils.h" +#include "oat_file_assistant.h" +#include "path_utils.h" +#include "runtime.h" #include "tools/tools.h" namespace art { @@ -33,12 +48,57 @@ namespace artd { namespace { +using ::aidl::com::android::server::art::ArtifactsPath; +using ::aidl::com::android::server::art::GetOptimizationStatusResult; using ::android::base::Error; using ::android::base::Result; +using ::android::base::Split; +using ::android::base::StringPrintf; using ::ndk::ScopedAStatus; constexpr const char* kServiceName = "artd"; +Result<std::vector<std::string>> GetBootClassPath() { + const char* env_value = getenv("BOOTCLASSPATH"); + if (env_value == nullptr || strlen(env_value) == 0) { + return Errorf("Failed to get environment variable 'BOOTCLASSPATH'"); + } + return Split(env_value, ":"); +} + +Result<std::vector<std::string>> GetBootImageLocations(bool deny_art_apex_data_files) { + std::string error_msg; + std::string android_root = GetAndroidRootSafe(&error_msg); + if (!error_msg.empty()) { + return Errorf("Failed to get ANDROID_ROOT: {}", error_msg); + } + + std::string location_str = GetDefaultBootImageLocation(android_root, deny_art_apex_data_files); + return Split(location_str, ":"); +} + +// Deletes a file. Returns the size of the deleted file, or 0 if the deleted file is empty or an +// error occurs. +int64_t GetSizeAndDeleteFile(const std::string& path) { + std::error_code ec; + int64_t size = std::filesystem::file_size(path, ec); + if (ec) { + // It is okay if the file does not exist. We don't have to log it. + if (ec.value() != ENOENT) { + LOG(ERROR) << StringPrintf( + "Failed to get the file size of '%s': %s", path.c_str(), ec.message().c_str()); + } + return 0; + } + + if (!std::filesystem::remove(path, ec)) { + LOG(ERROR) << StringPrintf("Failed to remove '%s': %s", path.c_str(), ec.message().c_str()); + return 0; + } + + return size; +} + } // namespace ScopedAStatus Artd::isAlive(bool* _aidl_return) { @@ -46,6 +106,64 @@ ScopedAStatus Artd::isAlive(bool* _aidl_return) { return ScopedAStatus::ok(); } +ScopedAStatus Artd::deleteArtifacts(const ArtifactsPath& in_artifactsPath, int64_t* _aidl_return) { + Result<std::string> oat_path = BuildOatPath(in_artifactsPath); + if (!oat_path.ok()) { + return ScopedAStatus::fromExceptionCodeWithMessage(EX_ILLEGAL_STATE, + oat_path.error().message().c_str()); + } + + *_aidl_return = 0; + *_aidl_return += GetSizeAndDeleteFile(*oat_path); + *_aidl_return += GetSizeAndDeleteFile(OatPathToVdexPath(*oat_path)); + *_aidl_return += GetSizeAndDeleteFile(OatPathToArtPath(*oat_path)); + + return ScopedAStatus::ok(); +} + +ScopedAStatus Artd::getOptimizationStatus(const std::string& in_dexFile, + const std::string& in_instructionSet, + const std::string& in_classLoaderContext, + GetOptimizationStatusResult* _aidl_return) { + Result<OatFileAssistant::RuntimeOptions> runtime_options = GetRuntimeOptions(); + if (!runtime_options.ok()) { + return ScopedAStatus::fromExceptionCodeWithMessage( + EX_ILLEGAL_STATE, + ("Failed to get runtime options: " + runtime_options.error().message()).c_str()); + } + + std::unique_ptr<ClassLoaderContext> context; + std::string error_msg; + auto oat_file_assistant = OatFileAssistant::Create( + in_dexFile.c_str(), + in_instructionSet.c_str(), + in_classLoaderContext.c_str(), + /*load_executable=*/false, + /*only_load_trusted_executable=*/true, + std::make_unique<OatFileAssistant::RuntimeOptions>(std::move(*runtime_options)), + &context, + &error_msg); + if (oat_file_assistant == nullptr) { + return ScopedAStatus::fromExceptionCodeWithMessage( + EX_ILLEGAL_STATE, ("Failed to create OatFileAssistant: " + error_msg).c_str()); + } + + std::string ignored_odex_status; + oat_file_assistant->GetOptimizationStatus(&_aidl_return->compilerFilter, + &_aidl_return->compilationReason, + &_aidl_return->locationDebugString, + &ignored_odex_status); + + // We ignore odex_status because it is not meaningful. It can only be either "up-to-date", + // "apk-more-recent", or "io-error-no-oat", which means it doesn't give us information in addition + // to what we can learn from compiler_filter because compiler_filter will be the actual compiler + // filter, "run-from-apk-fallback", and "run-from-apk" in those three cases respectively. + DCHECK(ignored_odex_status == "up-to-date" || ignored_odex_status == "apk-more-recent" || + ignored_odex_status == "io-error-no-oat"); + + return ScopedAStatus::ok(); +} + Result<void> Artd::Start() { ScopedAStatus status = ScopedAStatus::fromStatus( AServiceManager_registerLazyService(this->asBinder().get(), kServiceName)); @@ -58,5 +176,53 @@ Result<void> Artd::Start() { return {}; } +Result<OatFileAssistant::RuntimeOptions> Artd::GetRuntimeOptions() { + // We don't cache this system property because it can change. + bool use_jit_zygote = UseJitZygote(); + + if (!HasRuntimeOptionsCache()) { + OR_RETURN(BuildRuntimeOptionsCache()); + } + + return OatFileAssistant::RuntimeOptions{ + .image_locations = cached_boot_image_locations_, + .boot_class_path = cached_boot_class_path_, + .boot_class_path_locations = cached_boot_class_path_, + .use_jit_zygote = use_jit_zygote, + .deny_art_apex_data_files = cached_deny_art_apex_data_files_, + .apex_versions = cached_apex_versions_, + }; +} + +Result<void> Artd::BuildRuntimeOptionsCache() { + // This system property can only be set by odsign on boot, so it won't change. + bool deny_art_apex_data_files = DenyArtApexDataFiles(); + + std::vector<std::string> image_locations = + OR_RETURN(GetBootImageLocations(deny_art_apex_data_files)); + std::vector<std::string> boot_class_path = OR_RETURN(GetBootClassPath()); + std::string apex_versions = + Runtime::GetApexVersions(ArrayRef<const std::string>(boot_class_path)); + + cached_boot_image_locations_ = std::move(image_locations); + cached_boot_class_path_ = std::move(boot_class_path); + cached_apex_versions_ = std::move(apex_versions); + cached_deny_art_apex_data_files_ = deny_art_apex_data_files; + + return {}; +} + +bool Artd::HasRuntimeOptionsCache() const { return !cached_boot_image_locations_.empty(); } + +bool Artd::UseJitZygote() const { + return props_->GetBool("dalvik.vm.profilebootclasspath", + "persist.device_config.runtime_native_boot.profilebootclasspath", + /*default_value=*/false); +} + +bool Artd::DenyArtApexDataFiles() const { + return !props_->GetBool("odsign.verification.success", /*default_value=*/false); +} + } // namespace artd } // namespace art diff --git a/artd/artd.h b/artd/artd.h index f01d9a8a23..cb7a12e0ac 100644 --- a/artd/artd.h +++ b/artd/artd.h @@ -17,18 +17,54 @@ #ifndef ART_ARTD_ARTD_H_ #define ART_ARTD_ARTD_H_ +#include <string> +#include <vector> + #include "aidl/com/android/server/art/BnArtd.h" #include "android-base/result.h" #include "android/binder_auto_utils.h" +#include "oat_file_assistant.h" +#include "tools/system_properties.h" namespace art { namespace artd { class Artd : public aidl::com::android::server::art::BnArtd { public: + explicit Artd(std::unique_ptr<art::tools::SystemProperties> props = + std::make_unique<art::tools::SystemProperties>()) + : props_(std::move(props)) {} + ndk::ScopedAStatus isAlive(bool* _aidl_return) override; + ndk::ScopedAStatus deleteArtifacts( + const aidl::com::android::server::art::ArtifactsPath& in_artifactsPath, + int64_t* _aidl_return) override; + + ndk::ScopedAStatus getOptimizationStatus( + const std::string& in_dexFile, + const std::string& in_instructionSet, + const std::string& in_classLoaderContext, + aidl::com::android::server::art::GetOptimizationStatusResult* _aidl_return) override; + android::base::Result<void> Start(); + + private: + android::base::Result<OatFileAssistant::RuntimeOptions> GetRuntimeOptions(); + + android::base::Result<void> BuildRuntimeOptionsCache(); + + bool HasRuntimeOptionsCache() const; + + bool UseJitZygote() const; + + bool DenyArtApexDataFiles() const; + + std::vector<std::string> cached_boot_image_locations_; + std::vector<std::string> cached_boot_class_path_; + std::string cached_apex_versions_; + bool cached_deny_art_apex_data_files_; + std::unique_ptr<art::tools::SystemProperties> props_; }; } // namespace artd diff --git a/artd/artd_test.cc b/artd/artd_test.cc index 14bccc2999..129e31cd01 100644 --- a/artd/artd_test.cc +++ b/artd/artd_test.cc @@ -16,26 +16,53 @@ #include "artd.h" +#include <filesystem> +#include <functional> #include <memory> +#include "android-base/file.h" +#include "android-base/logging.h" +#include "android-base/scopeguard.h" #include "android/binder_interface_utils.h" #include "base/common_art_test.h" +#include "gmock/gmock.h" #include "gtest/gtest.h" namespace art { namespace artd { namespace { +using ::aidl::com::android::server::art::ArtifactsPath; +using ::android::base::make_scope_guard; +using ::android::base::ScopeGuard; +using ::testing::_; +using ::testing::ContainsRegex; +using ::testing::HasSubstr; +using ::testing::MockFunction; + +ScopeGuard<std::function<void()>> ScopedSetLogger(android::base::LogFunction&& logger) { + android::base::LogFunction old_logger = android::base::SetLogger(std::move(logger)); + return make_scope_guard([old_logger = std::move(old_logger)]() mutable { + android::base::SetLogger(std::move(old_logger)); + }); +} + class ArtdTest : public CommonArtTest { protected: void SetUp() override { CommonArtTest::SetUp(); artd_ = ndk::SharedRefBase::make<Artd>(); + scratch_dir_ = std::make_unique<ScratchDir>(); } - void TearDown() override { CommonArtTest::TearDown(); } + void TearDown() override { + scratch_dir_.reset(); + CommonArtTest::TearDown(); + } std::shared_ptr<Artd> artd_; + std::unique_ptr<ScratchDir> scratch_dir_; + MockFunction<android::base::LogFunction> mock_logger_; }; TEST_F(ArtdTest, isAlive) { @@ -44,6 +71,130 @@ TEST_F(ArtdTest, isAlive) { EXPECT_TRUE(result); } +TEST_F(ArtdTest, deleteArtifacts) { + std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64"; + std::filesystem::create_directories(oat_dir); + android::base::WriteStringToFile("abcd", oat_dir + "/b.odex"); // 4 bytes. + android::base::WriteStringToFile("ab", oat_dir + "/b.vdex"); // 2 bytes. + android::base::WriteStringToFile("a", oat_dir + "/b.art"); // 1 byte. + + int64_t result = -1; + EXPECT_TRUE(artd_ + ->deleteArtifacts( + ArtifactsPath{ + .dexPath = scratch_dir_->GetPath() + "/a/b.apk", + .isa = "arm64", + .isInDalvikCache = false, + }, + &result) + .isOk()); + EXPECT_EQ(result, 4 + 2 + 1); + + EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.odex")); + EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.vdex")); + EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.art")); +} + +TEST_F(ArtdTest, deleteArtifactsMissingFile) { + // Missing VDEX file. + std::string oat_dir = dalvik_cache_ + "/arm64"; + std::filesystem::create_directories(oat_dir); + android::base::WriteStringToFile("abcd", oat_dir + "/a@b.apk@classes.dex"); // 4 bytes. + android::base::WriteStringToFile("a", oat_dir + "/a@b.apk@classes.art"); // 1 byte. + + auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction()); + EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(0); + + int64_t result = -1; + EXPECT_TRUE(artd_ + ->deleteArtifacts( + ArtifactsPath{ + .dexPath = "/a/b.apk", + .isa = "arm64", + .isInDalvikCache = true, + }, + &result) + .isOk()); + EXPECT_EQ(result, 4 + 1); + + EXPECT_FALSE(std::filesystem::exists(oat_dir + "/a@b.apk@classes.dex")); + EXPECT_FALSE(std::filesystem::exists(oat_dir + "/a@b.apk@classes.art")); +} + +TEST_F(ArtdTest, deleteArtifactsNoFile) { + auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction()); + EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(0); + + int64_t result = -1; + EXPECT_TRUE(artd_ + ->deleteArtifacts( + ArtifactsPath{ + .dexPath = android_data_ + "/a/b.apk", + .isa = "arm64", + .isInDalvikCache = false, + }, + &result) + .isOk()); + EXPECT_EQ(result, 0); +} + +TEST_F(ArtdTest, deleteArtifactsPermissionDenied) { + std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64"; + std::filesystem::create_directories(oat_dir); + android::base::WriteStringToFile("abcd", oat_dir + "/b.odex"); // 4 bytes. + android::base::WriteStringToFile("ab", oat_dir + "/b.vdex"); // 2 bytes. + android::base::WriteStringToFile("a", oat_dir + "/b.art"); // 1 byte. + + auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction()); + EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(3); + + auto scoped_inaccessible = ScopedInaccessible(oat_dir); + auto scoped_unroot = ScopedUnroot(); + + int64_t result = -1; + EXPECT_TRUE(artd_ + ->deleteArtifacts( + ArtifactsPath{ + .dexPath = scratch_dir_->GetPath() + "/a/b.apk", + .isa = "arm64", + .isInDalvikCache = false, + }, + &result) + .isOk()); + EXPECT_EQ(result, 0); +} + +TEST_F(ArtdTest, deleteArtifactsFileIsDir) { + // VDEX file is a directory. + std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64"; + std::filesystem::create_directories(oat_dir); + std::filesystem::create_directories(oat_dir + "/b.vdex"); + android::base::WriteStringToFile("abcd", oat_dir + "/b.odex"); // 4 bytes. + android::base::WriteStringToFile("a", oat_dir + "/b.art"); // 1 byte. + + auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction()); + EXPECT_CALL(mock_logger_, + Call(_, _, _, _, _, ContainsRegex(R"re(Failed to get the file size.*b\.vdex)re"))) + .Times(1); + + int64_t result = -1; + EXPECT_TRUE(artd_ + ->deleteArtifacts( + ArtifactsPath{ + .dexPath = scratch_dir_->GetPath() + "/a/b.apk", + .isa = "arm64", + .isInDalvikCache = false, + }, + &result) + .isOk()); + EXPECT_EQ(result, 4 + 1); + + // The directory is kept because getting the file size failed. + EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.odex")); + EXPECT_TRUE(std::filesystem::exists(oat_dir + "/b.vdex")); + EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.art")); +} + } // namespace } // namespace artd } // namespace art diff --git a/artd/binder/Android.bp b/artd/binder/Android.bp index ad8474f369..b6fd5b8241 100644 --- a/artd/binder/Android.bp +++ b/artd/binder/Android.bp @@ -31,6 +31,10 @@ aidl_interface { backend: { java: { enabled: true, + apex_available: [ + "com.android.art", + "com.android.art.debug", + ], }, cpp: { enabled: false, @@ -40,9 +44,7 @@ aidl_interface { apex_available: [ "com.android.art", "com.android.art.debug", - "com.android.compos", ], - min_sdk_version: "31", }, }, unstable: true, @@ -50,4 +52,5 @@ aidl_interface { "//system/tools/aidl/build", "//art:__subpackages__", ], + min_sdk_version: "31", } diff --git a/artd/binder/com/android/server/art/ArtifactsPath.aidl b/artd/binder/com/android/server/art/ArtifactsPath.aidl new file mode 100644 index 0000000000..f69b439d93 --- /dev/null +++ b/artd/binder/com/android/server/art/ArtifactsPath.aidl @@ -0,0 +1,31 @@ +/* + * 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. + */ + +package com.android.server.art; + +/** + * Represents the path to the optimized artifacts of a dex file (i.e., ART, OAT, and VDEX files). + * + * @hide + */ +parcelable ArtifactsPath { + /** The absolute path starting with '/' to the dex file (i.e., APK or JAR file). */ + @utf8InCpp String dexPath; + /** The instruction set of the optimized artifacts. */ + @utf8InCpp String isa; + /** Whether the optimized artifacts are in the dalvik-cache folder. */ + boolean isInDalvikCache; +} diff --git a/artd/binder/com/android/server/art/GetOptimizationStatusResult.aidl b/artd/binder/com/android/server/art/GetOptimizationStatusResult.aidl new file mode 100644 index 0000000000..99a2e37798 --- /dev/null +++ b/artd/binder/com/android/server/art/GetOptimizationStatusResult.aidl @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package com.android.server.art; + +/** + * The result of {@code IArtd.getOptimizationStatus}. Each field corresponds to a field in + * {@code com.android.server.art.model.OptimizationStatus.DexFileOptimizationStatus}. + * + * @hide + */ +parcelable GetOptimizationStatusResult { + @utf8InCpp String compilerFilter; + @utf8InCpp String compilationReason; + @utf8InCpp String locationDebugString; +} diff --git a/artd/binder/com/android/server/art/IArtd.aidl b/artd/binder/com/android/server/art/IArtd.aidl index 58b2aae3b9..a1df266de0 100644 --- a/artd/binder/com/android/server/art/IArtd.aidl +++ b/artd/binder/com/android/server/art/IArtd.aidl @@ -16,8 +16,16 @@ package com.android.server.art; -/** {@hide} */ +/** @hide */ interface IArtd { // Test to see if the artd service is available. boolean isAlive(); + + /** Deletes artifacts and returns the released space, in bytes. */ + long deleteArtifacts(in com.android.server.art.ArtifactsPath artifactsPath); + + /** Returns the optimization status of a dex file. */ + com.android.server.art.GetOptimizationStatusResult getOptimizationStatus( + @utf8InCpp String dexFile, @utf8InCpp String instructionSet, + @utf8InCpp String classLoaderContext); } diff --git a/artd/path_utils.cc b/artd/path_utils.cc new file mode 100644 index 0000000000..c4d9031220 --- /dev/null +++ b/artd/path_utils.cc @@ -0,0 +1,98 @@ +/* + * 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 "path_utils.h" + +#include <filesystem> + +#include "aidl/com/android/server/art/BnArtd.h" +#include "android-base/errors.h" +#include "android-base/result.h" +#include "android-base/strings.h" +#include "arch/instruction_set.h" +#include "base/file_utils.h" +#include "oat_file_assistant.h" + +namespace art { +namespace artd { + +namespace { + +using ::aidl::com::android::server::art::ArtifactsPath; +using ::android::base::EndsWith; +using ::android::base::Error; +using ::android::base::Result; + +Result<void> ValidateAbsoluteNormalPath(const std::string& path_str) { + if (path_str.empty()) { + return Errorf("Path is empty"); + } + std::filesystem::path path(path_str); + if (!path.is_absolute()) { + return Errorf("Path '{}' is not an absolute path", path_str); + } + if (path.lexically_normal() != path_str) { + return Errorf("Path '{}' is not in normal form", path_str); + } + return {}; +} + +Result<void> ValidateDexPath(const std::string& dex_path) { + OR_RETURN(ValidateAbsoluteNormalPath(dex_path)); + if (!EndsWith(dex_path, ".apk") && !EndsWith(dex_path, ".jar")) { + return Errorf("Dex path '{}' has an invalid extension", dex_path); + } + return {}; +} + +} // namespace + +Result<std::string> BuildOatPath(const ArtifactsPath& artifacts_path) { + OR_RETURN(ValidateDexPath(artifacts_path.dexPath)); + + InstructionSet isa = GetInstructionSetFromString(artifacts_path.isa.c_str()); + if (isa == InstructionSet::kNone) { + return Errorf("Instruction set '{}' is invalid", artifacts_path.isa.c_str()); + } + + std::string error_msg; + std::string path; + if (artifacts_path.isInDalvikCache) { + // Apps' OAT files are never in ART APEX data. + if (!OatFileAssistant::DexLocationToOatFilename( + artifacts_path.dexPath, isa, /*deny_art_apex_data_files=*/true, &path, &error_msg)) { + return Error() << error_msg; + } + return path; + } else { + if (!OatFileAssistant::DexLocationToOdexFilename( + artifacts_path.dexPath, isa, &path, &error_msg)) { + return Error() << error_msg; + } + return path; + } +} + +std::string OatPathToVdexPath(const std::string& oat_path) { + return ReplaceFileExtension(oat_path, "vdex"); +} + +std::string OatPathToArtPath(const std::string& oat_path) { + return ReplaceFileExtension(oat_path, "art"); +} + +} // namespace artd +} // namespace art diff --git a/artd/path_utils.h b/artd/path_utils.h new file mode 100644 index 0000000000..970143a9c7 --- /dev/null +++ b/artd/path_utils.h @@ -0,0 +1,39 @@ +/* + * 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_ARTD_PATH_UTILS_H_ +#define ART_ARTD_PATH_UTILS_H_ + +#include "aidl/com/android/server/art/BnArtd.h" +#include "android-base/result.h" + +namespace art { +namespace artd { + +// Returns the absolute path to the OAT file built from the `ArtifactsPath`. +android::base::Result<std::string> BuildOatPath( + const aidl::com::android::server::art::ArtifactsPath& artifacts_path); + +// Returns the path to the VDEX file that corresponds to the OAT file. +std::string OatPathToVdexPath(const std::string& oat_path); + +// Returns the path to the ART file that corresponds to the OAT file. +std::string OatPathToArtPath(const std::string& oat_path); + +} // namespace artd +} // namespace art + +#endif // ART_ARTD_PATH_UTILS_H_ diff --git a/artd/path_utils_test.cc b/artd/path_utils_test.cc new file mode 100644 index 0000000000..9ce40c5d41 --- /dev/null +++ b/artd/path_utils_test.cc @@ -0,0 +1,86 @@ +/* + * 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 "path_utils.h" + +#include "aidl/com/android/server/art/BnArtd.h" +#include "android-base/result-gmock.h" +#include "base/common_art_test.h" +#include "gtest/gtest.h" + +namespace art { +namespace artd { +namespace { + +using ::aidl::com::android::server::art::ArtifactsPath; +using ::android::base::testing::HasError; +using ::android::base::testing::HasValue; +using ::android::base::testing::WithMessage; + +class PathUtilsTest : public CommonArtTest {}; + +TEST_F(PathUtilsTest, BuildOatPath) { + EXPECT_THAT( + BuildOatPath(ArtifactsPath{.dexPath = "/a/b.apk", .isa = "arm64", .isInDalvikCache = false}), + HasValue("/a/oat/arm64/b.odex")); +} + +TEST_F(PathUtilsTest, BuildOatPathDalvikCache) { + EXPECT_THAT( + BuildOatPath(ArtifactsPath{.dexPath = "/a/b.apk", .isa = "arm64", .isInDalvikCache = true}), + HasValue(android_data_ + "/dalvik-cache/arm64/a@b.apk@classes.dex")); +} + +TEST_F(PathUtilsTest, BuildOatPathEmptyDexPath) { + EXPECT_THAT(BuildOatPath(ArtifactsPath{.dexPath = "", .isa = "arm64", .isInDalvikCache = false}), + HasError(WithMessage("Path is empty"))); +} + +TEST_F(PathUtilsTest, BuildOatPathRelativeDexPath) { + EXPECT_THAT( + BuildOatPath(ArtifactsPath{.dexPath = "a/b.apk", .isa = "arm64", .isInDalvikCache = false}), + HasError(WithMessage("Path 'a/b.apk' is not an absolute path"))); +} + +TEST_F(PathUtilsTest, BuildOatPathNonNormalDexPath) { + EXPECT_THAT(BuildOatPath(ArtifactsPath{ + .dexPath = "/a/c/../b.apk", .isa = "arm64", .isInDalvikCache = false}), + HasError(WithMessage("Path '/a/c/../b.apk' is not in normal form"))); +} + +TEST_F(PathUtilsTest, BuildOatPathInvalidDexExtension) { + EXPECT_THAT(BuildOatPath(ArtifactsPath{ + .dexPath = "/a/b.invalid", .isa = "arm64", .isInDalvikCache = false}), + HasError(WithMessage("Dex path '/a/b.invalid' has an invalid extension"))); +} + +TEST_F(PathUtilsTest, BuildOatPathInvalidIsa) { + EXPECT_THAT(BuildOatPath( + ArtifactsPath{.dexPath = "/a/b.apk", .isa = "invalid", .isInDalvikCache = false}), + HasError(WithMessage("Instruction set 'invalid' is invalid"))); +} + +TEST_F(PathUtilsTest, OatPathToVdexPath) { + EXPECT_EQ(OatPathToVdexPath("/a/oat/arm64/b.odex"), "/a/oat/arm64/b.vdex"); +} + +TEST_F(PathUtilsTest, OatPathToArtPath) { + EXPECT_EQ(OatPathToArtPath("/a/oat/arm64/b.odex"), "/a/oat/arm64/b.art"); +} + +} // namespace +} // namespace artd +} // namespace art diff --git a/build/Android.bp b/build/Android.bp index c0323b4a35..136019443d 100644 --- a/build/Android.bp +++ b/build/Android.bp @@ -40,9 +40,9 @@ art_clang_tidy_errors = [ "performance-faster-string-find", "performance-for-range-copy", "performance-implicit-conversion-in-loop", - "performance-noexcept-move-constructor", "performance-unnecessary-copy-initialization", "performance-unnecessary-value-param", + "performance-noexcept-move-constructor", ] art_clang_tidy_allowed = [ diff --git a/libartservice/service/Android.bp b/libartservice/service/Android.bp index 8805430e16..7bd4c34baf 100644 --- a/libartservice/service/Android.bp +++ b/libartservice/service/Android.bp @@ -82,6 +82,8 @@ java_sdk_library { "java/**/*.java", ], static_libs: [ + "artd-aidl-java", + "modules-utils-shell-command-handler", ], plugins: ["java_api_finder"], jarjar_rules: "jarjar-rules.txt", @@ -127,11 +129,15 @@ android_test { "androidx.test.ext.junit", "androidx.test.ext.truth", "androidx.test.runner", + "artd-aidl-java", "mockito-target-minus-junit4", "service-art.impl", + // Statically link against system server to allow us to mock system + // server APIs. This won't work on master-art, but it's fine because we + // don't run this test on master-art. + "services.core", ], - sdk_version: "system_server_current", min_sdk_version: "31", test_suites: ["general-tests"], diff --git a/libartservice/service/api/system-server-current.txt b/libartservice/service/api/system-server-current.txt index c7844e0780..d35f8c760e 100644 --- a/libartservice/service/api/system-server-current.txt +++ b/libartservice/service/api/system-server-current.txt @@ -3,6 +3,38 @@ package com.android.server.art { public final class ArtManagerLocal { ctor public ArtManagerLocal(); + method @NonNull public com.android.server.art.model.DeleteResult deleteOptimizedArtifacts(@NonNull com.android.server.pm.snapshot.PackageDataSnapshot, @NonNull String); + method @NonNull public com.android.server.art.model.DeleteResult deleteOptimizedArtifacts(@NonNull com.android.server.pm.snapshot.PackageDataSnapshot, @NonNull String, int); + method @NonNull public com.android.server.art.model.OptimizationStatus getOptimizationStatus(@NonNull com.android.server.pm.snapshot.PackageDataSnapshot, @NonNull String); + method @NonNull public com.android.server.art.model.OptimizationStatus getOptimizationStatus(@NonNull com.android.server.pm.snapshot.PackageDataSnapshot, @NonNull String, int); + method public int handleShellCommand(@NonNull android.os.Binder, @NonNull android.os.ParcelFileDescriptor, @NonNull android.os.ParcelFileDescriptor, @NonNull android.os.ParcelFileDescriptor, @NonNull String[]); + } + +} + +package com.android.server.art.model { + + public class ArtFlags { + method public static int defaultDeleteFlags(); + method public static int defaultGetStatusFlags(); + field public static final int FLAG_FOR_PRIMARY_DEX = 1; // 0x1 + field public static final int FLAG_FOR_SECONDARY_DEX = 2; // 0x2 + } + + public class DeleteResult { + method public long getFreedBytes(); + } + + public class OptimizationStatus { + method @NonNull public java.util.List<com.android.server.art.model.OptimizationStatus.DexFileOptimizationStatus> getDexFileOptimizationStatuses(); + } + + public static class OptimizationStatus.DexFileOptimizationStatus { + method @NonNull public String getCompilationReason(); + method @NonNull public String getCompilerFilter(); + method @NonNull public String getDexFile(); + method @NonNull public String getInstructionSet(); + method @NonNull public String getLocationDebugString(); } } diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java index 64aec7bf6b..3a6bdc9ee4 100644 --- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java +++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java @@ -16,16 +16,251 @@ package com.android.server.art; +import static com.android.server.art.PrimaryDexUtils.DetailedPrimaryDexInfo; +import static com.android.server.art.PrimaryDexUtils.PrimaryDexInfo; +import static com.android.server.art.model.ArtFlags.DeleteFlags; +import static com.android.server.art.model.ArtFlags.GetStatusFlags; +import static com.android.server.art.model.OptimizationStatus.DexFileOptimizationStatus; + +import android.annotation.NonNull; import android.annotation.SystemApi; +import android.os.Binder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.art.IArtd; +import com.android.server.art.model.ArtFlags; +import com.android.server.art.model.DeleteResult; +import com.android.server.art.model.OptimizationStatus; +import com.android.server.art.wrapper.AndroidPackageApi; +import com.android.server.art.wrapper.PackageManagerLocal; +import com.android.server.art.wrapper.PackageState; +import com.android.server.pm.snapshot.PackageDataSnapshot; + +import java.util.ArrayList; +import java.util.List; /** * This class provides a system API for functionality provided by the ART module. * + * Note: Although this class is the entry point of ART services, this class is not a {@link + * SystemService}, and it does not publish a binder. Instead, it is a module loaded by the + * system_server process, registered in {@link LocalManagerRegistry}. {@link LocalManagerRegistry} + * specifies that in-process module interfaces should be named with the suffix {@code ManagerLocal} + * for consistency. + * * @hide */ @SystemApi(client = SystemApi.Client.SYSTEM_SERVER) public final class ArtManagerLocal { private static final String TAG = "ArtService"; - public ArtManagerLocal() {} + @NonNull private final Injector mInjector; + + public ArtManagerLocal() { + this(new Injector()); + } + + /** @hide */ + @VisibleForTesting + public ArtManagerLocal(Injector injector) { + mInjector = injector; + } + + /** + * Handles `cmd package art` sub-command. + * + * For debugging purposes only. Intentionally enforces root access to limit the usage. + * + * Note: This method is not an override of {@link Binder#handleShellCommand} because ART + * services does not publish a binder. Instead, it handles the `art` sub-command forwarded by + * the `package` service. The semantics of the parameters are the same as {@link + * Binder#handleShellCommand}. + * + * @return zero on success, non-zero on internal error (e.g., I/O error) + * @throws SecurityException if the caller is not root + * @throws IllegalArgumentException if the arguments are illegal + * @see ArtShellCommand#onHelp() + */ + public int handleShellCommand(@NonNull Binder target, @NonNull ParcelFileDescriptor in, + @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err, + @NonNull String[] args) { + return new ArtShellCommand(this, mInjector.getPackageManagerLocal()) + .exec(target, in.getFileDescriptor(), out.getFileDescriptor(), + err.getFileDescriptor(), args); + } + + /** + * Deletes optimized artifacts of a package. + * + * @throws IllegalArgumentException if the package is not found or the flags are illegal + * @throws IllegalStateException if an internal error occurs + */ + @NonNull + public DeleteResult deleteOptimizedArtifacts( + @NonNull PackageDataSnapshot snapshot, @NonNull String packageName) { + return deleteOptimizedArtifacts(snapshot, packageName, ArtFlags.defaultDeleteFlags()); + } + + /** + * Same as above, but allows to specify flags. + * + * @see #deleteOptimizedArtifacts(PackageDataSnapshot, String) + */ + @NonNull + public DeleteResult deleteOptimizedArtifacts(@NonNull PackageDataSnapshot snapshot, + @NonNull String packageName, @DeleteFlags int flags) { + if ((flags & ArtFlags.FLAG_FOR_PRIMARY_DEX) == 0 + && (flags & ArtFlags.FLAG_FOR_SECONDARY_DEX) == 0) { + throw new IllegalArgumentException("Nothing to delete"); + } + + PackageState pkgState = getPackageStateOrThrow(snapshot, packageName); + AndroidPackageApi pkg = getPackageOrThrow(pkgState); + + try { + long freedBytes = 0; + + if ((flags & ArtFlags.FLAG_FOR_PRIMARY_DEX) != 0) { + boolean isInDalvikCache = Utils.isInDalvikCache(pkgState); + for (PrimaryDexInfo dexInfo : PrimaryDexUtils.getDexInfo(pkg)) { + if (!dexInfo.hasCode()) { + continue; + } + for (String isa : Utils.getAllIsas(pkgState)) { + freedBytes += mInjector.getArtd().deleteArtifacts( + Utils.buildArtifactsPath(dexInfo.dexPath(), isa, isInDalvikCache)); + } + } + } + + if ((flags & ArtFlags.FLAG_FOR_SECONDARY_DEX) != 0) { + // TODO(jiakaiz): Implement this. + throw new UnsupportedOperationException( + "Deleting artifacts of secondary dex'es is not implemented yet"); + } + + return new DeleteResult(freedBytes); + } catch (RemoteException e) { + throw new IllegalStateException("An error occurred when calling artd", e); + } + } + + /** + * Returns the optimization status of a package. + * + * @throws IllegalArgumentException if the package is not found or the flags are illegal + * @throws IllegalStateException if an internal error occurs + */ + @NonNull + public OptimizationStatus getOptimizationStatus( + @NonNull PackageDataSnapshot snapshot, @NonNull String packageName) { + return getOptimizationStatus(snapshot, packageName, ArtFlags.defaultGetStatusFlags()); + } + + /** + * Same as above, but allows to specify flags. + * + * @see #getOptimizationStatus(PackageDataSnapshot, String) + */ + @NonNull + public OptimizationStatus getOptimizationStatus(@NonNull PackageDataSnapshot snapshot, + @NonNull String packageName, @GetStatusFlags int flags) { + if ((flags & ArtFlags.FLAG_FOR_PRIMARY_DEX) == 0 + && (flags & ArtFlags.FLAG_FOR_SECONDARY_DEX) == 0) { + throw new IllegalArgumentException("Nothing to check"); + } + + PackageState pkgState = getPackageStateOrThrow(snapshot, packageName); + AndroidPackageApi pkg = getPackageOrThrow(pkgState); + + try { + List<DexFileOptimizationStatus> statuses = new ArrayList<>(); + + if ((flags & ArtFlags.FLAG_FOR_PRIMARY_DEX) != 0) { + for (DetailedPrimaryDexInfo dexInfo : + PrimaryDexUtils.getDetailedDexInfo(pkgState, pkg)) { + if (!dexInfo.hasCode()) { + continue; + } + for (String isa : Utils.getAllIsas(pkgState)) { + GetOptimizationStatusResult result = + mInjector.getArtd().getOptimizationStatus( + dexInfo.dexPath(), isa, dexInfo.classLoaderContext()); + statuses.add(new DexFileOptimizationStatus(dexInfo.dexPath(), isa, + result.compilerFilter, result.compilationReason, + result.locationDebugString)); + } + } + } + + if ((flags & ArtFlags.FLAG_FOR_SECONDARY_DEX) != 0) { + // TODO(jiakaiz): Implement this. + throw new UnsupportedOperationException( + "Getting optimization status of secondary dex'es is not implemented yet"); + } + + return new OptimizationStatus(statuses); + } catch (RemoteException e) { + throw new IllegalStateException("An error occurred when calling artd", e); + } + } + + private PackageState getPackageStateOrThrow( + @NonNull PackageDataSnapshot snapshot, @NonNull String packageName) { + PackageState pkgState = mInjector.getPackageManagerLocal().getPackageState( + snapshot, Binder.getCallingUid(), packageName); + if (pkgState == null) { + throw new IllegalArgumentException("Package not found: " + packageName); + } + return pkgState; + } + + private AndroidPackageApi getPackageOrThrow(@NonNull PackageState pkgState) { + AndroidPackageApi pkg = pkgState.getAndroidPackage(); + if (pkg == null) { + throw new IllegalStateException("Unable to get package " + pkgState.getPackageName()); + } + return pkg; + } + + /** + * Injector pattern for testing purpose. + * + * @hide + */ + @VisibleForTesting + public static class Injector { + private final PackageManagerLocal mPackageManagerLocal; + + Injector() { + PackageManagerLocal packageManagerLocal = null; + try { + packageManagerLocal = PackageManagerLocal.getInstance(); + } catch (Exception e) { + // This is not a serious error. The reflection-based approach can be broken in some + // cases. This is fine because ART services is under development and no one depends + // on it. + // TODO(b/177273468): Make this a serious error when we switch to using the real + // APIs. + Log.w(TAG, "Unable to get fake PackageManagerLocal", e); + } + mPackageManagerLocal = packageManagerLocal; + } + + public PackageManagerLocal getPackageManagerLocal() { + return mPackageManagerLocal; + } + + public IArtd getArtd() { + IArtd artd = IArtd.Stub.asInterface(ServiceManager.waitForService("artd")); + if (artd == null) { + throw new IllegalStateException("Unable to connect to artd"); + } + return artd; + } + } } diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java new file mode 100644 index 0000000000..9a49aaecc2 --- /dev/null +++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java @@ -0,0 +1,108 @@ +/* + * 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. + */ + +package com.android.server.art; + +import static com.android.server.art.model.OptimizationStatus.DexFileOptimizationStatus; + +import android.os.Binder; +import android.os.Process; + +import com.android.modules.utils.BasicShellCommandHandler; +import com.android.server.art.model.ArtFlags; +import com.android.server.art.model.DeleteResult; +import com.android.server.art.model.OptimizationStatus; +import com.android.server.art.wrapper.PackageManagerLocal; +import com.android.server.pm.snapshot.PackageDataSnapshot; + +import java.io.PrintWriter; + +/** + * This class handles ART shell commands. + * + * @hide + */ +public final class ArtShellCommand extends BasicShellCommandHandler { + private static final String TAG = "ArtShellCommand"; + + private final ArtManagerLocal mArtManagerLocal; + private final PackageManagerLocal mPackageManagerLocal; + + public ArtShellCommand( + ArtManagerLocal artManagerLocal, PackageManagerLocal packageManagerLocal) { + mArtManagerLocal = artManagerLocal; + mPackageManagerLocal = packageManagerLocal; + } + + @Override + public int onCommand(String cmd) { + enforceRoot(); + PrintWriter pw = getOutPrintWriter(); + PackageDataSnapshot snapshot = mPackageManagerLocal.snapshot(); + switch (cmd) { + case "delete-optimized-artifacts": + DeleteResult result = mArtManagerLocal.deleteOptimizedArtifacts( + snapshot, getNextArgRequired(), ArtFlags.defaultDeleteFlags()); + pw.printf("Freed %d bytes\n", result.getFreedBytes()); + return 0; + case "get-optimization-status": + OptimizationStatus optimizationStatus = mArtManagerLocal.getOptimizationStatus( + snapshot, getNextArgRequired(), ArtFlags.defaultGetStatusFlags()); + for (DexFileOptimizationStatus status : + optimizationStatus.getDexFileOptimizationStatuses()) { + pw.printf("dexFile = %s, instructionSet = %s, compilerFilter = %s, " + + "compilationReason = %s, locationDebugString = %s\n", + status.getDexFile(), status.getInstructionSet(), + status.getCompilerFilter(), status.getCompilationReason(), + status.getLocationDebugString()); + } + return 0; + default: + // Handles empty, help, and invalid commands. + return handleDefaultCommands(cmd); + } + } + + @Override + public void onHelp() { + final PrintWriter pw = getOutPrintWriter(); + pw.println("ART service commands."); + pw.println("Note: The commands are used for internal debugging purposes only. There are no " + + "stability guarantees for them."); + pw.println(""); + pw.println("Usage: cmd package art [ARGS]..."); + pw.println(""); + pw.println("Supported commands:"); + pw.println(" help or -h"); + pw.println(" Print this help text."); + // TODO(jiakaiz): Also do operations for secondary dex'es by default. + pw.println(" delete-optimized-artifacts <package-name>"); + pw.println(" Delete the optimized artifacts of a package."); + pw.println(" By default, the command only deletes the optimized artifacts of primary " + + "dex'es."); + pw.println(" get-optimization-status <package-name>"); + pw.println(" Print the optimization status of a package."); + pw.println(" By default, the command only prints the optimization status of primary " + + "dex'es."); + } + + private void enforceRoot() { + final int uid = Binder.getCallingUid(); + if (uid != Process.ROOT_UID) { + throw new SecurityException("ART service shell commands need root access"); + } + } +} diff --git a/libartservice/service/java/com/android/server/art/LoggingArtd.java b/libartservice/service/java/com/android/server/art/LoggingArtd.java new file mode 100644 index 0000000000..811cb6f089 --- /dev/null +++ b/libartservice/service/java/com/android/server/art/LoggingArtd.java @@ -0,0 +1,60 @@ +/* + * 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. + */ + +package com.android.server.art; + +import android.os.IBinder; +import android.util.Log; + +/** + * An implementation of artd that logs the artd calls for debugging purposes. + * + * @hide + */ +public class LoggingArtd implements IArtd { + private static final String TAG = "LoggingArtd"; + + @Override + public IBinder asBinder() { + return null; + } + + @Override + public boolean isAlive() { + return true; + } + + @Override + public long deleteArtifacts(ArtifactsPath artifactsPath) { + Log.i(TAG, "deleteArtifacts " + artifactsPathToString(artifactsPath)); + return 0; + } + + @Override + public GetOptimizationStatusResult getOptimizationStatus( + String dexFile, String instructionSet, String classLoaderContext) { + Log.i(TAG, + "getOptimizationStatus " + dexFile + ", " + instructionSet + ", " + + classLoaderContext); + return new GetOptimizationStatusResult(); + } + + private String artifactsPathToString(ArtifactsPath artifactsPath) { + return String.format("ArtifactsPath{dexPath = \"%s\", isa = \"%s\", isInDalvikCache = %s}", + artifactsPath.dexPath, artifactsPath.isa, + String.valueOf(artifactsPath.isInDalvikCache)); + } +} diff --git a/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java b/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java new file mode 100644 index 0000000000..d6b8a592b2 --- /dev/null +++ b/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java @@ -0,0 +1,375 @@ +/* + * 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. + */ + +package com.android.server.art; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.pm.ApplicationInfo; +import android.text.TextUtils; +import android.util.SparseArray; + +import com.android.internal.annotations.Immutable; +import com.android.server.art.wrapper.AndroidPackageApi; +import com.android.server.art.wrapper.PackageState; +import com.android.server.art.wrapper.SharedLibraryInfo; + +import dalvik.system.DelegateLastClassLoader; +import dalvik.system.DexClassLoader; +import dalvik.system.PathClassLoader; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** @hide */ +public class PrimaryDexUtils { + private static final String SHARED_LIBRARY_LOADER_TYPE = PathClassLoader.class.getName(); + + /** + * Returns the basic information about all primary dex files belonging to the package. The + * return value is a list where the entry at index 0 is the information about the base APK, and + * the entry at index i is the information about the (i-1)-th split APK. + */ + @NonNull + public static List<PrimaryDexInfo> getDexInfo(@NonNull AndroidPackageApi pkg) { + return getDexInfoImpl(pkg) + .stream() + .map(builder -> builder.build()) + .collect(Collectors.toList()); + } + + /** + * Same as above, but requires {@link PackageState} in addition, and returns the detailed + * information, including the class loader context. + */ + @NonNull + public static List<DetailedPrimaryDexInfo> getDetailedDexInfo( + @NonNull PackageState pkgState, @NonNull AndroidPackageApi pkg) { + return getDetailedDexInfoImpl(pkgState, pkg) + .stream() + .map(builder -> builder.buildDetailed()) + .collect(Collectors.toList()); + } + + @NonNull + private static List<PrimaryDexInfoBuilder> getDexInfoImpl(@NonNull AndroidPackageApi pkg) { + List<PrimaryDexInfoBuilder> dexInfos = new ArrayList<>(); + + PrimaryDexInfoBuilder baseInfo = new PrimaryDexInfoBuilder(pkg.getBaseApkPath()); + baseInfo.mHasCode = pkg.isHasCode(); + baseInfo.mIsBaseApk = true; + dexInfos.add(baseInfo); + + String[] splitNames = pkg.getSplitNames(); + String[] splitCodePaths = pkg.getSplitCodePaths(); + int[] splitFlags = pkg.getSplitFlags(); + + for (int i = 0; i < splitNames.length; i++) { + PrimaryDexInfoBuilder splitInfo = new PrimaryDexInfoBuilder(splitCodePaths[i]); + splitInfo.mHasCode = + splitFlags != null && (splitFlags[i] & ApplicationInfo.FLAG_HAS_CODE) != 0; + splitInfo.mSplitIndex = i; + splitInfo.mSplitName = splitNames[i]; + dexInfos.add(splitInfo); + } + + return dexInfos; + } + + @NonNull + private static List<PrimaryDexInfoBuilder> getDetailedDexInfoImpl( + @NonNull PackageState pkgState, @NonNull AndroidPackageApi pkg) { + List<PrimaryDexInfoBuilder> dexInfos = getDexInfoImpl(pkg); + + PrimaryDexInfoBuilder baseApk = dexInfos.get(0); + assert baseApk.mIsBaseApk; + baseApk.mClassLoaderName = pkg.getClassLoaderName(); + File baseDexFile = new File(baseApk.mDexPath); + baseApk.mRelativeDexPath = baseDexFile.getName(); + + // Shared libraries are the dependencies of the base APK. + baseApk.mSharedLibrariesContext = encodeSharedLibraries(pkgState.getUsesLibraryInfos()); + + String[] splitClassLoaderNames = pkg.getSplitClassLoaderNames(); + SparseArray<int[]> splitDependencies = pkg.getSplitDependencies(); + boolean isIsolatedSplitLoading = + pkg.isIsolatedSplitLoading() && !Utils.isEmpty(splitDependencies); + + for (int i = 1; i < dexInfos.size(); i++) { + assert dexInfos.get(i).mSplitIndex == i - 1; + File splitDexFile = new File(dexInfos.get(i).mDexPath); + if (!splitDexFile.getParent().equals(baseDexFile.getParent())) { + throw new IllegalStateException( + "Split APK and base APK are in different directories: " + + splitDexFile.getParent() + " != " + baseDexFile.getParent()); + } + dexInfos.get(i).mRelativeDexPath = splitDexFile.getName(); + if (isIsolatedSplitLoading && dexInfos.get(i).mHasCode) { + dexInfos.get(i).mClassLoaderName = + splitClassLoaderNames[dexInfos.get(i).mSplitIndex]; + + // Keys and values of `splitDependencies` are `split index + 1` for split APK or 0 + // for base APK, so they can be regarded as indices to `dexInfos`. + int[] dependencies = splitDependencies.get(i); + if (!Utils.isEmpty(dependencies)) { + // We only care about the first dependency because it is the parent split. The + // rest are configuration splits, which we don't care. + dexInfos.get(i).mSplitDependency = dexInfos.get(dependencies[0]); + } + } + } + + if (isIsolatedSplitLoading) { + computeClassLoaderContextsIsolated(dexInfos); + } else { + computeClassLoaderContexts(dexInfos); + } + + return dexInfos; + } + + /** + * Computes class loader context for an app that didn't request isolated split loading. Stores + * the results in {@link PrimaryDexInfoBuilder#mClassLoaderContext}. + * + * In this case, all the splits will be loaded in the base apk class loader (in the order of + * their definition). + * + * The CLC for the base APK is `CLN[]{shared-libraries}`; the CLC for the n-th split APK is + * `CLN[base.apk, split_0.apk, ..., split_n-1.apk]{shared-libraries}`; where `CLN` is the + * class loader name for the base APK. + */ + private static void computeClassLoaderContexts(@NonNull List<PrimaryDexInfoBuilder> dexInfos) { + String baseClassLoaderName = dexInfos.get(0).mClassLoaderName; + String sharedLibrariesContext = dexInfos.get(0).mSharedLibrariesContext; + List<String> classpath = new ArrayList<>(); + for (PrimaryDexInfoBuilder dexInfo : dexInfos) { + if (dexInfo.mHasCode) { + dexInfo.mClassLoaderContext = encodeClassLoader(baseClassLoaderName, classpath, + null /* parentContext */, sharedLibrariesContext); + } + // Note that the splits with no code are not removed from the classpath computation. + // I.e., split_n might get the split_n-1 in its classpath dependency even if split_n-1 + // has no code. + // The splits with no code do not matter for the runtime which ignores APKs without code + // when doing the classpath checks. As such we could actually filter them but we don't + // do it in order to keep consistency with how the apps are loaded. + classpath.add(dexInfo.mRelativeDexPath); + } + } + + /** + * Computes class loader context for an app that requested for isolated split loading. Stores + * the results in {@link PrimaryDexInfoBuilder#mClassLoaderContext}. + * + * In this case, each split will be loaded with a separate class loader, whose context is a + * chain formed from inter-split dependencies. + * + * The CLC for the base APK is `CLN[]{shared-libraries}`; the CLC for the n-th split APK that + * depends on the base APK is `CLN_n[];CLN[base.apk]{shared-libraries}`; the CLC for the n-th + * split APK that depends on the m-th split APK is + * `CLN_n[];CLN_m[split_m.apk];...;CLN[base.apk]{shared-libraries}`; where `CLN` is the base + * class loader name for the base APK, `CLN_i` is the class loader name for the i-th split APK, + * and `...` represents the ancestors along the dependency chain. + * + * Specially, if a split does not have any dependency, the CLC for it is `CLN_n[]`. + */ + private static void computeClassLoaderContextsIsolated( + @NonNull List<PrimaryDexInfoBuilder> dexInfos) { + for (PrimaryDexInfoBuilder dexInfo : dexInfos) { + if (dexInfo.mHasCode) { + dexInfo.mClassLoaderContext = encodeClassLoader(dexInfo.mClassLoaderName, + null /* classpath */, getParentContextRecursive(dexInfo), + dexInfo.mSharedLibrariesContext); + } + } + } + + /** + * Computes the parent class loader context, recursively. Caches results in {@link + * PrimaryDexInfoBuilder#mContextForChildren}. + */ + @Nullable + private static String getParentContextRecursive(@NonNull PrimaryDexInfoBuilder dexInfo) { + if (dexInfo.mSplitDependency == null) { + return null; + } + PrimaryDexInfoBuilder parent = dexInfo.mSplitDependency; + if (parent.mContextForChildren == null) { + parent.mContextForChildren = + encodeClassLoader(parent.mClassLoaderName, List.of(parent.mRelativeDexPath), + getParentContextRecursive(parent), parent.mSharedLibrariesContext); + } + return parent.mContextForChildren; + } + + /** + * Returns class loader context in the format of + * `CLN[classpath...]{share-libraries};parent-context`, where `CLN` is the class loader name. + */ + @NonNull + private static String encodeClassLoader(@Nullable String classLoaderName, + @Nullable List<String> classpath, @Nullable String parentContext, + @Nullable String sharedLibrariesContext) { + StringBuilder classLoaderContext = new StringBuilder(); + + classLoaderContext.append(encodeClassLoaderName(classLoaderName)); + + classLoaderContext.append( + "[" + (classpath != null ? String.join(":", classpath) : "") + "]"); + + if (!TextUtils.isEmpty(sharedLibrariesContext)) { + classLoaderContext.append(sharedLibrariesContext); + } + + if (!TextUtils.isEmpty(parentContext)) { + classLoaderContext.append(";" + parentContext); + } + + return classLoaderContext.toString(); + } + + @NonNull + private static String encodeClassLoaderName(@Nullable String classLoaderName) { + // `PathClassLoader` and `DexClassLoader` are grouped together because they have the same + // behavior. For null values we default to "PCL". This covers the case where a package does + // not specify any value for its class loader. + if (classLoaderName == null || PathClassLoader.class.getName().equals(classLoaderName) + || DexClassLoader.class.getName().equals(classLoaderName)) { + return "PCL"; + } else if (DelegateLastClassLoader.class.getName().equals(classLoaderName)) { + return "DLC"; + } else { + throw new IllegalStateException("Unsupported classLoaderName: " + classLoaderName); + } + } + + /** + * Returns shared libraries context in the format of + * `{PCL[library_1_dex_1.jar:library_1_dex_2.jar:...]{library_1-dependencies}#PCL[ + * library_1_dex_2.jar:library_2_dex_2.jar:...]{library_2-dependencies}#...}`. + */ + @Nullable + private static String encodeSharedLibraries(@Nullable List<SharedLibraryInfo> sharedLibraries) { + if (Utils.isEmpty(sharedLibraries)) { + return null; + } + return sharedLibraries.stream() + .map(library + -> encodeClassLoader(SHARED_LIBRARY_LOADER_TYPE, library.getAllCodePaths(), + null /* parentContext */, + encodeSharedLibraries(library.getDependencies()))) + .collect(Collectors.joining("#", "{", "}")); + } + + /** Basic information about a primary dex file (either the base APK or a split APK). */ + @Immutable + public static class PrimaryDexInfo { + private final @NonNull String mDexPath; + private final boolean mHasCode; + private final boolean mIsBaseApk; + private final int mSplitIndex; + private final @Nullable String mSplitName; + + PrimaryDexInfo(@NonNull String dexPath, boolean hasCode, boolean isBaseApk, int splitIndex, + @Nullable String splitName) { + mDexPath = dexPath; + mHasCode = hasCode; + mIsBaseApk = isBaseApk; + mSplitIndex = splitIndex; + mSplitName = splitName; + } + + /** The path to the dex file. */ + public @NonNull String dexPath() { + return mDexPath; + } + + /** True if the dex file has code. */ + public boolean hasCode() { + return mHasCode; + } + + /** True if the dex file is the base APK. */ + public boolean isBaseApk() { + return mIsBaseApk; + } + + /** The index to {@link AndroidPackageApi#getSplitNames()}, or -1 for base APK. */ + public int splitIndex() { + return mSplitIndex; + } + + /** The name of the split, or null for base APK. */ + public @Nullable String splitName() { + return mSplitName; + } + } + + /** + * Detailed information about a primary dex file (either the base APK or a split APK). It + * contains the class loader context in addition to what is in {@link PrimaryDexInfo}, but + * producing it requires {@link PackageState}. + */ + @Immutable + public static class DetailedPrimaryDexInfo extends PrimaryDexInfo { + private final @Nullable String mClassLoaderContext; + + DetailedPrimaryDexInfo(@NonNull String dexPath, boolean hasCode, boolean isBaseApk, + int splitIndex, @Nullable String splitName, @Nullable String classLoaderContext) { + super(dexPath, hasCode, isBaseApk, splitIndex, splitName); + mClassLoaderContext = classLoaderContext; + } + + /** + * A string describing the structure of the class loader that the dex file is loaded with. + */ + public @Nullable String classLoaderContext() { + return mClassLoaderContext; + } + } + + private static class PrimaryDexInfoBuilder { + @NonNull String mDexPath; + boolean mHasCode = false; + boolean mIsBaseApk = false; + int mSplitIndex = -1; + @Nullable String mSplitName = null; + @Nullable String mRelativeDexPath = null; + @Nullable String mClassLoaderContext = null; + @Nullable String mClassLoaderName = null; + @Nullable PrimaryDexInfoBuilder mSplitDependency = null; + /** The class loader context of the shared libraries. Only applicable for the base APK. */ + @Nullable String mSharedLibrariesContext = null; + /** The class loader context for children to use when this dex file is used as a parent. */ + @Nullable String mContextForChildren = null; + + PrimaryDexInfoBuilder(@NonNull String dexPath) { + mDexPath = dexPath; + } + + PrimaryDexInfo build() { + return new PrimaryDexInfo(mDexPath, mHasCode, mIsBaseApk, mSplitIndex, mSplitName); + } + + DetailedPrimaryDexInfo buildDetailed() { + return new DetailedPrimaryDexInfo( + mDexPath, mHasCode, mIsBaseApk, mSplitIndex, mSplitName, mClassLoaderContext); + } + } +} diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java new file mode 100644 index 0000000000..9c67a0f5e4 --- /dev/null +++ b/libartservice/service/java/com/android/server/art/Utils.java @@ -0,0 +1,84 @@ +/* + * 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. + */ + +package com.android.server.art; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.SparseArray; + +import com.android.server.art.ArtifactsPath; +import com.android.server.art.wrapper.PackageManagerLocal; +import com.android.server.art.wrapper.PackageState; + +import dalvik.system.VMRuntime; + +import java.util.Collection; +import java.util.List; + +/** @hide */ +public final class Utils { + private Utils() {} + + /** + * Checks if given array is null or has zero elements. + */ + public static <T> boolean isEmpty(@Nullable Collection<T> array) { + return array == null || array.isEmpty(); + } + + /** + * Checks if given array is null or has zero elements. + */ + public static <T> boolean isEmpty(@Nullable SparseArray<T> array) { + return array == null || array.size() == 0; + } + + /** + * Checks if given array is null or has zero elements. + */ + public static boolean isEmpty(@Nullable int[] array) { + return array == null || array.length == 0; + } + + @NonNull + public static List<String> getAllIsas(@NonNull PackageState pkgState) { + String primaryCpuAbi = pkgState.getPrimaryCpuAbi(); + String secondaryCpuAbi = pkgState.getSecondaryCpuAbi(); + if (primaryCpuAbi != null) { + if (secondaryCpuAbi != null) { + return List.of(VMRuntime.getInstructionSet(primaryCpuAbi), + VMRuntime.getInstructionSet(secondaryCpuAbi)); + } + return List.of(VMRuntime.getInstructionSet(primaryCpuAbi)); + } + return List.of(); + } + + @NonNull + public static ArtifactsPath buildArtifactsPath( + @NonNull String dexPath, @NonNull String isa, boolean isInDalvikCache) { + ArtifactsPath artifactsPath = new ArtifactsPath(); + artifactsPath.dexPath = dexPath; + artifactsPath.isa = isa; + artifactsPath.isInDalvikCache = isInDalvikCache; + return artifactsPath; + } + + public static boolean isInDalvikCache(@NonNull PackageState pkg) { + return pkg.isSystem() && !pkg.isUpdatedSystemApp(); + } +} diff --git a/libartservice/service/java/com/android/server/art/model/ArtFlags.java b/libartservice/service/java/com/android/server/art/model/ArtFlags.java new file mode 100644 index 0000000000..0ecad74029 --- /dev/null +++ b/libartservice/service/java/com/android/server/art/model/ArtFlags.java @@ -0,0 +1,80 @@ +/* + * 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. + */ + +package com.android.server.art.model; + +import android.annotation.IntDef; +import android.annotation.SystemApi; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** @hide */ +@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) +public class ArtFlags { + /** Whether the operation is applied for primary dex'es. */ + public static final int FLAG_FOR_PRIMARY_DEX = 1 << 0; + /** Whether the operation is applied for secondary dex'es. */ + public static final int FLAG_FOR_SECONDARY_DEX = 1 << 1; + + /** + * Flags for {@link ArtManagerLocal#deleteOptimizedArtifacts(PackageDataSnapshot, String, int)}. + * + * @hide + */ + // clang-format off + @IntDef(flag = true, prefix = "FLAG_", value = { + FLAG_FOR_PRIMARY_DEX, + FLAG_FOR_SECONDARY_DEX, + }) + // clang-format on + @Retention(RetentionPolicy.SOURCE) + public @interface DeleteFlags {} + + /** + * Default flags that are used when + * {@link ArtManagerLocal#deleteOptimizedArtifacts(PackageDataSnapshot, String)} is called. + * Value: {@link #FLAG_FOR_PRIMARY_DEX}. + */ + public static @DeleteFlags int defaultDeleteFlags() { + return FLAG_FOR_PRIMARY_DEX; + } + + /** + * Flags for {@link ArtManagerLocal#getOptimizationStatus(PackageDataSnapshot, String, int)}. + * + * @hide + */ + // clang-format off + @IntDef(flag = true, prefix = "FLAG_", value = { + FLAG_FOR_PRIMARY_DEX, + FLAG_FOR_SECONDARY_DEX, + }) + // clang-format on + @Retention(RetentionPolicy.SOURCE) + public @interface GetStatusFlags {} + + /** + * Default flags that are used when + * {@link ArtManagerLocal#getOptimizationStatus(PackageDataSnapshot, String)} is called. + * Value: {@link #FLAG_FOR_PRIMARY_DEX}. + */ + public static @GetStatusFlags int defaultGetStatusFlags() { + return FLAG_FOR_PRIMARY_DEX; + } + + private ArtFlags() {} +} diff --git a/libartservice/service/java/com/android/server/art/model/DeleteResult.java b/libartservice/service/java/com/android/server/art/model/DeleteResult.java new file mode 100644 index 0000000000..fc40cbc2e1 --- /dev/null +++ b/libartservice/service/java/com/android/server/art/model/DeleteResult.java @@ -0,0 +1,35 @@ +/* + * 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. + */ + +package com.android.server.art.model; + +import android.annotation.SystemApi; + +/** @hide */ +@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) +public class DeleteResult { + private long mFreedBytes; + + /** @hide */ + public DeleteResult(long freedBytes) { + mFreedBytes = freedBytes; + } + + /** The amount of the disk space freed by the deletion, in bytes. */ + public long getFreedBytes() { + return mFreedBytes; + } +} diff --git a/libartservice/service/java/com/android/server/art/model/OptimizationStatus.java b/libartservice/service/java/com/android/server/art/model/OptimizationStatus.java new file mode 100644 index 0000000000..724b0ddfce --- /dev/null +++ b/libartservice/service/java/com/android/server/art/model/OptimizationStatus.java @@ -0,0 +1,120 @@ +/* + * 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. + */ + +package com.android.server.art.model; + +import android.annotation.NonNull; +import android.annotation.SystemApi; + +import com.android.internal.annotations.Immutable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Describes the optimization status of a package. + * + * @hide + */ +@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) +@Immutable +public class OptimizationStatus { + private final @NonNull List<DexFileOptimizationStatus> mDexFileOptimizationStatuses; + + /** @hide */ + public OptimizationStatus( + @NonNull List<DexFileOptimizationStatus> dexFileOptimizationStatuses) { + mDexFileOptimizationStatuses = dexFileOptimizationStatuses; + } + + /** The optimization status of each individual dex file. */ + @NonNull + public List<DexFileOptimizationStatus> getDexFileOptimizationStatuses() { + return mDexFileOptimizationStatuses; + } + + /** Describes the optimization status of a dex file. */ + @Immutable + public static class DexFileOptimizationStatus { + private final @NonNull String mDexFile; + private final @NonNull String mInstructionSet; + private final @NonNull String mCompilerFilter; + private final @NonNull String mCompilationReason; + private final @NonNull String mLocationDebugString; + + /** @hide */ + public DexFileOptimizationStatus(@NonNull String dexFile, @NonNull String instructionSet, + @NonNull String compilerFilter, @NonNull String compilationReason, + @NonNull String locationDebugString) { + mDexFile = dexFile; + mInstructionSet = instructionSet; + mCompilerFilter = compilerFilter; + mCompilationReason = compilationReason; + mLocationDebugString = locationDebugString; + } + + /** The absolute path to the dex file. */ + public @NonNull String getDexFile() { + return mDexFile; + } + + /** The instruction set. */ + public @NonNull String getInstructionSet() { + return mInstructionSet; + } + + /** + * A string that describes the compiler filter. + * + * Possible values are: + * <ul> + * <li>A valid value of the {@code --compiler-filer} option passed to {@code dex2oat}, if + * the optimized artifacts are valid. + * <li>{@code "run-from-apk"}, if the optimized artifacts do not exist. + * <li>{@code "run-from-apk-fallback"}, if the optimized artifacts exist but are invalid + * because the dex file has changed. + * <li>{@code "error"}, if an unexpected error occurs. + * </ul> + */ + public @NonNull String getCompilerFilter() { + return mCompilerFilter; + } + + /** + * A string that describes the compilation reason. + * + * Possible values are: + * <ul> + * <li>The compilation reason, in text format, passed to {@code dex2oat}. + * <li>{@code "unknown"}: if the reason is empty or the optimized artifacts do not exist. + * <li>{@code "error"}: if an unexpected error occurs. + * </ul> + */ + public @NonNull String getCompilationReason() { + return mCompilationReason; + } + + /** + * A human-readable string that describes the location of the optimized artifacts. + * + * Note that this string is for debugging purposes only. There is no stability guarantees + * for the format of the string. DO NOT use it programmatically. + */ + public @NonNull String getLocationDebugString() { + return mLocationDebugString; + } + } +} diff --git a/libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java b/libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java new file mode 100644 index 0000000000..90adb497fd --- /dev/null +++ b/libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java @@ -0,0 +1,117 @@ +/* + * 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. + */ + +package com.android.server.art.wrapper; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.SparseArray; + +/** @hide */ +public class AndroidPackageApi { + private final Object mPkg; + + AndroidPackageApi(@NonNull Object pkg) { + mPkg = pkg; + } + + Object getRealInstance() { + return mPkg; + } + + @NonNull + public String getBaseApkPath() { + try { + return (String) mPkg.getClass().getMethod("getBaseApkPath").invoke(mPkg); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + public boolean isHasCode() { + try { + return (boolean) mPkg.getClass().getMethod("isHasCode").invoke(mPkg); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @NonNull + public String[] getSplitNames() { + try { + return (String[]) mPkg.getClass().getMethod("getSplitNames").invoke(mPkg); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @NonNull + public String[] getSplitCodePaths() { + try { + return (String[]) mPkg.getClass().getMethod("getSplitCodePaths").invoke(mPkg); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @NonNull + public int[] getSplitFlags() { + try { + Class<?> parsingPackageImplClass = + Class.forName("com.android.server.pm.pkg.parsing.ParsingPackageImpl"); + return (int[]) parsingPackageImplClass.getMethod("getSplitFlags").invoke(mPkg); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @Nullable + public String getClassLoaderName() { + try { + return (String) mPkg.getClass().getMethod("getClassLoaderName").invoke(mPkg); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @NonNull + public String[] getSplitClassLoaderNames() { + try { + return (String[]) mPkg.getClass().getMethod("getSplitClassLoaderNames").invoke(mPkg); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @Nullable + public SparseArray<int[]> getSplitDependencies() { + try { + return (SparseArray<int[]>) mPkg.getClass() + .getMethod("getSplitDependencies") + .invoke(mPkg); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + public boolean isIsolatedSplitLoading() { + try { + return (boolean) mPkg.getClass().getMethod("isIsolatedSplitLoading").invoke(mPkg); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } +} diff --git a/libartservice/service/java/com/android/server/art/wrapper/PackageManagerLocal.java b/libartservice/service/java/com/android/server/art/wrapper/PackageManagerLocal.java new file mode 100644 index 0000000000..650d29ce87 --- /dev/null +++ b/libartservice/service/java/com/android/server/art/wrapper/PackageManagerLocal.java @@ -0,0 +1,79 @@ +/* + * 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. + */ + +package com.android.server.art.wrapper; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.UserHandle; + +import com.android.server.pm.snapshot.PackageDataSnapshot; + +/** @hide */ +public class PackageManagerLocal { + private final Object mPackageManagerInternal; + + /** + * Returns an instance this class, which is a reflection-based implementation of {@link + * com.android.server.pm.PackageManagerLocal}. + * Note: This is NOT a real system API! Use {@link LocalManagerRegistry} for getting a real + * instance. + */ + @NonNull + public static PackageManagerLocal getInstance() throws Exception { + Class<?> localServicesClass = Class.forName("com.android.server.LocalServices"); + Class<?> packageManagerInternalClass = + Class.forName("android.content.pm.PackageManagerInternal"); + Object packageManagerInternal = localServicesClass.getMethod("getService", Class.class) + .invoke(null, packageManagerInternalClass); + if (packageManagerInternal == null) { + throw new Exception("Failed to get PackageManagerInternal"); + } + return new PackageManagerLocal(packageManagerInternal); + } + + private PackageManagerLocal(@NonNull Object packageManagerInternal) { + mPackageManagerInternal = packageManagerInternal; + } + + @NonNull + public PackageDataSnapshot snapshot() { + try { + return (PackageDataSnapshot) mPackageManagerInternal.getClass() + .getMethod("snapshot") + .invoke(mPackageManagerInternal); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @Nullable + public PackageState getPackageState(@NonNull PackageDataSnapshot snapshot, + @NonNull int callingUid, @NonNull String packageName) { + try { + int userId = (int) UserHandle.class.getMethod("getUserId", int.class) + .invoke(null, callingUid); + Class<?> computerClass = Class.forName("com.android.server.pm.Computer"); + Object packageState = computerClass + .getMethod("getPackageStateForInstalledAndFiltered", + String.class, int.class, int.class) + .invoke(snapshot, packageName, callingUid, userId); + return packageState != null ? new PackageState(packageState) : null; + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } +} diff --git a/libartservice/service/java/com/android/server/art/wrapper/PackageState.java b/libartservice/service/java/com/android/server/art/wrapper/PackageState.java new file mode 100644 index 0000000000..223551466f --- /dev/null +++ b/libartservice/service/java/com/android/server/art/wrapper/PackageState.java @@ -0,0 +1,144 @@ +/* + * 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. + */ + +package com.android.server.art.wrapper; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.TextUtils; + +import java.util.List; +import java.util.stream.Collectors; + +/** @hide */ +public class PackageState { + private final Object mPkgState; + + PackageState(@NonNull Object pkgState) { + mPkgState = pkgState; + } + + @Nullable + public AndroidPackageApi getAndroidPackage() { + try { + Object pkg = mPkgState.getClass().getMethod("getAndroidPackage").invoke(mPkgState); + return pkg != null ? new AndroidPackageApi(pkg) : null; + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @NonNull + public String getPackageName() { + try { + return (String) mPkgState.getClass().getMethod("getPackageName").invoke(mPkgState); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @NonNull + public List<SharedLibraryInfo> getUsesLibraryInfos() { + try { + Object packageStateUnserialized = + mPkgState.getClass().getMethod("getTransientState").invoke(mPkgState); + var list = (List<?>) packageStateUnserialized.getClass() + .getMethod("getUsesLibraryInfos") + .invoke(packageStateUnserialized); + return list.stream() + .map(obj -> new SharedLibraryInfo(obj)) + .collect(Collectors.toList()); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @Nullable + public String getPrimaryCpuAbi() { + try { + String abi = + (String) mPkgState.getClass().getMethod("getPrimaryCpuAbi").invoke(mPkgState); + if (!TextUtils.isEmpty(abi)) { + return abi; + } + + // Default to the information in `AndroidPackageApi`. The defaulting behavior will + // eventually be done by `PackageState` internally. + AndroidPackageApi pkg = getAndroidPackage(); + if (pkg == null) { + // This should never happen because we check the existence of the package at the + // beginning of each ART Services method. + throw new IllegalStateException("Unable to get package " + getPackageName() + + ". This should never happen."); + } + + Class<?> androidPackageHiddenClass = + Class.forName("com.android.server.pm.parsing.pkg.AndroidPackageHidden"); + return (String) androidPackageHiddenClass.getMethod("getPrimaryCpuAbi") + .invoke(pkg.getRealInstance()); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @Nullable + public String getSecondaryCpuAbi() { + try { + String abi = + (String) mPkgState.getClass().getMethod("getSecondaryCpuAbi").invoke(mPkgState); + if (!TextUtils.isEmpty(abi)) { + return abi; + } + + // Default to the information in `AndroidPackageApi`. The defaulting behavior will + // eventually be done by `PackageState` internally. + AndroidPackageApi pkg = getAndroidPackage(); + if (pkg == null) { + // This should never happen because we check the existence of the package at the + // beginning of each ART Services method. + throw new IllegalStateException("Unable to get package " + getPackageName() + + ". This should never happen."); + } + + Class<?> androidPackageHiddenClass = + Class.forName("com.android.server.pm.parsing.pkg.AndroidPackageHidden"); + return (String) androidPackageHiddenClass.getMethod("getSecondaryCpuAbi") + .invoke(pkg.getRealInstance()); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + public boolean isSystem() { + try { + return (boolean) mPkgState.getClass().getMethod("isSystem").invoke(mPkgState); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + public boolean isUpdatedSystemApp() { + try { + Object packageStateUnserialized = + mPkgState.getClass().getMethod("getTransientState").invoke(mPkgState); + return (boolean) packageStateUnserialized.getClass() + .getMethod("isUpdatedSystemApp") + .invoke(packageStateUnserialized); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } +} diff --git a/libartservice/service/java/com/android/server/art/wrapper/README.md b/libartservice/service/java/com/android/server/art/wrapper/README.md new file mode 100644 index 0000000000..829fc1c7a6 --- /dev/null +++ b/libartservice/service/java/com/android/server/art/wrapper/README.md @@ -0,0 +1,11 @@ +This folder contains temporary wrappers that access system server internal +classes using reflection. Having the wrappers is the workaround for the current +time being where required system APIs are not finalized. The classes and methods +correspond to system APIs planned to be exposed. + +The mappings are: + +- `AndroidPackageApi`: `com.android.server.pm.pkg.AndroidPackageApi` +- `PackageManagerLocal`: `com.android.server.pm.PackageManagerLocal` +- `PackageState`: `com.android.server.pm.pkg.PackageState` +- `SharedLibraryInfo`: `android.content.pm.SharedLibraryInfo` diff --git a/libartservice/service/java/com/android/server/art/wrapper/SharedLibraryInfo.java b/libartservice/service/java/com/android/server/art/wrapper/SharedLibraryInfo.java new file mode 100644 index 0000000000..f2bde16b6c --- /dev/null +++ b/libartservice/service/java/com/android/server/art/wrapper/SharedLibraryInfo.java @@ -0,0 +1,56 @@ +/* + * 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. + */ + +package com.android.server.art.wrapper; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.util.List; +import java.util.stream.Collectors; + +/** @hide */ +public class SharedLibraryInfo { + private final Object mInfo; + + SharedLibraryInfo(@NonNull Object info) { + mInfo = info; + } + + @NonNull + public List<String> getAllCodePaths() { + try { + return (List<String>) mInfo.getClass().getMethod("getAllCodePaths").invoke(mInfo); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @Nullable + public List<SharedLibraryInfo> getDependencies() { + try { + var list = (List<?>) mInfo.getClass().getMethod("getDependencies").invoke(mInfo); + if (list == null) { + return null; + } + return list.stream() + .map(obj -> new SharedLibraryInfo(obj)) + .collect(Collectors.toList()); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } +} diff --git a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java index a27dfa5370..0e958baf1f 100644 --- a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java +++ b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java @@ -16,29 +16,217 @@ package com.android.server.art; +import static com.android.server.art.model.OptimizationStatus.DexFileOptimizationStatus; + import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.content.pm.ApplicationInfo; + import androidx.test.filters.SmallTest; -import com.android.server.art.ArtManagerLocal; +import com.android.server.art.model.DeleteResult; +import com.android.server.art.model.OptimizationStatus; +import com.android.server.art.wrapper.AndroidPackageApi; +import com.android.server.art.wrapper.PackageManagerLocal; +import com.android.server.art.wrapper.PackageState; +import com.android.server.pm.snapshot.PackageDataSnapshot; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.quality.Strictness; + +import java.util.List; @SmallTest -@RunWith(MockitoJUnitRunner.class) +@RunWith(Parameterized.class) public class ArtManagerLocalTest { + private static final String PKG_NAME = "com.example.foo"; + + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS); + + @Mock private ArtManagerLocal.Injector mInjector; + @Mock private PackageManagerLocal mPackageManagerLocal; + @Mock private IArtd mArtd; + private PackageState mPkgState; + + // True if the primary dex'es are in a readonly partition. + @Parameter(0) public boolean mIsInReadonlyPartition; + private ArtManagerLocal mArtManagerLocal; + @Parameters(name = "isInReadonlyPartition={0}") + public static Iterable<? extends Object> data() { + return List.of(false, true); + } + @Before - public void setUp() { - mArtManagerLocal = new ArtManagerLocal(); + public void setUp() throws Exception { + lenient().when(mInjector.getPackageManagerLocal()).thenReturn(mPackageManagerLocal); + lenient().when(mInjector.getArtd()).thenReturn(mArtd); + + mPkgState = createPackageState(); + lenient() + .when(mPackageManagerLocal.getPackageState(any(), anyInt(), eq(PKG_NAME))) + .thenReturn(mPkgState); + + mArtManagerLocal = new ArtManagerLocal(mInjector); + } + + @Test + public void testDeleteOptimizedArtifacts() throws Exception { + when(mArtd.deleteArtifacts(any())).thenReturn(1l); + + DeleteResult result = mArtManagerLocal.deleteOptimizedArtifacts( + mock(PackageDataSnapshot.class), PKG_NAME); + assertThat(result.getFreedBytes()).isEqualTo(4); + + verify(mArtd).deleteArtifacts(argThat(artifactsPath + -> artifactsPath.dexPath.equals("/data/app/foo/base.apk") + && artifactsPath.isa.equals("arm64") + && artifactsPath.isInDalvikCache == mIsInReadonlyPartition)); + verify(mArtd).deleteArtifacts(argThat(artifactsPath + -> artifactsPath.dexPath.equals("/data/app/foo/base.apk") + && artifactsPath.isa.equals("arm") + && artifactsPath.isInDalvikCache == mIsInReadonlyPartition)); + verify(mArtd).deleteArtifacts(argThat(artifactsPath + -> artifactsPath.dexPath.equals("/data/app/foo/split_0.apk") + && artifactsPath.isa.equals("arm64") + && artifactsPath.isInDalvikCache == mIsInReadonlyPartition)); + verify(mArtd).deleteArtifacts(argThat(artifactsPath + -> artifactsPath.dexPath.equals("/data/app/foo/split_0.apk") + && artifactsPath.isa.equals("arm") + && artifactsPath.isInDalvikCache == mIsInReadonlyPartition)); + verifyNoMoreInteractions(mArtd); + } + + @Test(expected = IllegalArgumentException.class) + public void testDeleteOptimizedArtifactsPackageNotFound() throws Exception { + when(mPackageManagerLocal.getPackageState(any(), anyInt(), eq(PKG_NAME))).thenReturn(null); + + mArtManagerLocal.deleteOptimizedArtifacts(mock(PackageDataSnapshot.class), PKG_NAME); + } + + @Test(expected = IllegalStateException.class) + public void testDeleteOptimizedArtifactsNoPackage() throws Exception { + when(mPkgState.getAndroidPackage()).thenReturn(null); + + mArtManagerLocal.deleteOptimizedArtifacts(mock(PackageDataSnapshot.class), PKG_NAME); } @Test - public void testScaffolding() { - assertThat(true).isTrue(); + public void testGetOptimizationStatus() throws Exception { + when(mArtd.getOptimizationStatus(any(), any(), any())) + .thenReturn(createGetOptimizationStatusResult( + "speed", "compilation-reason-0", "location-debug-string-0"), + createGetOptimizationStatusResult( + "speed-profile", "compilation-reason-1", "location-debug-string-1"), + createGetOptimizationStatusResult( + "verify", "compilation-reason-2", "location-debug-string-2"), + createGetOptimizationStatusResult( + "extract", "compilation-reason-3", "location-debug-string-3")); + + OptimizationStatus result = + mArtManagerLocal.getOptimizationStatus(mock(PackageDataSnapshot.class), PKG_NAME); + + List<DexFileOptimizationStatus> statuses = result.getDexFileOptimizationStatuses(); + assertThat(statuses.size()).isEqualTo(4); + + assertThat(statuses.get(0).getDexFile()).isEqualTo("/data/app/foo/base.apk"); + assertThat(statuses.get(0).getInstructionSet()).isEqualTo("arm64"); + assertThat(statuses.get(0).getCompilerFilter()).isEqualTo("speed"); + assertThat(statuses.get(0).getCompilationReason()).isEqualTo("compilation-reason-0"); + assertThat(statuses.get(0).getLocationDebugString()).isEqualTo("location-debug-string-0"); + + assertThat(statuses.get(1).getDexFile()).isEqualTo("/data/app/foo/base.apk"); + assertThat(statuses.get(1).getInstructionSet()).isEqualTo("arm"); + assertThat(statuses.get(1).getCompilerFilter()).isEqualTo("speed-profile"); + assertThat(statuses.get(1).getCompilationReason()).isEqualTo("compilation-reason-1"); + assertThat(statuses.get(1).getLocationDebugString()).isEqualTo("location-debug-string-1"); + + assertThat(statuses.get(2).getDexFile()).isEqualTo("/data/app/foo/split_0.apk"); + assertThat(statuses.get(2).getInstructionSet()).isEqualTo("arm64"); + assertThat(statuses.get(2).getCompilerFilter()).isEqualTo("verify"); + assertThat(statuses.get(2).getCompilationReason()).isEqualTo("compilation-reason-2"); + assertThat(statuses.get(2).getLocationDebugString()).isEqualTo("location-debug-string-2"); + + assertThat(statuses.get(3).getDexFile()).isEqualTo("/data/app/foo/split_0.apk"); + assertThat(statuses.get(3).getInstructionSet()).isEqualTo("arm"); + assertThat(statuses.get(3).getCompilerFilter()).isEqualTo("extract"); + assertThat(statuses.get(3).getCompilationReason()).isEqualTo("compilation-reason-3"); + assertThat(statuses.get(3).getLocationDebugString()).isEqualTo("location-debug-string-3"); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetOptimizationStatusPackageNotFound() throws Exception { + when(mPackageManagerLocal.getPackageState(any(), anyInt(), eq(PKG_NAME))).thenReturn(null); + + mArtManagerLocal.getOptimizationStatus(mock(PackageDataSnapshot.class), PKG_NAME); + } + + @Test(expected = IllegalStateException.class) + public void testGetOptimizationStatusNoPackage() throws Exception { + when(mPkgState.getAndroidPackage()).thenReturn(null); + + mArtManagerLocal.getOptimizationStatus(mock(PackageDataSnapshot.class), PKG_NAME); + } + + private AndroidPackageApi createPackage() { + AndroidPackageApi pkg = mock(AndroidPackageApi.class); + + lenient().when(pkg.getBaseApkPath()).thenReturn("/data/app/foo/base.apk"); + lenient().when(pkg.isHasCode()).thenReturn(true); + + // split_0 has code while split_1 doesn't. + lenient().when(pkg.getSplitNames()).thenReturn(new String[] {"split_0", "split_1"}); + lenient() + .when(pkg.getSplitCodePaths()) + .thenReturn( + new String[] {"/data/app/foo/split_0.apk", "/data/app/foo/split_1.apk"}); + lenient() + .when(pkg.getSplitFlags()) + .thenReturn(new int[] {ApplicationInfo.FLAG_HAS_CODE, 0}); + + return pkg; + } + + private PackageState createPackageState() { + PackageState pkgState = mock(PackageState.class); + + lenient().when(pkgState.getPackageName()).thenReturn(PKG_NAME); + lenient().when(pkgState.getPrimaryCpuAbi()).thenReturn("arm64-v8a"); + lenient().when(pkgState.getSecondaryCpuAbi()).thenReturn("armeabi-v7a"); + lenient().when(pkgState.isSystem()).thenReturn(mIsInReadonlyPartition); + lenient().when(pkgState.isUpdatedSystemApp()).thenReturn(false); + AndroidPackageApi pkg = createPackage(); + lenient().when(pkgState.getAndroidPackage()).thenReturn(pkg); + + return pkgState; + } + + private GetOptimizationStatusResult createGetOptimizationStatusResult( + String compilerFilter, String compilationReason, String locationDebugString) { + var getOptimizationStatusResult = new GetOptimizationStatusResult(); + getOptimizationStatusResult.compilerFilter = compilerFilter; + getOptimizationStatusResult.compilationReason = compilationReason; + getOptimizationStatusResult.locationDebugString = locationDebugString; + return getOptimizationStatusResult; } } diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexUtilsTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexUtilsTest.java new file mode 100644 index 0000000000..5a81e01631 --- /dev/null +++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexUtilsTest.java @@ -0,0 +1,231 @@ +/* + * 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. + */ + +package com.android.server.art; + +import static com.android.server.art.PrimaryDexUtils.DetailedPrimaryDexInfo; +import static com.android.server.art.PrimaryDexUtils.PrimaryDexInfo; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.pm.ApplicationInfo; +import android.util.SparseArray; + +import androidx.test.filters.SmallTest; + +import com.android.server.art.PrimaryDexUtils; +import com.android.server.art.wrapper.AndroidPackageApi; +import com.android.server.art.wrapper.PackageState; +import com.android.server.art.wrapper.SharedLibraryInfo; + +import dalvik.system.DelegateLastClassLoader; +import dalvik.system.DexClassLoader; +import dalvik.system.PathClassLoader; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.List; + +@SmallTest +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class PrimaryDexUtilsTest { + @Before + public void setUp() {} + + @Test + public void testGetDexInfo() { + List<PrimaryDexInfo> infos = + PrimaryDexUtils.getDexInfo(createPackage(false /* isIsolatedSplitLoading */)); + checkBasicInfo(infos); + } + + @Test + public void testGetDetailedDexInfo() { + List<DetailedPrimaryDexInfo> infos = PrimaryDexUtils.getDetailedDexInfo( + createPackageState(), createPackage(false /* isIsolatedSplitLoading */)); + checkBasicInfo(infos); + + String sharedLibrariesContext = "{" + + "PCL[library_2.jar]{PCL[library_1_dex_1.jar:library_1_dex_2.jar]}#" + + "PCL[library_3.jar]#" + + "PCL[library_4.jar]{PCL[library_1_dex_1.jar:library_1_dex_2.jar]}" + + "}"; + + assertThat(infos.get(0).classLoaderContext()).isEqualTo("PCL[]" + sharedLibrariesContext); + assertThat(infos.get(1).classLoaderContext()) + .isEqualTo("PCL[base.apk]" + sharedLibrariesContext); + assertThat(infos.get(2).classLoaderContext()).isEqualTo(null); + assertThat(infos.get(3).classLoaderContext()) + .isEqualTo("PCL[base.apk:split_0.apk:split_1.apk]" + sharedLibrariesContext); + assertThat(infos.get(4).classLoaderContext()) + .isEqualTo("PCL[base.apk:split_0.apk:split_1.apk:split_2.apk]" + + sharedLibrariesContext); + } + + @Test + public void testGetDetailedDexInfoIsolated() { + List<DetailedPrimaryDexInfo> infos = PrimaryDexUtils.getDetailedDexInfo( + createPackageState(), createPackage(true /* isIsolatedSplitLoading */)); + checkBasicInfo(infos); + + String sharedLibrariesContext = "{" + + "PCL[library_2.jar]{PCL[library_1_dex_1.jar:library_1_dex_2.jar]}#" + + "PCL[library_3.jar]#" + + "PCL[library_4.jar]{PCL[library_1_dex_1.jar:library_1_dex_2.jar]}" + + "}"; + + assertThat(infos.get(0).classLoaderContext()).isEqualTo("PCL[]" + sharedLibrariesContext); + assertThat(infos.get(1).classLoaderContext()) + .isEqualTo("PCL[];DLC[split_2.apk];PCL[base.apk]" + sharedLibrariesContext); + assertThat(infos.get(2).classLoaderContext()).isEqualTo(null); + assertThat(infos.get(3).classLoaderContext()) + .isEqualTo("DLC[];PCL[base.apk]" + sharedLibrariesContext); + assertThat(infos.get(4).classLoaderContext()).isEqualTo("PCL[]"); + assertThat(infos.get(5).classLoaderContext()).isEqualTo("PCL[];PCL[split_3.apk]"); + } + + private <T extends PrimaryDexInfo> void checkBasicInfo(List<T> infos) { + assertThat(infos.get(0).dexPath()).isEqualTo("/data/app/foo/base.apk"); + assertThat(infos.get(0).hasCode()).isTrue(); + assertThat(infos.get(0).isBaseApk()).isTrue(); + assertThat(infos.get(0).splitIndex()).isEqualTo(-1); + assertThat(infos.get(0).splitName()).isNull(); + + assertThat(infos.get(1).dexPath()).isEqualTo("/data/app/foo/split_0.apk"); + assertThat(infos.get(1).hasCode()).isTrue(); + assertThat(infos.get(1).isBaseApk()).isFalse(); + assertThat(infos.get(1).splitIndex()).isEqualTo(0); + assertThat(infos.get(1).splitName()).isEqualTo("split_0"); + + assertThat(infos.get(2).dexPath()).isEqualTo("/data/app/foo/split_1.apk"); + assertThat(infos.get(2).hasCode()).isFalse(); + assertThat(infos.get(2).isBaseApk()).isFalse(); + assertThat(infos.get(2).splitIndex()).isEqualTo(1); + assertThat(infos.get(2).splitName()).isEqualTo("split_1"); + + assertThat(infos.get(3).dexPath()).isEqualTo("/data/app/foo/split_2.apk"); + assertThat(infos.get(3).hasCode()).isTrue(); + assertThat(infos.get(3).isBaseApk()).isFalse(); + assertThat(infos.get(3).splitIndex()).isEqualTo(2); + assertThat(infos.get(3).splitName()).isEqualTo("split_2"); + + assertThat(infos.get(4).dexPath()).isEqualTo("/data/app/foo/split_3.apk"); + assertThat(infos.get(4).hasCode()).isTrue(); + assertThat(infos.get(4).isBaseApk()).isFalse(); + assertThat(infos.get(4).splitIndex()).isEqualTo(3); + assertThat(infos.get(4).splitName()).isEqualTo("split_3"); + + assertThat(infos.get(5).dexPath()).isEqualTo("/data/app/foo/split_4.apk"); + assertThat(infos.get(5).hasCode()).isTrue(); + assertThat(infos.get(5).isBaseApk()).isFalse(); + assertThat(infos.get(5).splitIndex()).isEqualTo(4); + assertThat(infos.get(5).splitName()).isEqualTo("split_4"); + } + + private AndroidPackageApi createPackage(boolean isIsolatedSplitLoading) { + AndroidPackageApi pkg = mock(AndroidPackageApi.class); + + when(pkg.getBaseApkPath()).thenReturn("/data/app/foo/base.apk"); + when(pkg.isHasCode()).thenReturn(true); + when(pkg.getClassLoaderName()).thenReturn(PathClassLoader.class.getName()); + + when(pkg.getSplitNames()) + .thenReturn(new String[] {"split_0", "split_1", "split_2", "split_3", "split_4"}); + when(pkg.getSplitCodePaths()) + .thenReturn(new String[] { + "/data/app/foo/split_0.apk", + "/data/app/foo/split_1.apk", + "/data/app/foo/split_2.apk", + "/data/app/foo/split_3.apk", + "/data/app/foo/split_4.apk", + }); + when(pkg.getSplitFlags()) + .thenReturn(new int[] { + ApplicationInfo.FLAG_HAS_CODE, + 0, + ApplicationInfo.FLAG_HAS_CODE, + ApplicationInfo.FLAG_HAS_CODE, + ApplicationInfo.FLAG_HAS_CODE, + }); + + if (isIsolatedSplitLoading) { + // split_0: PCL(PathClassLoader), depends on split_2. + // split_1: no code. + // split_2: DLC(DelegateLastClassLoader), depends on base. + // split_3: PCL(DexClassLoader), no dependency. + // split_4: PCL(null), depends on split_3. + when(pkg.isIsolatedSplitLoading()).thenReturn(true); + when(pkg.getSplitClassLoaderNames()) + .thenReturn(new String[] { + PathClassLoader.class.getName(), + null, + DelegateLastClassLoader.class.getName(), + DexClassLoader.class.getName(), + null, + }); + SparseArray<int[]> splitDependencies = new SparseArray<>(); + splitDependencies.set(1, new int[] {3}); + splitDependencies.set(3, new int[] {0}); + splitDependencies.set(5, new int[] {4}); + when(pkg.getSplitDependencies()).thenReturn(splitDependencies); + } else { + when(pkg.isIsolatedSplitLoading()).thenReturn(false); + } + + return pkg; + } + + private PackageState createPackageState() { + PackageState pkgState = mock(PackageState.class); + + when(pkgState.getPackageName()).thenReturn("com.example.foo"); + + // Base depends on library 2, 3, 4. + // Library 2, 4 depends on library 1. + List<SharedLibraryInfo> usesLibraryInfos = new ArrayList<>(); + + SharedLibraryInfo library1 = mock(SharedLibraryInfo.class); + when(library1.getAllCodePaths()) + .thenReturn(List.of("library_1_dex_1.jar", "library_1_dex_2.jar")); + when(library1.getDependencies()).thenReturn(null); + + SharedLibraryInfo library2 = mock(SharedLibraryInfo.class); + when(library2.getAllCodePaths()).thenReturn(List.of("library_2.jar")); + when(library2.getDependencies()).thenReturn(List.of(library1)); + usesLibraryInfos.add(library2); + + SharedLibraryInfo library3 = mock(SharedLibraryInfo.class); + when(library3.getAllCodePaths()).thenReturn(List.of("library_3.jar")); + when(library3.getDependencies()).thenReturn(null); + usesLibraryInfos.add(library3); + + SharedLibraryInfo library4 = mock(SharedLibraryInfo.class); + when(library4.getAllCodePaths()).thenReturn(List.of("library_4.jar")); + when(library4.getDependencies()).thenReturn(List.of(library1)); + usesLibraryInfos.add(library4); + + when(pkgState.getUsesLibraryInfos()).thenReturn(usesLibraryInfos); + + return pkgState; + } +} diff --git a/libartservice/service/javatests/com/android/server/art/UtilsTest.java b/libartservice/service/javatests/com/android/server/art/UtilsTest.java new file mode 100644 index 0000000000..da39eec664 --- /dev/null +++ b/libartservice/service/javatests/com/android/server/art/UtilsTest.java @@ -0,0 +1,67 @@ +/* + * 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 + */ + +package com.android.server.art; + +import static com.google.common.truth.Truth.assertThat; + +import android.util.SparseArray; + +import androidx.test.filters.SmallTest; + +import com.android.server.art.Utils; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.List; + +@SmallTest +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class UtilsTest { + @Test + public void testCollectionIsEmptyTrue() { + assertThat(Utils.isEmpty(List.of())).isTrue(); + } + + @Test + public void testCollectionIsEmptyFalse() { + assertThat(Utils.isEmpty(List.of(1))).isFalse(); + } + + @Test + public void testSparseArrayIsEmptyTrue() { + assertThat(Utils.isEmpty(new SparseArray<Integer>())).isTrue(); + } + + @Test + public void testSparseArrayIsEmptyFalse() { + SparseArray<Integer> array = new SparseArray<>(); + array.put(1, 1); + assertThat(Utils.isEmpty(array)).isFalse(); + } + + @Test + public void testArrayIsEmptyTrue() { + assertThat(Utils.isEmpty(new int[0])).isTrue(); + } + + @Test + public void testArrayIsEmptyFalse() { + assertThat(Utils.isEmpty(new int[] {1})).isFalse(); + } +} diff --git a/libarttools/Android.bp b/libarttools/Android.bp index 3df40a5daf..6746c8e394 100644 --- a/libarttools/Android.bp +++ b/libarttools/Android.bp @@ -49,12 +49,17 @@ cc_library { art_cc_defaults { name: "art_libarttools_tests_defaults", srcs: [ + "tools/cmdline_builder_test.cc", + "tools/system_properties_test.cc", "tools/tools_test.cc", ], shared_libs: [ "libbase", "libarttools", ], + static_libs: [ + "libgmock", + ], } // Version of ART gtest `art_libarttools_tests` bundled with the ART APEX on target. diff --git a/libarttools/tools/cmdline_builder.h b/libarttools/tools/cmdline_builder.h new file mode 100644 index 0000000000..13b79cacd0 --- /dev/null +++ b/libarttools/tools/cmdline_builder.h @@ -0,0 +1,145 @@ +/* + * 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 <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; + } + + 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..8509f73e66 --- /dev/null +++ b/libarttools/tools/cmdline_builder_test.cc @@ -0,0 +1,120 @@ +/* + * 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 "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()); +} + +} // 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/odrefresh/odrefresh_test.cc b/odrefresh/odrefresh_test.cc index 2457da5cc6..d28c0d8942 100644 --- a/odrefresh/odrefresh_test.cc +++ b/odrefresh/odrefresh_test.cc @@ -71,14 +71,14 @@ class MockExecUtils : public ExecUtils { public: // A workaround to avoid MOCK_METHOD on a method with an `std::string*` parameter, which will lead // to a conflict between gmock and android-base/logging.h (b/132668253). - int ExecAndReturnCode(std::vector<std::string>& arg_vector, - time_t, + int ExecAndReturnCode(const std::vector<std::string>& arg_vector, + int, bool*, std::string*) const override { return DoExecAndReturnCode(arg_vector); } - MOCK_METHOD(int, DoExecAndReturnCode, (std::vector<std::string> & arg_vector), (const)); + MOCK_METHOD(int, DoExecAndReturnCode, (const std::vector<std::string>& arg_vector), (const)); }; // Matches a flag that starts with `flag` and is a colon-separated list that contains an element diff --git a/runtime/Android.bp b/runtime/Android.bp index c5cd7c57da..809445bc0f 100644 --- a/runtime/Android.bp +++ b/runtime/Android.bp @@ -849,6 +849,9 @@ art_cc_defaults { "libunwindstack", "libziparchive", ], + static_libs: [ + "libgmock", + ], header_libs: [ "art_cmdlineparser_headers", // For parsed_options_test. ], diff --git a/runtime/exec_utils.cc b/runtime/exec_utils.cc index 463d4580cf..6e2a5b40f0 100644 --- a/runtime/exec_utils.cc +++ b/runtime/exec_utils.cc @@ -16,22 +16,32 @@ #include "exec_utils.h" +#include <poll.h> #include <sys/types.h> #include <sys/wait.h> +#include <unistd.h> + +#include "base/macros.h" + +#ifdef __BIONIC__ +#include <sys/pidfd.h> +#endif + +#include <cstdint> #include <string> #include <vector> +#include "android-base/scopeguard.h" #include "android-base/stringprintf.h" #include "android-base/strings.h" - #include "runtime.h" namespace art { -using android::base::StringPrintf; - namespace { +using ::android::base::StringPrintf; + std::string ToCommandLine(const std::vector<std::string>& args) { return android::base::Join(args, ' '); } @@ -40,7 +50,7 @@ std::string ToCommandLine(const std::vector<std::string>& args) { // If there is a runtime (Runtime::Current != nullptr) then the subprocess is created with the // same environment that existed when the runtime was started. // Returns the process id of the child process on success, -1 otherwise. -pid_t ExecWithoutWait(std::vector<std::string>& arg_vector) { +pid_t ExecWithoutWait(const std::vector<std::string>& arg_vector, std::string* error_msg) { // Convert the args to char pointers. const char* program = arg_vector[0].c_str(); std::vector<char*> args; @@ -65,110 +75,124 @@ pid_t ExecWithoutWait(std::vector<std::string>& arg_vector) { } else { execve(program, &args[0], envp); } - PLOG(ERROR) << "Failed to execve(" << ToCommandLine(arg_vector) << ")"; - // _exit to avoid atexit handlers in child. - _exit(1); + // This should be regarded as a crash rather than a normal return. + PLOG(FATAL) << "Failed to execute (" << ToCommandLine(arg_vector) << ")"; + UNREACHABLE(); + } else if (pid == -1) { + *error_msg = StringPrintf("Failed to execute (%s) because fork failed: %s", + ToCommandLine(arg_vector).c_str(), + strerror(errno)); + return -1; } else { return pid; } } -} // namespace - -int ExecAndReturnCode(std::vector<std::string>& arg_vector, std::string* error_msg) { - pid_t pid = ExecWithoutWait(arg_vector); - if (pid == -1) { - *error_msg = StringPrintf("Failed to execv(%s) because fork failed: %s", - ToCommandLine(arg_vector).c_str(), strerror(errno)); - return -1; - } - - // wait for subprocess to finish +int WaitChild(pid_t pid, const std::vector<std::string>& arg_vector, std::string* error_msg) { int status = -1; pid_t got_pid = TEMP_FAILURE_RETRY(waitpid(pid, &status, 0)); if (got_pid != pid) { - *error_msg = StringPrintf("Failed after fork for execv(%s) because waitpid failed: " - "wanted %d, got %d: %s", - ToCommandLine(arg_vector).c_str(), pid, got_pid, strerror(errno)); + *error_msg = + StringPrintf("Failed to execute (%s) because waitpid failed: wanted %d, got %d: %s", + ToCommandLine(arg_vector).c_str(), + pid, + got_pid, + strerror(errno)); return -1; } - if (WIFEXITED(status)) { - return WEXITSTATUS(status); + if (!WIFEXITED(status)) { + *error_msg = + StringPrintf("Failed to execute (%s) because the child process is terminated by signal %d", + ToCommandLine(arg_vector).c_str(), + WTERMSIG(status)); + return -1; } - return -1; + return WEXITSTATUS(status); } -int ExecAndReturnCode(std::vector<std::string>& arg_vector, - time_t timeout_secs, - bool* timed_out, - std::string* error_msg) { - *timed_out = false; - - // Start subprocess. - pid_t pid = ExecWithoutWait(arg_vector); - if (pid == -1) { - *error_msg = StringPrintf("Failed to execv(%s) because fork failed: %s", - ToCommandLine(arg_vector).c_str(), strerror(errno)); +int WaitChildWithTimeout(pid_t pid, + const std::vector<std::string>& arg_vector, + int timeout_sec, + bool* timed_out, + std::string* error_msg) { + auto cleanup = android::base::make_scope_guard([&]() { + kill(pid, SIGKILL); + std::string ignored_error_msg; + WaitChild(pid, arg_vector, &ignored_error_msg); + }); + +#ifdef __BIONIC__ + int pidfd = pidfd_open(pid, /*flags=*/0); +#else + // There is no glibc wrapper for pidfd_open. + constexpr int SYS_pidfd_open = 434; + int pidfd = syscall(SYS_pidfd_open, pid, /*flags=*/0); +#endif + if (pidfd < 0) { + *error_msg = StringPrintf("pidfd_open failed for pid %d: %s", pid, strerror(errno)); return -1; } - // Add SIGCHLD to the signal set. - sigset_t child_mask, original_mask; - sigemptyset(&child_mask); - sigaddset(&child_mask, SIGCHLD); - if (sigprocmask(SIG_BLOCK, &child_mask, &original_mask) == -1) { - *error_msg = StringPrintf("Failed to set sigprocmask(): %s", strerror(errno)); + struct pollfd pfd; + pfd.fd = pidfd; + pfd.events = POLLIN; + int poll_ret = TEMP_FAILURE_RETRY(poll(&pfd, /*nfds=*/1, timeout_sec * 1000)); + + close(pidfd); + + if (poll_ret < 0) { + *error_msg = StringPrintf("poll failed for pid %d: %s", pid, strerror(errno)); return -1; } - - // Wait for a SIGCHLD notification. - errno = 0; - timespec ts = {timeout_secs, 0}; - int wait_result = TEMP_FAILURE_RETRY(sigtimedwait(&child_mask, nullptr, &ts)); - int wait_errno = errno; - - // Restore the original signal set. - if (sigprocmask(SIG_SETMASK, &original_mask, nullptr) == -1) { - *error_msg = StringPrintf("Fail to restore sigprocmask(): %s", strerror(errno)); - if (wait_result == 0) { - return -1; - } + if (poll_ret == 0) { + *timed_out = true; + *error_msg = StringPrintf("Child process %d timed out after %ds. Killing it", pid, timeout_sec); + return -1; } - // Having restored the signal set, see if we need to terminate the subprocess. - if (wait_result == -1) { - if (wait_errno == EAGAIN) { - *error_msg = "Timed out."; - *timed_out = true; - } else { - *error_msg = StringPrintf("Failed to sigtimedwait(): %s", strerror(errno)); - } - if (kill(pid, SIGKILL) != 0) { - PLOG(ERROR) << "Failed to kill() subprocess: "; - } + cleanup.Disable(); + return WaitChild(pid, arg_vector, error_msg); +} + +} // namespace + +int ExecAndReturnCode(const std::vector<std::string>& arg_vector, std::string* error_msg) { + // Start subprocess. + pid_t pid = ExecWithoutWait(arg_vector, error_msg); + if (pid == -1) { + return -1; } // Wait for subprocess to finish. - int status = -1; - pid_t got_pid = TEMP_FAILURE_RETRY(waitpid(pid, &status, 0)); - if (got_pid != pid) { - *error_msg = StringPrintf("Failed after fork for execv(%s) because waitpid failed: " - "wanted %d, got %d: %s", - ToCommandLine(arg_vector).c_str(), pid, got_pid, strerror(errno)); + return WaitChild(pid, arg_vector, error_msg); +} + +int ExecAndReturnCode(const std::vector<std::string>& arg_vector, + int timeout_sec, + bool* timed_out, + std::string* error_msg) { + *timed_out = false; + + // Start subprocess. + pid_t pid = ExecWithoutWait(arg_vector, error_msg); + if (pid == -1) { return -1; } - if (WIFEXITED(status)) { - return WEXITSTATUS(status); - } - return -1; -} + // Wait for subprocess to finish. + return WaitChildWithTimeout(pid, arg_vector, timeout_sec, timed_out, error_msg); +} -bool Exec(std::vector<std::string>& arg_vector, std::string* error_msg) { +bool Exec(const std::vector<std::string>& arg_vector, std::string* error_msg) { int status = ExecAndReturnCode(arg_vector, error_msg); - if (status != 0) { - *error_msg = StringPrintf("Failed execv(%s) because non-0 exit status", - ToCommandLine(arg_vector).c_str()); + if (status < 0) { + // Internal error. The error message is already set. + return false; + } + if (status > 0) { + *error_msg = + StringPrintf("Failed to execute (%s) because the child process returns non-zero exit code", + ToCommandLine(arg_vector).c_str()); return false; } return true; diff --git a/runtime/exec_utils.h b/runtime/exec_utils.h index 7ce0a9c20a..ff90ebdfb3 100644 --- a/runtime/exec_utils.h +++ b/runtime/exec_utils.h @@ -29,13 +29,13 @@ namespace art { // of the runtime (Runtime::Current()) was started. If no instance of the runtime was started, it // will use the current environment settings. -bool Exec(std::vector<std::string>& arg_vector, /*out*/ std::string* error_msg); -int ExecAndReturnCode(std::vector<std::string>& arg_vector, /*out*/ std::string* error_msg); +bool Exec(const std::vector<std::string>& arg_vector, /*out*/ std::string* error_msg); +int ExecAndReturnCode(const std::vector<std::string>& arg_vector, /*out*/ std::string* error_msg); // Execute the command specified in `argv_vector` in a subprocess with a timeout. // Returns the process exit code on success, -1 otherwise. -int ExecAndReturnCode(std::vector<std::string>& arg_vector, - time_t timeout_secs, +int ExecAndReturnCode(const std::vector<std::string>& arg_vector, + int timeout_sec, /*out*/ bool* timed_out, /*out*/ std::string* error_msg); @@ -44,20 +44,21 @@ class ExecUtils { public: virtual ~ExecUtils() = default; - virtual bool Exec(std::vector<std::string>& arg_vector, /*out*/ std::string* error_msg) const { + virtual bool Exec(const std::vector<std::string>& arg_vector, + /*out*/ std::string* error_msg) const { return art::Exec(arg_vector, error_msg); } - virtual int ExecAndReturnCode(std::vector<std::string>& arg_vector, + virtual int ExecAndReturnCode(const std::vector<std::string>& arg_vector, /*out*/ std::string* error_msg) const { return art::ExecAndReturnCode(arg_vector, error_msg); } - virtual int ExecAndReturnCode(std::vector<std::string>& arg_vector, - time_t timeout_secs, + virtual int ExecAndReturnCode(const std::vector<std::string>& arg_vector, + int timeout_sec, /*out*/ bool* timed_out, /*out*/ std::string* error_msg) const { - return art::ExecAndReturnCode(arg_vector, timeout_secs, timed_out, error_msg); + return art::ExecAndReturnCode(arg_vector, timeout_sec, timed_out, error_msg); } }; diff --git a/runtime/exec_utils_test.cc b/runtime/exec_utils_test.cc index dc789aa292..aa53739cfa 100644 --- a/runtime/exec_utils_test.cc +++ b/runtime/exec_utils_test.cc @@ -16,16 +16,34 @@ #include "exec_utils.h" +#include <unistd.h> + #include "android-base/stringprintf.h" #include "base/file_utils.h" #include "base/memory_tool.h" #include "common_runtime_test.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" namespace art { std::string PrettyArguments(const char* signature); std::string PrettyReturnType(const char* signature); +bool IsPidfdSupported() { +#ifdef __BIONIC__ + return true; +#else + constexpr int SYS_pidfd_open = 434; + int pidfd = syscall(SYS_pidfd_open, getpid(), /*flags=*/0); + if (pidfd < 0) { + return false; + } + close(pidfd); + return true; +#endif +} + class ExecUtilsTest : public CommonRuntimeTest {}; TEST_F(ExecUtilsTest, ExecSuccess) { @@ -44,9 +62,6 @@ TEST_F(ExecUtilsTest, ExecSuccess) { } TEST_F(ExecUtilsTest, ExecError) { - // This will lead to error messages in the log. - ScopedLogSeverity sls(LogSeverity::FATAL); - std::vector<std::string> command; command.push_back("bogus"); std::string error_msg; @@ -115,6 +130,10 @@ static std::vector<std::string> SleepCommand(int sleep_seconds) { } TEST_F(ExecUtilsTest, ExecTimeout) { + if (!IsPidfdSupported()) { + GTEST_SKIP() << "pidfd not supported"; + } + static constexpr int kSleepSeconds = 5; static constexpr int kWaitSeconds = 1; std::vector<std::string> command = SleepCommand(kSleepSeconds); @@ -125,6 +144,10 @@ TEST_F(ExecUtilsTest, ExecTimeout) { } TEST_F(ExecUtilsTest, ExecNoTimeout) { + if (!IsPidfdSupported()) { + GTEST_SKIP() << "pidfd not supported"; + } + static constexpr int kSleepSeconds = 1; static constexpr int kWaitSeconds = 5; std::vector<std::string> command = SleepCommand(kSleepSeconds); @@ -134,4 +157,15 @@ TEST_F(ExecUtilsTest, ExecNoTimeout) { EXPECT_FALSE(timed_out); } +TEST_F(ExecUtilsTest, ExecTimeoutNotSupported) { + if (IsPidfdSupported()) { + GTEST_SKIP() << "pidfd supported"; + } + + std::string error_msg; + bool timed_out; + ASSERT_EQ(ExecAndReturnCode({"command"}, /*timeout_sec=*/0, &timed_out, &error_msg), -1); + EXPECT_THAT(error_msg, testing::HasSubstr("pidfd_open failed for pid")); +} + } // namespace art |