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