Freitag, 25. März 2011

Böse Überraschungen bei lange laufenden Programmen, die mittels Runtime.exec() gestartet wurden

Heute möchte ich auf einen Fehler eingehen, den ich in den letzten Jahren bereits in verschiedenen Projekten entdeckt habe.
Manchmal ist es notwendig - aus einem Java-Programm heraus - externe Programme zu starten. Dafür gibt es die Methode Runtime.exec(). Die ruft man auf und übergibt das zu startende Programm. 
Soweit so gut. Allerdings gibt es da ein kleines Detail, welches oft übersehen wird. Selbst Programme die erstmal scheinbar erwartungsgemäß laufen, können deshalb nach einiger Zeit unerklärlich „blockieren“ oder gar abstürzen.
Was passiert da? 
Die meisten Programme schreiben Ausgaben oder Debug-Meldungen nach stdout bzw. stderr. Diese Ausgaben landen dann je nach Plattform in irgendwelchen Puffern des Betriebssystems. Was nun passiert ist vom jeweiligen Betriebsystem abhängig. Bei manchen Systemen kann der Puffer ins „Unendliche“ wachsen und wachsen. In diesem Fall könnte das System irgendwann ausbremst werden, da womöglich Speicher ausgelagert werden muss. Bei anderen Betriebsystemen ist der Puffer irgendwann voll und das externe Programm wird blockiert oder gar vom Betriebsystem beendet - oder verhält sich sonst wie merkwürdig.
Das Java-Programm sollte also regelmäßig den Inhalt von stdout und stderr „abholen“ damit es da keine Probleme gibt. Die folgende Klasse übernimmt das. Anwendungsbeispiele:
StreamConsumer.consumeOutputs(Runtime.getRuntime().exec(....));
Falls man nach Beendigung des Prozesses an ggf. aufgetretene Meldungen interessiert ist:
Process process = Runtime.getRuntime().exec(....); 
StreamConsumer errors = new StreamConsumer(process.getErrorStream()); 
StreamConsumer output = new StreamConsumer(process.getInputStream()); 
process.waitFor();
System.out.println(errors.getCollectedOutput());
System.out.println(output.getCollectedOutput());
Und nun die Klasse:
public class StreamConsumer {
    private final StreamConsumerImpl impl;
    static public void consumeOutputs(final Process process) {
        new StreamConsumer(process.getInputStream());
        new StreamConsumer(process.getErrorStream());
    }
    public StreamConsumer(InputStream inputStream) {
        this(inputStream, false);
    }
    public StreamConsumer(final InputStream inputStream, final boolean keepOutput) {
        impl = new StreamConsumerImpl(inputStream, keepOutput);
        Thread thread = new Thread(impl);
        thread.setName("StreamConsumer Thread");
        thread.start();
    }
    public String getCollectedOutput() {
        return impl.getCollectedOutput();
    }
    private final class StreamConsumerImpl implements Runnable {
        private final BufferedReader bufferedReader;
        private final StringBuffer collectedOutput;
        private StreamConsumerImpl(InputStream inputStream, boolean keepOutput) {
            this.bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            if (keepOutput == true) {
                collectedOutput = new StringBuffer();
            } else {
                collectedOutput = null;
            }
        }
        public void run() {
            try {
                String outputPiece;
                do {
                    outputPiece = bufferedReader.readLine();
                    if (collectedOutput != null && outputPiece != null) {
                        collectedOutput.append(outputPiece);
                    }
                } while (outputPiece != null);
            } catch (IOException ioe) {
                System.out.println("Konnte Ausgaben des Prozesses nicht abfragen");
                ioe.printStackTrace();
            } finally {
                if (bufferedReader != null) {
                    try {
                        bufferedReader.close();
                    } catch (IOException exception) {
                        System.out.println("Problem beim Schliessen des Ausgabe-Streams");
                        exception.printStackTrace();
                    }
                }
            }
        }
        private String getCollectedOutput() {
            return collectedOutput != null ? collectedOutput.toString() : null;
        }
    }
}

Keine Kommentare:

Kommentar veröffentlichen