2.2 Process management in Java - ProcessBuilder and Process
PSP class notes by Vicente Martínez is licensed under CC BY-NC-SA 4.0
2.2 Process management in Java ProcessBuilder and Process
2.2.1 Preparation and setting of a process
The class to set the running attributes for a new process, before it is being run, is the ProcessBuilder
class.
This is an auxiliary class for the Process and is instantiated to manage a collection of process attributes. We can invoke the start
method to create a new process with the attributes defined by the instance of the ProcessBuilder class.
Repeated calls to the start method would create a new process with the same attributes.
The ProcessBuilder class defines two constructors, such as:
ProcessBuilder(List<String> command)
ProcessBuilder(String... command)
The meaning implied by the parameters passed to both constructors is same. In the first constructor, the command to be executed, along with command line arguments, is passed in a list of strings. And, in the second constructor, the command and the command line arguments are specified through the varargs parameter. We can use either of the constructors, depending upon the way to pass the parameter.
Arguments vs Parameters
If we want to launch a command with parameters, the command cannot be sent to ProcessBuilder in raw mode, it must be processed and converted into a List in order to make it work.
// Different modes to pass the command to ProcessBuilder constructors
// 1st mode: using a string. It fails with parameters,
// Only works with commands having arguments
String command1 = "notepad.exe prueba1.txt"
ProcessBuilder pb = new ProcessBuilder(command1);
// 2nd mode: using an array of strings. It also works with parameters
String[] command2 = {"cmd", "/c", "dir", "/o"};
ProcessBuilder pb = new ProcessBuilder(command2);
// 3rd mode: using a string and splitting it to convert into a List
String command3 = "c:/windows/system32/shutdown -s -t 0";
// Regular expresion \s means splitting the string by blank spaces
ProcessBuilder pb = new ProcessBuilder(command3.split("\\s"));
OS shutdown
You can use shutdown -s command to shutdown system. For windows OS, you need to provide full path of shutdown command e.g. C:\Windows\System32\shutdown.
Here you can use -s switch to shutdown system, -r switch to restart system, -h to put the system into hibernation, and -t switch to specify time delay.
Activity psp.activities.U2A1_Shutdowner
Create a new Java application project (package psp.activities & main class U2A1_Shutdowner) Using the command line, ask the user for the action he wants to do with the computer (shutdown ,restart or suspend) and how much time he needs before shutting down the system.
Find information about the shutdown command in GNU/Linux and make your app work in both systems.
Your app has to prepare the right command for the answers the user has given and for the OS it is running on.
Get the ProcessBuilder.command() result and show it on the console in a readable format.
U2A1_Shutdowner solution
public class U2A1_Shutdowner {
public static void main(String[] args) throws IOException {
// Ask for the required information to prepare the command
Scanner keyboard = new Scanner(System.in);
System.out.print("Select your option (s-shutdown / r-reboot / h-hibernate): ");
String shutdownOption = keyboard.nextLine();
System.out.print("How much seconds will the command wait to be run? (0 means immediately): ");
String shutdownTime = keyboard.nextLine();
// Prepare the command
String command;
if (System.getProperty("os.name").toLowerCase().startsWith("windows")) {
command = "C:/Windows/System32/shutdown -" + shutdownOption + " -t " + shutdownTime;
} else {
command = "shutdown -" + shutdownOption + " -t " + shutdownTime;
}
// Prepare the process and launch it
ProcessBuilder shutdowner = new ProcessBuilder(command.split("\\s"));
//shutdowner.start();
// Show the command to be run
System.out.print("El comando a ejecutar es: ");
for (String commandPart: shutdowner.command()) {
System.out.print(commandPart + " ");
}
System.out.println("");
}
}
Setting the command at runtime
If we want to set the command to be run at runtime, or at the time the ProcessBuilder instance is created we still don't know the command, it can be set later by using the command(String).
The same way as the constructors, we have two versions of command method
command(List<String> command)
command(String... command)
and there's also another command method, without parameters, to retrieve the command and parameters already set for the ProcessBuilder instance. Once we have the parameters list, we can modify it using List methods.
// Sets and modifies the command after ProcessBuilder object is created
String command = "java -jar install.jar -install"; // tmp dir is missing
ProcessBuilder pbuilder = new ProcessBuilder(command.split("\\s"));
if (isWindows) {
pbuilder.command().add(0, "cmd"); // Sets the 1st element
pbuilder.command().add(1, "/c"); // Sets the 2nd element
pbuilder.command().add("c:/temp"); // Sets the last element
// Command to run cmd /c java -jar install.jar -install c:/temp
} else {
pbuilder.command().add(0, "sh"); // Sets the 1st element
pbuilder.command().add(1, "-c"); // Sets the 2nd element
pbuilder.command().add("/tmp"); // Sets the last element
// Command to run: sh -c java -jar install.jar -install /tmp
}
// Starts the process
pbuilder.start();
Additional settings for a process
Some of the settings that can be changed for a process are:
Set the working directory where the process will be run We can override the default working directory of the current process by calling the directory method and passing a File object. By default, the current working directory is set to the value returned by the user.dir system property.
// Change working directory for the running process pbuilder.directory(new File(System.getProperty("user.home")));
Set-up a custom key-value map and modify an existing one using builder.environment()
// Retrieve and modify the process environment Map<String, String> environment = pbuilder.environment(); // Get the PATH environment variable and add a new directory String systemPath = environment.get("path") + ";c:/users/public"; environment.replace("path", systemPath); // Add a new environment variable and use it as a part of the command environment.put("GREETING", "Hola Mundo"); processBuilder.command("/bin/bash", "-c", "echo $GREETING");
Environment variables vs System properties
With Runtime we also accessed System properties, that are different from this environment ones.
Redirect input and output streams to custom replacements
Inherit both of them to the streams of the current JVM process using builder.inheritIO()
This two settings will be covered later in this unit.
Activity psp.activities.U2A2_WorkingDirectory
Create a new Java application project (package psp.activities & main class U2A2_WorkingDirectory ) Prepare a process to run the dir/ls command to check that the directory listing is for the directory pointed by the user.dir property. In the same application, change the value for the user.dir property. Finally, set a working directory for the process.
Print the user.dir environment value for the three scenarios after being changed.
U2A2_WorkingDirectory solution
public class U2A2_WorkingDirectory {
public static void main(String[] args) throws IOException, InterruptedException {
// Prepare the command
String command;
if (System.getProperty("os.name").toLowerCase().startsWith("windows")) {
command = "cmd /c dir";
} else {
command = "sh -c ls";
}
//1st - Default working directory
// Prepare the process
ProcessBuilder commander = new ProcessBuilder(command.split("\\s"));
commander.inheritIO();
// Show Process and System properties
System.out.println("Working directory: " + commander.directory());
System.out.println("user.dir variable: " + System.getProperty("user.dir"));
// Launch the process and show its result
// Working directory is null but the process is run on the current dir
commander.start().waitFor();
//2nd - Change user.dir but not the working directory
// Change the user.dir system property
System.setProperty("user.dir", System.getProperty("user.home"));
// Prepare the process
commander = new ProcessBuilder(command.split("\\s"));
commander.inheritIO();
// Show Process and System properties
System.out.println("Working directory: " + commander.directory());
System.out.println("user.dir variable: " + System.getProperty("user.dir"));
// Launch the process and show its result
// Working directory is null but the process is run on the current dir
commander.start().waitFor();
// 3rd - Change the working directory
// Prepare the process
commander = new ProcessBuilder(command.split("\\s"));
commander.inheritIO();
// Show Process and System properties
commander.directory(new File(System.getProperty("user.home")));
System.out.println("Working directory: " + commander.directory());
System.out.println("user.dir variable: " + System.getProperty("user.dir"));
// Launch the process and show its result
// Working directory is user.home and the process is run on it
commander.start().waitFor();
}
}
2.2.2 Process control from parent
The Process
is an abstract class defined in the java.lang package that encapsulates the runtime information of a program in execution. The start
method invoked by the ProcessBuilder class returns a reference to this class instance. There is an another way to create an instance of this class, through the exec
method of the Runtime instance.
The methods defined by the Process class can be used to perform input/output operations from the process, check the exit status of the process, wait for it to complete, and terminate the process. These methods, however, are not built to work on special processes of the native platform like daemon processes, shell scripts, and so on.
Input/ output from the child process
Intriguingly, the process created by the start() method does not own a console. Instead, it redirects (stdin, stdout, stderr) to the parent process. If need be, we can access them via streams obtained using methods defined in the class, such as getInputStream(), getOutputStream() and getErrorSteam(). These are the ways we can feed input to and get results from the sub processes.
Some of the common methods defined in this class are:
method | Description |
---|---|
int exitValue() | Exit code returned from the process executed |
Boolean isAlive() | Checks if the invoking process is still running. |
int waitFor() | Parent process waits for the child process to end. The integer value returned by the method is the exit code by the process. |
Boolean waitFor(long timeOut, TimeUnit unit) | Overloaded method of previous one. We can specify the wait time. This method returns true if the process has terminated and false if timeout has occurred. |
void destroy() | These two methods are used to kill or terminate the process. One, the second, just does it forcibly. |
Process destroyForcibly() |
Let’s write a simple Java program to open an application as a separate process. After it is opened, the program would wait for, say, 10 seconds and then destroy the process, which will immediately close the application.
public class ProcessDemo {
public static void main(String[] args) throws Exception {
ProcessBuilder pb = new ProcessBuilder("C:/Program Files (x86)/Notepad++/notepad++.exe");
// Effectively launch the process
Process p = pb.start();
// Check is process is alive or not
boolean alive = p.isAlive();
// Wait for the process to end for 10 seconds.
if (p.waitFor(10, TimeUnit.SECONDS)) {
System.out.println("Process has finished");
} else {
System.out.println("Timeout. Process hasn't finished");
}
// Force process termination.
p.destroy();
// Check again if process remains alive
alive = p.isAlive();
// Get the process exit value
int status = p.exitValue();
}
}
Exit codes
An exit code, or sometimes known as a return code, is the code returned to a parent process by an executable. The standard exit code is 0 for success and any number from 1 to 255 for anything else.
Activity psp.activities.U2A3_ExitValue
Create a new Java application project (package psp.activities & main class U2A3_ExitValue) Prepare a process to run different commands (notepad, calc, shell commands) one after each other, and make your application get their exit code. Print it.
Commands can be hardcoded. As an optional improvement for this activity you can ask the user for the command and make your app interactive. There must be an option to exit the app (empty command for instance).
Try with non-existing applications or using wrong arguments/parameters for commands.
Can you force a process not to be successful?
How can you know your own process exit code?
U2A3_ExitValue solution
public class U2A3_ExitValue {
public static void main(String[] args) {
do {
// Código para pedir un programa/comando a ejecutar
Scanner teclado = new Scanner(System.in);
System.out.println("Introduce el programa / comando que quieres ejecutar (intro para acabar): ");
String comando = teclado.nextLine();
if (comando.equals("")) System.exit(0);
try {
// Preparamos el entrono de ejecución del proceso
// Como no sabemos el contenido del comando, forzamos su conversión
// a una lista para que no haya problemas con su ejecución
ProcessBuilder pb = new ProcessBuilder(comando.split("\\s"));
// Lanzamos el proceso hijo
Process p = pb.start();
// Esperamos a que acabe para recoger el valor de salida
int exitValue = p.waitFor();
if (exitValue == 0) {
System.out.println("El comando " + pb.command().toString() + " ha finalizado bien");
} else {
System.out.println("El comando " + pb.command().toString() + " ha finalizado con errores. Código (" + exitValue + ")");
}
} catch (InterruptedException | IOException ex) {
System.err.println(ex.getLocalizedMessage());
ex.printStackTrace();
}
} while (true);
}
}
Exceptions management
Call to method waitFor implies that the parent process gets locked until child process ends, or until a signal from the system (Exception) is received.
It's better to handle exceptions than to throw them to upper levels.
2.2.3 Spawn a java application from a class into the same project
For some activities you'll be required to create the parent and the child processes as Java applications. Then, from one the classes you will need to launch the other one.
This implies that both classes are gonna have a main method. So, in the project properties we'll need to set which is the main class that will be run first, usually the Launcher
class (parent process).
Before one class can run the other, at least the second one (child process) must be compiled, that is, the .class file has to be generated into the build/classes directory.
Them and only then we can set the process environment to spawn a new process from an existing class. Here is the sample code
// Prepare the environment and the command
ProcessBuilder pb = new ProcessBuilder("java", "psp.u2.actividad10.Sumador");
pb.directory(new File("build/classes"));
Process p = pb.start();
Activity psp.activities.U2A4_Launcher
Create a new Java application project (package psp.activities & main class U2A4_Launcher).
Into the project create another class, U2A4_Commander with a main method that receives a program name as a unique parameter in the main. Make this application to create and run a process for the program and wait until that process has finished.
This class will return always the same value the launched program did.
System.exit() method
- Zero. The zero status code should be used when the program execution went fine, i.e., the program is terminated successfully.
- Non-Zero. A nonzero status code indicates abnormal termination. Java allows us to use different values for different kinds of errors.
Now, make the U2A4_Launcher class ask the user for an application name and launch the Commander class passing it the name of the application entered by the user.
Get the exitValue from Commander and show it's value, telling if the process worked fine or if it failed.
U2A4_Launcher solution
public class U2A4_Launcher {
public static void main(String[] args) {
// Código para pedir un programa/comando a ejecutar
Scanner teclado = new Scanner(System.in);
System.out.println("Introduce el programa / comando que quieres ejecutar: ");
String comando = teclado.nextLine();
try {
// Preparamos el entrono de ejecución del proceso
// Como no sabemos el contenido del comando, forzamos su conversión
// a una lista para que no haya problemas con su ejecución
comando = "java psp.activities.U2A4_Commander " + comando;
ProcessBuilder pb = new ProcessBuilder(comando.split("\\s"));
pb.directory(new File("build/classes"));
// Lanzamos el proceso hijo
Process p = pb.start();
// Esperamos a que acabe para recoger el valor de salida
int exitValue = p.waitFor();
if (exitValue == 0) {
System.out.println("El comando " + pb.command().toString() + " ha finalizado bien");
} else {
System.out.println("El comando " + pb.command().toString() + " ha finalizado con errores. Código (" + exitValue + ")");
}
} catch (InterruptedException | IOException ex) {
System.err.println(ex.getLocalizedMessage());
ex.printStackTrace();
}
}
}
public class U2A4_Commander {
public static void main(String[] args) throws Exception {
// Lectura de información desde los parámetros de entrada
// Se supone que recibimos: args[0] args[1] args[2] ..... args[args.length-1 --> comando a ejecutar
String comando = "";
for (int i = 0; i < args.length; i++) {
comando += args[i] + " ";
}
comando.trim();
ProcessBuilder pb = new ProcessBuilder(comando.split("\\s"));
// Lanzamos el proceso hijo
Process p = pb.start();
// Esperamos a que acabe para recoger el valor de salida
int exitValue = p.waitFor();
System.exit(exitValue);
}
}
Child classes programming
Every class must be coded to be run independently or just as a child process. Think that in Netbeans all classes are run as child classes from the IDE.
That's why the code, child or parent, needs to be done without thinking how they are gonna be called. The code must be independent just like processes are one from each other.
2.2.4 Current Java Process Information
We can now obtain a lot of information about the process via the API java.lang.ProcessHandle.Info
API:
- the command used to start the process
- the arguments of the command
- time instant when the process was started
- total time spent by it and the user who created it
Getting information about the current process
Here's how we can do that for the current process:
// Get information about the current process
ProcessHandle processHandle = ProcessHandle.current();
ProcessHandle.Info processInfo = processHandle.info();
System.out.println("PID: " + processHandle.pid());
System.out.println("Arguments: " + processInfo.arguments());
System.out.println("Command: " + processInfo.command());
System.out.println("Instant: " + processInfo.startInstant());
System.out.println("Total CPU duration: " + processInfo.totalCpuDuration());
System.out.println("User: " + processInfo.user());
Getting information about a child process from parent
It is also possible to get the process information of a newly spawned process. In this case, after we spawn the process and get an instance of the java.lang.Process
, we invoke the toHandle()
method on it to get an instance of java.lang.ProcessHandle.
// Get information about a child process from parent
Process process = processBuilder.inheritIO().start();
ProcessHandle childProcessHandle = process.toHandle();
ProcessHandle.Info childProcessInfo = childProcessHandle.info();
The rest of the details remain the same as in the section above