• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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