Ausgabe von Runtime.exec() lesen

wargos

Grünschnabel
Hallo zusammen,

ich habe mit Swing einen Dialog gebastetelt, der beim Klick auf einen Button drei in C++ geschriebene Prozesse mit Runtime.exec() startet, zwei Render-Server und einen Client der ihnen die Grafikdaten bereitstellt.

In dem Dialog möchte ich in drei JTextAreas die Ausgaben dieser Prozesse als Log anzeigen lassen.

Wenn ich mich der Inputstreams, OutputStreams und ErrorStreams der Prozesse mit dispose entledige funktioniert alles tadellos, nur eben ohne die Möglichkeit die Ausgabe der Prozesse aufzuzeichen.

Erlaube ich z.B. den InputStream/Ausgabe des Clients ohne ihn auszulesen, so starten die Prozesse erst wenn ich den Dialog schließe.

Erlaube ich den InputStream/Ausgabe des Clients und lese ihn mit der Standardlösung mittels InputStream und BufferedReader aus, so wird die GUI meines Dialogs durch die while()-Schleife blockiert, mit der ich den Buffer auslesen lasse so lange der Client läuft.


Gibt es in Java so etwas wie eine onIdle-Funkion, die das Programm zyklisch durchläuft wenn es gerade nichts zu tun hat, wo ich meine Log auslesen und gleichzeitig in die TextArea schreiben lassen kann ohne dass dabei die GUI blockiert?

Oder gibt es für so etwas eine "Patentlösung"?


Hier noch der Code den ich bisher habe:


textAreaClientLog.setText("Status: wird gestartet...\n\n");

Process processClient = Runtime.getRuntime().exec(stringClientLaunch);

processClient.getErrorStream().close();
processClient.getOutputStream().close();

InputStream streamClient = processClient.getInputStream();

BufferedReader bufferClient = new BufferedReader(new InputStreamReader(streamClient));

while((stringClientLog = bufferClient.readLine()) != null)
{
textAreaClientLog.setText(textAreaClientLog.getText() + "\n" + stringClientLog);
}

processClient.getInputStream().close();


mfg wargos
 
Moin,
also erstmal würde ich dafür den ProcessBuilder nehmen ... damit kannst du dann z.B. den SubProcess killen.
Dann würde ich das Auslesen vom Process.getInputStream() und Process.getErrorStream() in 2 seperate Threads auslagern und mit invokeLater(Runnable) starten.
Dann solltest du natürlich drauf achten das du für jeden Process das Action-Handling ebenfalls in einen Thread auslagern. Dadurch solltest du zumindest verhindern das die GUI freezed.

PS : nutz doch bitte [code=java][/code] für Source-Codes
 
Hallo,

das mit dem Auslagern in separate Threads ist schön, aber man sollte sich besser mit dem SwingWorker auseinander setzen, der ist für solche Dinge denke ich, was GUI und Hintergrundaktivität angeht, besser geeignet. Er beachtet das Java Memory Model und sorgt dafür, dass seine Swing operationen auch dort ausgeführt werden wo sie es sollen im EDT. Durch die Struktur des Swingworkers macht man da schonmal viel weniger Fehler, als wenn man selbst Threads anstößt.
 
Zuletzt bearbeitet:
@mike0007:
Auch mit separaten Threads ist es möglich, alle Swing-Operationen ausschließlich im EDT auszuführen. Das Problem ist, dass der SwingWorker stark limitiert ist, also kaum Handlungsmöglichkeiten gibt.
 
Hallo,

ich benutze den swingworker in angepasster form , man kann damit sehr schöne frameworks realisieren, die man an seine komponenten hefetn kann, ich hab damit keinerlei probleme. Früher ohne den swingworker hat man sehr viele fehler gemacht, weil man sehr schnell vergessen hat ob man nun im edt und oder in einem anderen thread rumpfuscht. Aber ich denke, das ist geschmacks sache, sollte auch nur eine ergänzung sein.
 
Hallo,

ich habe es erst einmal mit dem SwingWorker versucht. Ich starte die Prozesse zunächst ganz normal und lasse den Inputstream mit einem swingworker auslesen. Auf einen ProcessBuilder habe ich verzichtet, da ich die Prozesse auch so killen kann.

