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 // TODO(b/135203078): Hide PackageImpl constructor? 83 ParsedPackage pkg = new PackageImpl(p); 84 85 p.recycle(); 86 87 sCachedPackageReadCount.incrementAndGet(); 88 89 return pkg; 90 } 91 92 @VisibleForTesting toCacheEntry(ParsedPackage pkg)93 protected byte[] toCacheEntry(ParsedPackage pkg) { 94 return toCacheEntryStatic(pkg); 95 96 } 97 98 /** static version of {@link #toCacheEntry} for unit tests. */ 99 @VisibleForTesting toCacheEntryStatic(ParsedPackage pkg)100 public static byte[] toCacheEntryStatic(ParsedPackage pkg) { 101 final Parcel p = Parcel.obtain(); 102 final PackageParserCacheHelper.WriteHelper helper = new PackageParserCacheHelper.WriteHelper(p); 103 104 pkg.writeToParcel(p, 0 /* flags */); 105 106 helper.finishAndUninstall(); 107 108 byte[] serialized = p.marshall(); 109 p.recycle(); 110 111 return serialized; 112 } 113 114 /** 115 * Given a {@code packageFile} and a {@code cacheFile} returns whether the 116 * cache file is up to date based on the mod-time of both files. 117 */ isCacheUpToDate(File packageFile, File cacheFile)118 private static boolean isCacheUpToDate(File packageFile, File cacheFile) { 119 try { 120 // NOTE: We don't use the File.lastModified API because it has the very 121 // non-ideal failure mode of returning 0 with no excepions thrown. 122 // The nio2 Files API is a little better but is considerably more expensive. 123 final StructStat pkg = Os.stat(packageFile.getAbsolutePath()); 124 final StructStat cache = Os.stat(cacheFile.getAbsolutePath()); 125 return pkg.st_mtime < cache.st_mtime; 126 } catch (ErrnoException ee) { 127 // The most common reason why stat fails is that a given cache file doesn't 128 // exist. We ignore that here. It's easy to reason that it's safe to say the 129 // cache isn't up to date if we see any sort of exception here. 130 // 131 // (1) Exception while stating the package file : This should never happen, 132 // and if it does, we do a full package parse (which is likely to throw the 133 // same exception). 134 // (2) Exception while stating the cache file : If the file doesn't exist, the 135 // cache is obviously out of date. If the file *does* exist, we can't read it. 136 // We will attempt to delete and recreate it after parsing the package. 137 if (ee.errno != OsConstants.ENOENT) { 138 Slog.w("Error while stating package cache : ", ee); 139 } 140 141 return false; 142 } 143 } 144 145 /** 146 * Returns the cached parse result for {@code packageFile} for parse flags {@code flags}, 147 * or {@code null} if no cached result exists. 148 */ getCachedResult(File packageFile, int flags)149 public ParsedPackage getCachedResult(File packageFile, int flags) { 150 final String cacheKey = getCacheKey(packageFile, flags); 151 final File cacheFile = new File(mCacheDir, cacheKey); 152 153 try { 154 // If the cache is not up to date, return null. 155 if (!isCacheUpToDate(packageFile, cacheFile)) { 156 return null; 157 } 158 159 final byte[] bytes = IoUtils.readFileAsByteArray(cacheFile.getAbsolutePath()); 160 return fromCacheEntry(bytes); 161 } catch (Throwable e) { 162 Slog.w(TAG, "Error reading package cache: ", e); 163 164 // If something went wrong while reading the cache entry, delete the cache file 165 // so that we regenerate it the next time. 166 cacheFile.delete(); 167 return null; 168 } 169 } 170 171 /** 172 * Caches the parse result for {@code packageFile} with flags {@code flags}. 173 */ cacheResult(File packageFile, int flags, ParsedPackage parsed)174 public void cacheResult(File packageFile, int flags, ParsedPackage parsed) { 175 try { 176 final String cacheKey = getCacheKey(packageFile, flags); 177 final File cacheFile = new File(mCacheDir, cacheKey); 178 179 if (cacheFile.exists()) { 180 if (!cacheFile.delete()) { 181 Slog.e(TAG, "Unable to delete cache file: " + cacheFile); 182 } 183 } 184 185 final byte[] cacheEntry = toCacheEntry(parsed); 186 187 if (cacheEntry == null) { 188 return; 189 } 190 191 try (FileOutputStream fos = new FileOutputStream(cacheFile)) { 192 fos.write(cacheEntry); 193 } catch (IOException ioe) { 194 Slog.w(TAG, "Error writing cache entry.", ioe); 195 cacheFile.delete(); 196 } 197 } catch (Throwable e) { 198 Slog.w(TAG, "Error saving package cache.", e); 199 } 200 } 201 202 /** 203 * Delete the cache files for the given {@code packageFile}. 204 */ cleanCachedResult(@onNull File packageFile)205 public void cleanCachedResult(@NonNull File packageFile) { 206 final String packageName = packageFile.getName(); 207 final File[] files = FileUtils.listFilesOrEmpty(mCacheDir, 208 (dir, name) -> name.startsWith(packageName)); 209 for (File file : files) { 210 if (!file.delete()) { 211 Slog.e(TAG, "Unable to clean cache file: " + file); 212 } 213 } 214 } 215 } 216