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