Java 9's other new enhancements, Part 3: The Process API

JEP 102 updates the Process API, making it easier to access process data and more

javaqa j9 process api
Credit: Jer Thorp/Flickr

JEP 102: Process API Updates enhances the java.lang.Process class and introduces the java.lang.ProcessHandle interface with its nested Info interface to overcome limitations that often force developers to resort to native code; for example, to obtain the native process ID (PID). This post introduces you to these upgrades.

Enhancing Process, and introducing ProcessHandle and ProcessHandle.Info

Java 9 adds several new methods to the abstract Process class that let you identify direct child or descendent processes, obtain this Process's PID, return a snapshot of information about this Process, obtain a completable future to receive asynchronous notification when this Process exits, and more:

  • Stream<ProcessHandle> children()
  • Stream<ProcessHandle> descendants()
  • long getPid()
  • ProcessHandle.Info info()
  • CompletableFuture<Process> onExit()
  • boolean supportsNormalTermination()
  • ProcessHandle toHandle()

More than half of these methods work with the new ProcessHandle interface, which identifies and provides control of native processes. For example, toHandle() returns an object whose class implements ProcessHandle, and which is associated with this Process. ProcessHandle's methods are listed below:

  • static Stream<ProcessHandle> allProcesses()
  • Stream<ProcessHandle> children()
  • int compareTo(ProcessHandle other)
  • static ProcessHandle current()
  • Stream<ProcessHandle> descendants()
  • boolean destroy()
  • boolean destroyForcibly()
  • long getPid()
  • ProcessHandle.Info info()
  • boolean isAlive()
  • static Optional<ProcessHandle> of(long pid)
  • CompletableFuture<ProcessHandle> onExit()
  • Optional<ProcessHandle> parent()
  • boolean supportsNormalTermination()

Various Process methods delegate to their ProcessHandle counterparts by invoking toHandle() followed by the method name. For example, getPid() invokes toHandle().getPid() and info() invokes toHandle().info(), which returns a ProcessHandle.Info object. The nested Info interface provides the following methods:

  • Optional<String[]> arguments()
  • Optional<String> command()
  • Optional<String> commandLine()
  • Optional<Instant> startInstant()
  • Optional<Duration> totalCpuDuration()
  • Optional<String> user()

Each method returns a java.util.Optional instance that may contain a non-null object reference or null, and is useful for avoiding java.lang.NullPointerException. You'll learn how to work with these methods along with various ProcessHandle and new Process methods in subsequent sections.

Obtaining the PID

Process's long getPid() method returns the PID of the invoking process. The method's return type is long instead of int because PIDs are unsigned integers, the largest positive int value is around 2 million, and Linux can accommodate PIDs up to around 4 million. Listing 1 demonstrates getPid().

Listing 1. Obtaining and outputting a PID

import java.io.IOException;

public class ProcessDemo
{
   public static void main(String[] args) throws IOException
   {
      Process p = new ProcessBuilder("notepad.exe").start();
      System.out.println(p.getPid());
   }
}

The java.lang.ProcessBuilder class (introduced in Java 5) constructs a process builder for the Windows notepad.exe program. The start() method is invoked to start notepad.exe, returning a Process object to interact with the new process. Process's getPid() method is subsequently invoked on the Process object and its value is output.

Compile Listing 1 as follows:

javac ProcessDemo.java

Run the resulting application as follows:

java ProcessDemo

I observed a new window for notepad.exe along with an unsigned integer that varies from run to run. Here's an example:

9480

Perhaps you're wondering what happens with getPid() when the process cannot be started or it terminates before this method is called. In the first case, start() throws java.io.IOException. In the second case, getPid() continues to return the PID after the process terminates.

Obtaining process information

ProcessHandle.Info defines several methods that return process information, such as the process's executable pathname, the process's start time, and the process's user. Listing 2 presents the source code to an application that dumps this and other information on the current process and another process to the standard output.

Listing 2. Obtaining and outputting process information

import java.io.IOException;

