JShell.java revision 3095:cf000bae9c31
1262395Sbapt/* 2262395Sbapt * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. 3262395Sbapt * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4262395Sbapt * 5262395Sbapt * This code is free software; you can redistribute it and/or modify it 6262395Sbapt * under the terms of the GNU General Public License version 2 only, as 7262395Sbapt * published by the Free Software Foundation. Oracle designates this 8262395Sbapt * particular file as subject to the "Classpath" exception as provided 9262395Sbapt * by Oracle in the LICENSE file that accompanied this code. 10262395Sbapt * 11262395Sbapt * This code is distributed in the hope that it will be useful, but WITHOUT 12262395Sbapt * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13262395Sbapt * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14262395Sbapt * version 2 for more details (a copy is included in the LICENSE file that 15262395Sbapt * accompanied this code). 16262395Sbapt * 17262395Sbapt * You should have received a copy of the GNU General Public License version 18262395Sbapt * 2 along with this work; if not, write to the Free Software Foundation, 19262395Sbapt * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20262395Sbapt * 21262395Sbapt * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22262395Sbapt * or visit www.oracle.com if you need additional information or have any 23262395Sbapt * questions. 24262395Sbapt */ 25262395Sbapt 26262395Sbaptpackage jdk.jshell; 27262395Sbapt 28262395Sbaptimport java.io.ByteArrayInputStream; 29262395Sbaptimport java.io.IOException; 30262395Sbaptimport java.io.InputStream; 31262395Sbaptimport java.io.PrintStream; 32262395Sbaptimport java.util.ArrayList; 33262395Sbaptimport java.util.Collections; 34268896Sbaptimport java.util.HashMap; 35262395Sbaptimport java.util.List; 36262395Sbaptimport java.util.Map; 37262395Sbaptimport java.util.Objects; 38262395Sbaptimport java.util.function.BiFunction; 39262395Sbaptimport java.util.function.Consumer; 40262395Sbapt 41262395Sbaptimport java.util.function.Supplier; 42262395Sbaptimport jdk.internal.jshell.debug.InternalDebugControl; 43262395Sbaptimport static java.util.stream.Collectors.collectingAndThen; 44262395Sbaptimport static java.util.stream.Collectors.toList; 45262395Sbaptimport static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT; 46262395Sbaptimport static jdk.jshell.Util.expunge; 47262395Sbaptimport jdk.jshell.Snippet.Status; 48262395Sbapt 49262395Sbapt/** 50262395Sbapt * The JShell evaluation state engine. This is the central class in the JShell 51262395Sbapt * API. A <code>JShell</code> instance holds the evolving compilation and 52262395Sbapt * execution state. The state is changed with the instance methods 53262395Sbapt * {@link jdk.jshell.JShell#eval(java.lang.String) eval(String)}, 54262395Sbapt * {@link jdk.jshell.JShell#drop(jdk.jshell.PersistentSnippet) drop(PersistentSnippet)} and 55262395Sbapt * {@link jdk.jshell.JShell#addToClasspath(java.lang.String) addToClasspath(String)}. 56262395Sbapt * The majority of methods query the state. 57262395Sbapt * A <code>JShell</code> instance also allows registering for events with 58262395Sbapt * {@link jdk.jshell.JShell#onSnippetEvent(java.util.function.Consumer) onSnippetEvent(Consumer)} 59262395Sbapt * and {@link jdk.jshell.JShell#onShutdown(java.util.function.Consumer) onShutdown(Consumer)}, which 60262395Sbapt * are unregistered with 61262395Sbapt * {@link jdk.jshell.JShell#unsubscribe(jdk.jshell.JShell.Subscription) unsubscribe(Subscription)}. 62262395Sbapt * Access to the source analysis utilities is via 63262395Sbapt * {@link jdk.jshell.JShell#sourceCodeAnalysis()}. 64262395Sbapt * When complete the instance should be closed to free resources -- 65268896Sbapt * {@link jdk.jshell.JShell#close()}. 66268896Sbapt * <p> 67262395Sbapt * An instance of <code>JShell</code> is created with 68262395Sbapt * <code>JShell.create()</code>. 69262395Sbapt * <p> 70262395Sbapt * This class is not thread safe, except as noted, all access should be through 71268896Sbapt * a single thread. 72262395Sbapt * @see jdk.jshell 73262395Sbapt * @author Robert Field 74262395Sbapt */ 75262395Sbaptpublic class JShell implements AutoCloseable { 76268896Sbapt 77268896Sbapt final SnippetMaps maps; 78262395Sbapt final KeyMap keyMap; 79262395Sbapt final TaskFactory taskFactory; 80262395Sbapt final InputStream in; 81262395Sbapt final PrintStream out; 82262395Sbapt final PrintStream err; 83262395Sbapt final Supplier<String> tempVariableNameGenerator; 84262395Sbapt final BiFunction<Snippet, Integer, String> idGenerator; 85262395Sbapt 86268896Sbapt private int nextKeyIndex = 1; 87262395Sbapt 88262395Sbapt final Eval eval; 89262395Sbapt final ClassTracker classTracker; 90262395Sbapt private final Map<Subscription, Consumer<JShell>> shutdownListeners = new HashMap<>(); 91262395Sbapt private final Map<Subscription, Consumer<SnippetEvent>> keyStatusListeners = new HashMap<>(); 92262395Sbapt private boolean closed = false; 93262395Sbapt 94 95 private ExecutionControl executionControl = null; 96 private SourceCodeAnalysis sourceCodeAnalysis = null; 97 98 99 JShell(Builder b) { 100 this.in = b.in; 101 this.out = b.out; 102 this.err = b.err; 103 this.tempVariableNameGenerator = b.tempVariableNameGenerator; 104 this.idGenerator = b.idGenerator; 105 106 this.maps = new SnippetMaps(this); 107 maps.setPackageName("REPL"); 108 this.keyMap = new KeyMap(this); 109 this.taskFactory = new TaskFactory(this); 110 this.eval = new Eval(this); 111 this.classTracker = new ClassTracker(this); 112 } 113 114 /** 115 * Builder for <code>JShell</code> instances. 116 * Create custom instances of <code>JShell</code> by using the setter 117 * methods on this class. After zero or more of these, use the 118 * {@link #build()} method to create a <code>JShell</code> instance. 119 * These can all be chained. For example, setting the remote output and 120 * error streams: 121 * <pre> 122 * <code> 123 * JShell myShell = 124 * JShell.builder() 125 * .out(myOutStream) 126 * .err(myErrStream) 127 * .build(); </code> </pre> 128 * If no special set-up is needed, just use 129 * <code>JShell.builder().build()</code> or the short-cut equivalent 130 * <code>JShell.create()</code>. 131 */ 132 public static class Builder { 133 134 InputStream in = new ByteArrayInputStream(new byte[0]); 135 PrintStream out = System.out; 136 PrintStream err = System.err; 137 Supplier<String> tempVariableNameGenerator = null; 138 BiFunction<Snippet, Integer, String> idGenerator = null; 139 140 Builder() { } 141 142 /** 143 * Input for the running evaluation (it's <code>System.in</code>). Note: 144 * applications that use <code>System.in</code> for snippet or other 145 * user input cannot use <code>System.in</code> as the input stream for 146 * the remote process. 147 * <p> 148 * The default, if this is not set, is to provide an empty input stream 149 * -- <code>new ByteArrayInputStream(new byte[0])</code>. 150 * 151 * @param in the <code>InputStream</code> to be channelled to 152 * <code>System.in</code> in the remote execution process. 153 * @return the <code>Builder</code> instance (for use in chained 154 * initialization). 155 */ 156 public Builder in(InputStream in) { 157 this.in = in; 158 return this; 159 } 160 161 /** 162 * Output for the running evaluation (it's <code>System.out</code>). 163 * The controlling process and 164 * the remote process can share <code>System.out</code>. 165 * <p> 166 * The default, if this is not set, is <code>System.out</code>. 167 * 168 * @param out the <code>PrintStream</code> to be channelled to 169 * <code>System.out</code> in the remote execution process. 170 * @return the <code>Builder</code> instance (for use in chained 171 * initialization). 172 */ 173 public Builder out(PrintStream out) { 174 this.out = out; 175 return this; 176 } 177 178 /** 179 * Error output for the running evaluation (it's 180 * <code>System.err</code>). The controlling process and the remote 181 * process can share <code>System.err</code>. 182 * <p> 183 * The default, if this is not set, is <code>System.err</code>. 184 * 185 * @param err the <code>PrintStream</code> to be channelled to 186 * <code>System.err</code> in the remote execution process. 187 * @return the <code>Builder</code> instance (for use in chained 188 * initialization). 189 */ 190 public Builder err(PrintStream err) { 191 this.err = err; 192 return this; 193 } 194 195 /** 196 * Set a generator of temp variable names for 197 * {@link jdk.jshell.VarSnippet} of 198 * {@link jdk.jshell.Snippet.SubKind#TEMP_VAR_EXPRESSION_SUBKIND}. 199 * <p> 200 * Do not use this method unless you have explicit need for it. 201 * <p> 202 * The generator will be used for newly created VarSnippet 203 * instances. The name of a variable is queried with 204 * {@link jdk.jshell.VarSnippet#name()}. 205 * <p> 206 * The callback is sent during the processing of the snippet, the 207 * JShell state is not stable. No calls whatsoever on the 208 * <code>JShell</code> instance may be made from the callback. 209 * <p> 210 * The generated name must be unique within active snippets. 211 * <p> 212 * The default behavior (if this is not set or <code>generator</code> 213 * is null) is to generate the name as a sequential number with a 214 * prefixing dollar sign ("$"). 215 * 216 * @param generator the <code>Supplier</code> to generate the temporary 217 * variable name string or <code>null</code>. 218 * @return the <code>Builder</code> instance (for use in chained 219 * initialization). 220 */ 221 public Builder tempVariableNameGenerator(Supplier<String> generator) { 222 this.tempVariableNameGenerator = generator; 223 return this; 224 } 225 226 /** 227 * Set the generator of identifying names for Snippets. 228 * <p> 229 * Do not use this method unless you have explicit need for it. 230 * <p> 231 * The generator will be used for newly created Snippet instances. The 232 * identifying name (id) is accessed with 233 * {@link jdk.jshell.Snippet#id()} and can be seen in the 234 * <code>StackTraceElement.getFileName()</code> for a 235 * {@link jdk.jshell.EvalException} and 236 * {@link jdk.jshell.UnresolvedReferenceException}. 237 * <p> 238 * The inputs to the generator are the {@link jdk.jshell.Snippet} and an 239 * integer. The integer will be the same for two Snippets which would 240 * overwrite one-another, but otherwise is unique. 241 * <p> 242 * The callback is sent during the processing of the snippet and the 243 * Snippet and the state as a whole are not stable. No calls to change 244 * system state (including Snippet state) should be made. Queries of 245 * Snippet may be made except to {@link jdk.jshell.Snippet#id()}. No 246 * calls on the <code>JShell</code> instance may be made from the 247 * callback, except to 248 * {@link #status(jdk.jshell.Snippet) status(Snippet)}. 249 * <p> 250 * The default behavior (if this is not set or <code>generator</code> 251 * is null) is to generate the id as the integer converted to a string. 252 * 253 * @param generator the <code>BiFunction</code> to generate the id 254 * string or <code>null</code>. 255 * @return the <code>Builder</code> instance (for use in chained 256 * initialization). 257 */ 258 public Builder idGenerator(BiFunction<Snippet, Integer, String> generator) { 259 this.idGenerator = generator; 260 return this; 261 } 262 263 /** 264 * Build a JShell state engine. This is the entry-point to all JShell 265 * functionality. This creates a remote process for execution. It is 266 * thus important to close the returned instance. 267 * 268 * @return the state engine. 269 */ 270 public JShell build() { 271 return new JShell(this); 272 } 273 } 274 275 // --- public API --- 276 277 /** 278 * Create a new JShell state engine. 279 * That is, create an instance of <code>JShell</code>. 280 * <p> 281 * Equivalent to {@link JShell#builder() JShell.builder()}{@link JShell.Builder#build() .build()}. 282 * @return an instance of <code>JShell</code>. 283 */ 284 public static JShell create() { 285 return builder().build(); 286 } 287 288 /** 289 * Factory method for <code>JShell.Builder</code> which, in-turn, is used 290 * for creating instances of <code>JShell</code>. 291 * Create a default instance of <code>JShell</code> with 292 * <code>JShell.builder().build()</code>. For more construction options 293 * see {@link jdk.jshell.JShell.Builder}. 294 * @return an instance of <code>Builder</code>. 295 * @see jdk.jshell.JShell.Builder 296 */ 297 public static Builder builder() { 298 return new Builder(); 299 } 300 301 /** 302 * Access to source code analysis functionality. 303 * An instance of <code>JShell</code> will always return the same 304 * <code>SourceCodeAnalysis</code> instance from 305 * <code>sourceCodeAnalysis()</code>. 306 * @return an instance of {@link SourceCodeAnalysis SourceCodeAnalysis} 307 * which can be used for source analysis such as completion detection and 308 * completion suggestions. 309 */ 310 public SourceCodeAnalysis sourceCodeAnalysis() { 311 if (sourceCodeAnalysis == null) { 312 sourceCodeAnalysis = new SourceCodeAnalysisImpl(this); 313 } 314 return sourceCodeAnalysis; 315 } 316 317 /** 318 * Evaluate the input String, including definition and/or execution, if 319 * applicable. The input is checked for errors, unless the errors can be 320 * deferred (as is the case with some unresolvedDependencies references), 321 * errors will abort evaluation. The input should be 322 * exactly one complete snippet of source code, that is, one expression, 323 * statement, variable declaration, method declaration, class declaration, 324 * or import. 325 * To break arbitrary input into individual complete snippets, use 326 * {@link SourceCodeAnalysis#analyzeCompletion(String)}. 327 * <p> 328 * For imports, the import is added. Classes, interfaces. methods, 329 * and variables are defined. The initializer of variables, statements, 330 * and expressions are executed. 331 * The modifiers public, protected, private, static, and final are not 332 * allowed on op-level declarations and are ignored with a warning. 333 * Synchronized, native, abstract, and default top-level methods are not 334 * allowed and are errors. 335 * If a previous definition of a declaration is overwritten then there will 336 * be an event showing its status changed to OVERWRITTEN, this will not 337 * occur for dropped, rejected, or already overwritten declarations. 338 * <p> 339 * The execution environment is out of process. If the evaluated code 340 * causes the execution environment to terminate, this <code>JShell</code> 341 * instance will be closed but the calling process and VM remain valid. 342 * @param input The input String to evaluate 343 * @return the list of events directly or indirectly caused by this evaluation. 344 * @throws IllegalStateException if this <code>JShell</code> instance is closed. 345 * @see SourceCodeAnalysis#analyzeCompletion(String) 346 * @see JShell#onShutdown(java.util.function.Consumer) 347 */ 348 public List<SnippetEvent> eval(String input) throws IllegalStateException { 349 checkIfAlive(); 350 List<SnippetEvent> events = eval.eval(input); 351 events.forEach(this::notifyKeyStatusEvent); 352 return Collections.unmodifiableList(events); 353 } 354 355 /** 356 * Remove a declaration from the state. 357 * @param snippet The snippet to remove 358 * @return The list of events from updating declarations dependent on the 359 * dropped snippet. 360 * @throws IllegalStateException if this <code>JShell</code> instance is closed. 361 * @throws IllegalArgumentException if the snippet is not associated with 362 * this <code>JShell</code> instance. 363 */ 364 public List<SnippetEvent> drop(PersistentSnippet snippet) throws IllegalStateException { 365 checkIfAlive(); 366 checkValidSnippet(snippet); 367 List<SnippetEvent> events = eval.drop(snippet); 368 events.forEach(this::notifyKeyStatusEvent); 369 return Collections.unmodifiableList(events); 370 } 371 372 /** 373 * The specified path is added to the end of the classpath used in eval(). 374 * Note that the unnamed package is not accessible from the package in which 375 * {@link JShell#eval()} code is placed. 376 * @param path the path to add to the classpath. 377 */ 378 public void addToClasspath(String path) { 379 taskFactory.addToClasspath(path); // Compiler 380 executionControl().commandAddToClasspath(path); // Runtime 381 } 382 383 /** 384 * Attempt to stop currently running evaluation. When called while 385 * the {@link #eval(java.lang.String) } method is running and the 386 * user's code being executed, an attempt will be made to stop user's code. 387 * Note that typically this method needs to be called from a different thread 388 * than the one running the {@code eval} method. 389 * <p> 390 * If the {@link #eval(java.lang.String) } method is not running, does nothing. 391 * <p> 392 * The attempt to stop the user's code may fail in some case, which may include 393 * when the execution is blocked on an I/O operation, or when the user's code is 394 * catching the {@link ThreadDeath} exception. 395 */ 396 public void stop() { 397 if (executionControl != null) 398 executionControl.commandStop(); 399 } 400 401 /** 402 * Close this state engine. Frees resources. Should be called when this 403 * state engine is no longer needed. 404 */ 405 @Override 406 public void close() { 407 if (!closed) { 408 closeDown(); 409 executionControl().commandExit(); 410 } 411 } 412 413 /** 414 * Return all snippets. 415 * @return the snippets for all current snippets in id order. 416 * @throws IllegalStateException if this JShell instance is closed. 417 */ 418 public List<Snippet> snippets() throws IllegalStateException { 419 checkIfAlive(); 420 return Collections.unmodifiableList(maps.snippetList()); 421 } 422 423 /** 424 * Returns the active variable snippets. 425 * This convenience method is equivalent to <code>snippets()</code> filtered for 426 * {@link jdk.jshell.Snippet.Status#isActive status(snippet).isActive} 427 * <code>&& snippet.kind() == Kind.VARIABLE</code> 428 * and cast to <code>VarSnippet</code>. 429 * @return the active declared variables. 430 * @throws IllegalStateException if this JShell instance is closed. 431 */ 432 public List<VarSnippet> variables() throws IllegalStateException { 433 return snippets().stream() 434 .filter(sn -> status(sn).isActive && sn.kind() == Snippet.Kind.VAR) 435 .map(sn -> (VarSnippet) sn) 436 .collect(collectingAndThen(toList(), Collections::unmodifiableList)); 437 } 438 439 /** 440 * Returns the active method snippets. 441 * This convenience method is equivalent to <code>snippets()</code> filtered for 442 * {@link jdk.jshell.Snippet.Status#isActive status(snippet).isActive} 443 * <code>&& snippet.kind() == Kind.METHOD</code> 444 * and cast to MethodSnippet. 445 * @return the active declared methods. 446 * @throws IllegalStateException if this JShell instance is closed. 447 */ 448 public List<MethodSnippet> methods() throws IllegalStateException { 449 return snippets().stream() 450 .filter(sn -> status(sn).isActive && sn.kind() == Snippet.Kind.METHOD) 451 .map(sn -> (MethodSnippet)sn) 452 .collect(collectingAndThen(toList(), Collections::unmodifiableList)); 453 } 454 455 /** 456 * Returns the active type declaration (class, interface, annotation type, and enum) snippets. 457 * This convenience method is equivalent to <code>snippets()</code> filtered for 458 * {@link jdk.jshell.Snippet.Status#isActive status(snippet).isActive} 459 * <code>&& snippet.kind() == Kind.TYPE_DECL</code> 460 * and cast to TypeDeclSnippet. 461 * @return the active declared type declarations. 462 * @throws IllegalStateException if this JShell instance is closed. 463 */ 464 public List<TypeDeclSnippet> types() throws IllegalStateException { 465 return snippets().stream() 466 .filter(sn -> status(sn).isActive && sn.kind() == Snippet.Kind.TYPE_DECL) 467 .map(sn -> (TypeDeclSnippet) sn) 468 .collect(collectingAndThen(toList(), Collections::unmodifiableList)); 469 } 470 471 /** 472 * Returns the active import snippets. 473 * This convenience method is equivalent to <code>snippets()</code> filtered for 474 * {@link jdk.jshell.Snippet.Status#isActive status(snippet).isActive} 475 * <code>&& snippet.kind() == Kind.IMPORT</code> 476 * and cast to ImportSnippet. 477 * @return the active declared import declarations. 478 * @throws IllegalStateException if this JShell instance is closed. 479 */ 480 public List<ImportSnippet> imports() throws IllegalStateException { 481 return snippets().stream() 482 .filter(sn -> status(sn).isActive && sn.kind() == Snippet.Kind.IMPORT) 483 .map(sn -> (ImportSnippet) sn) 484 .collect(collectingAndThen(toList(), Collections::unmodifiableList)); 485 } 486 487 /** 488 * Return the status of the snippet. 489 * This is updated either because of an explicit <code>eval()</code> call or 490 * an automatic update triggered by a dependency. 491 * @param snippet the <code>Snippet</code> to look up 492 * @return the status corresponding to this snippet 493 * @throws IllegalStateException if this <code>JShell</code> instance is closed. 494 * @throws IllegalArgumentException if the snippet is not associated with 495 * this <code>JShell</code> instance. 496 */ 497 public Status status(Snippet snippet) { 498 return checkValidSnippet(snippet).status(); 499 } 500 501 /** 502 * Return the diagnostics of the most recent evaluation of the snippet. 503 * The evaluation can either because of an explicit <code>eval()</code> call or 504 * an automatic update triggered by a dependency. 505 * @param snippet the <code>Snippet</code> to look up 506 * @return the diagnostics corresponding to this snippet. This does not 507 * include unresolvedDependencies references reported in <code>unresolvedDependencies()</code>. 508 * @throws IllegalStateException if this <code>JShell</code> instance is closed. 509 * @throws IllegalArgumentException if the snippet is not associated with 510 * this <code>JShell</code> instance. 511 */ 512 public List<Diag> diagnostics(Snippet snippet) { 513 return Collections.unmodifiableList(checkValidSnippet(snippet).diagnostics()); 514 } 515 516 /** 517 * For {@link jdk.jshell.Snippet.Status#RECOVERABLE_DEFINED RECOVERABLE_DEFINED} or 518 * {@link jdk.jshell.Snippet.Status#RECOVERABLE_NOT_DEFINED RECOVERABLE_NOT_DEFINED} 519 * declarations, the names of current unresolved dependencies for 520 * the snippet. 521 * The returned value of this method, for a given method may change when an 522 * <code>eval()</code> or <code>drop()</code> of another snippet causes 523 * an update of a dependency. 524 * @param snippet the declaration <code>Snippet</code> to look up 525 * @return the list of symbol names that are currently unresolvedDependencies. 526 * @throws IllegalStateException if this <code>JShell</code> instance is closed. 527 * @throws IllegalArgumentException if the snippet is not associated with 528 * this <code>JShell</code> instance. 529 */ 530 public List<String> unresolvedDependencies(DeclarationSnippet snippet) { 531 return Collections.unmodifiableList(checkValidSnippet(snippet).unresolved()); 532 } 533 534 /** 535 * Get the current value of a variable. 536 * @param snippet the variable Snippet whose value is queried. 537 * @return the current value of the variable referenced by snippet. 538 * @throws IllegalStateException if this <code>JShell</code> instance is closed. 539 * @throws IllegalArgumentException if the snippet is not associated with 540 * this <code>JShell</code> instance. 541 * @throws IllegalArgumentException if the variable's status is anything but 542 * {@link jdk.jshell.Snippet.Status#VALID}. 543 */ 544 public String varValue(VarSnippet snippet) throws IllegalStateException { 545 checkIfAlive(); 546 checkValidSnippet(snippet); 547 if (snippet.status() != Status.VALID) { 548 throw new IllegalArgumentException("Snippet parameter of varValue() '" + 549 snippet + "' must be VALID, it is: " + snippet.status()); 550 } 551 String value = executionControl().commandVarValue(maps.classFullName(snippet), snippet.name()); 552 return expunge(value); 553 } 554 555 /** 556 * Register a callback to be called when the Status of a snippet changes. 557 * Each call adds a new subscription. 558 * @param listener Action to perform when the Status changes. 559 * @return A token which can be used to {@linkplain JShell#unsubscribe unsubscribe} this subscription. 560 * @throws IllegalStateException if this <code>JShell</code> instance is closed. 561 */ 562 public Subscription onSnippetEvent(Consumer<SnippetEvent> listener) 563 throws IllegalStateException { 564 return onX(keyStatusListeners, listener); 565 } 566 567 /** 568 * Register a callback to be called when this JShell instance terminates. 569 * This occurs either because the client process has ended (e.g. called System.exit(0)) 570 * or the connection has been shutdown, as by close(). 571 * Each call adds a new subscription. 572 * @param listener Action to perform when the state terminates. 573 * @return A token which can be used to {@linkplain JShell#unsubscribe unsubscribe} this subscription. 574 * @throws IllegalStateException if this JShell instance is closed 575 */ 576 public Subscription onShutdown(Consumer<JShell> listener) 577 throws IllegalStateException { 578 return onX(shutdownListeners, listener); 579 } 580 581 /** 582 * Cancel a callback subscription. 583 * @param token The token corresponding to the subscription to be unsubscribed. 584 */ 585 public void unsubscribe(Subscription token) { 586 synchronized (this) { 587 token.remover.accept(token); 588 } 589 } 590 591 /** 592 * Subscription is a token for referring to subscriptions so they can 593 * be {@linkplain JShell#unsubscribe unsubscribed}. 594 */ 595 public class Subscription { 596 597 Consumer<Subscription> remover; 598 599 Subscription(Consumer<Subscription> remover) { 600 this.remover = remover; 601 } 602 } 603 604 // --- private / package-private implementation support --- 605 606 ExecutionControl executionControl() { 607 if (executionControl == null) { 608 this.executionControl = new ExecutionControl(new JDIEnv(this), maps, this); 609 try { 610 executionControl.launch(); 611 } catch (IOException ex) { 612 throw new InternalError("Launching JDI execution engine threw: " + ex.getMessage(), ex); 613 } 614 } 615 return executionControl; 616 } 617 618 void debug(int flags, String format, Object... args) { 619 if (InternalDebugControl.debugEnabled(this, flags)) { 620 err.printf(format, args); 621 } 622 } 623 624 void debug(Exception ex, String where) { 625 if (InternalDebugControl.debugEnabled(this, 0xFFFFFFFF)) { 626 err.printf("Fatal error: %s: %s\n", where, ex.getMessage()); 627 ex.printStackTrace(err); 628 } 629 } 630 631 /** 632 * Generate the next key index, indicating a unique snippet signature. 633 * @return the next key index 634 */ 635 int nextKeyIndex() { 636 return nextKeyIndex++; 637 } 638 639 private synchronized <T> Subscription onX(Map<Subscription, Consumer<T>> map, Consumer<T> listener) 640 throws IllegalStateException { 641 Objects.requireNonNull(listener); 642 checkIfAlive(); 643 Subscription token = new Subscription(map::remove); 644 map.put(token, listener); 645 return token; 646 } 647 648 private synchronized void notifyKeyStatusEvent(SnippetEvent event) { 649 keyStatusListeners.values().forEach(l -> l.accept(event)); 650 } 651 652 private synchronized void notifyShutdownEvent(JShell state) { 653 shutdownListeners.values().forEach(l -> l.accept(state)); 654 } 655 656 void closeDown() { 657 if (!closed) { 658 // Send only once 659 closed = true; 660 notifyShutdownEvent(this); 661 } 662 } 663 664 /** 665 * Check if this JShell has been closed 666 * @throws IllegalStateException if it is closed 667 */ 668 private void checkIfAlive() throws IllegalStateException { 669 if (closed) { 670 throw new IllegalStateException("JShell (" + this + ") has been closed."); 671 } 672 } 673 674 /** 675 * Check a Snippet parameter coming from the API user 676 * @param sn the Snippet to check 677 * @throws NullPointerException if Snippet parameter is null 678 * @throws IllegalArgumentException if Snippet is not from this JShell 679 * @return the input Snippet (for chained calls) 680 */ 681 private Snippet checkValidSnippet(Snippet sn) { 682 if (sn == null) { 683 throw new NullPointerException("Snippet must not be null"); 684 } else { 685 if (sn.key().state() != this) { 686 throw new IllegalArgumentException("Snippet not from this JShell"); 687 } 688 return sn; 689 } 690 } 691 692} 693