001/* Run a list of commands. 002 003 Copyright (c) 2006-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.util; 028 029import java.io.BufferedReader; 030import java.io.File; 031import java.io.IOException; 032import java.io.InputStream; 033import java.io.InputStreamReader; 034import java.util.HashMap; 035import java.util.Iterator; 036import java.util.List; 037import java.util.Map; 038import java.util.regex.Pattern; 039 040/** Execute commands in a subprocess and send the results to stderr and stdout. 041 <p>As an alternative to this class, see 042 {@link ptolemy.gui.JTextAreaExec}, which uses Swing; and 043 {@link ptolemy.util.StringBufferExec}, which writes to a StringBuffer. 044 045 <p>Sample usage: 046 <pre> 047 List execCommands = new LinkedList(); 048 execCommands.add("date"); 049 execCommands.add("sleep 3"); 050 execCommands.add("date"); 051 execCommands.add("notACommand"); 052 053 final StreamExec exec = new StreamExec(); 054 exec.setCommands(execCommands); 055 056 exec.start(); 057 </pre> 058 059 060 <p>Loosely based on Example1.java from 061 <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> 062 <p>See also 063 <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> 064 and 065 <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> 066 067 @see ptolemy.gui.JTextAreaExec 068 @see ptolemy.util.StringBufferExec 069 070 @author Christopher Hylands 071 @version $Id$ 072 @since Ptolemy II 5.2 073 @Pt.ProposedRating Red (cxh) 074 @Pt.AcceptedRating Red (cxh) 075 */ 076public class StreamExec implements ExecuteCommands { 077 078 /** Create a StreamExec. */ 079 public StreamExec() { 080 // Does nothing? 081 } 082 083 /////////////////////////////////////////////////////////////////// 084 //// public methods //// 085 086 /** Append to the path of the subprocess. If directoryName is already 087 * in the path, then it is not appended. 088 * @param directoryName The name of the directory to append to the path. 089 */ 090 @Override 091 public void appendToPath(String directoryName) { 092 // FIXME: Code Duplication from JTextAreaExec.java 093 if (_debug) { 094 stdout("StreamExec.appendToPath(): " + directoryName + "\n"); 095 } 096 097 // Might be Path, might be PATH 098 String keyPath = "PATH"; 099 String path = getenv(keyPath); 100 if (path == null) { 101 path = getenv("Path"); 102 if (path != null) { 103 keyPath = "Path"; 104 } 105 if (_debug) { 106 stdout("StreamExec.appendToPath() Path: " + path + "\n"); 107 } 108 } else { 109 if (_debug) { 110 stdout("StreamExec.appendToPath() PATH: " + path + "\n"); 111 } 112 } 113 114 if (path == null || path.indexOf(File.pathSeparatorChar + directoryName 115 + File.pathSeparatorChar) == -1) { 116 if (_debug) { 117 stdout("StreamExec.appendToPath() updating\n"); 118 } 119 _envp = StreamExec.updateEnvironment(keyPath, File.pathSeparatorChar 120 + directoryName + File.pathSeparatorChar); 121 122 if (_debug) { 123 // For debugging 124 for (String element : _envp) { 125 stdout("StreamExec.appendToPath() " + element); 126 } 127 } 128 } 129 } 130 131 /** Cancel any running commands. */ 132 @Override 133 public void cancel() { 134 //_worker.interrupt(); 135 if (_process != null) { 136 _process.destroy(); 137 } 138 } 139 140 /** Clear the text area, status bar and progress bar. */ 141 @Override 142 public void clear() { 143 updateStatusBar(""); 144 _updateProgressBar(0); 145 } 146 147 /** Get the value of the environment of the subprocess. 148 * @param key The key for which to search. 149 * @return The value of the key. If the key is not set, then 150 * null is returned. If appendToPath() has been called, and 151 * the then the environment for the subprocess is checked, which 152 * might be different than the environment for the current process 153 * because appendToPath() was called. Note that that key is searched 154 * for in a case-insensitive mode. 155 */ 156 @Override 157 public String getenv(String key) { 158 // FIXME: Code Duplication from JTextAreaExec.java 159 if (_envp == null) { 160 // Sigh. System.getEnv("PATH") and System.getEnv("Path") 161 // will return the same thing, even though the variable 162 // is Path. Updating PATH is wrong, the subprocess will 163 // not see the change. So, we search the env for a direct 164 // match 165 Map<String, String> environmentMap = System.getenv(); 166 return environmentMap.get(key); 167 } 168 for (String element : _envp) { 169 if (key.regionMatches(false /*ignoreCase*/, 0, element, 0, 170 key.length())) { 171 return element.substring(key.length() + 1, element.length()); 172 } 173 } 174 return null; 175 } 176 177 /** Return the value of the pattern log. 178 * <p>Calling this method resets the log of previously received 179 * matches.</p> 180 * @return Any strings sent that match the value of the pattern. 181 * The matches for stdout are returned first, then the matches 182 * for stdout. 183 * @see #setPattern(String) 184 * @see #stdout(String) 185 */ 186 public String getPatternLog() { 187 String patternOutLog = _patternOutLog.toString(); 188 String patternErrorLog = _patternErrorLog.toString(); 189 _patternOutLog = new StringBuffer(); 190 _patternErrorLog = new StringBuffer(); 191 return patternOutLog + patternErrorLog; 192 } 193 194 /** Return the return code of the last subprocess that was executed. 195 * @return the return code of the last subprocess that was executed. 196 */ 197 @Override 198 public int getLastSubprocessReturnCode() { 199 return _subprocessReturnCode; 200 } 201 202 /** Set the list of commands. 203 * @param commands A list of Strings, where each element is a command. 204 */ 205 @Override 206 public void setCommands(List commands) { 207 _commands = commands; 208 } 209 210 /** Set the pattern that is used to search data sent to stdout. 211 * <p>If the value of the pattern argument is non-null, then 212 * each time {@link #stdout(String)} is called, the value of 213 * the argument to stdout is compared with the pattern 214 * regex. If there is a match, then the value is appended 215 * to a StringBuffer that whose value may be obtained with 216 * the {@link #getPatternLog()} method.</p> 217 * <p>Calling this method resets the log of previously received 218 * matches.</p> 219 * @param pattern The pattern used. 220 * @see #getPatternLog() 221 */ 222 public void setPattern(String pattern) { 223 _pattern = Pattern.compile(pattern); 224 _patternOutLog = new StringBuffer(); 225 _patternErrorLog = new StringBuffer(); 226 } 227 228 /** Determine whether the last subprocess is waited for or not. 229 * @param waitForLastSubprocess True if the {@link #start()} 230 * method should wait for the last subprocess to finish. 231 */ 232 public void setWaitForLastSubprocess(boolean waitForLastSubprocess) { 233 _waitForLastSubprocess = waitForLastSubprocess; 234 } 235 236 /** Set the working directory of the subprocess. 237 * @param workingDirectory The working directory of the 238 * subprocess. If this argument is null, then the subprocess is 239 * executed in the working directory of the current process. 240 */ 241 @Override 242 public void setWorkingDirectory(File workingDirectory) { 243 _workingDirectory = workingDirectory; 244 } 245 246 /** Start running the commands. 247 * By default, the start() method returns after the last subprocess 248 * finishes. See {@link #setWaitForLastSubprocess(boolean)}. 249 */ 250 @Override 251 public void start() { 252 String returnValue = _executeCommands(); 253 updateStatusBar(returnValue); 254 stdout(returnValue); 255 } 256 257 /** Append the text message to stderr. A derived class could 258 * append to a StringBuffer. {@link ptolemy.gui.JTextAreaExec} appends to a 259 * JTextArea. The output automatically gets a trailing newline 260 * appended. 261 * @param text The text to append to standard error. 262 */ 263 @Override 264 public void stderr(final String text) { 265 if (_pattern != null && _pattern.matcher(text).matches()) { 266 _patternErrorLog.append(text + _eol); 267 } 268 System.err.println(text); 269 System.err.flush(); 270 } 271 272 /** Append the text message to the output. A derived class could 273 * append to a StringBuffer. {@link ptolemy.gui.JTextAreaExec} appends to a 274 * JTextArea. 275 * The output automatically gets a trailing newline appended. 276 * <p>If {@link #setPattern(String)} has been called with a 277 * non-null argument, then any text that matches the pattern 278 * regex will be appended to a log file. The log file 279 * may be read with {@link #getPatternLog()}.</p> 280 * @param text The text to append to standard out. 281 */ 282 @Override 283 public void stdout(final String text) { 284 if (_pattern != null && _pattern.matcher(text).matches()) { 285 _patternOutLog.append(text + _eol); 286 } 287 System.out.println(text); 288 System.out.flush(); 289 } 290 291 /** Update the environment and return the results. 292 * Read the environment for the current process, append the value 293 * of the value parameter to the environment variable named by 294 * the key parameter. 295 * @param key The environment variable to be updated. 296 * @param value The value to append 297 * @return An array of strings that consists of the subprocess 298 * environment variable names and values in the form 299 * <code>name=value</code> with the environment variable 300 * named by the key parameter updated to include the value 301 * of the value parameter. 302 */ 303 public static String[] updateEnvironment(String key, String value) { 304 // This is static so that we can share it among 305 // ptolemy.util.StreamExec 306 // StringBufferExec, which extends Stream Exec 307 // and 308 // ptolemy.gui.JTextAreaExec, which extends JPanel 309 310 Map<String, String> env = new HashMap(System.getenv()); 311 312 env.put(key, value + env.get(key)); 313 String[] envp = new String[env.size()]; 314 315 int i = 0; 316 Iterator entries = env.entrySet().iterator(); 317 while (entries.hasNext()) { 318 Map.Entry entry = (Map.Entry) entries.next(); 319 envp[i++] = entry.getKey() + "=" + entry.getValue(); 320 // System.out.println("StreamExec(): " + envp[i-1]); 321 } 322 return envp; 323 } 324 325 /** Set the text of the status bar. In this base class, do 326 * nothing, derived classes may update a status bar. 327 * @param text The text with which the status bar is updated. 328 */ 329 @Override 330 public void updateStatusBar(final String text) { 331 } 332 333 /////////////////////////////////////////////////////////////////// 334 //// protected methods //// 335 336 /** Set the maximum of the progress bar. In this base class, do 337 * nothing, derived classes may update the size of the progress bar. 338 * @param size The maximum size of the progress bar. 339 */ 340 protected void _setProgressBarMaximum(int size) { 341 } 342 343 /////////////////////////////////////////////////////////////////// 344 //// private methods //// 345 346 /** Execute the commands in the list. Update the output with 347 * the command being run and the output. 348 */ 349 private String _executeCommands() { 350 try { 351 Runtime runtime = Runtime.getRuntime(); 352 353 try { 354 if (_process != null) { 355 _process.destroy(); 356 } 357 358 _setProgressBarMaximum(_commands.size()); 359 360 int commandCount = 0; 361 Iterator commands = _commands.iterator(); 362 363 while (commands.hasNext()) { 364 _updateProgressBar(++commandCount); 365 366 if (Thread.interrupted()) { 367 throw new InterruptedException(); 368 } 369 370 // Preprocess by removing lines that begin with '#' 371 // and converting substrings that begin and end 372 // with double quotes into one array element. 373 final String[] commandTokens = StringUtilities 374 .tokenizeForExec((String) commands.next()); 375 376 if (_workingDirectory != null) { 377 stdout("In \"" + _workingDirectory 378 + "\", about to execute:"); 379 } else { 380 stdout("About to execute:"); 381 } 382 383 StringBuffer commandBuffer = new StringBuffer(); 384 StringBuffer statusCommand = new StringBuffer(); 385 for (String commandToken : commandTokens) { 386 if (commandBuffer.length() > 1) { 387 commandBuffer.append(" "); 388 } 389 commandBuffer.append(commandToken); 390 391 // Accumulate the first 50 chars for use in 392 // the status buffer. 393 if (statusCommand.length() < 50) { 394 if (statusCommand.length() > 0) { 395 statusCommand.append(" "); 396 } 397 398 statusCommand.append(commandToken); 399 } 400 } 401 stdout(" " 402 + StringUtilities.split(commandBuffer.toString())); 403 if (statusCommand.length() >= 50) { 404 statusCommand.append(" . . ."); 405 } 406 407 updateStatusBar("Executing: " + statusCommand.toString()); 408 409 // If _envp is null, then no environment changes. 410 _process = runtime.exec(commandTokens, _envp, 411 _workingDirectory); 412 413 // Set up a Thread to read in any error messages 414 _StreamReaderThread errorGobbler = new _StreamReaderThread( 415 _process.getErrorStream(), this); 416 417 // Set up a Thread to read in any output messages 418 _StreamReaderThread outputGobbler = new _StreamReaderThread( 419 _process.getInputStream(), this); 420 421 // Start up the Threads 422 errorGobbler.start(); 423 outputGobbler.start(); 424 425 if (commands.hasNext() || _waitForLastSubprocess) { 426 try { 427 _subprocessReturnCode = _process.waitFor(); 428 429 synchronized (this) { 430 _process = null; 431 } 432 433 if (_subprocessReturnCode != 0) { 434 break; 435 } 436 } catch (InterruptedException interrupted) { 437 stderr("InterruptedException: " + interrupted); 438 throw interrupted; 439 } 440 } 441 } 442 } catch (final IOException io) { 443 stderr("IOException: " + io); 444 } 445 } catch (InterruptedException e) { 446 _process.destroy(); 447 _updateProgressBar(0); 448 return "Interrupted"; // SwingWorker.get() returns this 449 } 450 451 return "All Done"; // or this 452 } 453 454 /** Update the progress bar. In this base class, do nothing. 455 * @param i The current location of the progress bar. 456 */ 457 private void _updateProgressBar(final int i) { 458 } 459 460 /////////////////////////////////////////////////////////////////// 461 //// inner classes //// 462 /** Private class that reads a stream in a thread and updates the 463 * the StreamExec. 464 */ 465 private static class _StreamReaderThread extends Thread { 466 467 // FindBugs suggests making this class static so as to decrease 468 // the size of instances and avoid dangling references. 469 470 /** Construct a StreamReaderThread. 471 * @param inputStream the stream from which to read. 472 * @param streamExec The StreamExec to be written. 473 */ 474 _StreamReaderThread(InputStream inputStream, StreamExec streamExec) { 475 _inputStream = inputStream; 476 _streamExec = streamExec; 477 } 478 479 /** Read lines from the _inputStream and output them. */ 480 @Override 481 public void run() { 482 try { 483 InputStreamReader inputStreamReader = new InputStreamReader( 484 _inputStream); 485 BufferedReader bufferedReader = new BufferedReader( 486 inputStreamReader); 487 String line = null; 488 489 while ((line = bufferedReader.readLine()) != null) { 490 _streamExec.stdout( /*_streamType + ">" +*/ 491 line); 492 } 493 } catch (IOException ioe) { 494 _streamExec.stderr("IOException: " + ioe); 495 } 496 } 497 498 /** Stream from which to read. */ 499 private InputStream _inputStream; 500 501 /** StreamExec which is written. */ 502 private StreamExec _streamExec; 503 } 504 505 /////////////////////////////////////////////////////////////////// 506 //// private variables //// 507 508 /** The list of command to be executed. Each entry in the list is 509 * a String. It might be better to have each element of the list 510 * be an String [] so that the shell can interpret each word in 511 * the command. 512 */ 513 private List _commands; 514 515 private final boolean _debug = false; 516 517 /** End of line character. */ 518 private static final String _eol; 519 520 static { 521 _eol = StringUtilities.getProperty("line.separator"); 522 } 523 524 /** The environment, which is an array of Strings of the form 525 * <code>name=value</code>. If this variable is null, then 526 * the environment of the calling process is used. 527 */ 528 private String[] _envp; 529 530 /** The regex pattern used to match against the output of 531 * the subprocess. 532 */ 533 private Pattern _pattern; 534 535 /** The StringBuffer that contains matches to calls to stdout(). */ 536 private StringBuffer _patternErrorLog; 537 538 /** The StringBuffer that contains matches to calls to stderr(). */ 539 private StringBuffer _patternOutLog; 540 541 /** The Process that we are running. */ 542 private Process _process; 543 544 /** The return code of the last Runtime.exec() command. */ 545 private int _subprocessReturnCode; 546 547 /** True if we should wait for the last subprocess. */ 548 private boolean _waitForLastSubprocess = true; 549 550 /** The working directory of the subprocess. If null, then 551 * the subprocess is executed in the working directory of the current 552 * process. 553 */ 554 private File _workingDirectory; 555}