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