Java:
try
{
	processClient = Runtime.getRuntime().exec(stringClientLaunch);
	processServer1 = Runtime.getRuntime().exec(stringServer1Launch);
	processServer2 = Runtime.getRuntime().exec(stringServer2Launch);

	processClient.getErrorStream().close();
	processClient.getOutputStream().close();

	processServer1.getOutputStream().close();
	processServer1.getErrorStream().close();
	processServer1.getOutputStream().close();

	processServer2.getOutputStream().close();
	processServer2.getErrorStream().close();
	processServer2.getOutputStream().close();

	workerClient.execute();
	workerServer1.execute();
	workerServer2.execute();
}
catch(IOException ioex)
{
	ioex.printStackTrace();
}



Die SingWorker haben diese Form:

Java:
SwingWorker<String, Void> workerClient = new SwingWorker<String, Void>()
{
	@Override
	protected String doInBackground()
	{
		String result = "";

		InputStream streamClient;

		BufferedReader bufferClient;

		try
		{
			textAreaClientLog.setText(textAreaClientLog.getText() + stringLogStarted);


			streamClient  = processClient.getInputStream();

			bufferClient = new BufferedReader(new InputStreamReader(streamClient));

			while(processClient != null)
			{
				if((result = bufferClient.readLine()) != null)
					textAreaClientLog.setText(textAreaClientLog.getText() + "\n" + result);
			}
		}
		catch(Exception err)
		{
			err.printStackTrace();
		}

		return result;
	}

	@Override
	protected void done()
	{
		if(processClient != null)
		{
			processClient.destroy();
			processClient = null;
		}

		textAreaClientLog.setText(textAreaClientLog.getText()
								+ stringLogInactive);
	}
};

Funktioniert für die Log des Clients wunderbar. Die Log wird synchron zum Prozess ausgegeben und die GUI wird nicht blockiert. Allerdings hängen die Ausgaben der Server immer noch hinterher. Diese erscheinen erst wenn die entsprechenden Prozesse terminiert werden.

Verlagere ich mehr als eine Input-Abfrage in einen SwingWorker, so bleiben zunächst die Logs nach kurzer Zeit hängen, und dann schließlich auch der Client und die Serverprozesse.
 
Bitte beachten, dass doInBackground() nicht im EDT ausgeführt wird! Dort gehören lange GUI blockierende Arbeiten hinein, wenn man von dort aus Änderungen an der GUI machen möchte, gehören dort wieder EventQueue.invokeLater() oder EventQueue.InvokeAndWait() hinein. Ist aber extrem unschön!

Dazu kann man mit den Funktionen publish() und process() zwischenergebnisse "veröffentlichen". Dazu müsste Void (2ter Generics Parameter) in deiner generics angabe des Swingworker wohl zu String geändert werden, dann kann man eben jede gelesene Zeile an die GUI übergeben. process() wird im EDT ausgeführt und wird immer dan ausgeführt, wenn man publish() in doInBackground() aufruft.

Übrigens die Methode get() kann man verwenden um Thread-save das Ergebniss von doInBackground() zu erhalten (auch Zwischenergebnisse) (Was hab ich da fürn Käse geschrieben? - Sorry es war spät)

Du solltest auf done() verzichten und mit publish() jede gelesene Zeile in process() mit deiner GUI Komponente aufrufen (also hier den Text an deine TextArea anhängen; dazu get() verwenden, um an die zwischenergebnisse zu gelangen).
 
Zuletzt bearbeitet:
Ich muss hier etwas ziemlich falsch machen. Ich habe jetzt process() und publish() eingebaut und done() rausgenommen mit dem Ergebnis, dass es tatsächlich schlechter funktioniert als vorher.

Die Prozesse starten und funktionieren normal, aber null Ausgabe in der Log da die GUI komplett einfriert.