import java.time.Duration;
import java.time.Instant;

public class ProcessDemo
{
   public static void main(String[] args) 
      throws InterruptedException, IOException
   {
      dumpProcessInfo(ProcessHandle.current());
      Process p = new ProcessBuilder("notepad.exe", "C:\\temp\\names.txt").start();
      dumpProcessInfo(p.toHandle());
      p.waitFor();
      dumpProcessInfo(p.toHandle());
   }

   static void dumpProcessInfo(ProcessHandle ph)
   {
      System.out.println("PROCESS INFORMATION");
      System.out.println("===================");
      System.out.printf("Process id: %d%n", ph.getPid());
      ProcessHandle.Info info = ph.info();
      System.out.printf("Command: %s%n", info.command().orElse(""));
      String[] args = info.arguments().orElse(new String[]{});
      System.out.println("Arguments:");
      for (String arg: args)
         System.out.printf("   %s%n", arg);
      System.out.printf("Command line: %s%n", info.commandLine().orElse(""));
      System.out.printf("Start time: %s%n", 
                        info.startInstant().orElse(Instant.now()).toString());
      System.out.printf("Run time duration: %sms%n",
                        info.totalCpuDuration()
                            .orElse(Duration.ofMillis(0)).toMillis());
      System.out.printf("Owner: %s%n", info.user().orElse(""));
      System.out.println();
   }
}

main() first invokes ProcessHandle.current() to obtain the current process's handle and passes this handle to a dumpProcessInfo() invocation to dump information about this process. It next launches notepad.exe, dumping its process information. After waiting for notepad.exe to terminate, main() dumps its information a second time.

dumpProcessInfo() first outputs a header, outputs the PID, and obtains the process handle's Info object. Next, it invokes command() and other Info methods, outputting their values. If the method would return null (because the information isn't available), the value passed to Optional's orElse() method is returned instead.

Compile Listing 2 and run the resulting application. I observed a new window for notepad.exe, and also observed the following output from one run:

PROCESS INFORMATION
===================
Process id: 1140
Command: C:\PROGRA~1\Java\jdk-9\bin\java.exe
Arguments:
Command line: 
Start time: 2017-03-02T22:24:40.998Z
Run time duration: 890ms
Owner: jeff\jeffrey

PROCESS INFORMATION
===================
Process id: 5516
Command: C:\Windows\System32\notepad.exe
Arguments:
Command line: 
Start time: 2017-03-02T22:24:41.763Z
Run time duration: 0ms
Owner: jeff\jeffrey

PROCESS INFORMATION
===================
Process id: 5516
Command: 
Arguments:
Command line: 
Start time: 2017-03-02T22:24:41.763Z
Run time duration: 234ms
Owner: jeff\jeffrey

The third PROCESS INFORMATION section doesn't appear until the window is dismissed. Info's arguments() method doesn't return the C:\temp\names.txt command-line argument, possibly because the information isn't available, or possibly because of a bug. After the process terminates, command() returns null. Finally, commandLine() returns null when either command() or arguments() returns null.

Obtaining information on all processes

ProcessHandle's allProcesses() method returns a Java 8 Streams API stream of process handles describing all processes visible to the current process (that is, the process invoking allProcesses()). Listing 3's application source code uses Streams to obtain these handles and, for each of the first four handles, dump information about the process.

Listing 3. Obtaining and outputting information on four processes visible to the current process

import java.io.IOException;

import java.time.Duration;
import java.time.Instant;

public class ProcessDemo
{
   public static void main(String[] args)
   {
      ProcessHandle.allProcesses()
                   .filter(ph -> ph.info().command().isPresent())
                   .limit(4)
                   .forEach((process) -> dumpProcessInfo(process));
   }

