001/* A JTextArea that takes a list of commands and runs them.
002
003 Copyright (c) 2001-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.gui;
028
029import java.awt.Color;
030import java.awt.event.ActionEvent;
031import java.awt.event.ActionListener;
032import java.awt.event.WindowAdapter;
033import java.awt.event.WindowEvent;
034import java.awt.event.WindowListener;
035import java.io.BufferedReader;
036import java.io.File;
037import java.io.IOException;
038import java.io.InputStream;
039import java.io.InputStreamReader;
040import java.util.Iterator;
041import java.util.LinkedList;
042import java.util.List;
043import java.util.Map;
044import java.util.concurrent.CancellationException;
045import java.util.concurrent.ExecutionException;
046import java.util.concurrent.TimeUnit;
047import java.util.concurrent.TimeoutException;
048
049import javax.swing.BorderFactory;
050import javax.swing.BoxLayout;
051import javax.swing.JButton;
052import javax.swing.JComponent;
053import javax.swing.JFrame;
054import javax.swing.JLabel;
055import javax.swing.JPanel;
056import javax.swing.JProgressBar;
057import javax.swing.JScrollPane;
058import javax.swing.JTextArea;
059import javax.swing.SwingConstants;
060import javax.swing.SwingUtilities;
061import javax.swing.SwingWorker;
062import javax.swing.border.Border;
063
064import ptolemy.util.ExecuteCommands;
065import ptolemy.util.StreamExec;
066import ptolemy.util.StringUtilities;
067
068/** Execute commands in a subprocess and display them in a JTextArea.
069
070 <p>As an alternative to this class, see
071 {@link ptolemy.util.StringBufferExec}, which writes to a StringBuffer,
072 and
073 {@link ptolemy.util.StreamExec}, which writes to stderr and stdout.
074
075 <p>Loosely based on Example1.java from
076 <a href="http://java.sun.com/products/jfc/tsc/articles/threads/threads2.html">http://java.sun.com/products/jfc/tsc/articles/threads/threads2.html</a>
077 <p>See also
078 <a href="http://developer.java.sun.com/developer/qow/archive/135/index.jsp">http://developer.java.sun.com/developer/qow/archive/135/index.jsp</a> <i>(1/11: Broken)</i>
079 and
080 <a href="http://jw.itworld.com/javaworld/jw-12-2000/jw-1229-traps.html">http://jw.itworld.com/javaworld/jw-12-2000/jw-1229-traps.html</a>.</p>
081
082 @see ptolemy.util.StringBufferExec
083 @see ptolemy.util.StreamExec
084
085 @author Christopher Hylands
086 @version $Id$
087 @since Ptolemy II 2.0
088 @Pt.ProposedRating Red (cxh)
089 @Pt.AcceptedRating Red (cxh)
090
091 */
092@SuppressWarnings("serial")
093public class JTextAreaExec extends JPanel implements ExecuteCommands {
094    /** Create the JTextArea, progress bar, status text field and
095     *  optionally Start, Cancel and Clear buttons.
096     *
097     *  @param name A String containing the name to label the JTextArea
098     *  with.
099     *  @param showButtons True if the Start, Cancel and Clear buttons
100     *  should be  made visible.
101     */
102    public JTextAreaExec(String name, boolean showButtons) {
103        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
104
105        _jTextArea = new JTextArea("", 20, 100);
106        _jTextArea.setEditable(false);
107
108        JScrollPane jScrollPane = new JScrollPane(_jTextArea);
109        add(jScrollPane);
110
111        setBorder(BorderFactory.createTitledBorder(
112                BorderFactory.createLineBorder(Color.black), name));
113
114        _progressBar = new JProgressBar();
115
116        _startButton = new JButton("Start");
117        _startButton.addActionListener(_startListener);
118        _enableStartButton();
119
120        _cancelButton = new JButton("Cancel");
121        _cancelButton.addActionListener(_interruptListener);
122        _cancelButton.setEnabled(false);
123
124        _clearButton = new JButton("Clear");
125        _clearButton.addActionListener(_clearListener);
126        _clearButton.setEnabled(true);
127
128        Border spaceBelow = BorderFactory.createEmptyBorder(0, 0, 5, 0);
129
130        if (showButtons) {
131            JComponent buttonBox = new JPanel();
132            buttonBox.add(_startButton);
133            buttonBox.add(_cancelButton);
134            buttonBox.add(_clearButton);
135
136            setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
137            add(buttonBox);
138            buttonBox.setBorder(spaceBelow);
139            _statusBar = new JLabel("Click Start to begin",
140                    SwingConstants.CENTER);
141        } else {
142            _statusBar = new JLabel("", SwingConstants.CENTER);
143        }
144
145        add(_progressBar);
146        add(_statusBar);
147        _statusBar.setAlignmentX(CENTER_ALIGNMENT);
148
149        Border progressBarBorder = _progressBar.getBorder();
150        _progressBar.setBorder(BorderFactory.createCompoundBorder(spaceBelow,
151                progressBarBorder));
152    }
153
154    ///////////////////////////////////////////////////////////////////
155    ////                         public methods                    ////
156
157    /** Append the text message to the JTextArea and include a trailing
158     *  newline.
159     *  @param text The text message to be appended.
160     */
161    public void appendJTextArea(final String text) {
162        Runnable doAppendJTextArea = new Runnable() {
163            @Override
164            public void run() {
165                // Oddly, we can just use '\n' here,
166                // we do not need to call
167                // System.getProperties("line.separator")
168                _jTextArea.append(text + '\n');
169
170                // Scroll down as we generate text.
171                _jTextArea.setCaretPosition(_jTextArea.getText().length());
172            }
173        };
174
175        SwingUtilities.invokeLater(doAppendJTextArea);
176    }
177
178    /** Append to the path of the subprocess.  If directoryName is already
179     *  in the path, then it is not appended.
180     *  @param directoryName The name of the directory to append to the path.
181     */
182    @Override
183    public void appendToPath(String directoryName) {
184        if (_debug) {
185            stdout("JTextArea.appendToPath(): " + directoryName + "\n");
186        }
187
188        // Might be Path, might be PATH
189        String keyPath = "PATH";
190        String path = getenv(keyPath);
191        if (path == null) {
192            path = getenv("Path");
193            if (path != null) {
194                keyPath = "Path";
195            }
196            if (_debug) {
197                stdout("JTextArea.appendToPath() Path: " + path + "\n");
198            }
199        } else {
200            if (_debug) {
201                stdout("JTextArea.appendToPath() PATH: " + path + "\n");
202            }
203        }
204
205        if (path == null || path.indexOf(File.pathSeparatorChar + directoryName
206                + File.pathSeparatorChar) == -1) {
207            if (_debug) {
208                stdout("JTextArea.appendToPath() updating\n");
209            }
210            _envp = StreamExec.updateEnvironment(keyPath, File.pathSeparatorChar
211                    + directoryName + File.pathSeparatorChar);
212
213            if (_debug) {
214                // For debugging
215                for (String element : _envp) {
216                    stdout("JTextArea.appendToPath() " + element);
217                }
218            }
219        }
220    }
221
222    /** Cancel any running commands. */
223    @Override
224    public void cancel() {
225        _cancelButton.doClick();
226    }
227
228    /** Clear the text area, status bar and progress bar. */
229    @Override
230    public void clear() {
231        _clearButton.doClick();
232        updateStatusBar("");
233        _updateProgressBar(0);
234    }
235
236    /** Get the value of the environment of the subprocess.
237     *  @param key  The environment variable.
238     *  @return The value of the key.  If the key is not set, then
239     *  null is returned.  If appendToPath() has been called, and
240     *  the then the environment for the subprocess is checked, which
241     *  might be different than the environment for the current process
242     *  because appendToPath() was called.  Note that that key is searched
243     *  for in a case-insensitive mode.
244     */
245    @Override
246    public String getenv(String key) {
247        // FIXME: Code Duplication from StreamExec.java
248        if (_envp == null) {
249
250            // Sigh.  System.getEnv("PATH") and System.getEnv("Path")
251            // will return the same thing, even though the variable
252            // is Path.  Updating PATH is wrong, the subprocess will
253            // not see the change.  So, we search the env for a direct
254            // match
255            Map<String, String> environmentMap = System.getenv();
256
257            if (_debug) {
258                stdout("JTextArea.getenv(" + key + "), _envp null, returning: "
259                        + environmentMap.get(key));
260            }
261
262            return environmentMap.get(key);
263        }
264        for (String element : _envp) {
265            String envpKey = element.substring(0, element.indexOf("="));
266            if (key.length() == envpKey.length() && key.regionMatches(
267                    false /*ignoreCase*/, 0, envpKey, 0, envpKey.length())) {
268
269                if (_debug) {
270                    stdout("JTextArea.getenv(" + key + "), \"" + envpKey
271                            + "\"\n\t_envp not null, returning: "
272                            + element.substring(key.length() + 1,
273                                    element.length()));
274                }
275                return element.substring(key.length() + 1, element.length());
276            }
277        }
278        return null;
279    }
280
281    /** Return the return code of the last subprocess that was executed.
282     *  @return the return code of the last subprocess that was executed.
283     */
284    @Override
285    public int getLastSubprocessReturnCode() {
286        return _subprocessReturnCode;
287    }
288
289    /** Return the Start button.
290     *  This method is used to get the Start button so we can
291     *  set the focus to it.
292     *  @return the Start button.
293     */
294    public JButton getStartButton() {
295        return _startButton;
296    }
297
298    /** Main method used for testing.
299     *  To run a simple test, use:
300     *  <pre>
301     *        java -classpath $PTII ptolemy.gui.JTextAreaExec
302     *  </pre>
303     *  @param args The command line arguments, currently ignored.
304     */
305    public static void main(String[] args) {
306        try {
307            // Run this in the Swing Event Thread.
308            Runnable doActions = new Runnable() {
309                @Override
310                public void run() {
311                    try {
312                        JFrame jFrame = new JFrame("JTextAreaExec Example");
313                        WindowListener windowListener = new WindowAdapter() {
314                            @Override
315                            public void windowClosing(WindowEvent e) {
316                                StringUtilities.exit(0);
317                            }
318                        };
319
320                        jFrame.addWindowListener(windowListener);
321
322                        List execCommands = new LinkedList();
323                        execCommands.add("date");
324                        execCommands.add("sleep 5");
325                        execCommands.add("date");
326                        execCommands.add("javac");
327
328                        final JTextAreaExec exec = new JTextAreaExec(
329                                "JTextAreaExec Tester", true);
330                        exec.setCommands(execCommands);
331                        jFrame.getContentPane().add(exec);
332                        jFrame.pack();
333                        jFrame.setVisible(true);
334
335                        exec.getStartButton().requestFocus();
336                        exec.start();
337                    } catch (Exception ex) {
338                        System.err.println(ex.toString());
339                        ex.printStackTrace();
340                    }
341                }
342            };
343            SwingUtilities.invokeAndWait(doActions);
344        } catch (Exception ex) {
345            System.err.println(ex.toString());
346            ex.printStackTrace();
347        }
348    }
349
350    /** Set the list of commands.
351     *  @param commands a List of Strings, where each element is a command.
352     */
353    @Override
354    public void setCommands(List commands) {
355        _commands = commands;
356        _enableStartButton();
357    }
358
359    /** Set the working directory of the subprocess.
360     *  @param workingDirectory The working directory of the
361     *  subprocess.  If this argument is null, then the subprocess is
362     *  executed in the working directory of the current process.
363     */
364    @Override
365    public void setWorkingDirectory(File workingDirectory) {
366        _workingDirectory = workingDirectory;
367    }
368
369    /** Start running the commands. */
370    @Override
371    public void start() {
372        _startButton.doClick();
373    }
374
375    /** Append the text message to stderr.
376     *  The output automatically gets a trailing newline appended.
377     *  @param text The text to append to standard error.
378     */
379    @Override
380    public void stderr(final String text) {
381        appendJTextArea(text);
382    }
383
384    /** Append the text message to the output.
385     *  The output automatically gets a trailing newline appended.
386     *  @param text The text to append to standard out.
387     */
388    @Override
389    public void stdout(final String text) {
390        appendJTextArea(text);
391    }
392
393    /** Update the status area with the text message.
394     *  @param text The text with which the status area is updated.
395     */
396    @Override
397    public void updateStatusBar(final String text) {
398        Runnable doUpdateStatusBar = new Runnable() {
399            @Override
400            public void run() {
401                _statusBar.setText(text);
402                _jTextArea.append(text + '\n');
403            }
404        };
405
406        SwingUtilities.invokeLater(doUpdateStatusBar);
407    }
408
409    ///////////////////////////////////////////////////////////////////
410    ////                         private methods                   ////
411    // Enable the Start button if there are any commands in the list.
412    private void _enableStartButton() {
413        if (_commands != null && _commands.size() > 0) {
414            _startButton.setEnabled(true);
415        } else {
416            _startButton.setEnabled(false);
417        }
418    }
419
420    // Execute the commands in the list.  Update the JTextArea with
421    // the command being run and the output.  Update the progress bar
422    // and the status bar.
423    private Object _executeCommands() {
424        // FIXME: Exec, KeyStoreActor, JTextAreaExec have duplicate code
425        try {
426            Runtime runtime = Runtime.getRuntime();
427
428            try {
429                if (_process != null) {
430                    _process.destroy();
431                }
432
433                _progressBar.setMaximum(_commands.size());
434
435                int commandCount = 0;
436                Iterator commands = _commands.iterator();
437
438                while (commands.hasNext()) {
439                    _updateProgressBar(++commandCount);
440
441                    if (Thread.interrupted()) {
442                        throw new InterruptedException();
443                    }
444
445                    // Preprocess by removing lines that begin with '#'
446                    // and converting substrings that begin and end
447                    // with double quotes into one array element.
448                    final String[] commandTokens = StringUtilities
449                            .tokenizeForExec((String) commands.next());
450
451                    if (commandTokens.length < 1) {
452                        stdout("Warning, an empty string was passed as a command.");
453                        continue;
454                    }
455                    stdout("In \"" + _workingDirectory
456                            + "\", about to execute:\n");
457
458                    StringBuffer statusCommand = new StringBuffer();
459
460                    for (String commandToken : commandTokens) {
461                        appendJTextArea("        " + commandToken);
462
463                        // Accumulate the first 50 chars for use in
464                        // the status buffer.
465                        if (statusCommand.length() < 50) {
466                            if (statusCommand.length() > 0) {
467                                statusCommand.append(" ");
468                            }
469
470                            statusCommand.append(commandToken);
471                        }
472                    }
473
474                    if (statusCommand.length() >= 50) {
475                        statusCommand.append(" . . .");
476                    }
477
478                    _statusBar
479                            .setText("Executing: " + statusCommand.toString());
480
481                    // If _envp is null, then no environment changes.
482                    _process = runtime.exec(commandTokens, _envp,
483                            _workingDirectory);
484
485                    // Set up a Thread to read in any error messages
486                    _StreamReaderThread errorGobbler = new _StreamReaderThread(
487                            _process.getErrorStream(), this);
488
489                    // Set up a Thread to read in any output messages
490                    _StreamReaderThread outputGobbler = new _StreamReaderThread(
491                            _process.getInputStream(), this);
492
493                    // Start up the Threads
494                    errorGobbler.start();
495                    outputGobbler.start();
496
497                    try {
498                        _subprocessReturnCode = _process.waitFor();
499
500                        if (_subprocessReturnCode != 0) {
501                            // FIXME: If we get a segfault in the subprocess, then it
502                            // would be nice to get the error in the display.  However,
503                            // there is no data to be found in the process error stream?
504                            appendJTextArea("Warning, process returned "
505                                    + _subprocessReturnCode);
506
507                            synchronized (this) {
508                                _process = null;
509                            }
510                            break;
511                        }
512
513                        synchronized (this) {
514                            _process = null;
515                        }
516
517                    } catch (InterruptedException interrupted) {
518                        appendJTextArea("InterruptedException: " + interrupted);
519                        throw interrupted;
520                    }
521                }
522
523                appendJTextArea("All Done.");
524            } catch (final IOException io) {
525                appendJTextArea("IOException: " + io);
526            }
527        } catch (InterruptedException e) {
528            _process.destroy();
529            _updateProgressBar(0);
530            return "Interrupted"; // SwingWorker.get() returns this
531        }
532
533        return "All Done"; // or this
534    }
535
536    // This action listener, called by the Clear button, clears
537    // the text area
538    private ActionListener _clearListener = new ActionListener() {
539        @Override
540        public void actionPerformed(ActionEvent event) {
541            Runnable doAppendJTextArea = new Runnable() {
542                @Override
543                public void run() {
544                    _jTextArea.setText(null);
545                }
546            };
547
548            SwingUtilities.invokeLater(doAppendJTextArea);
549        }
550    };
551
552    // This action listener, called by the Cancel button, interrupts
553    // the _worker thread which is running this._executeCommands().
554    // Note that the _executeCommands() method handles
555    // InterruptedExceptions cleanly.
556    private ActionListener _interruptListener = new ActionListener() {
557        @Override
558        public void actionPerformed(ActionEvent event) {
559            _cancelButton.setEnabled(false);
560            appendJTextArea("Cancel button was pressed");
561            _worker.cancel(true);
562            _process.destroy();
563            _enableStartButton();
564        }
565    };
566
567    // This action listener, called by the Start button, effectively
568    // forks the thread that does the work.
569    private ActionListener _startListener = new ActionListener() {
570        @Override
571        public void actionPerformed(ActionEvent event) {
572            _startButton.setEnabled(false);
573            _cancelButton.setEnabled(true);
574            _statusBar.setText("Working...");
575
576            _worker = new SwingWorker<Object, Void>() {
577                @Override
578                public Object doInBackground() {
579                    return _executeCommands();
580                }
581
582                @Override
583                public void done() {
584                    _enableStartButton();
585                    _cancelButton.setEnabled(false);
586                    _updateProgressBar(0);
587                    try {
588                        _statusBar.setText(get(1, TimeUnit.SECONDS).toString());
589                    } catch (CancellationException ex) {
590                        _statusBar.setText("Cancelled.");
591                    } catch (ExecutionException ex1) {
592                        _statusBar
593                                .setText("The computation threw an exception: "
594                                        + ex1.getCause());
595                    } catch (InterruptedException ex2) {
596                        _statusBar.setText(
597                                "The worker thread was interrupted while waiting, which is probably not a problem.");
598                    } catch (TimeoutException ex3) {
599                        _statusBar.setText(
600                                "The wait to get the execution result timed out, which is unusual, but probably not a problem.");
601                    }
602                }
603            };
604            _worker.execute();
605        }
606    };
607
608    // When the _worker needs to update the GUI we do so by queuing a
609    // Runnable for the event dispatching thread with
610    // SwingUtilities.invokeLater().  In this case we're just changing
611    // the value of the progress bar.
612    private void _updateProgressBar(final int i) {
613        Runnable doSetProgressBarValue = new Runnable() {
614            @Override
615            public void run() {
616                //_jTextArea.append(Integer.valueOf(i).toString());
617                _progressBar.setValue(i);
618            }
619        };
620
621        SwingUtilities.invokeLater(doSetProgressBarValue);
622    }
623
624    ///////////////////////////////////////////////////////////////////
625    ////                         inner classes                     ////
626    // Private class that reads a stream in a thread and updates the
627    // JTextArea.
628    private static class _StreamReaderThread extends Thread {
629        _StreamReaderThread(InputStream inputStream,
630                JTextAreaExec jTextAreaExec) {
631            _inputStream = inputStream;
632            _jTextAreaExec = jTextAreaExec;
633        }
634
635        // Read lines from the _inputStream and output them to the
636        // JTextArea.
637        @Override
638        public void run() {
639            try {
640                InputStreamReader inputStreamReader = new InputStreamReader(
641                        _inputStream);
642                BufferedReader bufferedReader = new BufferedReader(
643                        inputStreamReader);
644                String line = null;
645
646                while ((line = bufferedReader.readLine()) != null) {
647                    _jTextAreaExec.appendJTextArea(line);
648                }
649            } catch (IOException ioe) {
650                _jTextAreaExec.appendJTextArea("IOException: " + ioe);
651            }
652        }
653
654        // Stream to read from.
655        private InputStream _inputStream;
656
657        // Description of the Stream that we print, usually "OUTPUT" or "ERROR"
658        //private String _streamType;
659
660        // JTextArea to update
661        private JTextAreaExec _jTextAreaExec;
662    }
663
664    ///////////////////////////////////////////////////////////////////
665    ////                         private variables                 ////
666
667    /** The Cancel Button. */
668    private JButton _cancelButton;
669
670    /** The Clear Button. */
671    private JButton _clearButton;
672
673    /** The list of command to be executed.  Each entry in the list is
674     * a String.  It might be better to have each element of the list
675     * be an String [] so that the shell can interpret each word in
676     * the command.
677     */
678    private List _commands = null;
679
680    private final boolean _debug = false;
681
682    /** The environment, which is an array of Strings of the form
683     *  <code>name=value</code>.  If this variable is null, then
684     *  the environment of the calling process is used.
685     */
686    private String[] _envp;
687
688    /** JTextArea to write the command and the output of the command. */
689    private JTextArea _jTextArea;
690
691    /** The Process that we are running. */
692    private Process _process;
693
694    /** The return code of the last Runtime.exec() command. */
695    private int _subprocessReturnCode;
696
697    /** Progress bar where the length of the bar is the total number
698     * of commands being run.
699     */
700    private JProgressBar _progressBar;
701
702    /** Label at the bottom that provides feedback as to what is happening. */
703    private JLabel _statusBar;
704
705    /** The Start Button. */
706    private JButton _startButton;
707
708    /** SwingWorker that actually does the work. */
709    private SwingWorker _worker;
710
711    /** The working directory of the subprocess.  If null, then
712     *  the subprocess is executed in the working directory of the current
713     *  process.
714     */
715    private File _workingDirectory;
716}