Over the past year, several microservices I have worked on responded to specific events and then executed native OS processes, for example launching custom C++ applications, Python scripts, etc. In addition to simply launching processes, those services also needed to obtain information for executing processes upon request, or shut down processes upon receiving shut down events. A lot of what the services were doing was controlling native processes in response to specific external events, whether via JMS queues, Kafka topics, or even XML files dropped in specific directories.

Since the microservices were implemented in Java, I had to use the less-than-stellar Process API, which provides only the most basic support. Even though a few additional features were added in Java 8 - such as being able to check if a process is alive using Process#isAlive and waiting for process exit with a timeout - you still cannot obtain a handle to a running process by its process ID nor can you even get the process ID of a Process object. As a result of the limitations I wrote a bunch of utilities that basically call out to native programs like grep and pgrep to gather information on running processes, child processes for a specific process ID, and so on. Even worse, to simply find the process ID for a Process instance I used reflection to directly access the private pid field in the java.lang.UNIXProcess class (which first required checking that we were actually dealing with a UNIXProcess instance, by comparing the class name as a string, since UNIXProcess is package-private and thus you cannot obtain its Class instance).

Most people writing and talking about Java 9 are excited about things like the new module system in Project Jigsaw; the Java shell/REPL; the HTTP/2 client; convenience factory methods for collections; and so on. But I am maybe even more excited about the process API improvements, since it means I can get rid of a lot of the hackery I used to obtain process information. Some of the information you can now obtain from a Process instance includes:

  • Whether the process supports normal termination (i.e. any of the "non-forcible" kill signals in Linux)
  • The process ID (i.e. the "pid"), and yes it's about time
  • A handle to the current process
  • A handle to the parent process, if one exists
  • A stream of handles to the direct children of the process
  • A stream of handles to the descendants (direct children, their children, and so on recursively)
  • A stream of handles to all processes visible to the current process
  • Process metadata such as the full command line, arguments, start instant, owning user, and total CPU duration

For example, to obtain the process ID (written as a unit test, and using AssertJ assertions):


@Test
public void getPid() throws IOException {
    ProcessBuilder builder = new ProcessBuilder("/bin/sleep", "5");
    Process proc = builder.start();
    assertThat(proc.getPid()).isGreaterThan(0);
}

Or, to obtain all sorts of different process metadata using ProcessHandle (which is also new in JDK 9 via the info() method in Process):


@Test
public void processInfo() throws IOException {
    ProcessBuilder builder = new ProcessBuilder("/bin/sleep", "5");
    Process proc = builder.start();
    ProcessHandle.Info info = proc.info();
    assertThat(info.arguments().orElse(new String[] {})).containsExactly("5");
    assertThat(info.command().orElse(null)).isEqualTo("/bin/sleep");
    assertThat(info.commandLine().orElse(null)).isEqualTo("/bin/sleep 5");
    assertThat(info.user().orElse(null)).isEqualTo(System.getProperty("user.name"));
    assertThat(info.startInstant().orElse(null)).isLessThanOrEqualTo(Instant.now());
}

Note in the above test that every method in the ProcessHandle.Info returns an Optional, which is the reason for the orElse in the assertions. Another thing that I really needed - and thankfully JDK 9 now provides - is the ability to get a handle to an existing process by its process ID using the ProcessHandle#of method. Here is a simple example as a unit test:


@Test
public void getProcessHandleForExistingProcess() throws IOException {
    ProcessBuilder builder = new ProcessBuilder("/bin/sleep", "5");
    Process proc = builder.start();
    long pid = proc.getPid();

    ProcessHandle handle = ProcessHandle.of(pid).orElseThrow(IllegalStateException::new);
    assertThat(handle.getPid()).isEqualTo(pid);
    assertThat(handle.info().commandLine().orElse(null)).isEqualTo("/bin/sleep 5");
}

As with the ProcessHandle.Info methods, ProcessHandle#of returns an Optional so again that is the reason for the orElseThrow. In a real application you might take some other action if the returned Optional is empty, or maybe you just throw an exception as the above test does.

As a last example, here is a test that launches a sleep process, then streams all visible processes and finds the launched sleep process:


@Test
public void allProcesses() throws IOException {
    ProcessBuilder builder = new ProcessBuilder("/bin/sleep", "5");
    builder.start();

    String sleep = ProcessHandle.allProcesses()
            .map(handle -> handle.info().command().orElse(String.valueOf(handle.getPid())))
            .filter(cmd -> cmd.equals("/bin/sleep"))
            .findFirst()
            .orElse(null);
    assertThat(sleep).isNotNull();
}

In the above test, since allProcesses returns a Stream we can use normal Java 8 stream API features like map, filter, and so on. In this example, we first map (transform) the ProcessHandle to the command (i.e. "sleep") or the process ID if the command Optional is empty. Next we filter on whether the command equals /bin/sleep and call findFirst which returns an Optional, and finally use orElse to return null if the returned Optional was empty. Of course the above test can fail if, for example, there already happens to be a /bin/sleep 5 process executing in the operating system but we won't really worry about that here.

One last piece of information that might be needed is the current process, i.e. a process needs get a handle to its own process. You can now accomplish this easily by calling ProcessHandle.current(). The Javadoc notes that you cannot use the returned handle to destroy the current process, and says to use System#exit instead.

In addition to the process information shown in the above examples, there is also a new onExit method that returns a CompletableFuture that "provides the ability to trigger dependent functions or actions that may be run synchronously or asynchronously upon process termination" according to the Javadoc. The following example shows an example that uses the native cmp program to compare two files, and upon exit applies a lambda expression to check whether the exit value is zero (meaning the two files are identical). Finally, it uses the Future#get method with a 1 second timeout (to avoid blocking indefinitely) to obtain the result:


Process proc = new ProcessBuilder("/usr/bin/cmp", "/tmp/file1.txt", "/tmp/file2.txt").start();
Future<Boolean> areIdentical = proc.onExit().thenApply(proc1 -> proc1.exitValue() == 0);
if (areIdentical.get(1, TimeUnit.SECONDS)) { ... }

So a big thanks to the Java team at Oracle (I can't believe I just thanked Oracle) for adding these new features! In the "real world" where systems are heterogenous and need to integrate in myriad ways, having a much more featureful and robust process API helps a lot for any system that needs to launch, monitor, and destroy native processes.

Comment