Java:
SwingWorker workerClient = new SwingWorker<String, String>()
{
	@Override
	protected String doInBackground()
	{
		String result = "";

		InputStream streamClient;

		BufferedReader bufferClient;

		try
		{
			textAreaClientLog.setText(textAreaClientLog.getText() + stringLogStarted);


			streamClient  = processClient.getInputStream();

			bufferClient = new BufferedReader(new InputStreamReader(streamClient));

			while(processClient != null)
			{
				if((result = bufferClient.readLine()) != null)
					this.publish(result);
			}
		}
		catch(Exception err)
		{
			err.printStackTrace();
		}

		return result;
	}

	@Override
	protected void process(List<String> result)
	{
		try
		{
			textAreaClientLog.setText(textAreaClientLog.getText() + "\n" + this.get());
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
		catch(ExecutionException ex)
		{
			ex.printStackTrace();
		}
	}
};
 
Sorry ich hab oben was falsch wiedergegeben. get() gibt nur die Endergebnisse zurück!
Das führt zur Blockade. Hier die richtige Lösung. get() nur in done() benutzen, sonst gibt es glaube ich böse Dinge :) Nochn Tipp am Rande, alle Swing Listener werden automatisch im EDT ausgeführt.

TestProgramm:
Java:
package de;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JTextArea;

@SuppressWarnings("serial")
public class SwingTest extends JFrame
{

	private JTextArea	textAreaClientLog;
	private JButton		button;

	public static void main(String[] args)
	{
		EventQueue.invokeLater(new Runnable()
		{

			@Override
			public void run()
			{
				SwingTest t = new SwingTest();
				t.setVisible(true);

			}
		});

	}

	public SwingTest()
	{
		setLayout(new BorderLayout());
		setSize(600, 500);
		setDefaultCloseOperation(EXIT_ON_CLOSE);

		textAreaClientLog = new JTextArea();
		button = new JButton("Run me!");

		button.addActionListener(new ActionListener()
		{

			@Override
			public void actionPerformed(ActionEvent e)
			{
				MySwingWorker s = new MySwingWorker(textAreaClientLog);
				s.execute();
			}
		});

		add(button, BorderLayout.SOUTH);
		add(textAreaClientLog, BorderLayout.CENTER);

	}

}

Worker:
Java:
package de;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JTextArea;
import javax.swing.SwingWorker;

public class MySwingWorker extends SwingWorker<List<String>, String>
{

	private Process					processClient;
	private InputStream			streamClient;
	private BufferedReader	bufferClient;
	private JTextArea				textAreaClientLog;

	public MySwingWorker(JTextArea textAreaClientLog)
	{
		this.textAreaClientLog = textAreaClientLog;
		try
		{
			processClient = Runtime.getRuntime().exec("ping google.de /n 10");
			streamClient = processClient.getInputStream();
			bufferClient = new BufferedReader(new InputStreamReader(streamClient));

		}
		catch (Exception err)
		{
			err.printStackTrace();
		}

	}

	@Override
	protected List<String> doInBackground()
	{
		// Kein EDT, weil hier wird richtig lange und hart gearbeitet
		// also keine GUI-Zugriffe!
		String result = "";
		ArrayList<String> resultList = new ArrayList<String>();
		try
		{

			while (processClient != null)
			{
				if ((result = bufferClient.readLine()) != null)
				{
					// Jetzt sagen wir process() das Zwiechenergebniss
					// dort wird es dann im EDT verarbeitet
					publish(result);
					resultList.add(result);
				}
			}
		}
		catch (Exception err)
		{
			err.printStackTrace();
		}
		finally
		{

			processClient.destroy();

			close(streamClient);
			close(bufferClient);
		}

		return resultList;
	}

	@Override
	protected void process(List<String> result)
	{
		// Hier sind wir im EDT also GUI zugriffe erlaubt
		textAreaClientLog.setText(textAreaClientLog.getText() + "\n"
				+ result.get(0));

	}

	@Override
	protected void done()
	{
		// und hier sind wir wieder im EDT
		try
		{
			List<String> ergebnisse = get();

			// Was man auch immer damit tun will :)

		}
		catch (Exception e)
		{
			e.printStackTrace();
		}

		super.done();
	}

	private void close(InputStream in)
	{
		try
		{
			if (in != null)
				in.close();
		}
		catch (Exception e)
		{}
	}

	private void close(Reader in)
	{
		try
		{
			if (in != null)
				in.close();
		}
		catch (Exception e)
		{}
	}

}
 
