001/* A tableau for evaluating Exec expression interactively.
002
003 Copyright (c) 2003-2014 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026 */
027package ptolemy.actor.gui.exec;
028
029import java.awt.BorderLayout;
030import java.io.BufferedReader;
031import java.io.IOException;
032import java.io.InputStream;
033import java.io.InputStreamReader;
034import java.net.URL;
035
036import javax.swing.BoxLayout;
037import javax.swing.JPanel;
038
039import ptolemy.actor.gui.Effigy;
040import ptolemy.actor.gui.Tableau;
041import ptolemy.actor.gui.TableauFactory;
042import ptolemy.actor.gui.TableauFrame;
043import ptolemy.gui.ShellInterpreter;
044import ptolemy.gui.ShellTextArea;
045import ptolemy.kernel.util.IllegalActionException;
046import ptolemy.kernel.util.NameDuplicationException;
047import ptolemy.kernel.util.NamedObj;
048import ptolemy.util.StringUtilities;
049
050///////////////////////////////////////////////////////////////////
051//// ExecShellTableau
052
053/**
054 A tableau that provides a Exec Shell for interacting with the Bash shell.
055
056 @author Christopher Hylands and Edward A. Lee
057 @version $Id$
058 @since Ptolemy II 3.0
059 @Pt.ProposedRating Red (cxh)
060 @Pt.AcceptedRating Red (cxh)
061 */
062public class ExecShellTableau extends Tableau implements ShellInterpreter {
063    /** Create a new tableau.
064     *  The tableau is itself an entity contained by the effigy
065     *  and having the specified name.  The frame is not made visible
066     *  automatically.  You must call show() to make it visible.
067     *  @param container The containing effigy.
068     *  @param name The name of this tableau within the specified effigy.
069     *  @exception IllegalActionException If the tableau is not acceptable
070     *   to the specified container.
071     *  @exception NameDuplicationException If the container already contains
072     *   an entity with the specified name.
073     */
074    public ExecShellTableau(ExecShellEffigy container, String name)
075            throws IllegalActionException, NameDuplicationException {
076        super(container, name);
077        frame = new ExecShellFrame(this);
078        setFrame(frame);
079
080        try {
081            _interpreter = Runtime.getRuntime().exec("bash -i");
082        } catch (IOException ex) {
083            throw new IllegalActionException(this, ex,
084                    "Failed to create Process");
085        }
086    }
087
088    ///////////////////////////////////////////////////////////////////
089    ////                         public methods                    ////
090
091    /** Evaluate the specified command.
092     *  @param command The command.
093     *  @return The return value of the command, or null if there is none.
094     *  @exception Exception If something goes wrong processing the command.
095     */
096    @Override
097    public String evaluateCommand(String command) throws Exception {
098        _executeCommand(command);
099
100        // FIXME: this is _so_ wrong
101        return "";
102    }
103
104    /** Return true if the specified command is complete (ready
105     *  to be interpreted).
106     *  @param command The command.
107     *  @return True
108     */
109    @Override
110    public boolean isCommandComplete(String command) {
111        return true;
112    }
113
114    /** Append the text message to text area.
115     *  The output automatically gets a trailing newline appended.
116     *  @param text The test to be appended.
117     */
118    public void stderr( /*final*/String text) {
119        frame.shellTextArea.appendJTextArea(text + "\n");
120    }
121
122    /** Append the text message to the text area.
123     *  The output automatically gets a trailing newline appended.
124     *  @param text The test to be appended.
125     */
126    public void stdout(final String text) {
127        frame.shellTextArea.appendJTextArea(text + "\n");
128    }
129
130    ///////////////////////////////////////////////////////////////////
131    ////                         public variables                  ////
132
133    /** The frame in which text is written. */
134    public ExecShellFrame frame;
135
136    ///////////////////////////////////////////////////////////////////
137    ////                         private variables                 ////
138    // The Exec interpreter
139    // FIXME: Perhaps the interpreter should be in its own thread?
140    private Process _interpreter;
141
142    ///////////////////////////////////////////////////////////////////
143    ////                         inner classes                     ////
144
145    /** The frame that is created by an instance of ExecShellTableau.
146     */
147    @SuppressWarnings("serial")
148    public static class ExecShellFrame extends TableauFrame {
149        // FindBugs suggested refactoring this into a static class.
150
151        /** Construct a frame to display the ExecShell window.
152         *  After constructing this, it is necessary
153         *  to call setVisible(true) to make the frame appear.
154         *  This is typically accomplished by calling show() on
155         *  enclosing tableau.
156         *  @param execShellTableau The tableau responsible for this frame.
157         *  @exception IllegalActionException If the model rejects the
158         *   configuration attribute.
159         *  @exception NameDuplicationException If a name collision occurs.
160         */
161        public ExecShellFrame(ExecShellTableau execShellTableau)
162                throws IllegalActionException, NameDuplicationException {
163            super(execShellTableau);
164
165            JPanel component = new JPanel();
166            component.setLayout(new BoxLayout(component, BoxLayout.Y_AXIS));
167
168            shellTextArea = new ShellTextArea();
169            shellTextArea.setInterpreter(execShellTableau);
170            shellTextArea.mainPrompt = "% ";
171            component.add(shellTextArea);
172            getContentPane().add(component, BorderLayout.CENTER);
173        }
174
175        ///////////////////////////////////////////////////////////////////
176        ////                         public variables                  ////
177
178        /** The text area tableau used for input and output. */
179        public ShellTextArea shellTextArea;
180
181        ///////////////////////////////////////////////////////////////////
182        ////                         protected methods                 ////
183        @Override
184        protected void _help() {
185            try {
186                URL doc = getClass().getClassLoader()
187                        .getResource("ptolemy/actor/gui/ptjacl/help.htm");
188                getConfiguration().openModel(null, doc, doc.toExternalForm());
189            } catch (Exception ex) {
190                System.out.println("ExecShellTableau._help(): " + ex);
191                _about();
192            }
193        }
194    }
195
196    /** A factory that creates a control panel to display a Exec Shell.
197     */
198    public static class Factory extends TableauFactory {
199        /** Create a factory with the given name and container.
200         *  @param container The container.
201         *  @param name The name.
202         *  @exception IllegalActionException If the container is incompatible
203         *   with this attribute.
204         *  @exception NameDuplicationException If the name coincides with
205         *   an attribute already in the container.
206         */
207        public Factory(NamedObj container, String name)
208                throws IllegalActionException, NameDuplicationException {
209            super(container, name);
210        }
211
212        ///////////////////////////////////////////////////////////////////
213        ////                         public methods                    ////
214
215        /** Create a new instance of ExecShellTableau in the specified
216         *  effigy. It is the responsibility of callers of
217         *  this method to check the return value and call show().
218         *  @param effigy The model effigy.
219         *  @return A new control panel tableau if the effigy is
220         *    a PtolemyEffigy, or null otherwise.
221         *  @exception Exception If the factory should be able to create a
222         *   tableau for the effigy, but something goes wrong.
223         */
224        @Override
225        public Tableau createTableau(Effigy effigy) throws Exception {
226            // NOTE: Can create any number of tableaux within the same
227            // effigy.  Is this what we want?
228            if (effigy instanceof ExecShellEffigy) {
229                return new ExecShellTableau((ExecShellEffigy) effigy,
230                        "ExecShellTableau");
231            } else {
232                return null;
233            }
234        }
235    }
236
237    ///////////////////////////////////////////////////////////////////
238    ////                         private methods                   ////
239    // Execute the command.  Update the output with
240    // the command being run and the output.
241    private String _executeCommand(String command) {
242        if (command == null || command.length() == 0) {
243            return "";
244        }
245
246        try {
247            Runtime runtime = Runtime.getRuntime();
248
249            try {
250                //if (_process != null) {
251                //    _process.destroy();
252                //}
253                // Preprocess by removing lines that begin with '#'
254                // and converting substrings that begin and end
255                // with double quotes into one array element.
256                final String[] commandTokens = StringUtilities
257                        .tokenizeForExec(command);
258
259                //stdout("About to execute:\n");
260                StringBuffer statusCommand = new StringBuffer();
261
262                for (String commandToken : commandTokens) {
263                    //stdout("        " + commandTokens[i]);
264                    // Accumulate the first 50 chars for use in
265                    // the status buffer.
266                    if (statusCommand.length() < 50) {
267                        if (statusCommand.length() > 0) {
268                            statusCommand.append(" ");
269                        }
270
271                        statusCommand.append(commandToken);
272                    }
273                }
274
275                if (statusCommand.length() >= 50) {
276                    statusCommand.append(" . . .");
277                }
278
279                //updateStatusBar("Executing: "
280                //        + statusCommand.toString());
281                _interpreter = runtime.exec(commandTokens);
282
283                // Set up a Thread to read in any error messages
284                _StreamReaderThread errorGobbler = new _StreamReaderThread(
285                        _interpreter.getErrorStream(), this);
286
287                // Set up a Thread to read in any output messages
288                _StreamReaderThread outputGobbler = new _StreamReaderThread(
289                        _interpreter.getInputStream(), this);
290
291                // Start up the Threads
292                errorGobbler.start();
293                outputGobbler.start();
294
295                try {
296                    /*int processReturnCode = */_interpreter.waitFor();
297
298                    synchronized (this) {
299                        _interpreter = null;
300                    }
301
302                    //if (processReturnCode != 0) break;
303                } catch (InterruptedException interrupted) {
304                    stderr("InterruptedException: " + interrupted);
305                    throw interrupted;
306                }
307            } catch (final IOException io) {
308                stderr("IOException: " + ptolemy.kernel.util.KernelException
309                        .stackTraceToString(io));
310            }
311        } catch (InterruptedException e) {
312            //_interpreter.destroy();
313            return "Interrupted"; // SwingWorker.get() returns this
314        }
315
316        return "All Done"; // or this
317    }
318
319    ///////////////////////////////////////////////////////////////////
320    ////                         inner classes                     ////
321    // Private class that reads a stream in a thread and updates the
322    // JTextArea.
323    private static class _StreamReaderThread extends Thread {
324        // FindBugs suggests making this class static so as to decrease
325        // the size of instances and avoid dangling references.
326
327        _StreamReaderThread(InputStream inputStream,
328                ExecShellTableau execShellTableau) {
329            _inputStream = inputStream;
330            _execShellTableau = execShellTableau;
331        }
332
333        // Read lines from the _inputStream and output them.
334        @Override
335        public void run() {
336            try {
337                InputStreamReader inputStreamReader = new InputStreamReader(
338                        _inputStream);
339                BufferedReader bufferedReader = new BufferedReader(
340                        inputStreamReader);
341                String line = null;
342
343                while ((line = bufferedReader.readLine()) != null) {
344                    _execShellTableau.stdout( /*_streamType + ">" +*/
345                            line);
346                }
347            } catch (IOException ioe) {
348                _execShellTableau.stderr("IOException: " + ioe);
349            }
350        }
351
352        // Stream to read from.
353        private InputStream _inputStream;
354
355        private ExecShellTableau _execShellTableau;
356    }
357}