1. Overview
The process API in Java had been quite primitive prior to Java 5, the only way to spawn a new process was to use the Runtime.getRuntime().exec() API. Then in Java 5, ProcessBuilder API was introduced which supported a cleaner way of spawning new processes.
Java 9 is adding a new way of getting information about current and any spawned processes.
In this article, we will look at both of these enhancements.
2. 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
Here’s how we can do that:
@Test public void givenCurrentProcess_whenInvokeGetInfo_thenSuccess() throws IOException { ProcessHandle processHandle = ProcessHandle.current(); ProcessHandle.Info processInfo = processHandle.info(); assertNotNull(processHandle.getPid()); assertEquals(false, processInfo.arguments().isPresent()); assertEquals(true, processInfo.command().isPresent()); assertTrue(processInfo.command().get().contains("java")); assertEquals(true, processInfo.startInstant().isPresent()); assertEquals(true, processInfo.totalCpuDuration().isPresent()); assertEquals(true, processInfo.user().isPresent()); }
It is important to note that java.lang.ProcessHandle.Info is a public interface defined within another interface java.lang.ProcessHandle. The JDK provider (Oracle JDK, Open JDK, Zulu or others) should provide implementations to these interfaces in such a way that these implementations return the relevant information for the processes.
3. Spawned Process Information
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.
The rest of the details remain the same as in the section above:
String javaCmd = ProcessUtils.getJavaCmd().getAbsolutePath(); ProcessBuilder processBuilder = new ProcessBuilder(javaCmd, "-version"); Process process = processBuilder.inheritIO().start(); ProcessHandle processHandle = process.toHandle();
4. Enumerating Live Processes in the System
We can list all the processes currently in the system, which are visible to the current process. The returned list is a snapshot at the time when the API was invoked, so it’s possible that some processes terminated after taking the snapshot or some new processes were added.
In order to do that, we can use the static method allProcesses() available in the java.lang.ProcessHandle interface which returns us a Stream of ProcessHandle:
@Test public void givenLiveProcesses_whenInvokeGetInfo_thenSuccess() { Stream<ProcessHandle> liveProcesses = ProcessHandle.allProcesses(); liveProcesses.filter(ProcessHandle::isAlive) .forEach(ph -> { assertNotNull(ph.getPid()); assertEquals(true, ph.info() .command() .isPresent()); }); }
5. Enumerating Child Processes
There are two variants to do this:
- get direct children of the current process
- get all the descendants of the current process
The former is achieved by using the method children() and the latter is achieved by using the method descendants():
@Test public void givenProcess_whenGetChildProcess_thenSuccess() throws IOException{ int childProcessCount = 5; for (int i = 0; i < childProcessCount; i++){ String javaCmd = ProcessUtils.getJavaCmd() .getAbsolutePath(); ProcessBuilder processBuilder = new ProcessBuilder(javaCmd, "-version"); processBuilder.inheritIO().start(); } Stream<ProcessHandle> children = ProcessHandle.current().children(); children.filter(ProcessHandle::isAlive) .forEach(ph -> log.info("PID: {}, Cmd: {}", ph.getPid(), ph.info().command())); // and for descendants Stream<ProcessHandle> descendants = ProcessHandle.current().descendants(); descendants.filter(ProcessHandle::isAlive) .forEach(ph -> log.info("PID: {}, Cmd: {}", ph.getPid(), ph.info().command())); }
6. Triggering Dependent Actions on Process Termination
We might want to run something on termination of the process. This can be achieved by using the onExit() method in the java.lang.ProcessHandle interface. The method returns us a CompletableFuture which provides the ability to trigger dependent operations when the CompletableFuture is completed.
Here, the CompletableFuture indicates the process has completed, but it doesn’t matter if the process has completed successfully or not. We invoke the get() method on the CompletableFuture, to wait for its completion:
@Test public void givenProcess_whenAddExitCallback_thenSuccess() throws Exception { String javaCmd = ProcessUtils.getJavaCmd() .getAbsolutePath(); ProcessBuilder processBuilder = new ProcessBuilder(javaCmd, "-version"); Process process = processBuilder.inheritIO() .start(); ProcessHandle processHandle = process.toHandle(); log.info("PID: {} has started", processHandle.getPid()); CompletableFuture<ProcessHandle> onProcessExit = processHandle.onExit(); onProcessExit.get(); assertEquals(false, processHandle.isAlive()); onProcessExit.thenAccept(ph -> { log.info("PID: {} has stopped", ph.getPid()); }); }
The onExit() method is available in the java.lang.Process interface as well.
7. Conclusion
In this tutorial, we covered interesting additions to the Process API in Java 9 that give us much more control over the running and spawned processes.
The code used in this article can be found over on GitHub.