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