1/*
2 * Copyright (C) 2016 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
17package android.webkit;
18
19import android.app.LoadedApk;
20import android.content.pm.PackageInfo;
21import android.os.Build;
22import android.os.SystemService;
23import android.os.ZygoteProcess;
24import android.text.TextUtils;
25import android.util.AndroidRuntimeException;
26import android.util.Log;
27
28import com.android.internal.annotations.GuardedBy;
29
30import java.io.File;
31import java.io.IOException;
32import java.util.ArrayList;
33import java.util.Arrays;
34import java.util.List;
35import java.util.concurrent.TimeoutException;
36
37/** @hide */
38public class WebViewZygote {
39    private static final String LOGTAG = "WebViewZygote";
40
41    private static final String WEBVIEW_ZYGOTE_SERVICE_32 = "webview_zygote32";
42    private static final String WEBVIEW_ZYGOTE_SERVICE_64 = "webview_zygote64";
43    private static final String WEBVIEW_ZYGOTE_SOCKET = "webview_zygote";
44
45    /**
46     * Lock object that protects all other static members.
47     */
48    private static final Object sLock = new Object();
49
50    /**
51     * Instance that maintains the socket connection to the zygote. This is null if the zygote
52     * is not running or is not connected.
53     */
54    @GuardedBy("sLock")
55    private static ZygoteProcess sZygote;
56
57    /**
58     * Variable that allows us to determine whether the WebView zygote Service has already been
59     * started.
60     */
61    @GuardedBy("sLock")
62    private static boolean sStartedService = false;
63
64    /**
65     * Information about the selected WebView package. This is set from #onWebViewProviderChanged().
66     */
67    @GuardedBy("sLock")
68    private static PackageInfo sPackage;
69
70    /**
71     * Cache key for the selected WebView package's classloader. This is set from
72     * #onWebViewProviderChanged().
73     */
74    @GuardedBy("sLock")
75    private static String sPackageCacheKey;
76
77    /**
78     * Flag for whether multi-process WebView is enabled. If this is false, the zygote
79     * will not be started.
80     */
81    @GuardedBy("sLock")
82    private static boolean sMultiprocessEnabled = false;
83
84    public static ZygoteProcess getProcess() {
85        synchronized (sLock) {
86            if (sZygote != null) return sZygote;
87
88            waitForServiceStartAndConnect();
89            return sZygote;
90        }
91    }
92
93    public static String getPackageName() {
94        synchronized (sLock) {
95            return sPackage.packageName;
96        }
97    }
98
99    public static boolean isMultiprocessEnabled() {
100        synchronized (sLock) {
101            return sMultiprocessEnabled && sPackage != null;
102        }
103    }
104
105    public static void setMultiprocessEnabled(boolean enabled) {
106        synchronized (sLock) {
107            sMultiprocessEnabled = enabled;
108
109            // When toggling between multi-process being on/off, start or stop the
110            // service. If it is enabled and the zygote is not yet started, bring up the service.
111            // Otherwise, bring down the service. The name may be null if the package
112            // information has not yet been resolved.
113            final String serviceName = getServiceNameLocked();
114            if (serviceName == null) return;
115
116            if (enabled) {
117                if (!sStartedService) {
118                    SystemService.start(serviceName);
119                    sStartedService = true;
120                }
121            } else {
122                SystemService.stop(serviceName);
123                sStartedService = false;
124                sZygote = null;
125            }
126        }
127    }
128
129    public static void onWebViewProviderChanged(PackageInfo packageInfo, String cacheKey) {
130        synchronized (sLock) {
131            sPackage = packageInfo;
132            sPackageCacheKey = cacheKey;
133
134            // If multi-process is not enabled, then do not start the zygote service.
135            if (!sMultiprocessEnabled) {
136                return;
137            }
138
139            final String serviceName = getServiceNameLocked();
140            sZygote = null;
141
142            // The service may enter the RUNNING state before it opens the socket,
143            // so connectToZygoteIfNeededLocked() may still fail.
144            if (SystemService.isStopped(serviceName)) {
145                SystemService.start(serviceName);
146            } else {
147                SystemService.restart(serviceName);
148            }
149            sStartedService = true;
150        }
151    }
152
153    private static void waitForServiceStartAndConnect() {
154        if (!sStartedService) {
155            throw new AndroidRuntimeException("Tried waiting for the WebView Zygote Service to " +
156                    "start running without first starting the service.");
157        }
158
159        String serviceName;
160        synchronized (sLock) {
161            serviceName = getServiceNameLocked();
162        }
163        try {
164            SystemService.waitForState(serviceName, SystemService.State.RUNNING, 5000);
165        } catch (TimeoutException e) {
166            Log.e(LOGTAG, "Timed out waiting for " + serviceName);
167            return;
168        }
169
170        synchronized (sLock) {
171            connectToZygoteIfNeededLocked();
172        }
173    }
174
175    @GuardedBy("sLock")
176    private static String getServiceNameLocked() {
177        if (sPackage == null)
178            return null;
179
180        if (Arrays.asList(Build.SUPPORTED_64_BIT_ABIS).contains(
181                    sPackage.applicationInfo.primaryCpuAbi)) {
182            return WEBVIEW_ZYGOTE_SERVICE_64;
183        }
184
185        return WEBVIEW_ZYGOTE_SERVICE_32;
186    }
187
188    @GuardedBy("sLock")
189    private static void connectToZygoteIfNeededLocked() {
190        if (sZygote != null) {
191            return;
192        }
193
194        if (sPackage == null) {
195            Log.e(LOGTAG, "Cannot connect to zygote, no package specified");
196            return;
197        }
198
199        final String serviceName = getServiceNameLocked();
200        if (!SystemService.isRunning(serviceName)) {
201            Log.e(LOGTAG, serviceName + " is not running");
202            return;
203        }
204
205        try {
206            sZygote = new ZygoteProcess(WEBVIEW_ZYGOTE_SOCKET, null);
207
208            // All the work below is usually done by LoadedApk, but the zygote can't talk to
209            // PackageManager or construct a LoadedApk since it's single-threaded pre-fork, so
210            // doesn't have an ActivityThread and can't use Binder.
211            // Instead, figure out the paths here, in the system server where we have access to
212            // the package manager. Reuse the logic from LoadedApk to determine the correct
213            // paths and pass them to the zygote as strings.
214            final List<String> zipPaths = new ArrayList<>(10);
215            final List<String> libPaths = new ArrayList<>(10);
216            LoadedApk.makePaths(null, false, sPackage.applicationInfo, zipPaths, libPaths);
217            final String librarySearchPath = TextUtils.join(File.pathSeparator, libPaths);
218            final String zip = (zipPaths.size() == 1) ? zipPaths.get(0) :
219                    TextUtils.join(File.pathSeparator, zipPaths);
220
221            waitForZygote();
222
223            Log.d(LOGTAG, "Preloading package " + zip + " " + librarySearchPath);
224            sZygote.preloadPackageForAbi(zip, librarySearchPath, sPackageCacheKey,
225                                         Build.SUPPORTED_ABIS[0]);
226        } catch (Exception e) {
227            Log.e(LOGTAG, "Error connecting to " + serviceName, e);
228            sZygote = null;
229        }
230    }
231
232    /**
233     * Wait until a connection to the Zygote can be established.
234     */
235    private static void waitForZygote() {
236        while (true) {
237            try {
238                final ZygoteProcess.ZygoteState zs =
239                        ZygoteProcess.ZygoteState.connect(WEBVIEW_ZYGOTE_SOCKET);
240                zs.close();
241                break;
242            } catch (IOException ioe) {
243                Log.w(LOGTAG, "Got error connecting to zygote, retrying. msg= " + ioe.getMessage());
244            }
245
246            try {
247                Thread.sleep(1000);
248            } catch (InterruptedException ie) {
249            }
250        }
251    }
252}
253