• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.adservices.service.js;
18 
19 import static com.android.adservices.service.js.JSScriptEngineCommonConstants.WASM_MODULE_BYTES_ID;
20 
21 import android.annotation.Nullable;
22 import android.annotation.SuppressLint;
23 import android.content.Context;
24 import android.os.Trace;
25 
26 import androidx.javascriptengine.IsolateStartupParameters;
27 import androidx.javascriptengine.JavaScriptIsolate;
28 import androidx.javascriptengine.JavaScriptSandbox;
29 import androidx.javascriptengine.MemoryLimitExceededException;
30 import androidx.javascriptengine.SandboxDeadException;
31 
32 import com.android.adservices.LoggerFactory;
33 import com.android.adservices.concurrency.AdServicesExecutors;
34 import com.android.adservices.service.common.RetryStrategy;
35 import com.android.adservices.service.exception.JSExecutionException;
36 import com.android.adservices.service.profiling.JSScriptEngineLogConstants;
37 import com.android.adservices.service.profiling.Profiler;
38 import com.android.adservices.service.profiling.StopWatch;
39 import com.android.adservices.service.profiling.Tracing;
40 import com.android.adservices.shared.common.ApplicationContextSingleton;
41 import com.android.internal.annotations.VisibleForTesting;
42 
43 import com.google.common.base.Preconditions;
44 import com.google.common.util.concurrent.ClosingFuture;
45 import com.google.common.util.concurrent.FluentFuture;
46 import com.google.common.util.concurrent.FutureCallback;
47 import com.google.common.util.concurrent.Futures;
48 import com.google.common.util.concurrent.ListenableFuture;
49 import com.google.common.util.concurrent.ListeningExecutorService;
50 
51 import java.io.Closeable;
52 import java.util.List;
53 import java.util.Set;
54 
55 import javax.annotation.concurrent.GuardedBy;
56 
57 /**
58  * A convenience class to execute JS scripts using a JavaScriptSandbox. Because arguments to the
59  * {@link #evaluate(String, List, IsolateSettings)} methods are set at JavaScriptSandbox level,
60  * calls to that methods are serialized to avoid one scripts being able to interfere one another.
61  *
62  * <p>The class is re-entrant, for best performance when using it on multiple thread is better to
63  * have every thread using its own instance.
64  */
65 public final class JSScriptEngine {
66 
67     @VisibleForTesting public static final String TAG = JSScriptEngine.class.getSimpleName();
68 
69     public static final String NON_SUPPORTED_MAX_HEAP_SIZE_EXCEPTION_MSG =
70             "JS isolate does not support Max heap size";
71     public static final String JS_SCRIPT_ENGINE_CONNECTION_EXCEPTION_MSG =
72             "Unable to create isolate";
73     public static final String JS_SCRIPT_ENGINE_SANDBOX_DEAD_MSG =
74             "Unable to evaluate on isolate due to sandbox dead exception";
75     public static final String JS_EVALUATE_METHOD_NAME = "JSScriptEngine#evaluate";
76     public static final Set<Class<? extends Exception>> RETRYABLE_EXCEPTIONS_FROM_JS_ENGINE =
77             Set.of(JSScriptEngineConnectionException.class);
78     private static final Object sJSScriptEngineLock = new Object();
79 
80     // TODO(b/366228321): should not need a sLogger and mLogger, but it's used on initialization
81     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
82 
83     @SuppressLint("StaticFieldLeak")
84     @GuardedBy("sJSScriptEngineLock")
85     private static JSScriptEngine sSingleton;
86 
87     private final Context mContext;
88     private final JavaScriptSandboxProvider mJsSandboxProvider;
89     private final ListeningExecutorService mExecutorService;
90     private final Profiler mProfiler;
91     private final LoggerFactory.Logger mLogger;
92 
93     /**
94      * Extracting the logic to create the JavaScriptSandbox in a factory class for better
95      * testability. This factory class creates a single instance of {@link JavaScriptSandbox} until
96      * the instance is invalidated by calling {@link
97      * JavaScriptSandboxProvider#destroyCurrentInstance()}. The instance is returned wrapped in a
98      * {@code Future}
99      *
100      * <p>Throws {@link JSSandboxIsNotAvailableException} if JS Sandbox is not available in the
101      * current version of the WebView
102      */
103     @VisibleForTesting
104     static final class JavaScriptSandboxProvider {
105         private final Object mSandboxLock = new Object();
106         private StopWatch mSandboxInitStopWatch;
107         private final Profiler mProfiler;
108         private final LoggerFactory.Logger mLogger;
109 
110         @GuardedBy("mSandboxLock")
111         private FluentFuture<JavaScriptSandbox> mFutureSandbox;
112 
JavaScriptSandboxProvider(Profiler profiler, LoggerFactory.Logger logger)113         JavaScriptSandboxProvider(Profiler profiler, LoggerFactory.Logger logger) {
114             mProfiler = profiler;
115             mLogger = logger;
116         }
117 
getFutureInstance(Context context)118         public FluentFuture<JavaScriptSandbox> getFutureInstance(Context context) {
119             synchronized (mSandboxLock) {
120                 if (mFutureSandbox == null) {
121                     if (!AvailabilityChecker.isJSSandboxAvailable()) {
122                         JSSandboxIsNotAvailableException exception =
123                                 new JSSandboxIsNotAvailableException();
124                         mLogger.e(
125                                 exception,
126                                 "JS Sandbox is not available in this version of WebView "
127                                         + "or WebView is not installed at all!");
128                         mFutureSandbox =
129                                 FluentFuture.from(Futures.immediateFailedFuture(exception));
130                         return mFutureSandbox;
131                     }
132 
133                     mLogger.d("Creating JavaScriptSandbox");
134                     mSandboxInitStopWatch =
135                             mProfiler.start(JSScriptEngineLogConstants.SANDBOX_INIT_TIME);
136 
137                     mFutureSandbox =
138                             FluentFuture.from(
139                                     JavaScriptSandbox.createConnectedInstanceAsync(
140                                             // This instance will have the same lifetime
141                                             // of the PPAPI process
142                                             context.getApplicationContext()));
143 
144                     mFutureSandbox.addCallback(
145                             new FutureCallback<JavaScriptSandbox>() {
146                                 @Override
147                                 public void onSuccess(JavaScriptSandbox result) {
148                                     mSandboxInitStopWatch.stop();
149                                     mLogger.d("JSScriptEngine created.");
150                                 }
151 
152                                 @Override
153                                 public void onFailure(Throwable t) {
154                                     mSandboxInitStopWatch.stop();
155                                     mLogger.e(t, "JavaScriptSandbox initialization failed");
156                                 }
157                             },
158                             AdServicesExecutors.getLightWeightExecutor());
159                 }
160 
161                 return mFutureSandbox;
162             }
163         }
164 
destroyIfCurrentInstance( JavaScriptSandbox javaScriptSandbox)165         public ListenableFuture<Void> destroyIfCurrentInstance(
166                 JavaScriptSandbox javaScriptSandbox) {
167             mLogger.d("Destroying specific instance of JavaScriptSandbox");
168             synchronized (mSandboxLock) {
169                 if (mFutureSandbox != null) {
170                     ListenableFuture<JavaScriptSandbox> futureSandbox = mFutureSandbox;
171                     return mFutureSandbox
172                             .<Void>transform(
173                                     jsSandbox -> {
174                                         synchronized (mSandboxLock) {
175                                             if (mFutureSandbox != futureSandbox) {
176                                                 mLogger.d(
177                                                         "mFutureSandbox is already set to a"
178                                                                 + " different future which "
179                                                                 + "indicates"
180                                                                 + " this is not the active "
181                                                                 + "sandbox");
182                                                 return null;
183                                             }
184                                             if (jsSandbox == javaScriptSandbox) {
185                                                 mLogger.d(
186                                                         "Closing connection from JSScriptEngine to"
187                                                                 + " JavaScriptSandbox as the "
188                                                                 + "sandbox"
189                                                                 + " requested is the current "
190                                                                 + "instance");
191                                                 jsSandbox.close();
192                                                 mFutureSandbox = null;
193                                             } else {
194                                                 mLogger.d(
195                                                         "Not closing the connection from"
196                                                                 + " JSScriptEngine to "
197                                                                 + "JavaScriptSandbox"
198                                                                 + "  as this is not the same "
199                                                                 + "instance"
200                                                                 + " as requested");
201                                             }
202                                             return null;
203                                         }
204                                     },
205                                     AdServicesExecutors.getLightWeightExecutor())
206                             .catching(
207                                     Throwable.class,
208                                     t -> {
209                                         mLogger.e(
210                                                 t,
211                                                 "JavaScriptSandbox initialization failed,"
212                                                         + " cannot close");
213                                         return null;
214                                     },
215                                     AdServicesExecutors.getLightWeightExecutor());
216                 } else {
217                     return Futures.immediateVoidFuture();
218                 }
219             }
220         }
221 
222         /**
223          * Closes the connection with {@code JavaScriptSandbox}. Any running computation will fail.
224          * A new call to {@link #getFutureInstance(Context)} will create the instance again.
225          */
226         public ListenableFuture<Void> destroyCurrentInstance() {
227             mLogger.d("Destroying JavaScriptSandbox");
228             synchronized (mSandboxLock) {
229                 if (mFutureSandbox != null) {
230                     ListenableFuture<Void> result =
231                             mFutureSandbox
232                                     .<Void>transform(
233                                             jsSandbox -> {
234                                                 mLogger.d(
235                                                         "Closing connection from JSScriptEngine to"
236                                                                 + " JavaScriptSandbox");
237                                                 jsSandbox.close();
238                                                 return null;
239                                             },
240                                             AdServicesExecutors.getLightWeightExecutor())
241                                     .catching(
242                                             Throwable.class,
243                                             t -> {
244                                                 mLogger.e(
245                                                         t,
246                                                         "JavaScriptSandbox initialization failed,"
247                                                                 + " cannot close");
248                                                 return null;
249                                             },
250                                             AdServicesExecutors.getLightWeightExecutor());
251                     mFutureSandbox = null;
252                     return result;
253                 } else {
254                     return Futures.immediateVoidFuture();
255                 }
256             }
257         }
258     }
259 
260     /** Gets the singleton instance. */
261     public static JSScriptEngine getInstance() {
262         synchronized (sJSScriptEngineLock) {
263             if (sSingleton == null) {
264                 Profiler profiler = Profiler.createNoOpInstance(TAG);
265                 Context context = ApplicationContextSingleton.get();
266                 sLogger.i("Creating JSScriptEngine singleton using default logger (%s)", sLogger);
267                 sSingleton =
268                         new JSScriptEngine(
269                                 context,
270                                 new JavaScriptSandboxProvider(profiler, sLogger),
271                                 profiler,
272                                 // There is no blocking call or IO code in the service logic
273                                 AdServicesExecutors.getLightWeightExecutor(),
274                                 sLogger);
275             }
276 
277             return sSingleton;
278         }
279     }
280 
281     /**
282      * @return a singleton JSScriptEngine instance with the given profiler and logger
283      * @throws IllegalStateException if an existing instance exists
284      */
285     @VisibleForTesting
286     public static JSScriptEngine getInstanceForTesting(
287             Profiler profiler, LoggerFactory.Logger logger) {
288         synchronized (sJSScriptEngineLock) {
289             // If there is no instance already created or the instance was shutdown
290             if (sSingleton != null) {
291                 throw new IllegalStateException(
292                         "Unable to initialize test JSScriptEngine multiple times using"
293                                 + " the real JavaScriptSandboxProvider.");
294             }
295             Context context = ApplicationContextSingleton.get();
296             sLogger.d(
297                     "Creating new instance for JSScriptEngine for tests using profiler %s and"
298                             + " logger %s",
299                     profiler, logger);
300             sSingleton =
301                     new JSScriptEngine(
302                             context,
303                             new JavaScriptSandboxProvider(profiler, logger),
304                             profiler,
305                             AdServicesExecutors.getLightWeightExecutor(),
306                             logger);
307             return sSingleton;
308         }
309     }
310 
311     /**
312      * @deprecated TODO(b/366228321): used only by JSScriptEngineE2ETest because it needs to update
313      *     the singleton with a mockLogger
314      */
315     @Deprecated
316     @VisibleForTesting
317     public static JSScriptEngine updateSingletonForE2ETest(LoggerFactory.Logger logger) {
318         synchronized (sJSScriptEngineLock) {
319             sLogger.d(
320                     "Setting JSScriptEngine singleton for tests using logger %s (previous singleton"
321                             + " was %s)",
322                     logger, sSingleton);
323             Context context = ApplicationContextSingleton.get();
324             Profiler profiler = Profiler.createNoOpInstance(TAG);
325             sSingleton =
326                     new JSScriptEngine(
327                             context,
328                             new JavaScriptSandboxProvider(profiler, logger),
329                             profiler,
330                             AdServicesExecutors.getLightWeightExecutor(),
331                             logger);
332             return sSingleton;
333         }
334     }
335 
336     /**
337      * @deprecated TODO(b/366228321): used only by JSScriptEngineE2ETest because it needs to update
338      *     the singleton with a mockLogger
339      */
340     @Deprecated
341     @VisibleForTesting
342     public static void resetSingletonForE2ETest() {
343         synchronized (sJSScriptEngineLock) {
344             sLogger.i("resetSingletonForE2ETest(): releasing %s", sSingleton);
345             sSingleton = null;
346         }
347     }
348 
349     /**
350      * This method will instantiate a new instance of JSScriptEngine every time. It is intended to
351      * be used with a fake/mock {@link JavaScriptSandboxProvider}. Using a real one would cause
352      * exception when trying to create the second instance of {@link JavaScriptSandbox}.
353      *
354      * @return a new JSScriptEngine instance
355      */
356     @VisibleForTesting
357     // TODO(b/311183933): Remove passed in Context from static method.
358     @SuppressWarnings("AvoidStaticContext")
359     public static JSScriptEngine createNewInstanceForTesting(
360             Context context,
361             JavaScriptSandboxProvider jsSandboxProvider,
362             Profiler profiler,
363             LoggerFactory.Logger logger) {
364         return new JSScriptEngine(
365                 context,
366                 jsSandboxProvider,
367                 profiler,
368                 AdServicesExecutors.getLightWeightExecutor(),
369                 logger);
370     }
371 
372     /**
373      * Closes the connection with JavaScriptSandbox. Any running computation will be terminated. It
374      * is not necessary to recreate instances of {@link JSScriptEngine} after this call; new calls
375      * to {@code evaluate} for existing instance will cause the connection to WV to be restored if
376      * necessary.
377      *
378      * @return A future to be used by tests needing to know when the sandbox close call happened.
379      */
380     public ListenableFuture<Void> shutdown() {
381         return FluentFuture.from(mJsSandboxProvider.destroyCurrentInstance())
382                 .transformAsync(
383                         ignored -> {
384                             synchronized (sJSScriptEngineLock) {
385                                 sSingleton = null;
386                             }
387                             mLogger.d("shutdown successful for JSScriptEngine");
388                             return Futures.immediateVoidFuture();
389                         },
390                         mExecutorService)
391                 .catching(
392                         Throwable.class,
393                         throwable -> {
394                             mLogger.e(throwable, "shutdown unsuccessful for JSScriptEngine");
395                             throw new IllegalStateException(
396                                     "Shutdown unsuccessful for JSScriptEngine", throwable);
397                         },
398                         mExecutorService);
399     }
400 
401     @VisibleForTesting
402     @SuppressWarnings("FutureReturnValueIgnored")
403     JSScriptEngine(
404             Context context,
405             JavaScriptSandboxProvider jsSandboxProvider,
406             Profiler profiler,
407             ListeningExecutorService executorService,
408             LoggerFactory.Logger logger) {
409         this.mContext = context;
410         this.mJsSandboxProvider = jsSandboxProvider;
411         this.mProfiler = profiler;
412         this.mExecutorService = executorService;
413         this.mLogger = logger;
414         // Forcing initialization of JavaScriptSandbox
415         jsSandboxProvider.getFutureInstance(mContext);
416     }
417 
418     /**
419      * Same as {@link #evaluate(String, List, String, IsolateSettings, RetryStrategy)} where the
420      * entry point function name is {@link JSScriptEngineCommonConstants#ENTRY_POINT_FUNC_NAME}.
421      */
422     public ListenableFuture<String> evaluate(
423             String jsScript,
424             List<JSScriptArgument> args,
425             IsolateSettings isolateSettings,
426             RetryStrategy retryStrategy) {
427         return evaluate(
428                 jsScript,
429                 args,
430                 JSScriptEngineCommonConstants.ENTRY_POINT_FUNC_NAME,
431                 isolateSettings,
432                 retryStrategy);
433     }
434 
435     /**
436      * Invokes the function {@code entryFunctionName} defined by the JS code in {@code jsScript} and
437      * return the result. It will reset the JavaScriptSandbox status after evaluating the script.
438      *
439      * @param jsScript The JS script
440      * @param args The arguments to pass when invoking {@code entryFunctionName}
441      * @param entryFunctionName The name of a function defined in {@code jsScript} that should be
442      *     invoked.
443      * @return A {@link ListenableFuture} containing the JS string representation of the result of
444      *     {@code entryFunctionName}'s invocation
445      */
446     public ListenableFuture<String> evaluate(
447             String jsScript,
448             List<JSScriptArgument> args,
449             String entryFunctionName,
450             IsolateSettings isolateSettings,
451             RetryStrategy retryStrategy) {
452         return evaluateInternal(
453                 jsScript, args, entryFunctionName, null, isolateSettings, retryStrategy, true);
454     }
455 
456     /**
457      * Invokes the JS code in {@code jsScript} and return the result. It will reset the
458      * JavaScriptSandbox status after evaluating the script.
459      *
460      * @param jsScript The JS script
461      * @return A {@link ListenableFuture} containing the JS string representation of the result of
462      *     {@code entryFunctionName}'s invocation
463      */
464     public ListenableFuture<String> evaluate(
465             String jsScript, IsolateSettings isolateSettings, RetryStrategy retryStrategy) {
466         return evaluateInternal(
467                 jsScript, List.of(), "", null, isolateSettings, retryStrategy, false);
468     }
469 
470     /**
471      * Loads the WASM module defined by {@code wasmBinary}, invokes the function {@code
472      * entryFunctionName} defined by the JS code in {@code jsScript} and return the result. It will
473      * reset the JavaScriptSandbox status after evaluating the script. The function is expected to
474      * accept all the arguments defined in {@code args} plus an extra final parameter of type {@code
475      * WebAssembly.Module}.
476      *
477      * @param jsScript The JS script
478      * @param args The arguments to pass when invoking {@code entryFunctionName}
479      * @param entryFunctionName The name of a function defined in {@code jsScript} that should be
480      *     invoked.
481      * @return A {@link ListenableFuture} containing the JS string representation of the result of
482      *     {@code entryFunctionName}'s invocation
483      */
484     public ListenableFuture<String> evaluate(
485             String jsScript,
486             byte[] wasmBinary,
487             List<JSScriptArgument> args,
488             String entryFunctionName,
489             IsolateSettings isolateSettings,
490             RetryStrategy retryStrategy) {
491         return evaluateInternal(
492                 jsScript,
493                 args,
494                 entryFunctionName,
495                 wasmBinary,
496                 isolateSettings,
497                 retryStrategy,
498                 true);
499     }
500 
501     private ListenableFuture<String> evaluateInternal(
502             String jsScript,
503             List<JSScriptArgument> args,
504             String entryFunctionName,
505             @Nullable byte[] wasmBinary,
506             IsolateSettings isolateSettings,
507             RetryStrategy retryStrategy,
508             boolean generateEntryPointWrapper) {
509         return retryStrategy.call(
510                 () ->
511                         ClosingFuture.from(mJsSandboxProvider.getFutureInstance(mContext))
512                                 .transformAsync(
513                                         (closer, jsSandbox) ->
514                                                 evaluateOnSandbox(
515                                                         closer,
516                                                         jsSandbox,
517                                                         jsScript,
518                                                         args,
519                                                         entryFunctionName,
520                                                         wasmBinary,
521                                                         isolateSettings,
522                                                         generateEntryPointWrapper),
523                                         mExecutorService)
524                                 .finishToFuture(),
525                 RETRYABLE_EXCEPTIONS_FROM_JS_ENGINE,
526                 mLogger,
527                 JS_EVALUATE_METHOD_NAME);
528     }
529 
530     private ClosingFuture<String> evaluateOnSandbox(
531             ClosingFuture.DeferredCloser closer,
532             JavaScriptSandbox jsSandbox,
533             String jsScript,
534             List<JSScriptArgument> args,
535             String entryFunctionName,
536             @Nullable byte[] wasmBinary,
537             IsolateSettings isolateSettings,
538             boolean generateEntryPointWrapper) {
539 
540         boolean hasWasmModule = wasmBinary != null;
541         if (hasWasmModule) {
542             Preconditions.checkState(
543                     isWasmSupported(jsSandbox),
544                     "Cannot evaluate a JS script depending on WASM on the JS"
545                             + " Sandbox available on this device");
546         }
547 
548         JavaScriptIsolate jsIsolate = createIsolate(jsSandbox, isolateSettings);
549         closer.eventuallyClose(new CloseableIsolateWrapper(jsIsolate, mLogger), mExecutorService);
550 
551         if (hasWasmModule) {
552             mLogger.d(
553                     "Evaluating JS script with associated WASM on thread %s",
554                     Thread.currentThread().getName());
555             try {
556                 jsIsolate.provideNamedData(WASM_MODULE_BYTES_ID, wasmBinary);
557             } catch (IllegalStateException ise) {
558                 mLogger.d(ise, "Unable to pass WASM byte array to JS Isolate");
559                 throw new JSExecutionException("Unable to pass WASM byte array to JS Isolate", ise);
560             }
561         } else {
562             mLogger.d("Evaluating JS script on thread %s", Thread.currentThread().getName());
563         }
564 
565         if (isolateSettings.getIsolateConsoleMessageInLogsEnabled()) {
566             if (isConsoleCallbackSupported(jsSandbox)) {
567                 mLogger.d("Logging console messages from Javascript Isolate.");
568                 jsIsolate.setConsoleCallback(
569                         mExecutorService,
570                         consoleMessage ->
571                                 mLogger.v(
572                                         "Javascript Console Message: %s",
573                                         consoleMessage.toString()));
574             } else {
575                 mLogger.d("Logging console messages from Javascript Isolate is not available.");
576             }
577         } else {
578             mLogger.d("Logging console messages from Javascript Isolate is disabled.");
579         }
580 
581         StringBuilder fullScript = new StringBuilder(jsScript);
582         if (generateEntryPointWrapper) {
583             String entryPointCall =
584                     JSScriptEngineCommonCodeGenerator.generateEntryPointCallingCode(
585                             args, entryFunctionName, hasWasmModule);
586 
587             fullScript.append("\n");
588             fullScript.append(entryPointCall);
589         }
590         mLogger.v("Calling JavaScriptSandbox for script %s", fullScript);
591 
592         StopWatch jsExecutionStopWatch =
593                 mProfiler.start(JSScriptEngineLogConstants.JAVA_EXECUTION_TIME);
594         int traceCookie = Tracing.beginAsyncSection(Tracing.JSSCRIPTENGINE_EVALUATE_ON_SANDBOX);
595         return ClosingFuture.from(jsIsolate.evaluateJavaScriptAsync(fullScript.toString()))
596                 .transform(
597                         (ignoredCloser, result) -> {
598                             jsExecutionStopWatch.stop();
599                             mLogger.v("JavaScriptSandbox result is " + result);
600                             Tracing.endAsyncSection(
601                                     Tracing.JSSCRIPTENGINE_EVALUATE_ON_SANDBOX, traceCookie);
602                             return result;
603                         },
604                         mExecutorService)
605                 .catching(
606                         Exception.class,
607                         (ignoredCloser, exception) -> {
608                             mLogger.v(
609                                     "Failure running JS in JavaScriptSandbox: "
610                                             + exception.getMessage());
611                             jsExecutionStopWatch.stop();
612                             Tracing.endAsyncSection(
613                                     Tracing.JSSCRIPTENGINE_EVALUATE_ON_SANDBOX, traceCookie);
614                             if (exception instanceof SandboxDeadException) {
615                                 /*
616                                    Although we are already checking for this during createIsolate
617                                    method, the creation might be successful in edge cases.
618                                    However the evaluation will fail with SandboxDeadException.
619                                    Whenever we encounter this error, we should ensure to destroy
620                                    the current instance as all other evaluations will fail.
621                                 */
622                                 mJsSandboxProvider.destroyIfCurrentInstance(jsSandbox);
623                                 throw new JSScriptEngineConnectionException(
624                                         JS_SCRIPT_ENGINE_SANDBOX_DEAD_MSG, exception);
625                             } else if (exception instanceof MemoryLimitExceededException) {
626                                 /*
627                                   In case of androidx.javascriptengine.MemoryLimitExceededException
628                                   we should not retry the JS Evaluation but close the current
629                                   instance of Javascript Sandbox.
630                                 */
631                                 mJsSandboxProvider.destroyIfCurrentInstance(jsSandbox);
632                             }
633                             throw new JSExecutionException(
634                                     "Failure running JS in JavaScriptSandbox: "
635                                             + exception.getMessage(),
636                                     exception);
637                         },
638                         mExecutorService);
639     }
640 
641     private boolean isWasmSupported(JavaScriptSandbox jsSandbox) {
642         boolean wasmCompilationSupported =
643                 jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_WASM_COMPILATION);
644         // We will pass the WASM binary via `provideNamesData`
645         // The JS will read the data using android.consumeNamedDataAsArrayBuffer
646         boolean provideConsumeArrayBufferSupported =
647                 jsSandbox.isFeatureSupported(
648                         JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER);
649         // The call android.consumeNamedDataAsArrayBuffer to read the WASM byte array
650         // returns a promises so all our code will be in a promise chain
651         boolean promiseReturnSupported =
652                 jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN);
653         mLogger.v(
654                 String.format(
655                         "Is WASM supported? WASM_COMPILATION: %b  PROVIDE_CONSUME_ARRAY_BUFFER: %b,"
656                                 + " PROMISE_RETURN: %b",
657                         wasmCompilationSupported,
658                         provideConsumeArrayBufferSupported,
659                         promiseReturnSupported));
660         return wasmCompilationSupported
661                 && provideConsumeArrayBufferSupported
662                 && promiseReturnSupported;
663     }
664 
665     /**
666      * @return a future value indicating if the JS Sandbox installed on the device supports console
667      *     message callback.
668      */
669     @VisibleForTesting
670     public ListenableFuture<Boolean> isConsoleCallbackSupported() {
671         return mJsSandboxProvider
672                 .getFutureInstance(mContext)
673                 .transform(this::isConsoleCallbackSupported, mExecutorService);
674     }
675 
676     private boolean isConsoleCallbackSupported(JavaScriptSandbox javaScriptSandbox) {
677         boolean isConsoleCallbackSupported =
678                 javaScriptSandbox.isFeatureSupported(
679                         JavaScriptSandbox.JS_FEATURE_CONSOLE_MESSAGING);
680         mLogger.v("isConsoleCallbackSupported: %b", isConsoleCallbackSupported);
681         return isConsoleCallbackSupported;
682     }
683 
684     /**
685      * @return a future value indicating if the JS Sandbox installed on the device supports WASM
686      *     execution or an error if the connection to the JS Sandbox failed.
687      */
688     public ListenableFuture<Boolean> isWasmSupported() {
689         return mJsSandboxProvider
690                 .getFutureInstance(mContext)
691                 .transform(this::isWasmSupported, mExecutorService);
692     }
693 
694     /**
695      * @return a future value indicating if the JS Sandbox installed on the device supports
696      *     configurable Heap size.
697      */
698     @VisibleForTesting
699     public ListenableFuture<Boolean> isConfigurableHeapSizeSupported() {
700         return mJsSandboxProvider
701                 .getFutureInstance(mContext)
702                 .transform(this::isConfigurableHeapSizeSupported, mExecutorService);
703     }
704 
705     private boolean isConfigurableHeapSizeSupported(JavaScriptSandbox jsSandbox) {
706         boolean isConfigurableHeapSupported =
707                 jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE);
708         mLogger.v("Is configurable max heap size supported? : %b", isConfigurableHeapSupported);
709         return isConfigurableHeapSupported;
710     }
711 
712     /**
713      * @return a future value indicating if the JS Sandbox installed on the device supports
714      *     evaluation without transaction limits.
715      */
716     public ListenableFuture<Boolean> isLargeTransactionsSupported() {
717         return mJsSandboxProvider
718                 .getFutureInstance(mContext)
719                 .transform(this::isLargeTransactionsSupported, mExecutorService);
720     }
721 
722     private boolean isLargeTransactionsSupported(JavaScriptSandbox javaScriptSandbox) {
723         boolean isLargeTransactionsSupported =
724                 javaScriptSandbox.isFeatureSupported(
725                         JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT);
726         mLogger.v(
727                 "Is evaluate without transaction limit supported? : %b",
728                 isLargeTransactionsSupported);
729         return isLargeTransactionsSupported;
730     }
731 
732     /**
733      * Creates a new isolate. This method handles the case where the `JavaScriptSandbox` process has
734      * been terminated by closing this connection. The ongoing call will fail, we won't try to
735      * recover it to keep the code simple.
736      *
737      * <p>Throws error in case, we have enforced max heap memory restrictions and isolate does not
738      * support that feature
739      */
740     @SuppressWarnings("UnclosedTrace")
741     // This is false-positives lint result. The trace is closed in finally.
742     private JavaScriptIsolate createIsolate(
743             JavaScriptSandbox jsSandbox, IsolateSettings isolateSettings) {
744         Trace.beginSection(Tracing.JSSCRIPTENGINE_CREATE_ISOLATE);
745 
746         // TODO (b/321237839): Clean up exception handling after upgrading javascriptengine
747         //  dependency to beta1
748         StopWatch isolateStopWatch =
749                 mProfiler.start(JSScriptEngineLogConstants.ISOLATE_CREATE_TIME);
750         try {
751             if (!isConfigurableHeapSizeSupported(jsSandbox)) {
752                 mLogger.e("Memory limit enforcement required, but not supported by Isolate");
753                 throw new IllegalStateException(NON_SUPPORTED_MAX_HEAP_SIZE_EXCEPTION_MSG);
754             }
755 
756             mLogger.d(
757                     "Creating JS isolate with memory limit: %d bytes",
758                     isolateSettings.getMaxHeapSizeBytes());
759             IsolateStartupParameters startupParams = new IsolateStartupParameters();
760             startupParams.setMaxHeapSizeBytes(isolateSettings.getMaxHeapSizeBytes());
761             return jsSandbox.createIsolate(startupParams);
762         } catch (RuntimeException jsSandboxPossiblyDisconnected) {
763             mLogger.e(
764                     jsSandboxPossiblyDisconnected,
765                     "JavaScriptSandboxProcess is threw exception, cannot create an isolate to run"
766                             + " JS code into (Disconnected?). Resetting connection with"
767                             + " AwJavaScriptSandbox to enable future calls.");
768             mJsSandboxProvider.destroyIfCurrentInstance(jsSandbox);
769             throw new JSScriptEngineConnectionException(
770                     JS_SCRIPT_ENGINE_CONNECTION_EXCEPTION_MSG, jsSandboxPossiblyDisconnected);
771         } finally {
772             isolateStopWatch.stop();
773             Trace.endSection();
774         }
775     }
776 
777     /**
778      * Returns {@code true} if there is an active {@link JSScriptEngine} instance, false otherwise.
779      */
780     public static boolean hasActiveInstance() {
781         synchronized (sJSScriptEngineLock) {
782             return sSingleton != null;
783         }
784     }
785 
786     /**
787      * Checks if JS Sandbox is available in the WebView version that is installed on the device
788      * before attempting to create it. Attempting to create JS Sandbox when it's not available
789      * results in returning of a null value.
790      */
791     public static class AvailabilityChecker {
792 
793         /**
794          * @return true if JS Sandbox is available in the current WebView version, false otherwise.
795          */
796         public static boolean isJSSandboxAvailable() {
797             return JavaScriptSandbox.isSupported();
798         }
799     }
800 
801     /**
802      * Wrapper class required to convert an {@link java.lang.AutoCloseable} {@link
803      * JavaScriptIsolate} into a {@link Closeable} type.
804      */
805     private static class CloseableIsolateWrapper implements Closeable {
806         final JavaScriptIsolate mIsolate;
807 
808         final LoggerFactory.Logger mLogger;
809 
810         CloseableIsolateWrapper(JavaScriptIsolate isolate, LoggerFactory.Logger logger) {
811             mIsolate = isolate;
812             mLogger = logger;
813         }
814 
815         @Override
816         public void close() {
817             Trace.beginSection(Tracing.JSSCRIPTENGINE_CLOSE_ISOLATE);
818             mLogger.d("Closing JavaScriptSandbox isolate");
819             // Closing the isolate will also cause the thread in JavaScriptSandbox to be
820             // terminated if it's still running.
821             // There is no need to verify if ISOLATE_TERMINATION is supported by
822             // JavaScriptSandbox because there is no new API but just new capability on
823             // the JavaScriptSandbox side for the existing API.
824             mIsolate.close();
825             Trace.endSection();
826         }
827     }
828 }
829