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