javaprocessbuilderprocesshandle

"No value present" when calling `ProcessHandle`'s `parent().get()`


import java.lang.ProcessBuilder.Redirect;

public class Main {    
  public static void main(String args[]){    
ProcessBuilder pb = new ProcessBuilder(args); 

try{
Process p = pb.start();
   System.out.println("child process is alive: " + p.isAlive());   
   System.out.println("child process pid: " + p.pid());
   System.out.println("parent of child process: " + p.toHandle().parent().get().pid());
   System.out.println("child process is alive: " + p.isAlive());      
   p.waitFor();

} catch (Exception e) {
    System.out.println("some exception with start process.");
    e.printStackTrace();    
}

  }
}

on Ubuntu 18.04, I run the program to run an external program sleep 10 without problme:

$ java Main  sleep 10
child process is alive: true
child process pid: 20559
parent of child process: 20539
child process is alive: true
$

But when I run the program to run an external program echo hello

$ java Main  echo hello
child process is alive: false
child process pid: 18534
some exception with start process.
java.util.NoSuchElementException: No value present
    at java.base/java.util.Optional.get(Optional.java:148)
    at Main.main(Main.java:11)

Why does it report error at get() in line 11 p.toHandle().parent().get().pid()?

If I comment out line 11, it will run without problem. Why is that?

If it is because the child exits before calling p.toHandle().parent().get().pid() at line 11,

What can I do to make the problem not happen? For example, how can I make the child last longer, so that p.toHandle().parent().get().pid() can work, even when I run a short lived program in the child process?


Solution

  • Why is the optional returned from parent() empty when called using echo hello?

    Because the child process is very short, and when you get to the command that asks for the parent, the child process no longer exists. Therefore, the operating system cannot be queried about that process anymore.

    Why doesn't this happen when calling getHandle() or pid()?

    Note that neither of these is an Optional. This means that the API guarantees that values will be returned in them. In fact, the implementation is such that the handle is created - including the process ID - as soon as the process is created by the operating system and returned in pb.start(). So you have a handle and you have a process ID - but the process with that ID is not guaranteed to be alive when you use it.

    The documentation specifically tells you not to make assumptions on the liveness or identity of the underlying process.

    The parent() call is currently implemented by querying the operating system using the child's pid. So the parent is not part of the process record when the process is created, and by the time you call it, may no longer be available.

    How to avoid this situation

    Whenever you use an Optional, do not use get. Especially not without checking if it's present or not beforehand (and using isPresent ... get is considered an "antipattern"). When you see an API gives you an Optional, think what you want to do if that Optional happens to be empty. Don't make assumptions that are not justified by the documentation.

    In this case you may want to display a default message in case the parent is not found. For example:

    System.out.println("parent of child process: "
        + p.toHandle().parent().map(ProcessHandle::pid)
                               .map(Object::toString)
                               .orElse("not available"));
    

    Or you could use the current process's pid as the default, or whatever else that you can come up with. Or you may throw a different kind of exception if the parent's identity is absolutely necessary for your work. See the documentation for Optional for various ways of elegantly working with it.

    There is no way to extend the underlying process - you could use a shell script that sleeps after it performs the command you have passed, but I find this solution to be clunky at best.