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}