001 /*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2013
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 http://www.gnu.org/licenses.
027 */
028
029 package edu.wisc.ssec.mcidasv.jython;
030
031 import static edu.wisc.ssec.mcidasv.util.Contract.notNull;
032
033 import java.util.Collections;
034 import java.util.List;
035 import java.util.concurrent.ArrayBlockingQueue;
036 import java.util.concurrent.BlockingQueue;
037
038 import org.python.core.PyObject;
039 import org.python.core.PyStringMap;
040 import org.python.core.PySystemState;
041
042 import org.slf4j.Logger;
043 import org.slf4j.LoggerFactory;
044
045 import edu.wisc.ssec.mcidasv.jython.OutputStreamDemux.OutputType;
046
047 /**
048 * This class represents a specialized {@link Thread} that creates and executes
049 * {@link Command}s. A {@link BlockingQueue} is used to maintain thread safety
050 * and to cause a {@code Runner} to wait when the queue is at capacity or has
051 * no {@code Command}s to execute.
052 */
053 public class Runner extends Thread {
054
055 private static final Logger logger = LoggerFactory.getLogger(Runner.class);
056
057 /** The maximum number of {@link Command}s that can be queued. */
058 private static final int QUEUE_CAPACITY = 10;
059
060 /**
061 * Acts like a global output stream that redirects data to whichever
062 * {@link Console} matches the current thread name.
063 */
064 private final OutputStreamDemux STD_OUT;
065
066 /**
067 * Acts like a global error stream that redirects data to whichever
068 * {@link Console} matches the current thread name.
069 */
070 private final OutputStreamDemux STD_ERR;
071
072 /** Queue of {@link Command}s awaiting execution. */
073 private final BlockingQueue<Command> queue;
074
075 /** */
076 private final Console console;
077
078 /** */
079 private final PySystemState systemState;
080
081 /** The Jython interpreter that will actually run the queued commands. */
082 private final Interpreter interpreter;
083
084 /** Not in use yet. */
085 private boolean interrupted = false;
086
087 /**
088 *
089 *
090 * @param console
091 */
092 public Runner(final Console console) {
093 this(console, Collections.<String>emptyList());
094 }
095
096 /**
097 *
098 *
099 * @param console
100 * @param commands
101 */
102 public Runner(final Console console, final List<String> commands) {
103 notNull(console, commands);
104 this.console = console;
105 this.STD_ERR = new OutputStreamDemux();
106 this.STD_OUT = new OutputStreamDemux();
107 this.queue = new ArrayBlockingQueue<Command>(QUEUE_CAPACITY, true);
108 this.systemState = new PySystemState();
109 this.interpreter = new Interpreter(systemState, STD_OUT, STD_ERR);
110 for (String command : commands) {
111 queueLine(command);
112 }
113 }
114
115 /**
116 * Registers a new callback handler. Currently this only forwards the new
117 * handler to {@link Interpreter#setCallbackHandler(ConsoleCallback)}.
118 *
119 * @param newCallback The callback handler to register.
120 */
121 protected void setCallbackHandler(final ConsoleCallback newCallback) {
122 queueCommand(new RegisterCallbackCommand(console, newCallback));
123 }
124
125 /**
126 * Fetches, copies, and returns the {@link #interpreter}'s local namespace.
127 *
128 * @return Copy of the interpreter's local namespace.
129 */
130 protected PyStringMap copyLocals() {
131 return ((PyStringMap)interpreter.getLocals()).copy();
132 }
133
134 /**
135 * Takes commands out of the queue and executes them. We get a lot of
136 * mileage out of BlockingQueue; it's thread-safe and will block if the
137 * queue is at capacity or empty.
138 *
139 * <p>Please note that this method <b>needs</b> to be the first method that
140 * gets called after creating a {@code Runner}.
141 */
142 public void run() {
143 synchronized (this) {
144 STD_OUT.addStream(console, interpreter, OutputType.NORMAL);
145 STD_ERR.addStream(console, interpreter, OutputType.ERROR);
146 }
147 while (true) {
148 try {
149 // woohoo for BlockingQueue!!
150 Command command = queue.take();
151 command.execute(interpreter);
152 } catch (Exception e) {
153 logger.error("failed to execute", e);
154 }
155 }
156 }
157
158 /**
159 * Queues up a series of Jython statements. Currently each command is
160 * treated as though the current user just entered it; the command appears
161 * in the input along with whatever output the command generates.
162 *
163 * @param source Batched command source. Anything but null is acceptable.
164 * @param batch The actual commands to execute.
165 */
166 public void queueBatch(final String source,
167 final List<String> batch)
168 {
169 queueCommand(new BatchCommand(console, source, batch));
170 }
171
172 /**
173 * Queues up a line of Jython for execution.
174 *
175 * @param line Text of the command.
176 */
177 public void queueLine(final String line) {
178 queueCommand(new LineCommand(console, line));
179 }
180
181 /**
182 * Queues the addition of an object to {@code interpreter}'s local
183 * namespace.
184 *
185 * @param name Object name as it will appear to {@code interpreter}.
186 * @param object Object to put in {@code interpreter}'s local namespace.
187 */
188 public void queueObject(final String name, final Object object) {
189 queueCommand(new InjectCommand(console, name, object));
190 }
191
192 /**
193 * Queues the removal of an object from {@code interpreter}'s local
194 * namespace.
195 *
196 * @param name Name of the object to be removed, <i>as it appears to
197 * Jython</i>.
198 *
199 * @see Runner#queueObject(String, Object)
200 */
201 public void queueRemoval(final String name) {
202 queueCommand(new EjectCommand(console, name));
203 }
204
205 /**
206 * Queues up a Jython file to be run by {@code interpreter}.
207 *
208 * @param name {@code __name__} attribute to use for loading {@code path}.
209 * @param path The path to the Jython file.
210 */
211 public void queueFile(final String name,
212 final String path)
213 {
214 queueCommand(new LoadFileCommand(console, name, path));
215 }
216
217 /**
218 * Queues up a command for execution.
219 *
220 * @param command Command to place in the execution queue.
221 */
222 private void queueCommand(final Command command) {
223 assert command != null : command;
224 try {
225 queue.put(command);
226 } catch (InterruptedException e) {
227 logger.warn("msg='{}' command='{}'", e.getMessage(), command);
228 }
229 }
230
231 @Override public String toString() {
232 return "[Runner@" + Integer.toHexString(hashCode()) +
233 ": interpreter=" + interpreter + ", interrupted=" + interrupted +
234 ", QUEUE_CAPACITY=" + QUEUE_CAPACITY + ", queue=" + queue + "]";
235 }
236 }