• 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.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