Index: build/android/incremental_install/java/org/chromium/incrementalinstall/ClassLoaderPatcher.java |
diff --git a/build/android/incremental_install/java/org/chromium/incrementalinstall/ClassLoaderPatcher.java b/build/android/incremental_install/java/org/chromium/incrementalinstall/ClassLoaderPatcher.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..66486c5f7ef23e7ae40e56113d087e961c21ba4d |
--- /dev/null |
+++ b/build/android/incremental_install/java/org/chromium/incrementalinstall/ClassLoaderPatcher.java |
@@ -0,0 +1,232 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.incrementalinstall; |
+ |
+import android.content.Context; |
+import android.os.Build; |
+import android.util.Log; |
+ |
+import java.io.File; |
+import java.io.FileInputStream; |
+import java.io.FileNotFoundException; |
+import java.io.FileOutputStream; |
+import java.io.IOException; |
+import java.util.List; |
+ |
+/** |
+ * Provides the ability to add native libraries and .dex files to an existing class loader. |
+ * Tested with Jellybean MR2 - Marshmellow. |
+ */ |
+final class ClassLoaderPatcher { |
+ private static final String TAG = "cr.incrementalinstall"; |
+ private final File mAppFilesSubDir; |
+ private final ClassLoader mClassLoader; |
+ private final Object mLibcoreOs; |
+ private final int mProcessUid; |
+ final boolean mIsPrimaryProcess; |
+ |
+ ClassLoaderPatcher(Context context) throws ReflectiveOperationException { |
+ mAppFilesSubDir = |
+ new File(context.getApplicationInfo().dataDir, "incremental-install-files"); |
+ mClassLoader = context.getClassLoader(); |
+ mLibcoreOs = Reflect.getField(Class.forName("libcore.io.Libcore"), "os"); |
+ mProcessUid = (Integer) Reflect.invokeMethod(mLibcoreOs, "getuid"); |
+ mIsPrimaryProcess = context.getApplicationInfo().uid == mProcessUid; |
+ Log.i(TAG, "uid=" + mProcessUid + " (isPrimary=" + mIsPrimaryProcess + ")"); |
+ } |
+ |
+ /** |
+ * Loads all dex files within |dexDir| into the app's ClassLoader. |
+ */ |
+ void loadDexFiles(File dexDir) throws ReflectiveOperationException, FileNotFoundException { |
+ Log.i(TAG, "Installing dex files from: " + dexDir); |
+ File[] dexFilesArr = dexDir.listFiles(); |
+ if (dexFilesArr == null) { |
+ throw new FileNotFoundException("Dex dir does not exist: " + dexDir); |
+ } |
+ // The optimized dex files will be owned by this process' user. |
+ // Store them within the app's data dir rather than on /data/local/tmp |
+ // so that they are still deleted (by the OS) when we uninstall |
+ // (even on a non-rooted device). |
+ File incrementalDexesDir = new File(mAppFilesSubDir, "optimized-dexes"); |
+ File isolatedDexesDir = new File(mAppFilesSubDir, "isolated-dexes"); |
+ File optimizedDir; |
+ |
+ if (mIsPrimaryProcess) { |
+ ensureAppFilesSubDirExists(); |
+ // Allows isolated processes to access the same files. |
+ incrementalDexesDir.mkdir(); |
+ incrementalDexesDir.setReadable(true, false); |
+ incrementalDexesDir.setExecutable(true, false); |
+ // Create a directory for isolated processes to create directories in. |
+ isolatedDexesDir.mkdir(); |
+ isolatedDexesDir.setWritable(true, false); |
+ isolatedDexesDir.setExecutable(true, false); |
+ |
+ optimizedDir = incrementalDexesDir; |
+ } else { |
+ // There is a UID check of the directory in dalvik.system.DexFile(): |
+ // https://android.googlesource.com/platform/libcore/+/45e0260/dalvik/src/main/java/dalvik/system/DexFile.java#101 |
+ // Rather than have each isolated process run DexOpt though, we use |
+ // symlinks within the directory to point at the browser process' |
+ // optimized dex files. |
+ optimizedDir = new File(isolatedDexesDir, "isolated-" + mProcessUid); |
+ optimizedDir.mkdir(); |
+ // Always wipe it out and re-create for simplicity. |
+ Log.i(TAG, "Creating dex file symlinks for isolated process"); |
+ for (File f : optimizedDir.listFiles()) { |
+ f.delete(); |
+ } |
+ for (File f : incrementalDexesDir.listFiles()) { |
+ String to = "../../" + incrementalDexesDir.getName() + "/" + f.getName(); |
+ File from = new File(optimizedDir, f.getName()); |
+ createSymlink(to, from); |
+ } |
+ } |
+ |
+ Log.i(TAG, "Code cache dir: " + optimizedDir); |
+ // TODO(agrieve): Might need to record classpath ordering if we ever have duplicate |
+ // class names (since then order will matter here). |
+ Log.i(TAG, "Loading " + dexFilesArr.length + " dex files"); |
+ |
+ Object dexPathList = Reflect.getField(mClassLoader, "pathList"); |
+ Object[] dexElements = (Object[]) Reflect.getField(dexPathList, "dexElements"); |
+ Object[] additionalElements = makeDexElements(dexFilesArr, optimizedDir); |
+ Reflect.setField( |
+ dexPathList, "dexElements", Reflect.concatArrays(dexElements, additionalElements)); |
+ } |
+ |
+ /** |
+ * Sets up all libraries within |libDir| to be loadable by System.loadLibrary(). |
+ */ |
+ void importNativeLibs(File libDir) throws ReflectiveOperationException, IOException { |
+ Log.i(TAG, "Importing native libraries from: " + libDir); |
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
+ libDir = prepareNativeLibsAndroidM(libDir); |
+ } |
+ addNativeLibrarySearchPath(libDir); |
+ } |
+ |
+ /** |
+ * Primary process: Copies native libraries into the app's data directory |
+ * Other processes: Waits for primary process to finish copying. |
+ */ |
+ private File prepareNativeLibsAndroidM(File libDir) throws IOException { |
+ File localLibsDir = new File(mAppFilesSubDir, "lib"); |
+ File copyLibsLockFile = new File(mAppFilesSubDir, "libcopy.lock"); |
+ // Due to a new SELinux policy, all libs must be copied into the app's |
+ // data directory first. |
+ // https://code.google.com/p/android/issues/detail?id=79480 |
+ if (mIsPrimaryProcess) { |
+ LockFile lockFile = LockFile.acquireRuntimeLock(copyLibsLockFile); |
+ if (lockFile == null) { |
+ LockFile.waitForRuntimeLock(copyLibsLockFile, 10 * 1000); |
+ } else { |
+ try { |
+ ensureAppFilesSubDirExists(); |
+ localLibsDir.mkdir(); |
+ localLibsDir.setReadable(true, false); |
+ localLibsDir.setExecutable(true, false); |
+ copyChangedFiles(libDir, localLibsDir); |
+ } finally { |
+ lockFile.release(); |
+ } |
+ } |
+ } else { |
+ // TODO: Work around this issue by using APK splits to install each dex / lib. |
+ throw new RuntimeException("Incremental install does not work on Android M+ " |
+ + "with isolated processes. Use the gn arg:\n" |
+ + " disable_incremental_isolated_processes=true\n" |
+ + "and try again."); |
+ } |
+ return localLibsDir; |
+ } |
+ |
+ @SuppressWarnings("unchecked") |
+ private void addNativeLibrarySearchPath(File nativeLibDir) throws ReflectiveOperationException { |
+ Object dexPathList = Reflect.getField(mClassLoader, "pathList"); |
+ Object currentDirs = Reflect.getField(dexPathList, "nativeLibraryDirectories"); |
+ File[] newDirs = new File[] { nativeLibDir }; |
+ // Switched from an array to an ArrayList in Lollipop. |
+ if (currentDirs instanceof List) { |
+ List<File> dirsAsList = (List<File>) currentDirs; |
+ dirsAsList.add(nativeLibDir); |
+ } else { |
+ File[] dirsAsArray = (File[]) currentDirs; |
+ Reflect.setField(dexPathList, "nativeLibraryDirectories", |
+ Reflect.concatArrays(dirsAsArray, newDirs)); |
+ } |
+ |
+ Object[] nativeLibraryPathElements; |
+ try { |
+ nativeLibraryPathElements = |
+ (Object[]) Reflect.getField(dexPathList, "nativeLibraryPathElements"); |
+ } catch (NoSuchFieldException e) { |
+ // This field doesn't exist pre-M. |
+ return; |
+ } |
+ Object[] additionalElements = makeNativePathElements(newDirs); |
+ Reflect.setField( |
+ dexPathList, "nativeLibraryPathElements", |
+ Reflect.concatArrays(nativeLibraryPathElements, additionalElements)); |
+ } |
+ |
+ private static void copyChangedFiles(File srcDir, File dstDir) throws IOException { |
+ // No need to delete stale libs since libraries are loaded explicitly. |
+ for (File f : srcDir.listFiles()) { |
+ // Note: Tried using hardlinks, but resulted in EACCES exceptions. |
+ File dest = new File(dstDir, f.getName()); |
+ copyIfModified(f, dest); |
+ } |
+ } |
+ |
+ private static void copyIfModified(File src, File dest) throws IOException { |
+ long lastModified = src.lastModified(); |
+ if (!dest.exists() || dest.lastModified() != lastModified) { |
+ Log.i(TAG, "Copying " + src + " -> " + dest); |
+ FileInputStream istream = new FileInputStream(src); |
+ FileOutputStream ostream = new FileOutputStream(dest); |
+ ostream.getChannel().transferFrom(istream.getChannel(), 0, istream.getChannel().size()); |
+ istream.close(); |
+ ostream.close(); |
+ dest.setReadable(true, false); |
+ dest.setExecutable(true, false); |
+ dest.setLastModified(lastModified); |
+ } |
+ } |
+ |
+ private void ensureAppFilesSubDirExists() { |
+ mAppFilesSubDir.mkdir(); |
+ mAppFilesSubDir.setExecutable(true, false); |
+ } |
+ |
+ private void createSymlink(String to, File from) throws ReflectiveOperationException { |
+ Reflect.invokeMethod(mLibcoreOs, "symlink", to, from.getAbsolutePath()); |
+ } |
+ |
+ private static Object[] makeNativePathElements(File[] paths) |
+ throws ReflectiveOperationException { |
+ Class<?> entryClazz = Class.forName("dalvik.system.DexPathList$Element"); |
+ Object[] entries = new Object[paths.length]; |
+ for (int i = 0; i < paths.length; ++i) { |
+ entries[i] = Reflect.newInstance(entryClazz, paths[i], true, null, null); |
+ } |
+ return entries; |
+ } |
+ |
+ private static Object[] makeDexElements(File[] files, File optimizedDirectory) |
+ throws ReflectiveOperationException { |
+ Class<?> entryClazz = Class.forName("dalvik.system.DexPathList$Element"); |
+ Class<?> clazz = Class.forName("dalvik.system.DexPathList"); |
+ Object[] entries = new Object[files.length]; |
+ File emptyDir = new File(""); |
+ for (int i = 0; i < files.length; ++i) { |
+ File file = files[i]; |
+ Object dexFile = Reflect.invokeMethod(clazz, "loadDexFile", file, optimizedDirectory); |
+ entries[i] = Reflect.newInstance(entryClazz, emptyDir, false, file, dexFile); |
+ } |
+ return entries; |
+ } |
+} |