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