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