001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2025 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}