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