1 /* 2 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.jshell; 27 28 import jdk.jshell.spi.ExecutionControl; 29 import java.io.ByteArrayInputStream; 30 import java.io.InputStream; 31 import java.io.PrintStream; 32 import java.text.MessageFormat; 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import java.util.Collections; 36 import java.util.HashMap; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.MissingResourceException; 40 import java.util.Objects; 41 import java.util.ResourceBundle; 42 import java.util.function.BiFunction; 43 import java.util.function.Consumer; 44 45 import java.util.function.Supplier; 46 import java.util.stream.Stream; 47 import jdk.internal.jshell.debug.InternalDebugControl; 48 import jdk.jshell.Snippet.Status; 49 import jdk.jshell.execution.JDIDefaultExecutionControl; 50 import jdk.jshell.spi.ExecutionControl.EngineTerminationException; 51 import jdk.jshell.spi.ExecutionControl.ExecutionControlException; 52 import jdk.jshell.spi.ExecutionEnv; 53 import static jdk.jshell.execution.Util.failOverExecutionControlGenerator; 54 import static java.util.stream.Collectors.collectingAndThen; 55 import static java.util.stream.Collectors.toList; 56 import static jdk.jshell.Util.expunge; 57 58 /** 59 * The JShell evaluation state engine. This is the central class in the JShell 60 * API. A {@code JShell} instance holds the evolving compilation and 61 * execution state. The state is changed with the instance methods 62 * {@link jdk.jshell.JShell#eval(java.lang.String) eval(String)}, 63 * {@link jdk.jshell.JShell#drop(jdk.jshell.PersistentSnippet) drop(PersistentSnippet)} and 64 * {@link jdk.jshell.JShell#addToClasspath(java.lang.String) addToClasspath(String)}. 65 * The majority of methods query the state. 66 * A {@code JShell} instance also allows registering for events with 67 * {@link jdk.jshell.JShell#onSnippetEvent(java.util.function.Consumer) onSnippetEvent(Consumer)} 68 * and {@link jdk.jshell.JShell#onShutdown(java.util.function.Consumer) onShutdown(Consumer)}, which 69 * are unregistered with 70 * {@link jdk.jshell.JShell#unsubscribe(jdk.jshell.JShell.Subscription) unsubscribe(Subscription)}. 71 * Access to the source analysis utilities is via 72 * {@link jdk.jshell.JShell#sourceCodeAnalysis()}. 73 * When complete the instance should be closed to free resources -- 74 * {@link jdk.jshell.JShell#close()}. 75 * <p> 76 * An instance of {@code JShell} is created with 77 * {@code JShell.create()}. 78 * <p> 79 * This class is not thread safe, except as noted, all access should be through 80 * a single thread. 81 * @author Robert Field 82 */ 83 public class JShell implements AutoCloseable { 84 85 final SnippetMaps maps; 86 final KeyMap keyMap; 87 final OuterWrapMap outerMap; 88 final TaskFactory taskFactory; 89 final InputStream in; 90 final PrintStream out; 91 final PrintStream err; 92 final Supplier<String> tempVariableNameGenerator; 93 final BiFunction<Snippet, Integer, String> idGenerator; 94 final List<String> extraRemoteVMOptions; 95 final List<String> extraCompilerOptions; 96 final ExecutionControl.Generator executionControlGenerator; 97 98 private int nextKeyIndex = 1; 99 100 final Eval eval; 101 final ClassTracker classTracker; 102 private final Map<Subscription, Consumer<JShell>> shutdownListeners = new HashMap<>(); 103 private final Map<Subscription, Consumer<SnippetEvent>> keyStatusListeners = new HashMap<>(); 104 private boolean closed = false; 105 106 private ExecutionControl executionControl = null; 107 private SourceCodeAnalysisImpl sourceCodeAnalysis = null; 108 109 private static final String L10N_RB_NAME = "jdk.jshell.resources.l10n"; 110 private static ResourceBundle outputRB = null; 111 112 JShell(Builder b) { 113 this.in = b.in; 114 this.out = b.out; 115 this.err = b.err; 116 this.tempVariableNameGenerator = b.tempVariableNameGenerator; 117 this.idGenerator = b.idGenerator; 118 this.extraRemoteVMOptions = b.extraRemoteVMOptions; 119 this.extraCompilerOptions = b.extraCompilerOptions; 120 this.executionControlGenerator = b.executionControlGenerator==null 121 ? failOverExecutionControlGenerator( 122 JDIDefaultExecutionControl.launch(), 123 JDIDefaultExecutionControl.listenLocalhost(), 124 JDIDefaultExecutionControl.listen()) 125 : b.executionControlGenerator; 126 127 this.maps = new SnippetMaps(this); 128 this.keyMap = new KeyMap(this); 129 this.outerMap = new OuterWrapMap(this); 130 this.taskFactory = new TaskFactory(this); 131 this.eval = new Eval(this); 132 this.classTracker = new ClassTracker(); 133 } 134 135 /** 136 * Builder for {@code JShell} instances. 137 * Create custom instances of {@code JShell} by using the setter 138 * methods on this class. After zero or more of these, use the 139 * {@link #build()} method to create a {@code JShell} instance. 140 * These can all be chained. For example, setting the remote output and 141 * error streams: 142 * <pre> 143 * {@code 144 * JShell myShell = 145 * JShell.builder() 146 * .out(myOutStream) 147 * .err(myErrStream) 148 * .build(); } </pre> 149 * If no special set-up is needed, just use 150 * {@code JShell.builder().build()} or the short-cut equivalent 151 * {@code JShell.create()}. 152 */ 153 public static class Builder { 154 155 InputStream in = new ByteArrayInputStream(new byte[0]); 156 PrintStream out = System.out; 157 PrintStream err = System.err; 158 Supplier<String> tempVariableNameGenerator = null; 159 BiFunction<Snippet, Integer, String> idGenerator = null; 160 List<String> extraRemoteVMOptions = new ArrayList<>(); 161 List<String> extraCompilerOptions = new ArrayList<>(); 162 ExecutionControl.Generator executionControlGenerator; 163 164 Builder() { } 165 166 /** 167 * Sets the input for the running evaluation (it's {@code System.in}). Note: 168 * applications that use {@code System.in} for snippet or other 169 * user input cannot use {@code System.in} as the input stream for 170 * the remote process. 171 * <p> 172 * The default, if this is not set, is to provide an empty input stream 173 * -- {@code new ByteArrayInputStream(new byte[0])}. 174 * 175 * @param in the {@code InputStream} to be channelled to 176 * {@code System.in} in the remote execution process 177 * @return the {@code Builder} instance (for use in chained 178 * initialization) 179 */ 180 public Builder in(InputStream in) { 181 this.in = in; 182 return this; 183 } 184 185 /** 186 * Sets the output for the running evaluation (it's {@code System.out}). 187 * The controlling process and 188 * the remote process can share {@code System.out}. 189 * <p> 190 * The default, if this is not set, is {@code System.out}. 191 * 192 * @param out the {@code PrintStream} to be channelled to 193 * {@code System.out} in the remote execution process 194 * @return the {@code Builder} instance (for use in chained 195 * initialization) 196 */ 197 public Builder out(PrintStream out) { 198 this.out = out; 199 return this; 200 } 201 202 /** 203 * Sets the error output for the running evaluation (it's 204 * {@code System.err}). The controlling process and the remote 205 * process can share {@code System.err}. 206 * <p> 207 * The default, if this is not set, is {@code System.err}. 208 * 209 * @param err the {@code PrintStream} to be channelled to 210 * {@code System.err} in the remote execution process 211 * @return the {@code Builder} instance (for use in chained 212 * initialization) 213 */ 214 public Builder err(PrintStream err) { 215 this.err = err; 216 return this; 217 } 218 219 /** 220 * Sets a generator of temp variable names for 221 * {@link jdk.jshell.VarSnippet} of 222 * {@link jdk.jshell.Snippet.SubKind#TEMP_VAR_EXPRESSION_SUBKIND}. 223 * <p> 224 * Do not use this method unless you have explicit need for it. 225 * <p> 226 * The generator will be used for newly created VarSnippet 227 * instances. The name of a variable is queried with 228 * {@link jdk.jshell.VarSnippet#name()}. 229 * <p> 230 * The callback is sent during the processing of the snippet, the 231 * JShell state is not stable. No calls whatsoever on the 232 * {@code JShell} instance may be made from the callback. 233 * <p> 234 * The generated name must be unique within active snippets. 235 * <p> 236 * The default behavior (if this is not set or {@code generator} 237 * is null) is to generate the name as a sequential number with a 238 * prefixing dollar sign ("$"). 239 * 240 * @param generator the {@code Supplier} to generate the temporary 241 * variable name string or {@code null} 242 * @return the {@code Builder} instance (for use in chained 243 * initialization) 244 */ 245 public Builder tempVariableNameGenerator(Supplier<String> generator) { 246 this.tempVariableNameGenerator = generator; 247 return this; 248 } 249 250 /** 251 * Sets the generator of identifying names for Snippets. 252 * <p> 253 * Do not use this method unless you have explicit need for it. 254 * <p> 255 * The generator will be used for newly created Snippet instances. The 256 * identifying name (id) is accessed with 257 * {@link jdk.jshell.Snippet#id()} and can be seen in the 258 * {@code StackTraceElement.getFileName()} for a 259 * {@link jdk.jshell.EvalException} and 260 * {@link jdk.jshell.UnresolvedReferenceException}. 261 * <p> 262 * The inputs to the generator are the {@link jdk.jshell.Snippet} and an 263 * integer. The integer will be the same for two Snippets which would 264 * overwrite one-another, but otherwise is unique. 265 * <p> 266 * The callback is sent during the processing of the snippet and the 267 * Snippet and the state as a whole are not stable. No calls to change 268 * system state (including Snippet state) should be made. Queries of 269 * Snippet may be made except to {@link jdk.jshell.Snippet#id()}. No 270 * calls on the {@code JShell} instance may be made from the 271 * callback, except to 272 * {@link #status(jdk.jshell.Snippet) status(Snippet)}. 273 * <p> 274 * The default behavior (if this is not set or {@code generator} 275 * is null) is to generate the id as the integer converted to a string. 276 * 277 * @param generator the {@code BiFunction} to generate the id 278 * string or {@code null} 279 * @return the {@code Builder} instance (for use in chained 280 * initialization) 281 */ 282 public Builder idGenerator(BiFunction<Snippet, Integer, String> generator) { 283 this.idGenerator = generator; 284 return this; 285 } 286 287 /** 288 * Sets additional VM options for launching the VM. 289 * 290 * @param options The options for the remote VM 291 * @return the {@code Builder} instance (for use in chained 292 * initialization) 293 */ 294 public Builder remoteVMOptions(String... options) { 295 this.extraRemoteVMOptions.addAll(Arrays.asList(options)); 296 return this; 297 } 298 299 /** 300 * Adds compiler options. These additional options will be used on 301 * parsing, analysis, and code generation calls to the compiler. 302 * Options which interfere with results are not supported and have 303 * undefined effects on JShell's operation. 304 * 305 * @param options the addition options for compiler invocations 306 * @return the {@code Builder} instance (for use in chained 307 * initialization) 308 */ 309 public Builder compilerOptions(String... options) { 310 this.extraCompilerOptions.addAll(Arrays.asList(options)); 311 return this; 312 } 313 314 /** 315 * Sets the custom engine for execution. Snippet execution will be 316 * provided by the specified {@link ExecutionControl} instance. 317 * 318 * @param executionControlGenerator the execution engine generator 319 * @return the {@code Builder} instance (for use in chained 320 * initialization) 321 */ 322 public Builder executionEngine(ExecutionControl.Generator executionControlGenerator) { 323 this.executionControlGenerator = executionControlGenerator; 324 return this; 325 } 326 327 /** 328 * Builds a JShell state engine. This is the entry-point to all JShell 329 * functionality. This creates a remote process for execution. It is 330 * thus important to close the returned instance. 331 * 332 * @return the state engine 333 */ 334 public JShell build() { 335 return new JShell(this); 336 } 337 } 338 339 // --- public API --- 340 341 /** 342 * Create a new JShell state engine. 343 * That is, create an instance of {@code JShell}. 344 * <p> 345 * Equivalent to {@link JShell#builder() JShell.builder()}{@link JShell.Builder#build() .build()}. 346 * @return an instance of {@code JShell}. 347 */ 348 public static JShell create() { 349 return builder().build(); 350 } 351 352 /** 353 * Factory method for {@code JShell.Builder} which, in-turn, is used 354 * for creating instances of {@code JShell}. 355 * Create a default instance of {@code JShell} with 356 * {@code JShell.builder().build()}. For more construction options 357 * see {@link jdk.jshell.JShell.Builder}. 358 * @return an instance of {@code Builder}. 359 * @see jdk.jshell.JShell.Builder 360 */ 361 public static Builder builder() { 362 return new Builder(); 363 } 364 365 /** 366 * Access to source code analysis functionality. 367 * An instance of {@code JShell} will always return the same 368 * {@code SourceCodeAnalysis} instance from 369 * {@code sourceCodeAnalysis()}. 370 * @return an instance of {@link SourceCodeAnalysis SourceCodeAnalysis} 371 * which can be used for source analysis such as completion detection and 372 * completion suggestions. 373 */ 374 public SourceCodeAnalysis sourceCodeAnalysis() { 375 if (sourceCodeAnalysis == null) { 376 sourceCodeAnalysis = new SourceCodeAnalysisImpl(this); 377 } 378 return sourceCodeAnalysis; 379 } 380 381 /** 382 * Evaluate the input String, including definition and/or execution, if 383 * applicable. The input is checked for errors, unless the errors can be 384 * deferred (as is the case with some unresolvedDependencies references), 385 * errors will abort evaluation. 386 * <p> 387 * The input should be 388 * exactly one complete snippet of source code, that is, one expression, 389 * statement, variable declaration, method declaration, class declaration, 390 * or import. 391 * To break arbitrary input into individual complete snippets, use 392 * {@link SourceCodeAnalysis#analyzeCompletion(String)}. 393 * <p> 394 * For imports, the import is added. Classes, interfaces. methods, 395 * and variables are defined. The initializer of variables, statements, 396 * and expressions are executed. 397 * The modifiers public, protected, private, static, and final are not 398 * allowed on op-level declarations and are ignored with a warning. 399 * Synchronized, native, abstract, and default top-level methods are not 400 * allowed and are errors. 401 * If a previous definition of a declaration is overwritten then there will 402 * be an event showing its status changed to OVERWRITTEN, this will not 403 * occur for dropped, rejected, or already overwritten declarations. 404 * <p> 405 * If execution environment is out of process, as is the default case, then 406 * if the evaluated code 407 * causes the execution environment to terminate, this {@code JShell} 408 * instance will be closed but the calling process and VM remain valid. 409 * @param input The input String to evaluate 410 * @return the list of events directly or indirectly caused by this evaluation. 411 * @throws IllegalStateException if this {@code JShell} instance is closed. 412 * @see SourceCodeAnalysis#analyzeCompletion(String) 413 * @see JShell#onShutdown(java.util.function.Consumer) 414 */ 415 public List<SnippetEvent> eval(String input) throws IllegalStateException { 416 SourceCodeAnalysisImpl a = sourceCodeAnalysis; 417 if (a != null) { 418 a.suspendIndexing(); 419 } 420 try { 421 checkIfAlive(); 422 List<SnippetEvent> events = eval.eval(input); 423 events.forEach(this::notifyKeyStatusEvent); 424 return Collections.unmodifiableList(events); 425 } finally { 426 if (a != null) { 427 a.resumeIndexing(); 428 } 429 } 430 } 431 432 /** 433 * Remove a declaration from the state. 434 * @param snippet The snippet to remove 435 * @return The list of events from updating declarations dependent on the 436 * dropped snippet. 437 * @throws IllegalStateException if this {@code JShell} instance is closed. 438 * @throws IllegalArgumentException if the snippet is not associated with 439 * this {@code JShell} instance. 440 */ 441 public List<SnippetEvent> drop(PersistentSnippet snippet) throws IllegalStateException { 442 checkIfAlive(); 443 checkValidSnippet(snippet); 444 List<SnippetEvent> events = eval.drop(snippet); 445 events.forEach(this::notifyKeyStatusEvent); 446 return Collections.unmodifiableList(events); 447 } 448 449 /** 450 * The specified path is added to the end of the classpath used in eval(). 451 * Note that the unnamed package is not accessible from the package in which 452 * {@link JShell#eval(String)} code is placed. 453 * @param path the path to add to the classpath. 454 */ 455 public void addToClasspath(String path) { 456 // Compiler 457 taskFactory.addToClasspath(path); 458 // Runtime 459 try { 460 executionControl().addToClasspath(path); 461 } catch (ExecutionControlException ex) { 462 debug(ex, "on addToClasspath(" + path + ")"); 463 } 464 if (sourceCodeAnalysis != null) { 465 sourceCodeAnalysis.classpathChanged(); 466 } 467 } 468 469 /** 470 * Attempt to stop currently running evaluation. When called while 471 * the {@link #eval(java.lang.String) } method is running and the 472 * user's code being executed, an attempt will be made to stop user's code. 473 * Note that typically this method needs to be called from a different thread 474 * than the one running the {@code eval} method. 475 * <p> 476 * If the {@link #eval(java.lang.String) } method is not running, does nothing. 477 * <p> 478 * The attempt to stop the user's code may fail in some case, which may include 479 * when the execution is blocked on an I/O operation, or when the user's code is 480 * catching the {@link ThreadDeath} exception. 481 */ 482 public void stop() { 483 if (executionControl != null) { 484 try { 485 executionControl.stop(); 486 } catch (ExecutionControlException ex) { 487 debug(ex, "on stop()"); 488 } 489 } 490 } 491 492 /** 493 * Close this state engine. Frees resources. Should be called when this 494 * state engine is no longer needed. 495 */ 496 @Override 497 public void close() { 498 if (!closed) { 499 closeDown(); 500 executionControl().close(); 501 } 502 } 503 504 /** 505 * Return all snippets. 506 * @return the snippets for all current snippets in id order. 507 * @throws IllegalStateException if this JShell instance is closed. 508 */ 509 public Stream<Snippet> snippets() throws IllegalStateException { 510 checkIfAlive(); 511 return maps.snippetList().stream(); 512 } 513 514 /** 515 * Returns the active variable snippets. 516 * This convenience method is equivalent to {@code snippets()} filtered for 517 * {@link jdk.jshell.Snippet.Status#isActive() status(snippet).isActive()} 518 * {@code && snippet.kind() == Kind.VARIABLE} 519 * and cast to {@code VarSnippet}. 520 * @return the active declared variables. 521 * @throws IllegalStateException if this JShell instance is closed. 522 */ 523 public Stream<VarSnippet> variables() throws IllegalStateException { 524 return snippets() 525 .filter(sn -> status(sn).isActive() && sn.kind() == Snippet.Kind.VAR) 526 .map(sn -> (VarSnippet) sn); 527 } 528 529 /** 530 * Returns the active method snippets. 531 * This convenience method is equivalent to {@code snippets()} filtered for 532 * {@link jdk.jshell.Snippet.Status#isActive() status(snippet).isActive()} 533 * {@code && snippet.kind() == Kind.METHOD} 534 * and cast to MethodSnippet. 535 * @return the active declared methods. 536 * @throws IllegalStateException if this JShell instance is closed. 537 */ 538 public Stream<MethodSnippet> methods() throws IllegalStateException { 539 return snippets() 540 .filter(sn -> status(sn).isActive() && sn.kind() == Snippet.Kind.METHOD) 541 .map(sn -> (MethodSnippet)sn); 542 } 543 544 /** 545 * Returns the active type declaration (class, interface, annotation type, and enum) snippets. 546 * This convenience method is equivalent to {@code snippets()} filtered for 547 * {@link jdk.jshell.Snippet.Status#isActive() status(snippet).isActive()} 548 * {@code && snippet.kind() == Kind.TYPE_DECL} 549 * and cast to TypeDeclSnippet. 550 * @return the active declared type declarations. 551 * @throws IllegalStateException if this JShell instance is closed. 552 */ 553 public Stream<TypeDeclSnippet> types() throws IllegalStateException { 554 return snippets() 555 .filter(sn -> status(sn).isActive() && sn.kind() == Snippet.Kind.TYPE_DECL) 556 .map(sn -> (TypeDeclSnippet) sn); 557 } 558 559 /** 560 * Returns the active import snippets. 561 * This convenience method is equivalent to {@code snippets()} filtered for 562 * {@link jdk.jshell.Snippet.Status#isActive() status(snippet).isActive()} 563 * {@code && snippet.kind() == Kind.IMPORT} 564 * and cast to ImportSnippet. 565 * @return the active declared import declarations. 566 * @throws IllegalStateException if this JShell instance is closed. 567 */ 568 public Stream<ImportSnippet> imports() throws IllegalStateException { 569 return snippets() 570 .filter(sn -> status(sn).isActive() && sn.kind() == Snippet.Kind.IMPORT) 571 .map(sn -> (ImportSnippet) sn); 572 } 573 574 /** 575 * Return the status of the snippet. 576 * This is updated either because of an explicit {@code eval()} call or 577 * an automatic update triggered by a dependency. 578 * @param snippet the {@code Snippet} to look up 579 * @return the status corresponding to this snippet 580 * @throws IllegalStateException if this {@code JShell} instance is closed. 581 * @throws IllegalArgumentException if the snippet is not associated with 582 * this {@code JShell} instance. 583 */ 584 public Status status(Snippet snippet) { 585 return checkValidSnippet(snippet).status(); 586 } 587 588 /** 589 * Return the diagnostics of the most recent evaluation of the snippet. 590 * The evaluation can either because of an explicit {@code eval()} call or 591 * an automatic update triggered by a dependency. 592 * @param snippet the {@code Snippet} to look up 593 * @return the diagnostics corresponding to this snippet. This does not 594 * include unresolvedDependencies references reported in {@code unresolvedDependencies()}. 595 * @throws IllegalStateException if this {@code JShell} instance is closed. 596 * @throws IllegalArgumentException if the snippet is not associated with 597 * this {@code JShell} instance. 598 */ 599 public Stream<Diag> diagnostics(Snippet snippet) { 600 return checkValidSnippet(snippet).diagnostics().stream(); 601 } 602 603 /** 604 * For {@link jdk.jshell.Snippet.Status#RECOVERABLE_DEFINED RECOVERABLE_DEFINED} or 605 * {@link jdk.jshell.Snippet.Status#RECOVERABLE_NOT_DEFINED RECOVERABLE_NOT_DEFINED} 606 * declarations, the names of current unresolved dependencies for 607 * the snippet. 608 * The returned value of this method, for a given method may change when an 609 * {@code eval()} or {@code drop()} of another snippet causes 610 * an update of a dependency. 611 * @param snippet the declaration {@code Snippet} to look up 612 * @return a stream of symbol names that are currently unresolvedDependencies. 613 * @throws IllegalStateException if this {@code JShell} instance is closed. 614 * @throws IllegalArgumentException if the snippet is not associated with 615 * this {@code JShell} instance. 616 */ 617 public Stream<String> unresolvedDependencies(DeclarationSnippet snippet) { 618 return checkValidSnippet(snippet).unresolved().stream(); 619 } 620 621 /** 622 * Get the current value of a variable. 623 * @param snippet the variable Snippet whose value is queried. 624 * @return the current value of the variable referenced by snippet. 625 * @throws IllegalStateException if this {@code JShell} instance is closed. 626 * @throws IllegalArgumentException if the snippet is not associated with 627 * this {@code JShell} instance. 628 * @throws IllegalArgumentException if the variable's status is anything but 629 * {@link jdk.jshell.Snippet.Status#VALID}. 630 */ 631 public String varValue(VarSnippet snippet) throws IllegalStateException { 632 checkIfAlive(); 633 checkValidSnippet(snippet); 634 if (snippet.status() != Status.VALID) { 635 throw new IllegalArgumentException( 636 messageFormat("jshell.exc.var.not.valid", snippet, snippet.status())); 637 } 638 String value; 639 try { 640 value = executionControl().varValue(snippet.classFullName(), snippet.name()); 641 } catch (EngineTerminationException ex) { 642 throw new IllegalStateException(ex.getMessage()); 643 } catch (ExecutionControlException ex) { 644 debug(ex, "In varValue()"); 645 return "[" + ex.getMessage() + "]"; 646 } 647 return expunge(value); 648 } 649 650 /** 651 * Register a callback to be called when the Status of a snippet changes. 652 * Each call adds a new subscription. 653 * @param listener Action to perform when the Status changes. 654 * @return A token which can be used to {@linkplain JShell#unsubscribe unsubscribe} this subscription. 655 * @throws IllegalStateException if this {@code JShell} instance is closed. 656 */ 657 public Subscription onSnippetEvent(Consumer<SnippetEvent> listener) 658 throws IllegalStateException { 659 return onX(keyStatusListeners, listener); 660 } 661 662 /** 663 * Register a callback to be called when this JShell instance terminates. 664 * This occurs either because the client process has ended (e.g. called System.exit(0)) 665 * or the connection has been shutdown, as by close(). 666 * Each call adds a new subscription. 667 * @param listener Action to perform when the state terminates. 668 * @return A token which can be used to {@linkplain JShell#unsubscribe unsubscribe} this subscription. 669 * @throws IllegalStateException if this JShell instance is closed 670 */ 671 public Subscription onShutdown(Consumer<JShell> listener) 672 throws IllegalStateException { 673 return onX(shutdownListeners, listener); 674 } 675 676 /** 677 * Cancel a callback subscription. 678 * @param token The token corresponding to the subscription to be unsubscribed. 679 */ 680 public void unsubscribe(Subscription token) { 681 synchronized (this) { 682 token.remover.accept(token); 683 } 684 } 685 686 /** 687 * Subscription is a token for referring to subscriptions so they can 688 * be {@linkplain JShell#unsubscribe unsubscribed}. 689 */ 690 public class Subscription { 691 692 Consumer<Subscription> remover; 693 694 Subscription(Consumer<Subscription> remover) { 695 this.remover = remover; 696 } 697 } 698 699 /** 700 * Provide the environment for a execution engine. 701 */ 702 class ExecutionEnvImpl implements ExecutionEnv { 703 704 @Override 705 public InputStream userIn() { 706 return in; 707 } 708 709 @Override 710 public PrintStream userOut() { 711 return out; 712 } 713 714 @Override 715 public PrintStream userErr() { 716 return err; 717 } 718 719 @Override 720 public List<String> extraRemoteVMOptions() { 721 return extraRemoteVMOptions; 722 } 723 724 @Override 725 public void closeDown() { 726 JShell.this.closeDown(); 727 } 728 729 } 730 731 // --- private / package-private implementation support --- 732 ExecutionControl executionControl() { 733 if (executionControl == null) { 734 try { 735 executionControl = executionControlGenerator.generate(new ExecutionEnvImpl()); 736 } catch (Throwable ex) { 737 throw new InternalError("Launching execution engine threw: " + ex.getMessage(), ex); 738 } 739 } 740 return executionControl; 741 } 742 743 void debug(int flags, String format, Object... args) { 744 InternalDebugControl.debug(this, err, flags, format, args); 745 } 746 747 void debug(Exception ex, String where) { 748 InternalDebugControl.debug(this, err, ex, where); 749 } 750 751 /** 752 * Generate the next key index, indicating a unique snippet signature. 753 * 754 * @return the next key index 755 */ 756 int nextKeyIndex() { 757 return nextKeyIndex++; 758 } 759 760 private synchronized <T> Subscription onX(Map<Subscription, Consumer<T>> map, Consumer<T> listener) 761 throws IllegalStateException { 762 Objects.requireNonNull(listener); 763 checkIfAlive(); 764 Subscription token = new Subscription(map::remove); 765 map.put(token, listener); 766 return token; 767 } 768 769 private synchronized void notifyKeyStatusEvent(SnippetEvent event) { 770 keyStatusListeners.values().forEach(l -> l.accept(event)); 771 } 772 773 private synchronized void notifyShutdownEvent(JShell state) { 774 shutdownListeners.values().forEach(l -> l.accept(state)); 775 } 776 777 void closeDown() { 778 if (!closed) { 779 // Send only once 780 closed = true; 781 try { 782 notifyShutdownEvent(this); 783 } catch (Throwable thr) { 784 // Don't care about dying exceptions 785 } 786 } 787 } 788 789 /** 790 * Check if this JShell has been closed 791 * @throws IllegalStateException if it is closed 792 */ 793 private void checkIfAlive() throws IllegalStateException { 794 if (closed) { 795 throw new IllegalStateException(messageFormat("jshell.exc.closed", this)); 796 } 797 } 798 799 /** 800 * Check a Snippet parameter coming from the API user 801 * @param sn the Snippet to check 802 * @throws NullPointerException if Snippet parameter is null 803 * @throws IllegalArgumentException if Snippet is not from this JShell 804 * @return the input Snippet (for chained calls) 805 */ 806 private Snippet checkValidSnippet(Snippet sn) { 807 if (sn == null) { 808 throw new NullPointerException(messageFormat("jshell.exc.null")); 809 } else { 810 if (sn.key().state() != this) { 811 throw new IllegalArgumentException(messageFormat("jshell.exc.alien")); 812 } 813 return sn; 814 } 815 } 816 817 /** 818 * Format using resource bundle look-up using MessageFormat 819 * 820 * @param key the resource key 821 * @param args 822 */ 823 String messageFormat(String key, Object... args) { 824 if (outputRB == null) { 825 try { 826 outputRB = ResourceBundle.getBundle(L10N_RB_NAME); 827 } catch (MissingResourceException mre) { 828 throw new InternalError("Cannot find ResourceBundle: " + L10N_RB_NAME); 829 } 830 } 831 String s; 832 try { 833 s = outputRB.getString(key); 834 } catch (MissingResourceException mre) { 835 throw new InternalError("Missing resource: " + key + " in " + L10N_RB_NAME); 836 } 837 return MessageFormat.format(s, args); 838 } 839 840 }