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