1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.pm.parsing; 18 19 import android.annotation.NonNull; 20 import android.content.pm.PackageParserCacheHelper; 21 import android.os.Environment; 22 import android.os.FileUtils; 23 import android.os.Parcel; 24 import android.system.ErrnoException; 25 import android.system.Os; 26 import android.system.OsConstants; 27 import android.system.StructStat; 28 import android.util.Slog; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.server.pm.ApexManager; 32 import com.android.server.pm.parsing.pkg.PackageImpl; 33 import com.android.server.pm.parsing.pkg.ParsedPackage; 34 35 import libcore.io.IoUtils; 36 37 import java.io.File; 38 import java.io.FileOutputStream; 39 import java.io.IOException; 40 import java.util.concurrent.atomic.AtomicInteger; 41 42 public class PackageCacher { 43 44 private static final String TAG = "PackageCacher"; 45 46 /** 47 * Total number of packages that were read from the cache. We use it only for logging. 48 */ 49 public static final AtomicInteger sCachedPackageReadCount = new AtomicInteger(); 50 51 @NonNull 52 private File mCacheDir; 53 PackageCacher(@onNull File cacheDir)54 public PackageCacher(@NonNull File cacheDir) { 55 this.mCacheDir = cacheDir; 56 } 57 58 /** 59 * Returns the cache key for a specified {@code packageFile} and {@code flags}. 60 */ getCacheKey(File packageFile, int flags)61 private String getCacheKey(File packageFile, int flags) { 62 StringBuilder sb = new StringBuilder(packageFile.getName()); 63 sb.append('-'); 64 sb.append(flags); 65 66 return sb.toString(); 67 } 68 69 @VisibleForTesting fromCacheEntry(byte[] bytes)70 protected ParsedPackage fromCacheEntry(byte[] bytes) { 71 return fromCacheEntryStatic(bytes); 72 } 73 74 /** static version of {@link #fromCacheEntry} for unit tests. */ 75 @VisibleForTesting fromCacheEntryStatic(byte[] bytes)76 public static ParsedPackage fromCacheEntryStatic(byte[] bytes) { 77 final Parcel p = Parcel.obtain(); 78 p.unmarshall(bytes, 0, bytes.length); 79 p.setDataPosition(0); 80 81 final PackageParserCacheHelper.ReadHelper helper = 82 new PackageParserCacheHelper.ReadHelper(p); 83 helper.startAndInstall(); 84 85 ParsedPackage pkg = new PackageImpl(p); 86 87 p.recycle(); 88 89 sCachedPackageReadCount.incrementAndGet(); 90 91 return pkg; 92 } 93 94 @VisibleForTesting toCacheEntry(ParsedPackage pkg)95 protected byte[] toCacheEntry(ParsedPackage pkg) { 96 return toCacheEntryStatic(pkg); 97 98 } 99 100 /** static version of {@link #toCacheEntry} for unit tests. */ 101 @VisibleForTesting toCacheEntryStatic(ParsedPackage pkg)102 public static byte[] toCacheEntryStatic(ParsedPackage pkg) { 103 final Parcel p = Parcel.obtain(); 104 final PackageParserCacheHelper.WriteHelper helper = 105 new PackageParserCacheHelper.WriteHelper(p); 106 107 ((PackageImpl) pkg).writeToParcel(p, 0 /* flags */); 108 109 helper.finishAndUninstall(); 110 111 byte[] serialized = p.marshall(); 112 p.recycle(); 113 114 return serialized; 115 } 116 117 /** 118 * Given a {@code packageFile} and a {@code cacheFile} returns whether the 119 * cache file is up to date based on the mod-time of both files. 120 */ isCacheUpToDate(File packageFile, File cacheFile)121 private static boolean isCacheUpToDate(File packageFile, File cacheFile) { 122 try { 123 // In case packageFile is located on one of /apex mount points it's mtime will always be 124 // 0. Instead, we can use mtime of the APEX file backing the corresponding mount point. 125 if (packageFile.toPath().startsWith(Environment.getApexDirectory().toPath())) { 126 File backingApexFile = ApexManager.getInstance().getBackingApexFile(packageFile); 127 if (backingApexFile == null) { 128 Slog.w(TAG, 129 "Failed to find APEX file backing " + packageFile.getAbsolutePath()); 130 } else { 131 packageFile = backingApexFile; 132 } 133 } 134 // NOTE: We don't use the File.lastModified API because it has the very 135 // non-ideal failure mode of returning 0 with no excepions thrown. 136 // The nio2 Files API is a little better but is considerably more expensive. 137 final StructStat pkg = Os.stat(packageFile.getAbsolutePath()); 138 final StructStat cache = Os.stat(cacheFile.getAbsolutePath()); 139 return pkg.st_mtime < cache.st_mtime; 140 } catch (ErrnoException ee) { 141 // The most common reason why stat fails is that a given cache file doesn't 142 // exist. We ignore that here. It's easy to reason that it's safe to say the 143 // cache isn't up to date if we see any sort of exception here. 144 // 145 // (1) Exception while stating the package file : This should never happen, 146 // and if it does, we do a full package parse (which is likely to throw the 147 // same exception). 148 // (2) Exception while stating the cache file : If the file doesn't exist, the 149 // cache is obviously out of date. If the file *does* exist, we can't read it. 150 // We will attempt to delete and recreate it after parsing the package. 151 if (ee.errno != OsConstants.ENOENT) { 152 Slog.w("Error while stating package cache : ", ee); 153 } 154 155 return false; 156 } 157 } 158 159 /** 160 * Returns the cached parse result for {@code packageFile} for parse flags {@code flags}, 161 * or {@code null} if no cached result exists. 162 */ getCachedResult(File packageFile, int flags)163 public ParsedPackage getCachedResult(File packageFile, int flags) { 164 final String cacheKey = getCacheKey(packageFile, flags); 165 final File cacheFile = new File(mCacheDir, cacheKey); 166 167 try { 168 // If the cache is not up to date, return null. 169 if (!isCacheUpToDate(packageFile, cacheFile)) { 170 return null; 171 } 172 173 final byte[] bytes = IoUtils.readFileAsByteArray(cacheFile.getAbsolutePath()); 174 return fromCacheEntry(bytes); 175 } catch (Throwable e) { 176 Slog.w(TAG, "Error reading package cache: ", e); 177 178 // If something went wrong while reading the cache entry, delete the cache file 179 // so that we regenerate it the next time. 180 cacheFile.delete(); 181 return null; 182 } 183 } 184 185 /** 186 * Caches the parse result for {@code packageFile} with flags {@code flags}. 187 */ cacheResult(File packageFile, int flags, ParsedPackage parsed)188 public void cacheResult(File packageFile, int flags, ParsedPackage parsed) { 189 try { 190 final String cacheKey = getCacheKey(packageFile, flags); 191 final File cacheFile = new File(mCacheDir, cacheKey); 192 193 if (cacheFile.exists()) { 194 if (!cacheFile.delete()) { 195 Slog.e(TAG, "Unable to delete cache file: " + cacheFile); 196 } 197 } 198 199 final byte[] cacheEntry = toCacheEntry(parsed); 200 201 if (cacheEntry == null) { 202 return; 203 } 204 205 try (FileOutputStream fos = new FileOutputStream(cacheFile)) { 206 fos.write(cacheEntry); 207 } catch (IOException ioe) { 208 Slog.w(TAG, "Error writing cache entry.", ioe); 209 cacheFile.delete(); 210 } 211 } catch (Throwable e) { 212 Slog.w(TAG, "Error saving package cache.", e); 213 } 214 } 215 216 /** 217 * Delete the cache files for the given {@code packageFile}. 218 */ cleanCachedResult(@onNull File packageFile)219 public void cleanCachedResult(@NonNull File packageFile) { 220 final String packageName = packageFile.getName(); 221 final File[] files = FileUtils.listFilesOrEmpty(mCacheDir, 222 (dir, name) -> name.startsWith(packageName)); 223 for (File file : files) { 224 if (!file.delete()) { 225 Slog.e(TAG, "Unable to clean cache file: " + file); 226 } 227 } 228 } 229 } 230