Java Getriebe

Java und NetBeans

Externe Prozesse sicher starten

Wer von Java aus ein anderes Programm starten möchte kann dies recht einfach über den Befehl Runtime.exec erledigen. Seit Java 1.5 gibt es die Klasse [javadoc]java.lang.ProcessBuilder[/javadoc] mit der die Manipulation der Parameter für den externen Prozess noch einfacher von der Hand geht. Da auch Runtime.exec den ProcessBuilder intern verwendet sollte dieser im eigenen Quelltext immer bevorzugt werden.

Interessant wird diese Funktionalität aber erst, wenn man von Java aus andere Konsolenanwendungen starten möchte. Also Programme die keine graphische Benutzeroberfläche haben und ihre Ausgaben in die so genannte „Konsole“ schreiben. Unter Windows werden hier oft die Batchdateien verwendet, unter Linux sind es die Shellskripte. Für diese Anwendungsfälle ist es wichtig die Ausgaben dieser externen Prozesse korrekt zu verarbeiten um keine Dauerwartescheife zu erzeugen.

Für diesen Zweck habe ich mir eine Hilfsklasse geschrieben, die mir die Verwaltung der Threads etwas abnimmt. Ich möchte hier aber der Einfachheit halber nur einen Mini-Version davon zeigen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package de.nigjo.javagetriebe;

import java.io.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Erlaubt die einfache Verwaltung von externen Prozessen.
 *
 * @author Jens Hofschroeer, http://blog.nigjo.de/
 */

public class MiniShell
{
  private Process controller;
  private StreamReader stdout;
  private StreamReader stderr;

  private MiniShell()
  {
  }

  public static MiniShell exec(String... command)
          throws IOException
  {
    MiniShell shell = new MiniShell();

    ProcessBuilder builder = new ProcessBuilder(command);
    shell.registerController(builder.start());

    return shell;
  }

  public List<String> getOutputLines()
  {
    waitFor();
    return stdout.getLines();
  }

  private void registerController(Process external)
  {
    this.controller = external;
    stdout = startReader(controller.getInputStream());
    stderr = startReader(controller.getErrorStream());
  }

  private StreamReader startReader(InputStream inputStream)
  {
    StreamReader reader = new StreamReader(inputStream);
    reader.start();
    return reader;
  }

  public void waitFor()
  {
    try
    {
      controller.waitFor();
    }
    catch(InterruptedException ex)
    {
      Logger.getLogger(MiniShell.class.getName()).log(
              Level.SEVERE, null, ex);
    }
    stdout.waitFor();
    stderr.waitFor();
  }

  private static class StreamReader extends Thread
  {
    private final InputStream stream;
    private List<String> lines;

    public StreamReader(InputStream stream)
    {
      super("streamreader");
      this.stream = stream;
    }

    @Override
    public void run()
    {
      Scanner s = new Scanner(stream);
      while(s.hasNextLine())
      {
        if(lines == null)
          lines = new ArrayList<String>();
        lines.add(s.nextLine());
      }
    }

    public List<String> getLines()
    {
      return lines;
    }

    public void waitFor()
    {
      while(isAlive())
      {
        try
        {
          TimeUnit.MILLISECONDS.sleep(100);
        }
        catch(InterruptedException ex)
        {
          return;
        }
      }
    }

  }
}

Der Quelltext hier ist leider ohne Kommentare. Das wichtigste an dieser Klasse passiert eigentlich in der Methode registerController(). Hier werden zwei Threads gestartet, die jeweils den Standard Ausgabestrom bzw den Standard Fehlerausgabestrom übergeben bekommen. Diese interne Klasse StreamReader liest einfach mit Hilfe eines [javadoc]java.util.Scanner[/javadoc] alle Zeilen des externen Prozesses ein. Genau hiermit wird verhindert, dass der Textcache des Processes überläuft und das Programm anhält. Über die Methode getOutputLines() lässt sich diese Ausgaben an späterer Stelle ohne weiteres Auslesen.

Wie schon erwähnt bietet diese kleine Klasse ziemlich viel Platz für Erweiterungen. So lassen sich durchaus auch Listener definieren mit denen die Ausgabe „on the fly“ auswerten lassen. Ebenso könnte man andere exec() Methoden erstellen die es erlauben ein Startverzeichnis anzugeben oder die Umgebungsvariablen manipulieren. Wenn man den ProcessBuilder selber erstellen möchte und nur die Verwaltung der Ausgabe der Klasse überlassen möchte ist auch folgende Methode denkbar:

1
2
3
4
5
6
public static MiniShell manageProcess(Process external)
{
  MiniShell shell = new MiniShell();
  shell.registerController(external);
  return shell;
}

Wer mehr über den [javadoc]java.lang.ProcessBuilder[/javadoc] erfahren möchte kann auch auch das Kapitel 11.8 aus der „Java Insel“ von Christian Ullenboom ansehen. Dort sind auch noch ein paar Neuerungen die in Java 7 hinzugekommen sind näher beschrieben.

2 Responses to Externe Prozesse sicher starten

  1. Elmar says:

    Vermutlich sollte in registerController() „stderr = startReader(controller.getErrorStream())“ stehen anstatt „controller.getInputStream()“?

  2. Ups. Stimmt. Ich habe es oben korrigiert. Danke für den Hinweis, Elmar.