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}