summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--artd/Android.bp7
-rw-r--r--artd/artd.cc166
-rw-r--r--artd/artd.h36
-rw-r--r--artd/artd_test.cc153
-rw-r--r--artd/binder/Android.bp7
-rw-r--r--artd/binder/com/android/server/art/ArtifactsPath.aidl31
-rw-r--r--artd/binder/com/android/server/art/GetOptimizationStatusResult.aidl29
-rw-r--r--artd/binder/com/android/server/art/IArtd.aidl10
-rw-r--r--artd/path_utils.cc98
-rw-r--r--artd/path_utils.h39
-rw-r--r--artd/path_utils_test.cc86
-rw-r--r--build/Android.bp2
-rw-r--r--libartservice/service/Android.bp8
-rw-r--r--libartservice/service/api/system-server-current.txt32
-rw-r--r--libartservice/service/java/com/android/server/art/ArtManagerLocal.java237
-rw-r--r--libartservice/service/java/com/android/server/art/ArtShellCommand.java108
-rw-r--r--libartservice/service/java/com/android/server/art/LoggingArtd.java60
-rw-r--r--libartservice/service/java/com/android/server/art/PrimaryDexUtils.java375
-rw-r--r--libartservice/service/java/com/android/server/art/Utils.java84
-rw-r--r--libartservice/service/java/com/android/server/art/model/ArtFlags.java80
-rw-r--r--libartservice/service/java/com/android/server/art/model/DeleteResult.java35
-rw-r--r--libartservice/service/java/com/android/server/art/model/OptimizationStatus.java120
-rw-r--r--libartservice/service/java/com/android/server/art/wrapper/AndroidPackageApi.java117
-rw-r--r--libartservice/service/java/com/android/server/art/wrapper/PackageManagerLocal.java79
-rw-r--r--libartservice/service/java/com/android/server/art/wrapper/PackageState.java144
-rw-r--r--libartservice/service/java/com/android/server/art/wrapper/README.md11
-rw-r--r--libartservice/service/java/com/android/server/art/wrapper/SharedLibraryInfo.java56
-rw-r--r--libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java202
-rw-r--r--libartservice/service/javatests/com/android/server/art/PrimaryDexUtilsTest.java231
-rw-r--r--libartservice/service/javatests/com/android/server/art/UtilsTest.java67
-rw-r--r--libarttools/Android.bp5
-rw-r--r--libarttools/tools/cmdline_builder.h145
-rw-r--r--libarttools/tools/cmdline_builder_test.cc120
-rw-r--r--libarttools/tools/system_properties.h104
-rw-r--r--libarttools/tools/system_properties_test.cc97
-rw-r--r--odrefresh/odrefresh_test.cc6
-rw-r--r--runtime/Android.bp3
-rw-r--r--runtime/exec_utils.cc184
-rw-r--r--runtime/exec_utils.h19
-rw-r--r--runtime/exec_utils_test.cc40
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