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