   static void dumpProcessInfo(ProcessHandle ph)
   {
      System.out.println("PROCESS INFORMATION");
      System.out.println("===================");
      System.out.printf("Process id: %d%n", ph.getPid());
      ProcessHandle.Info info = ph.info();
      System.out.printf("Command: %s%n", info.command().orElse(""));
      String[] args = info.arguments().orElse(new String[]{});
      System.out.println("Arguments:");
      for (String arg: args)
         System.out.printf("   %s%n", arg);
      System.out.printf("Command line: %s%n", info.commandLine().orElse(""));
      System.out.printf("Start time: %s%n", 
                        info.startInstant().orElse(Instant.now()).toString());
      System.out.printf("Run time duration: %sms%n",
                        info.totalCpuDuration()
                            .orElse(Duration.ofMillis(0)).toMillis());
      System.out.printf("Owner: %s%n", info.user().orElse(""));
      System.out.println();
   }
}

main() invokes allProcesses() and chains the resulting process handles stream to a filter that yields a new stream of process handles where a process executable pathname is present. (On my platform, the pathname isn't present when the process has terminated.) The limit(4) call yields a truncated stream of no more than four process handles. Finally, the forEach() call invokes dumpProcessInfo() on each process handle.

Compile Listing 3 and run the resulting application. I observed the followed output from one run:

PROCESS INFORMATION
===================
Process id: 8036
Command: C:\Windows\explorer.exe
Arguments:
Command line: 
Start time: 2017-03-02T16:21:14.436Z
Run time duration: 299328ms
Owner: jeff\jeffrey

PROCESS INFORMATION
===================
Process id: 10200
Command: C:\Windows\System32\dllhost.exe
Arguments:
Command line: 
Start time: 2017-03-02T16:21:16.255Z
Run time duration: 2000ms
Owner: jeff\jeffrey

PROCESS INFORMATION
===================
Process id: 1544
Command: C:\Program Files (x86)\WNSS\WNSS.exe
Arguments:
Command line: 
Start time: 2017-03-02T16:21:21.708Z
Run time duration: 862375ms
Owner: jeff\jeffrey

PROCESS INFORMATION
===================
Process id: 8156
Command: C:\Users\jeffrey\AppData\Local\SweetLabs App Platform\Engine\ServiceHostAppUpdater.exe
Arguments:
Command line: 
Start time: 2017-03-02T16:21:24.302Z
Run time duration: 2468ms
Owner: jeff\jeffrey

ProcessHandle's children() and descendents() methods behave similarly to allProcesses() except that they are non-static and, respectively, return process handles for direct children of and direct children plus their descendents (and their descendents, and so on) of the current process.

Triggering actions on process termination

Finally, ProcessHandle's onExit() method makes it possible for a thread to use this method's returned java.util.concurrent.CompletableFuture object to trigger actions to run synchronously or asynchronously when the process on which they depend terminates. Check out Listing 4.

Listing 4. Obtaining and outputting a process's PID when the process terminates

import java.io.IOException;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class ProcessDemo
{
   public static void main(String[] args) 
      throws ExecutionException, InterruptedException, IOException
   {
      Process p = new ProcessBuilder("notepad.exe").start();
      ProcessHandle ph = p.toHandle();
      CompletableFuture<ProcessHandle> onExit = ph.onExit();
      onExit.get();
      onExit.thenAccept(ph_ -> System.out.printf("PID %d terminated%n", 
                                                ph_.getPid()));
   }
}

main() first creates and starts a notepad.exe process. Next, it obtains this process's handle and uses this handle to obtain a completable future for the process. The onExit.get() call causes main() to wait for process termination, at which point thenAccept() executes its lambda, producing output similar to that shown here:

PID 7460 terminated

Conclusion

Java 9's Process API enhancements are a long overdue and welcome addition to Java. Although I've finished my introduction to these enhancements, there's more for you to explore. For example, check out Process's and ProcessHandle's supportsNormalTermination() method, and ProcessHandle's parent() method.

download
Get the source code for this post's applications. Created by Jeff Friesen for JavaWorld

The following software was used to develop the post's code:

  • 64-bit JDK 9ea+154

The post's code was tested on the following platform(s):

  • JVM on 64-bit Windows 8.1