001/* Execute a command in a subprocess.
002
003 Copyright (c) 2004-2018 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
027 */
028package ptolemy.actor.lib;
029
030import java.io.BufferedWriter;
031import java.io.File;
032import java.io.IOException;
033import java.io.InputStream;
034import java.io.InputStreamReader;
035import java.io.OutputStreamWriter;
036import java.util.Arrays;
037import java.util.LinkedList;
038import java.util.List;
039
040import ptolemy.actor.TypedIOPort;
041import ptolemy.actor.parameters.PortParameter;
042import ptolemy.data.ArrayToken;
043import ptolemy.data.BooleanToken;
044import ptolemy.data.IntToken;
045import ptolemy.data.RecordToken;
046import ptolemy.data.StringToken;
047import ptolemy.data.expr.FileParameter;
048import ptolemy.data.expr.Parameter;
049import ptolemy.data.type.ArrayType;
050import ptolemy.data.type.BaseType;
051import ptolemy.data.type.RecordType;
052import ptolemy.data.type.Type;
053import ptolemy.kernel.CompositeEntity;
054import ptolemy.kernel.util.IllegalActionException;
055import ptolemy.kernel.util.InternalErrorException;
056import ptolemy.kernel.util.NameDuplicationException;
057import ptolemy.kernel.util.Nameable;
058import ptolemy.util.StringUtilities;
059
060///////////////////////////////////////////////////////////////////
061//// Exec
062
063/**
064 Execute a command as a separately running subprocess.
065
066 <p>This actor uses java.lang.Runtime.exec() to invoke a subprocess
067 named by the <i>command</i> parameter in a specified <i>directory</i> with a
068 specified  <i>environment</i>.  Data from the <i>input</i> port (if any) is
069 passed to the input of the subprocess.  The subprocess is run until it
070 exits and then contents of the output and error streams of the
071 subprocess (if any) are passed to the <i>output</i> and <i>error</i>
072 ports.</p>
073
074 <p>If the subprocess generates no data on the output or error stream,
075 then the data on the corresponding port(s) will consist of the empty
076 string.</p>
077
078 <p> To get the effect of executing a command provided in a shell interpreter, set the
079 <i>prependPlatformDependentShellCommand</i> parameter to true.
080 This will prepend a default platform-dependent shell command to the command
081 you wish to execute so that your command is executed within the shell.
082 Alternatively, you can set <i>command</i> to "cmd" (Windows) or "sh" (Windows with Cygwin
083 or Linux), and then provide commands at the <i>input</i> port.
084 In this case, however, your model will only work on platforms that have the shell
085 command you have specified.
086 Note that in this case each command must be terminated with a newline.
087 For example, to open a model in vergil and run it, you can
088 set <i>command</i> to "sh" and use a Const actor to provide
089 on the <i>input</i> port the string:</p>
090 <pre>
091 "vergil -run model.xml\n exit\n"
092 </pre>
093
094 <p>A much more interesting actor could be written using a
095 Kahn Process Network.  This actor would generate output asynchronously
096 as the process was executing.</p>
097
098 <p>For information about Runtime.exec(), see:
099 <br><a href="http://www.javaworld.com/javaworld/jw-12-2007/jw-1229-traps.html" target="_top">http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html</a>
100 and
101 <a href="http://mindprod.com/jgloss/exec.html" target="_top">http://mindprod.com/jgloss/exec.html</a>.
102 </p>
103
104 @author Christopher Hylands Brooks, Contributors: Edward A. Lee, Daniel Crawl
105 @version $Id$
106 @since Ptolemy II 4.0
107 @Pt.ProposedRating Yellow (cxh) 2/5/04
108 @Pt.AcceptedRating Yellow (cxh) 2/24/04
109 */
110public class Exec extends LimitedFiringSource {
111    /** Construct an actor with the given container and name.
112     *  @param container The container.
113     *  @param name The name of this actor.
114     *  @exception IllegalActionException If the actor cannot be contained
115     *   by the proposed container.
116     *  @exception NameDuplicationException If the container already has an
117     *   actor with this name.
118     */
119    public Exec(CompositeEntity container, String name)
120            throws NameDuplicationException, IllegalActionException {
121        super(container, name);
122
123        // Uncomment the next line to see debugging statements
124        //addDebugListener(new ptolemy.kernel.util.StreamListener());
125        command = new PortParameter(this, "command");
126        // Make command be a StringParameter (no surrounding double quotes).
127        command.setStringMode(true);
128        command.setExpression("echo \"Hello world.\"");
129
130        new Parameter(command.getPort(), "_showName", BooleanToken.TRUE);
131
132        directory = new FileParameter(this, "directory");
133        new Parameter(directory, "allowFiles", BooleanToken.FALSE);
134        new Parameter(directory, "allowDirectories", BooleanToken.TRUE);
135        directory.setExpression("$CWD");
136
137        environment = new Parameter(this, "environment");
138
139        String[] labels = new String[] { "name", "value" };
140        Type[] values = new Type[] { BaseType.STRING, BaseType.STRING };
141
142        // An array of records {{name = "", value = ""}}
143        environment
144                .setTypeEquals(new ArrayType(new RecordType(labels, values)));
145
146        // Array with an empty name and value means
147        // default environment of the calling process.
148        environment.setExpression("{{name = \"\", value = \"\"}}");
149
150        error = new TypedIOPort(this, "error", false, true);
151        error.setTypeEquals(BaseType.STRING);
152        new Parameter(error, "_showName", BooleanToken.TRUE);
153
154        ignoreIOExceptionReadErrors = new Parameter(this,
155                "ignoreIOExceptionReadErrors", BooleanToken.FALSE);
156        ignoreIOExceptionReadErrors.setTypeEquals(BaseType.BOOLEAN);
157
158        input = new TypedIOPort(this, "input", true, false);
159        input.setTypeEquals(BaseType.STRING);
160        new Parameter(input, "_showName", BooleanToken.TRUE);
161
162        output.setTypeEquals(BaseType.STRING);
163        new Parameter(output, "_showName", BooleanToken.TRUE);
164
165        exitCode = new TypedIOPort(this, "exitCode", false, true);
166        exitCode.setTypeEquals(BaseType.INT);
167        new Parameter(exitCode, "_showName", BooleanToken.TRUE);
168
169        prependPlatformDependentShellCommand = new Parameter(this,
170                "prependPlatformDependentShellCommand", BooleanToken.FALSE);
171        prependPlatformDependentShellCommand.setTypeEquals(BaseType.BOOLEAN);
172
173        throwExceptionOnNonZeroReturn = new Parameter(this,
174                "throwExceptionOnNonZeroReturn", BooleanToken.TRUE);
175        throwExceptionOnNonZeroReturn.setTypeEquals(BaseType.BOOLEAN);
176
177        waitForProcess = new Parameter(this, "waitForProcess",
178                BooleanToken.TRUE);
179        waitForProcess.setTypeEquals(BaseType.BOOLEAN);
180
181        // Show the firingCountLimit parameter last.
182        firingCountLimit.moveToLast();
183    }
184
185    ///////////////////////////////////////////////////////////////////
186    ////                     ports and parameters                  ////
187
188    /** The command to be executed.  The command is parsed by
189     * {@link ptolemy.util.StringUtilities#tokenizeForExec(String)}
190     * into tokens and then executed as a separate subprocess.
191     * The initial default value is the string
192     * <code>echo "Hello, world."</code>.
193     *
194     * <p>The command parameter is read only once during fire().
195     * If you want to spawn another different command,
196     * use life cycle management actors such RunCompositeActor.</p>
197     */
198    public PortParameter command;
199
200    /** The directory in which to execute the command.
201     *  This parameter is read each time the subprocess is started
202     *  in fire(). Once the subprocess is running, this parameter is not
203     *  read again until fire() is called again.
204     *
205     *  <p>The initial default value of this parameter $CWD, which
206     *  corresponds with the value of the Java virtual machine
207     *  user.dir property which is the user's current working
208     *  directory.  Note that if we are running inside a menu launched
209     *  application, then ptolemy.actor.gui.jnlp.MenuApplication will
210     *  change user.dir to be the value of user.home, which is the
211     *  name of the user's home directory.</p>
212     *
213     *  <p>If the value of this parameter is the empty string,
214     *  then the working directory of the subprocess with be
215     *  inherited from the working directory of the parent
216     *  process.  Typically, this is the value of the
217     *  user.dir property.</p>
218     */
219    public FileParameter directory;
220
221    /** The environment in which to execute the command.
222     *  This parameter is read each time the subprocess is started
223     *  in fire(). Once the subprocess is running, this parameter is not
224     *  read again until fire() is called again.
225     *
226     *  <p>This parameter is an array of records that name an environment
227     *  variable and the value for the value.  The format is:</p>
228     *  <pre>
229     *  {{name = "<i>NAME1</i>", value = "<i>value1</i>"}...}
230     *  </pre>
231     *  Where <code><i>NAME1</i></code> is the name of the environment
232     *  variable, and <code><i>value1</i></code> is the value.
233     *  <p>For example <code>{{name = "PTII", value = "c:/ptII"}}</code>
234     *  would set the value of the <code>PTII</code> to <code>c:/ptII</code>.</p>
235     *
236     *  <p>If the initial value of the parameter is <code>{{name="",
237     *  value = ""}}</code>, then the environment from the calling or parent
238     *  process is used in the new command.</p>
239     *
240     *  <p>Note that if this parameter sets any environment variable,
241     *  then under Windows the other environment variables in the calling
242     *  or parent process might not be passed to the subprocess.  This
243     *  behaviour could be platform or JVM dependent. When in doubt,
244     *  try setting the <i>command</i> value to "env" to print out the
245     *  environment.</p>
246     */
247    public Parameter environment;
248
249    /** Data that is generated by the subprocess on its standard
250     *  error.  While the process is running, any error data generated
251     *  by the subprocess is stored until the subprocess exits and
252     *  then the stored error data is sent to the <i>error</i> port.
253     *  If the subprocess generates no data on standard error, then
254     *  the empty string (a string of length zero) is generated.
255     *  This port is an output port of type String.
256     */
257    public TypedIOPort error;
258
259    /** The exit code of the subprocess. Usually, a non-zero exit code
260     *  indicate that the subprocess had a problem.  This port is an output
261     *  port of type int.
262     */
263    public TypedIOPort exitCode;
264
265    /** If true, ignore IOException errors from the subprocess.
266     *  The initial default value is false, indicating that
267     *  read errors are not ignored.
268     */
269    public Parameter ignoreIOExceptionReadErrors;
270
271    /** Strings to pass to the standard input of the subprocess.
272     *  Note that a newline is not appended to the string.  If you
273     *  require a newline, add one using the AddSubtract actor.
274     *  This port is an input port of type String.
275     */
276    public TypedIOPort input;
277
278    /** Data that is generated by the subprocess on standard out.
279     *  While the process is running, any output data generated
280     *  by the subprocess is stored until the subprocess exits and
281     *  then the stored output data is sent to the <i>output</i> port.
282     *  If the subprocess generates no data on standard out, then
283     *  the empty string (a string of length zero) is generated.
284     *  This port is an output port of type String.
285     */
286    // NOTE: output port is inherited from parent class.
287    //public TypedIOPort output;
288
289    /** If true, then prepend the platform dependent shell command
290     *  to the parsed value of the command parameter.
291     *  By setting this argument to true, it is possible to invoke
292     *  commands in a platform neutral method.
293     *  <p>Under Windows NT or XP, the arguments "cmd.exe" and "/C"
294     *  are prepended.  Under Windows 95, the arguments "command.com"
295     *  and "/C" are prepended.  Under all other platforms, the
296     *  arguments "/bin/sh" and "-c" are prepended.
297     *  <p>By prepending sh or cmd, then this actor can use the
298     *  file redirection operations.
299     *  <p>The default value of this parameter is a boolean of value
300     *  false, which allows the user to arbitrarily invoke /bin/sh
301     *  scripts on all platforms.
302     */
303    public Parameter prependPlatformDependentShellCommand;
304
305    /** If true, then throw an exception if the subprocess returns
306     *  non-zero.
307     *  The default is a boolean of value true.
308     *  This parameter is ignored if <i>waitForProcess</i> is false.
309     */
310    public Parameter throwExceptionOnNonZeroReturn;
311
312    /** If true, then actor will wait until subprocess completes. The
313     *  default is a boolean of value true.
314     */
315    public Parameter waitForProcess;
316
317    ///////////////////////////////////////////////////////////////////
318    ////                         public methods                    ////
319
320    /** Invoke a subprocess, read the <i>input</i> data (if any) and
321     *  wait for the subprocess to terminate before sending any output
322     *  or error data to the appropriate ports.
323     *
324     *  <p>If there is no data on the <i>input</i> port, then the
325     *  subprocess executes without reading any input. If there is no
326     *  output or error data from the subprocess, then the empty
327     *  string is sent to the appropriate port(s).</p>
328     *
329     *  @exception IllegalActionException If the subprocess cannot be
330     *  started, if the input of the subprocess cannot be written,
331     *  if the subprocess gets interrupted, or if the return value
332     *  of the process is non-zero.
333     */
334    @Override
335    public void fire() throws IllegalActionException {
336        // NOTE: This used to be synchronized, but this causes a
337        // deadlock with the UI when parameters are edited while
338        // model is running.
339        super.fire();
340
341        String line = null;
342
343        _exec();
344
345        if (input.numberOfSources() > 0 && input.hasToken(0)) {
346            if ((line = ((StringToken) input.get(0)).stringValue()) != null) {
347                if (_debugging) {
348                    _debug("Exec: Input: '" + line + "'");
349                }
350
351                if (_inputBufferedWriter != null) {
352                    try {
353                        _inputBufferedWriter.write(line);
354                        _inputBufferedWriter.flush();
355                    } catch (IOException ex) {
356                        throw new IllegalActionException(this, ex,
357                                "Problem writing input '" + command + "'");
358                    }
359                }
360            }
361        }
362
363        boolean alreadySentOutput = false;
364
365        try {
366
367            // Close the stdin of the subprocess.
368            _process.getOutputStream().close();
369
370            boolean waitForProcessValue = ((BooleanToken) waitForProcess
371                    .getToken()).booleanValue();
372
373            if (waitForProcessValue) {
374                // The next line waits for the subprocess to finish.
375                int processReturnCode = _process.waitFor();
376
377                if (processReturnCode != 0) {
378                    // We could have a parameter that would enable
379                    // or disable this.
380                    String outputString = "";
381                    String errorString = "";
382
383                    try {
384                        errorString = _errorGobbler.getAndReset();
385                    } catch (Exception ex) {
386                        errorString = ex.toString();
387                    }
388
389                    try {
390                        outputString = _outputGobbler.getAndReset();
391                    } catch (Exception ex) {
392                        outputString = ex.toString();
393                    }
394
395                    boolean throwExceptionOnNonZeroReturnValue = ((BooleanToken) throwExceptionOnNonZeroReturn
396                            .getToken()).booleanValue();
397
398                    if (throwExceptionOnNonZeroReturnValue) {
399                        throw new IllegalActionException(this,
400                                "Executing command \""
401                                        + ((StringToken) command.getToken())
402                                                .stringValue()
403                                        + "\" returned a non-zero return value of "
404                                        + processReturnCode
405                                        + ".\nThe last input was: " + line
406                                        + ".\nThe standard output was: "
407                                        + outputString
408                                        + "\nThe error output was: "
409                                        + errorString);
410                    } else {
411                        error.send(0, new StringToken(errorString));
412                        output.send(0, new StringToken(outputString));
413                        alreadySentOutput = true;
414                    }
415                }
416
417                exitCode.send(0, new IntToken(processReturnCode));
418
419            }
420        } catch (InterruptedException interrupted) {
421            throw new InternalErrorException(this, interrupted,
422                    "_process.waitFor() was interrupted");
423        } catch (IOException io) {
424            throw new IllegalActionException(this, io,
425                    "Closing stdin of the subprocess threw an IOException.");
426
427        }
428
429        // if we sent output when the return value was non-zero, do not send
430        // additional output.
431        // see http://bugzilla.ecoinformatics.org/show_bug.cgi?id=4326
432        if (!alreadySentOutput) {
433            String outputString = _outputGobbler.getAndReset();
434            String errorString = _errorGobbler.getAndReset();
435
436            if (_debugging) {
437                _debug("Exec: Error: '" + errorString + "'");
438                _debug("Exec: Output: '" + outputString + "'");
439            }
440
441            // We could have a parameter that if it was set
442            // we would throw an exception if there was any error data.
443            error.send(0, new StringToken(errorString));
444            output.send(0, new StringToken(outputString));
445        }
446    }
447
448    /** Override the base class and terminate the process.
449     */
450    @Override
451    public void stop() {
452        // NOTE: This method used to be synchronized, as
453        // was the fire() method, but this caused deadlocks.  EAL
454        super.stop();
455        _terminateProcess();
456
457    }
458
459    /** Override the base class to stop waiting for input data.
460     */
461    @Override
462    public void stopFire() {
463        // NOTE: This method used to be synchronized, as
464        // was the fire() method, but this caused deadlocks.  EAL
465        super.stopFire();
466        _stopFireRequested = true;
467        _terminateProcess();
468    }
469
470    /** Terminate the subprocess.
471     *  This method is invoked exactly once per execution
472     *  of an application.  None of the other action methods should be
473     *  be invoked after it.
474     *  @exception IllegalActionException Not thrown in this base class.
475     */
476    @Override
477    public void wrapup() throws IllegalActionException {
478        _terminateProcess();
479    }
480
481    ///////////////////////////////////////////////////////////////////
482    ////                         private methods                   ////
483    // Execute a command, set _process to point to the subprocess
484    // and set up _errorGobbler and _outputGobbler to read data.
485    private void _exec() throws IllegalActionException {
486        // FIXME: Exec, KeyStoreActor, JTextAreaExec have duplicate code.
487        // This is a private method because fire() was getting too long.
488        File directoryAsFile = null;
489        try {
490            _stopFireRequested = false;
491
492            if (_process != null) {
493                // Note that we assume that _process is null upon entry
494                // to this method, but we check again here just to be sure.
495                _terminateProcess();
496            }
497
498            Runtime runtime = Runtime.getRuntime();
499
500            command.update();
501
502            List<String> commandList = new LinkedList<String>();
503
504            boolean prependPlatformDependentShellCommandValue = ((BooleanToken) prependPlatformDependentShellCommand
505                    .getToken()).booleanValue();
506            if (prependPlatformDependentShellCommandValue) {
507                commandList = _getCommandList();
508            }
509
510            // tokenizeForExec() handles substrings that start and end
511            // with a double quote so the substring is considered to
512            // be a single token and are returned as a single array
513            // element.
514            // FIXME: tokenizeForExec should return a List<String>
515            String[] commandArray = StringUtilities.tokenizeForExec(
516                    ((StringToken) command.getToken()).stringValue());
517            commandList.addAll(Arrays.asList(commandArray));
518
519            // If the directory parameter is the empty string,
520            // then directoryAsFile will be null.
521            // If we call java.lang.Runtime.exec() with a null
522            // value for the directory, then the working directory
523            // of the subprocess will be inherited from the current process.
524            // See https://projects.ecoinformatics.org/ecoinfo/issues/6676
525            directoryAsFile = directory.asFile();
526            if (directoryAsFile != null && !directoryAsFile.isDirectory()) {
527                throw new IllegalActionException(
528                        "No such directory: " + directoryAsFile);
529            }
530
531            if (_debugging) {
532                StringBuffer commands = new StringBuffer();
533                for (String aCommand : commandList) {
534                    commands.append(aCommand + " ");
535                }
536                _debug("About to exec \"" + commands + "\"\n in \""
537                        + (directoryAsFile == null
538                                ? "the working directory of the parent process"
539                                : directoryAsFile)
540                        + "\"\n with environment:");
541            }
542
543            // Process the environment parameter.
544            ArrayToken environmentTokens = (ArrayToken) environment.getToken();
545
546            if (_debugging) {
547                _debug("environmentTokens: " + environmentTokens);
548            }
549
550            String[] environmentArray = null;
551
552            if (environmentTokens != null && environmentTokens.length() >= 1) {
553                environmentArray = new String[environmentTokens.length()];
554
555                for (int i = 0; i < environmentTokens.length(); i++) {
556                    StringToken nameToken = (StringToken) ((RecordToken) environmentTokens
557                            .getElement(i)).get("name");
558                    StringToken valueToken = (StringToken) ((RecordToken) environmentTokens
559                            .getElement(i)).get("value");
560                    environmentArray[i] = nameToken.stringValue() + "="
561                            + valueToken.stringValue();
562
563                    if (_debugging) {
564                        _debug("  " + i + ". \"" + environmentArray[i] + "\"");
565                    }
566
567                    if (i == 0 && environmentTokens.length() == 1
568                            && environmentArray[0].equals("=")) {
569                        if (_debugging) {
570                            _debug("There is only one element, "
571                                    + "it is a string of length 0,\n so we "
572                                    + "pass Runtime.exec() an null "
573                                    + "environment so that we use\n "
574                                    + "the default environment");
575                        }
576
577                        environmentArray = null;
578                    }
579                }
580            }
581
582            commandArray = commandList.toArray(new String[commandList.size()]);
583            _process = runtime.exec(commandArray, environmentArray,
584                    directoryAsFile);
585
586            // Create two threads to read from the subprocess.
587            _outputGobbler = new _StreamReaderThread(_process.getInputStream(),
588                    "Exec Stdout Gobbler-" + _streamReaderThreadCount++, this);
589            _errorGobbler = new _StreamReaderThread(_process.getErrorStream(),
590                    "Exec Stderr Gobbler-" + _streamReaderThreadCount++, this);
591            _errorGobbler.start();
592            _outputGobbler.start();
593
594            if (_streamReaderThreadCount > 1000) {
595                // Avoid overflow in the thread count.
596                _streamReaderThreadCount = 0;
597            }
598
599            OutputStreamWriter inputStreamWriter = new OutputStreamWriter(
600                    _process.getOutputStream());
601            _inputBufferedWriter = new BufferedWriter(inputStreamWriter);
602        } catch (IOException ex) {
603            throw new IllegalActionException(this, ex,
604                    "Problem executing the command '" + command.getExpression()
605                            + "'\n" + "in the "
606                            + (directoryAsFile == null
607                                    ? "the working directory of the parent process."
608                                    : directoryAsFile + " directory."));
609        }
610    }
611
612    /** Get the command list arguments for exec. */
613    private List<String> _getCommandList() {
614        List<String> retval = new LinkedList<String>();
615
616        String osName = System.getProperty("os.name");
617        if (osName.equals("Windows 95")) {
618            retval.add("command.com");
619            retval.add("/C");
620        } else if (osName.startsWith("Windows")) {
621            retval.add("cmd.exe");
622            retval.add("/C");
623        } else {
624            retval.add("/bin/sh");
625            retval.add("-c");
626        }
627        return retval;
628    }
629
630    // Terminate the process and close any associated streams.
631    private void _terminateProcess() {
632        if (_process != null) {
633            _process.destroy();
634            _process = null;
635        }
636    }
637
638    ///////////////////////////////////////////////////////////////////
639    ////                         inner classes                     ////
640    // Private class that reads a stream in a thread and updates the
641    // stringBuffer.
642    private class _StreamReaderThread extends Thread {
643        /** Create a _StreamReaderThread.
644         *  @param inputStream The stream to read from.
645         *  @param name The name of this StreamReaderThread,
646         *  which is useful for debugging.
647         *  @param actor The parent actor of this thread, which
648         *  is used in error messages.
649         */
650        _StreamReaderThread(InputStream inputStream, String name,
651                Nameable actor) {
652            super(name);
653            _inputStream = inputStream;
654            _inputStreamReader = new InputStreamReader(_inputStream,
655                    java.nio.charset.Charset.defaultCharset());
656            _actor = actor;
657            _stringBuffer = new StringBuffer();
658        }
659
660        /** Read any remaining data in the input stream and return the
661         *  data read thus far.  Calling this method resets the
662         *  cache of data read thus far.
663         */
664        public String getAndReset() {
665            if (_debugging) {
666                try {
667                    _debug("getAndReset: Gobbler '" + getName() + "' Ready: "
668                            + _inputStreamReader.ready() + " Available: "
669                            + _inputStream.available());
670                } catch (Exception ex) {
671                    throw new InternalErrorException(ex);
672                }
673            }
674
675            try {
676                // Read any remaining data.
677                _read();
678            } catch (Throwable throwable) {
679                if (_debugging) {
680                    _debug("WARNING: getAndReset(): _read() threw an "
681                            + "exception, which we are ignoring.\n"
682                            + throwable.getMessage());
683                }
684            }
685
686            String results = _stringBuffer.toString();
687            _stringBuffer = new StringBuffer();
688
689            try {
690                _inputStreamReader.close();
691                _inputStreamReaderClosed = true;
692            } catch (Exception ex) {
693                throw new InternalErrorException(null, ex,
694                        getName() + " failed to close.");
695            }
696
697            return results;
698        }
699
700        /** Read lines from the inputStream and append them to the
701         *  stringBuffer.
702         */
703        @Override
704        public synchronized void run() {
705            if (!_inputStreamReaderClosed) {
706                _read();
707            }
708        }
709
710        // Read from the stream until we get to the end of the stream
711        // This is synchronized so that it is not called simultaneously
712        // from run() and getAndReset().
713        private synchronized void _read() {
714            // We read the data as a char[] instead of using readline()
715            // so that we can get strings that do not end in end of
716            // line chars.
717            char[] chars = new char[80];
718            int length; // Number of characters read.
719
720            try {
721                // Oddly, InputStreamReader.read() will return -1
722                // if there is no data present, but the string can still
723                // read.
724                while ((length = _inputStreamReader.read(chars, 0, 80)) != -1
725                        && !_stopRequested && !_stopFireRequested) {
726                    if (_debugging) {
727                        // Note that ready might be false here since
728                        // we already read the data.
729                        _debug("_read(): Gobbler '" + getName() + "' Ready: "
730                                + _inputStreamReader.ready() + " Value: '"
731                                + String.valueOf(chars, 0, length) + "'");
732                    }
733
734                    _stringBuffer.append(chars, 0, length);
735                }
736            } catch (Throwable throwable) {
737                // We set ignoreExceptionReadErrors to true for ExecDemos so that
738                // exporting html does not produce an error when the model exits after 30 seconds.
739                boolean ignoreIOExceptionReadErrorsValue = false;
740
741                try {
742                    ignoreIOExceptionReadErrorsValue = ((BooleanToken) ignoreIOExceptionReadErrors
743                            .getToken()).booleanValue();
744
745                } catch (IllegalActionException ex) {
746                    throw new InternalErrorException(_actor, ex, getName()
747                            + ": Could not get the value of the ignoreIOExceptionReadErrors "
748                            + "parameter while trying to throw " + throwable);
749                }
750                if (ignoreIOExceptionReadErrorsValue
751                        && throwable instanceof IOException) {
752                    new Exception("Warning: " + getFullName()
753                            + " had an exception, but "
754                            + "ignoreIOExceptionReadErrors was true and the exception was an "
755                            + "IOException, so it is being skipped.", throwable)
756                                    .printStackTrace();
757                } else {
758                    throw new InternalErrorException(_actor, throwable,
759                            getName() + ": Failed while reading from "
760                                    + _inputStream
761                                    + ". To avoid this, try setting the ignoreIOExceptionReadErrors parameter to true."
762                                    + throwable.getCause());
763                }
764            }
765        }
766
767        // The actor associated with this stream reader.
768        private Nameable _actor;
769
770        // Stream from which to read.
771        private InputStream _inputStream;
772
773        // Stream from which to read.
774        private InputStreamReader _inputStreamReader;
775
776        // Indicator that the stream has been closed.
777        private boolean _inputStreamReaderClosed = false;
778
779        // StringBuffer to update.
780        private StringBuffer _stringBuffer;
781    }
782
783    ///////////////////////////////////////////////////////////////////
784    ////                         private variables                 ////
785    // The subprocess gets its input from this BufferedWriter.
786    private BufferedWriter _inputBufferedWriter;
787
788    // StreamReader with which we read stderr.
789    private _StreamReaderThread _errorGobbler;
790
791    // StreamReader with which we read stdout.
792    private _StreamReaderThread _outputGobbler;
793
794    // The Process that we are running.
795    private Process _process;
796
797    // Indicator that stopFire() has been called.
798    private boolean _stopFireRequested = false;
799
800    // Instance count of output and error threads, used for debugging.
801    // When the value is greater than 1000, we reset it to 0.
802    private static int _streamReaderThreadCount = 0;
803}