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    }