Zuletzt bearbeitet:
Kein Problem aber erstmal ein großes Danke für die bisherige Hilfe.

Ich habe den SwingWorker nach deinem Beispiel mit leichten Änderungen umgesetzt. Anscheinend steht in der von process(List<String> result) empfangenden Liste jedoch nicht nur der zuletzt mit publish() verarbeitete String, sondern alle, die bis dato nicht von process() umgesetzt wurden.


SwingWorker:

Java:
import java.util.List;

import javax.swing.JTextArea;
import javax.swing.SwingWorker;

import java.io.InputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;


public class MyLogSwingWorker extends SwingWorker<String, String>
{
    private Process process;

    private InputStream streamOut;

    private BufferedReader bufferOut;
    
    private JTextArea textAreaLog;

    
    public MyLogSwingWorker(String stringLaunch, JTextArea textAreaLog)
    {
        this.textAreaLog = textAreaLog;

        try
        {
            process = Runtime.getRuntime().exec(stringLaunch);

            process.getOutputStream().close();
            process.getErrorStream().close();

            streamOut   = process.getInputStream();

            bufferOut   = new BufferedReader(new InputStreamReader(streamOut));
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
    }

    
    @Override
    protected String doInBackground()
    {
        String result = "";

        try
        {
            while(process != null)
            {
                if((result = bufferOut.readLine()) != null)
                {
                    publish(result);
                }
            }
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        finally
        {
            process.destroy();

            this.close(streamOut);
            this.close(bufferOut);
        }

        return result;
    }


    @Override
    protected void process(List<String> result)
    {
        for(int i = 0; i < result.size(); i++)
        {
             textAreaLog.setText(textAreaLog.getText() + "\n" + result.get(i));
        }
    }


    public boolean cancelProcess(boolean mayInterruptIfRunning)
    {
        if(process != null)
        {
            process.destroy();
            process = null;
        }

        return this.cancel(mayInterruptIfRunning);
    }


    private void close(InputStream stream)
    {
        try
        {
            if(stream != null)
                stream.close();
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
    }

    
    private void close(BufferedReader reader)
    {
        try
        {
            if(reader != null)
                reader.close();
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
    }
}



Der Aufruf in einer vom ActionListener aufgerufenen Funktion hat die folgende form:

Java:
processClient  = new MyLogSwingWorker(stringClientLaunch, textAreaClientLog);
processServer1 = new MyLogSwingWorker(stringServer1Launch, textAreaServer1Log);
processServer2 = new MyLogSwingWorker(stringServer2Launch, textAreaServer2Log);

processClient.execute();
processServer1.execute();
processServer2.execute();


Funktioniert zu 90%. Die GUI friert nicht ein, Client und Server werden ordentlich gestartet und laufen zuverlässig. Allerdings wird nur die Log des Clients in der entsprechenden JTextArea bzw. über System.out ausgegeben obwohl die Server laufen und ein zum Client identisches process() durchlaufen müssten.

Wenn die Prozesse von sich aus beendet werden, so erfolgt die Ausgabe der Serverlog dann im nachhinein.

Beende ich die Prozesse dagegen mit der GUI vorzeitig über das myCancel(), so bleibt eine Ausgabe bei den Server aus.

Eine Idee woran das liegen könnte?



[Update]

Ich habe eine Lösung gefunden. Anscheinend wird nicht jedes mal wenn SwingWorker.execute() aufgerufen wird auch ein neuer Thread im Swingworker-Thread-Pool gestartet. (siehe http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6880336)


Lösung:

Java:
ExecutorService executor = Executors.newCachedThreadPool();

processClient  = new MyLogSwingWorker(stringClientLaunch, textAreaClientLog);
processServer1 = new MyLogSwingWorker(stringServer1Launch, textAreaServer1Log);
processServer2 = new MyLogSwingWorker(stringServer2Launch, textAreaServer2Log);

executor.execute(processClient);
executor.execute(processServer1);
executor.execute(processServer2);


Und schon läuft alles einwandfrei.


Nochmals herzlichen Dank für die Hilfe.


mfg wargos
 
Zuletzt bearbeitet:
Zurück