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 java.io.ByteArrayInputStream;
  29 import java.io.IOException;
  30 import java.io.InputStream;
  31 import java.io.PrintStream;
  32 import java.util.ArrayList;
  33 import java.util.Collections;
  34 import java.util.HashMap;
  35 import java.util.List;
  36 import java.util.Map;
  37 import java.util.Objects;
  38 import java.util.function.BiFunction;
  39 import java.util.function.Consumer;
  40 
  41 import java.util.function.Supplier;
  42 import jdk.internal.jshell.debug.InternalDebugControl;
  43 import static java.util.stream.Collectors.collectingAndThen;
  44 import static java.util.stream.Collectors.toList;
  45 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT;
  46 import static jdk.jshell.Util.expunge;
  47 import jdk.jshell.Snippet.Status;
  48 
  49 /**
  50  * The JShell evaluation state engine.  This is the central class in the JShell
  51  * API.  A <code>JShell</code> instance holds the evolving compilation and
  52  * execution state.  The state is changed with the instance methods
  53  * {@link jdk.jshell.JShell#eval(java.lang.String) eval(String)},
  54  * {@link jdk.jshell.JShell#drop(jdk.jshell.PersistentSnippet) drop(PersistentSnippet)} and
  55  * {@link jdk.jshell.JShell#addToClasspath(java.lang.String) addToClasspath(String)}.
  56  * The majority of methods query the state.
  57  * A <code>JShell</code> instance also allows registering for events with
  58  * {@link jdk.jshell.JShell#onSnippetEvent(java.util.function.Consumer) onSnippetEvent(Consumer)}
  59  * and {@link jdk.jshell.JShell#onShutdown(java.util.function.Consumer) onShutdown(Consumer)}, which
  60  * are unregistered with
  61  * {@link jdk.jshell.JShell#unsubscribe(jdk.jshell.JShell.Subscription) unsubscribe(Subscription)}.
  62  * Access to the source analysis utilities is via
  63  * {@link jdk.jshell.JShell#sourceCodeAnalysis()}.
  64  * When complete the instance should be closed to free resources --
  65  * {@link jdk.jshell.JShell#close()}.
  66  * <p>
  67  * An instance of <code>JShell</code> is created with
  68  * <code>JShell.create()</code>.
  69  * <p>
  70  * This class is not thread safe, except as noted, all access should be through
  71  * a single thread.
  72  * @see jdk.jshell
  73  * @author Robert Field
  74  */
  75 public class JShell implements AutoCloseable {
  76 
  77     final SnippetMaps maps;
  78     final KeyMap keyMap;
  79     final TaskFactory taskFactory;
  80     final InputStream in;
  81     final PrintStream out;
  82     final PrintStream err;
  83     final Supplier<String> tempVariableNameGenerator;
  84     final BiFunction<Snippet, Integer, String> idGenerator;
  85 
  86     private int nextKeyIndex = 1;
  87 
  88     final Eval eval;
  89     final ClassTracker classTracker;
  90     private final Map<Subscription, Consumer<JShell>> shutdownListeners = new HashMap<>();
  91     private final Map<Subscription, Consumer<SnippetEvent>> keyStatusListeners = new HashMap<>();
  92     private boolean closed = false;
  93 
  94 
  95     private ExecutionControl executionControl = null;
  96     private SourceCodeAnalysisImpl 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         if (sourceCodeAnalysis != null) {
 382             sourceCodeAnalysis.classpathChanged();
 383         }
 384     }
 385 
 386     /**
 387      * Attempt to stop currently running evaluation. When called while
 388      * the {@link #eval(java.lang.String) } method is running and the
 389      * user's code being executed, an attempt will be made to stop user's code.
 390      * Note that typically this method needs to be called from a different thread
 391      * than the one running the {@code eval} method.
 392      * <p>
 393      * If the {@link #eval(java.lang.String) } method is not running, does nothing.
 394      * <p>
 395      * The attempt to stop the user's code may fail in some case, which may include
 396      * when the execution is blocked on an I/O operation, or when the user's code is
 397      * catching the {@link ThreadDeath} exception.
 398      */
 399     public void stop() {
 400         if (executionControl != null)
 401             executionControl.commandStop();
 402     }
 403 
 404     /**
 405      * Close this state engine. Frees resources. Should be called when this
 406      * state engine is no longer needed.
 407      */
 408     @Override
 409     public void close() {
 410         if (!closed) {
 411             closeDown();
 412             executionControl().commandExit();
 413         }
 414     }
 415 
 416     /**
 417      * Return all snippets.
 418      * @return the snippets for all current snippets in id order.
 419      * @throws IllegalStateException if this JShell instance is closed.
 420      */
 421     public List<Snippet> snippets() throws IllegalStateException {
 422         checkIfAlive();
 423         return Collections.unmodifiableList(maps.snippetList());
 424     }
 425 
 426     /**
 427      * Returns the active variable snippets.
 428      * This convenience method is equivalent to <code>snippets()</code> filtered for
 429      * {@link jdk.jshell.Snippet.Status#isActive status(snippet).isActive}
 430      * <code>&amp;&amp; snippet.kind() == Kind.VARIABLE</code>
 431      * and cast to <code>VarSnippet</code>.
 432      * @return the active declared variables.
 433      * @throws IllegalStateException if this JShell instance is closed.
 434      */
 435     public List<VarSnippet> variables() throws IllegalStateException {
 436         return snippets().stream()
 437                      .filter(sn -> status(sn).isActive && sn.kind() == Snippet.Kind.VAR)
 438                      .map(sn -> (VarSnippet) sn)
 439                      .collect(collectingAndThen(toList(), Collections::unmodifiableList));
 440     }
 441 
 442     /**
 443      * Returns the active method snippets.
 444      * This convenience method is equivalent to <code>snippets()</code> filtered for
 445      * {@link jdk.jshell.Snippet.Status#isActive status(snippet).isActive}
 446      * <code>&amp;&amp; snippet.kind() == Kind.METHOD</code>
 447      * and cast to MethodSnippet.
 448      * @return the active declared methods.
 449      * @throws IllegalStateException if this JShell instance is closed.
 450      */
 451     public List<MethodSnippet> methods() throws IllegalStateException {
 452         return snippets().stream()
 453                      .filter(sn -> status(sn).isActive && sn.kind() == Snippet.Kind.METHOD)
 454                      .map(sn -> (MethodSnippet)sn)
 455                      .collect(collectingAndThen(toList(), Collections::unmodifiableList));
 456     }
 457 
 458     /**
 459      * Returns the active type declaration (class, interface, annotation type, and enum) snippets.
 460      * This convenience method is equivalent to <code>snippets()</code> filtered for
 461      * {@link jdk.jshell.Snippet.Status#isActive status(snippet).isActive}
 462      * <code>&amp;&amp; snippet.kind() == Kind.TYPE_DECL</code>
 463      * and cast to TypeDeclSnippet.
 464      * @return the active declared type declarations.
 465      * @throws IllegalStateException if this JShell instance is closed.
 466      */
 467     public List<TypeDeclSnippet> types() throws IllegalStateException {
 468         return snippets().stream()
 469                 .filter(sn -> status(sn).isActive && sn.kind() == Snippet.Kind.TYPE_DECL)
 470                 .map(sn -> (TypeDeclSnippet) sn)
 471                 .collect(collectingAndThen(toList(), Collections::unmodifiableList));
 472     }
 473 
 474     /**
 475      * Returns the active import snippets.
 476      * This convenience method is equivalent to <code>snippets()</code> filtered for
 477      * {@link jdk.jshell.Snippet.Status#isActive status(snippet).isActive}
 478      * <code>&amp;&amp; snippet.kind() == Kind.IMPORT</code>
 479      * and cast to ImportSnippet.
 480      * @return the active declared import declarations.
 481      * @throws IllegalStateException if this JShell instance is closed.
 482      */
 483     public List<ImportSnippet> imports() throws IllegalStateException {
 484         return snippets().stream()
 485                 .filter(sn -> status(sn).isActive && sn.kind() == Snippet.Kind.IMPORT)
 486                 .map(sn -> (ImportSnippet) sn)
 487                 .collect(collectingAndThen(toList(), Collections::unmodifiableList));
 488     }
 489 
 490     /**
 491      * Return the status of the snippet.
 492      * This is updated either because of an explicit <code>eval()</code> call or
 493      * an automatic update triggered by a dependency.
 494      * @param snippet the <code>Snippet</code> to look up
 495      * @return the status corresponding to this snippet
 496      * @throws IllegalStateException if this <code>JShell</code> instance is closed.
 497      * @throws IllegalArgumentException if the snippet is not associated with
 498      * this <code>JShell</code> instance.
 499      */
 500     public Status status(Snippet snippet) {
 501         return checkValidSnippet(snippet).status();
 502     }
 503 
 504     /**
 505      * Return the diagnostics of the most recent evaluation of the snippet.
 506      * The evaluation can either because of an explicit <code>eval()</code> call or
 507      * an automatic update triggered by a dependency.
 508      * @param snippet the <code>Snippet</code> to look up
 509      * @return the diagnostics corresponding to this snippet.  This does not
 510      * include unresolvedDependencies references reported in <code>unresolvedDependencies()</code>.
 511      * @throws IllegalStateException if this <code>JShell</code> instance is closed.
 512      * @throws IllegalArgumentException if the snippet is not associated with
 513      * this <code>JShell</code> instance.
 514      */
 515     public List<Diag> diagnostics(Snippet snippet) {
 516         return Collections.unmodifiableList(checkValidSnippet(snippet).diagnostics());
 517     }
 518 
 519     /**
 520      * For {@link jdk.jshell.Snippet.Status#RECOVERABLE_DEFINED RECOVERABLE_DEFINED} or
 521      * {@link jdk.jshell.Snippet.Status#RECOVERABLE_NOT_DEFINED RECOVERABLE_NOT_DEFINED}
 522      * declarations, the names of current unresolved dependencies for
 523      * the snippet.
 524      * The returned value of this method, for a given method may change when an
 525      * <code>eval()</code> or <code>drop()</code> of another snippet causes
 526      * an update of a dependency.
 527      * @param snippet the declaration <code>Snippet</code> to look up
 528      * @return the list of symbol names that are currently unresolvedDependencies.
 529      * @throws IllegalStateException if this <code>JShell</code> instance is closed.
 530      * @throws IllegalArgumentException if the snippet is not associated with
 531      * this <code>JShell</code> instance.
 532      */
 533     public List<String> unresolvedDependencies(DeclarationSnippet snippet) {
 534         return Collections.unmodifiableList(checkValidSnippet(snippet).unresolved());
 535     }
 536 
 537     /**
 538      * Get the current value of a variable.
 539      * @param snippet the variable Snippet whose value is queried.
 540      * @return the current value of the variable referenced by snippet.
 541      * @throws IllegalStateException if this <code>JShell</code> instance is closed.
 542      * @throws IllegalArgumentException if the snippet is not associated with
 543      * this <code>JShell</code> instance.
 544      * @throws IllegalArgumentException if the variable's status is anything but
 545      * {@link jdk.jshell.Snippet.Status#VALID}.
 546      */
 547     public String varValue(VarSnippet snippet) throws IllegalStateException {
 548         checkIfAlive();
 549         checkValidSnippet(snippet);
 550         if (snippet.status() != Status.VALID) {
 551             throw new IllegalArgumentException("Snippet parameter of varValue() '" +
 552                     snippet + "' must be VALID, it is: " + snippet.status());
 553         }
 554         String value = executionControl().commandVarValue(maps.classFullName(snippet), snippet.name());
 555         return expunge(value);
 556     }
 557 
 558     /**
 559      * Register a callback to be called when the Status of a snippet changes.
 560      * Each call adds a new subscription.
 561      * @param listener Action to perform when the Status changes.
 562      * @return A token which can be used to {@linkplain JShell#unsubscribe unsubscribe} this subscription.
 563      * @throws IllegalStateException if this <code>JShell</code> instance is closed.
 564      */
 565     public Subscription onSnippetEvent(Consumer<SnippetEvent> listener)
 566             throws IllegalStateException {
 567         return onX(keyStatusListeners, listener);
 568     }
 569 
 570     /**
 571      * Register a callback to be called when this JShell instance terminates.
 572      * This occurs either because the client process has ended (e.g. called System.exit(0))
 573      * or the connection has been shutdown, as by close().
 574      * Each call adds a new subscription.
 575      * @param listener Action to perform when the state terminates.
 576      * @return A token which can be used to {@linkplain JShell#unsubscribe unsubscribe} this subscription.
 577      * @throws IllegalStateException if this JShell instance is closed
 578      */
 579     public Subscription onShutdown(Consumer<JShell> listener)
 580             throws IllegalStateException {
 581         return onX(shutdownListeners, listener);
 582     }
 583 
 584     /**
 585      * Cancel a callback subscription.
 586      * @param token The token corresponding to the subscription to be unsubscribed.
 587      */
 588     public void unsubscribe(Subscription token) {
 589         synchronized (this) {
 590             token.remover.accept(token);
 591         }
 592     }
 593 
 594     /**
 595      * Subscription is a token for referring to subscriptions so they can
 596      * be {@linkplain JShell#unsubscribe unsubscribed}.
 597      */
 598     public class Subscription {
 599 
 600         Consumer<Subscription> remover;
 601 
 602         Subscription(Consumer<Subscription> remover) {
 603             this.remover = remover;
 604         }
 605     }
 606 
 607     // --- private / package-private implementation support ---
 608 
 609     ExecutionControl executionControl() {
 610         if (executionControl == null) {
 611             this.executionControl = new ExecutionControl(new JDIEnv(this), maps, this);
 612             try {
 613                 executionControl.launch();
 614             } catch (IOException ex) {
 615                 throw new InternalError("Launching JDI execution engine threw: " + ex.getMessage(), ex);
 616             }
 617         }
 618         return executionControl;
 619     }
 620 
 621     void debug(int flags, String format, Object... args) {
 622         if (InternalDebugControl.debugEnabled(this, flags)) {
 623             err.printf(format, args);
 624         }
 625     }
 626 
 627     void debug(Exception ex, String where) {
 628         if (InternalDebugControl.debugEnabled(this, 0xFFFFFFFF)) {
 629             err.printf("Fatal error: %s: %s\n", where, ex.getMessage());
 630             ex.printStackTrace(err);
 631         }
 632     }
 633 
 634     /**
 635      * Generate the next key index, indicating a unique snippet signature.
 636      * @return the next key index
 637      */
 638     int nextKeyIndex() {
 639         return nextKeyIndex++;
 640     }
 641 
 642     private synchronized <T> Subscription onX(Map<Subscription, Consumer<T>> map, Consumer<T> listener)
 643             throws IllegalStateException {
 644         Objects.requireNonNull(listener);
 645         checkIfAlive();
 646         Subscription token = new Subscription(map::remove);
 647         map.put(token, listener);
 648         return token;
 649     }
 650 
 651     private synchronized void notifyKeyStatusEvent(SnippetEvent event) {
 652         keyStatusListeners.values().forEach(l -> l.accept(event));
 653     }
 654 
 655     private synchronized void notifyShutdownEvent(JShell state) {
 656         shutdownListeners.values().forEach(l -> l.accept(state));
 657     }
 658 
 659     void closeDown() {
 660         if (!closed) {
 661             // Send only once
 662             closed = true;
 663             notifyShutdownEvent(this);
 664         }
 665     }
 666 
 667     /**
 668      * Check if this JShell has been closed
 669      * @throws IllegalStateException if it is closed
 670      */
 671     private void checkIfAlive()  throws IllegalStateException {
 672         if (closed) {
 673             throw new IllegalStateException("JShell (" + this + ") has been closed.");
 674         }
 675     }
 676 
 677     /**
 678      * Check a Snippet parameter coming from the API user
 679      * @param sn the Snippet to check
 680      * @throws NullPointerException if Snippet parameter is null
 681      * @throws IllegalArgumentException if Snippet is not from this JShell
 682      * @return the input Snippet (for chained calls)
 683      */
 684     private Snippet checkValidSnippet(Snippet sn) {
 685         if (sn == null) {
 686             throw new NullPointerException("Snippet must not be null");
 687         } else {
 688             if (sn.key().state() != this) {
 689                 throw new IllegalArgumentException("Snippet not from this JShell");
 690             }
 691             return sn;
 692         }
 693     }
 694 
 695 }