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