001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.jython;
030
031import static java.util.Objects.requireNonNull;
032
033import java.util.Collections;
034import java.util.List;
035import java.util.concurrent.ArrayBlockingQueue;
036import java.util.concurrent.BlockingQueue;
037
038import org.python.core.PyStringMap;
039import org.python.core.PySystemState;
040
041import org.slf4j.Logger;
042import org.slf4j.LoggerFactory;
043
044import edu.wisc.ssec.mcidasv.jython.OutputStreamDemux.OutputType;
045
046/**
047 * This class represents a specialized {@link Thread} that creates and executes 
048 * {@link Command Commands}. A {@link BlockingQueue} is used to maintain
049 * thread safety and to cause a {@code Runner} to wait when the queue is at
050 * capacity or has no {@link Command Commands} to execute.
051 */
052public final class Runner extends Thread {
053
054    private static final Logger logger = LoggerFactory.getLogger(Runner.class);
055
056    /** The maximum number of {@link Command Commands} that can be queued. */
057    private static final int QUEUE_CAPACITY = 10;
058
059    /** 
060     * Acts like a global output stream that redirects data to whichever 
061     * {@link Console} matches the current thread name.
062     */
063    private final OutputStreamDemux STD_OUT;
064
065    /** 
066     * Acts like a global error stream that redirects data to whichever 
067     * {@link Console} matches the current thread name.
068     */
069    private final OutputStreamDemux STD_ERR;
070
071    /** Queue of {@link Command Commands} awaiting execution. */
072    private final BlockingQueue<Command> queue;
073
074    /** {@code Console} that created this {@code Runner} instance. */
075    private final Console console;
076
077    /** */
078    private final PySystemState systemState;
079
080    /** The Jython interpreter that will actually run the queued commands. */
081    private final Interpreter interpreter;
082
083    /** Not in use yet. */
084    private boolean interrupted = false;
085
086    /**
087     * Creates a new {@literal "console runner thread"} for a given
088     * {@link Console}.
089     * 
090     * @param console {@code Console} responsible for adding user input and
091     * displaying output. Cannot be {@code null}.
092     */
093    public Runner(final Console console) {
094        this(console, Collections.<String>emptyList());
095    }
096
097    /**
098     * Creates a new {@literal "console runner thread"} for a given
099     * {@link Console} and supplies a {@link List} of initial
100     * {@literal "commands"} to be executed.
101     *
102     * @param console {@code Console} responsible for adding user input and
103     * displaying output. Cannot be {@code null}.
104     * @param commands Lines to be run right away. Cannot be {@code null}.
105     */
106    public Runner(final Console console, final List<String> commands) {
107        requireNonNull(console);
108        requireNonNull(commands);
109        this.console = console;
110        this.STD_ERR = new OutputStreamDemux();
111        this.STD_OUT = new OutputStreamDemux();
112        this.queue = new ArrayBlockingQueue<>(QUEUE_CAPACITY, true);
113        this.systemState = new PySystemState();
114        this.interpreter = new Interpreter(systemState, STD_OUT, STD_ERR);
115        for (String command : commands) {
116            queueLine(command);
117        }
118    }
119
120    /**
121     * Registers a new callback handler. Currently this only forwards the new
122     * handler to {@link Interpreter#setCallbackHandler(ConsoleCallback)}.
123     * 
124     * @param newCallback The callback handler to register.
125     */
126    void setCallbackHandler(final ConsoleCallback newCallback) {
127        queueCommand(new RegisterCallbackCommand(console, newCallback));
128    }
129
130    /**
131     * Fetches, copies, and returns the {@link #interpreter}'s local namespace.
132     * 
133     * @return Copy of the interpreter's local namespace.
134     */
135    PyStringMap copyLocals() {
136        return ((PyStringMap)interpreter.getLocals()).copy();
137    }
138
139    /**
140     * Takes commands out of the queue and executes them. We get a lot of 
141     * mileage out of BlockingQueue; it's thread-safe and will block if the 
142     * queue is at capacity or empty.
143     * 
144     * <p>Please note that this method <b>needs</b> to be the first method that
145     * gets called after creating a {@code Runner}.
146     */
147    @Override public void run() {
148        synchronized (this) {
149            STD_OUT.addStream(console, interpreter, OutputType.NORMAL);
150            STD_ERR.addStream(console, interpreter, OutputType.ERROR);
151        }
152        while (true) {
153            try {
154                // woohoo for BlockingQueue!!
155                Command command = queue.take();
156                command.execute(interpreter);
157            } catch (Exception e) {
158                logger.error("failed to execute", e);
159            }
160        }
161    }
162
163    /**
164     * Queues up a series of Jython statements. Currently each command is 
165     * treated as though the current user just entered it; the command appears
166     * in the input along with whatever output the command generates.
167     * 
168     * @param source Batched command source. Anything but null is acceptable.
169     * @param batch The actual commands to execute.
170     */
171    public void queueBatch(final String source, final List<String> batch) {
172        queueCommand(new BatchCommand(console, source, batch));
173    }
174
175    /**
176     * Queues up a line of Jython for execution.
177     * 
178     * @param line Text of the command.
179     */
180    public void queueLine(final String line) {
181        queueCommand(new LineCommand(console, line));
182    }
183
184    /**
185     * Queues the addition of an object to {@code interpreter}'s local 
186     * namespace.
187     *
188     * @param name Object name as it will appear to {@code interpreter}.
189     * @param object Object to put in {@code interpreter}'s local namespace.
190     */
191    public void queueObject(final String name, final Object object) {
192        queueCommand(new InjectCommand(console, name, object));
193    }
194
195    /**
196     * Queues the removal of an object from {@code interpreter}'s local 
197     * namespace. 
198     * 
199     * @param name Name of the object to be removed, <i>as it appears to
200     * Jython</i>.
201     * 
202     * @see Runner#queueObject(String, Object)
203     */
204    public void queueRemoval(final String name) {
205        queueCommand(new EjectCommand(console, name));
206    }
207
208    /**
209     * Queues up a Jython file to be run by {@code interpreter}.
210     *
211     * @param name {@code __name__} attribute to use for loading {@code path}.
212     * @param path The path to the Jython file.
213     */
214    public void queueFile(final String name, final String path) {
215        queueCommand(new LoadFileCommand(console, name, path));
216    }
217
218    /**
219     * Queues up a command for execution.
220     * 
221     * @param command Command to place in the execution queue.
222     */
223    private void queueCommand(final Command command) {
224        assert command != null : command;
225        try {
226            queue.put(command);
227        } catch (InterruptedException e) {
228            logger.warn("msg='{}' command='{}'", e.getMessage(), command);
229        }
230    }
231
232    @Override public String toString() {
233        return "[Runner@" + Integer.toHexString(hashCode()) + 
234            ": interpreter=" + interpreter + ", interrupted=" + interrupted +
235            ", QUEUE_CAPACITY=" + QUEUE_CAPACITY + ", queue=" + queue + "]"; 
236    }
237}