3

I have a process (on Windows is named chromedriver.exe) which is created when I create a new instance of Selenium Chrome Driver.

So I'm not starting the process myself, and yet I would like a java.lang.Process instance representing that process, if that's possible.

The reason why I want to create such instance is that I want to call Process.waitFor() to wait until the process is actually terminated after I issued (if on Windows) a Runtime.getRuntime().exec("taskkill /F /IM chromedriver.exe").

I cannot introduce a new dependency on third-party libraries for this specific need only. I could just use anything from Apache Commons.

So the question is: is there a way from my code to get a Process instance representing a running process that was not started from my code ?

The reason why I want to be able to kill that process is that an abrupt termination like a JVM crash might not let WebDriver.quit() terminate the process. So before creating the ChromeDriver instance that causes the process to start I want to kill any possibly already existing such processes.

The reason why I'm using exec("taskkill...") to kill the process on Windows (I'd use killall on Linux) is that that's the only way I found so far, but if it is possible to get a Process instance that represent that process I'd also try using Process.destroy() to see if the "subprocess" mentioned in its description "Kills the subprocess" is referring to that process.

SantiBailors
  • 1,596
  • 3
  • 21
  • 44
  • 1
    Activiti and Camunda are business process engines, and what is meant by the word "process" there is something totally different than an operating system process - so these pieces of software are not relevant to your question. – Jesper Jul 10 '17 at 11:07
  • @Jesper Thanks, it didn't sound that way at all, but I actually didn't know what kind of products they were. I'll look into your heads-up and I'll fix my question accordingly. However [this](https://stackoverflow.com/q/43684381/2814308) is the Activiti question that apparently tricked me and I'm still in doubt about what they mean by "process" there. [This](https://stackoverflow.com/q/23269094/2814308) is the Camunda one – SantiBailors Jul 10 '17 at 11:31
  • With "process instance" in those two questions, what is meant is an instance of a business process in the Activiti or Camunda business process engine - which is something entirely different than an operating system process. – Jesper Jul 10 '17 at 11:33
  • @Jesper I removed from my question the references to those 3rd party products. Thanks for pointing that out. – SantiBailors Jul 10 '17 at 11:53
  • ---------- Not sure about this but, please check if this helps https://stackoverflow.com/questions/40467793/how-can-i-get-chromedriver-process-pid-using-java?rq=1 but – Depanker Sharma Jul 13 '17 at 04:37
  • @DepankerSharma Thanks for the pointer, I had actually seen that before posting, I couldn't use the solution in the answer because in my understanding that `p.waitFor();` is waiting for the `kill` process to terminate, not for the process being killed; also, to me that waiting for that process to terminate seems possible only because that process was started by the same code (with `Runtime.exec()`), while in my case the process I want to kill and wait until it's dead was already running, started not by my code. – SantiBailors Jul 13 '17 at 07:16

1 Answers1

5

In short, not with Java ≤ 8, but it is possible with Java 9 and java.lang.ProcessHandle.

A Process in Java is not meant to model an arbitrary OS process, it actually models a sub-process of the java process, so you can't attach to another running process, and expect to get all features a Process has:

The class Process provides methods for performing input from the process, performing output to the process, waiting for the process to complete, checking the exit status of the process, and destroying (killing) the process.

As you see, input and output requires your java process to have a handle on the other process's stdin/stdout/stderr, which only makes sense for a child process. The doc also mentions "the subprocess" everywhere, thus implying it's a child process.

In Java 9 the doc explicitly starts with:

Process provides control of native processes started by ProcessBuilder.start and Runtime.exec.

But Java 9 also brings ProcessHandle, which is exactly what you need:

ProcessHandle identifies and provides control of native processes. Each individual process can be monitored for liveness, list its children, get information about the process or destroy it. By comparison, Process instances were started by the current process and additionally provide access to the process input, output, and error streams.

So, if you can run your tests with Java 9, you can try using ProcessHandle.allProcesses() to find the process you want, and then kill it with .destroy(), monitor its liveness with isAlive(), or use onExit().

Optional<ProcessHandle> optional = ProcessHandle.allProcesses().filter(process -> {
    Optional<String> command = process.info().command();
    return command.isPresent() && command.get().equals("Chromedriver.exe");
}).findFirst();
if (optional.isPresent()) {
    ProcessHandle processHandle = optional.get();
    System.out.println("Killing process " + processHandle.pid());
    processHandle.destroy();
    try {
        System.out.println("Waiting for process " + processHandle.pid() + " to exit...");
        processHandle.onExit().get();
        System.out.println("Done !");
    } catch (InterruptedException|ExecutionException e) {
        e.printStackTrace();
    }
}

That being said, if you really want to, you can create a subclass of Process by yourself, that will provide limited functionality (ie nothing about I/O, only waitFor and destroy & such).

Below is an awful example quickly hacked using ideas from this Q&A for the Windows implementation.

  • find process by name using tasklist /FO CSV /FI "IMAGENAME eq Chromedriver.exe", to get the PID
  • kill it with taskkill
  • wait for it to end with a polling-loop that uses tasklist /FO CSV /FI "PID eq ..."

The Linux version uses pgrep, kill, and /proc/{pid}/.

Please note it's not really useful to wrap that as an actual instance of java.lang.Process, so I've separated the concerns with a tool class that implements the required features for windows, and a subclass of Process implemented as an exercise to show what's possible, and what is not possible (details in code). So if you need that, better use ProcessTool directly, not DummyProcess.

(I've tested this on another process, I can't speak for Chromedriver.exe, you might need to adapt a few details)

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Paths;
import java.util.Scanner;

public class WaitForProcessDemo {

    public static void main(String[] args) throws IOException, InterruptedException {
        Process p = getDummyProcess("Chromedriver");
        System.out.println("Killing...");
        p.destroy();
        System.out.println("Waiting...");
        p.waitFor();
        System.out.println("Done.");
    }

    private static Process getDummyProcess(String exeName) throws IOException {
        ProcessTool tool = new WindowsProcessTool();
        long pid = tool.findProcessByName(exeName);
        return new DummyProcess(pid, tool);
    }

    interface ProcessTool {
        long findProcessByName(String exeName) throws IOException;
        void killProcess(long pid) throws IOException;
        boolean isPidStillThere(long pid);
    }

    private static class WindowsProcessTool implements ProcessTool {

        @Override
        public long findProcessByName(String exeName) throws IOException {
            String processInfoCSV = findProcessInfoCSV("IMAGENAME eq " + exeName);
            String[] fields = processInfoCSV.split("\"");
            String pid = fields[3];
            return Integer.parseInt(pid);
        }

        String findProcessInfoCSV(String filter) throws IOException {
            Process p = new ProcessBuilder("tasklist", "-FO", "CSV", "/FI", filter)
                    .redirectErrorStream(true)
                    .start();
            Scanner scanner = new Scanner(p.getInputStream());
            scanner.nextLine(); // skip header line
            if (scanner.hasNextLine()) {
                return scanner.nextLine();
            }
            throw new IOException("No such process: " + filter);
        }

        @Override
        public void killProcess(long pid) throws IOException {
            new ProcessBuilder("taskkill", "/PID", String.valueOf(pid))
                    .redirectErrorStream(true)
                    .start();
        }

        @Override
        public boolean isPidStillThere(long pid) {
            try {
                findProcessInfoCSV("PID eq " + pid);
                return true;
            } catch (IOException e) {
                return false;
            }
        }
    }

    private static class LinuxProcessTool implements ProcessTool {

        @Override
        public long findProcessByName(String exeName) throws IOException {
            Process pgrep = new ProcessBuilder("pgrep", exeName)
                    .redirectErrorStream(true)
                    .start();
            Scanner scanner = new Scanner(pgrep.getInputStream());
            return Long.parseLong(scanner.nextLine());
        }

        @Override
        public void killProcess(long pid) throws IOException {
            new ProcessBuilder("kill", String.valueOf(pid))
                    .redirectErrorStream(true)
                    .start();
        }

        @Override
        public boolean isPidStillThere(long pid) {
            return Paths.get("/proc", String.valueOf(pid)).toFile().isDirectory();
        }
    }

    /*
     * Broken & incomplete implementation of java.lang.Process, implemented as an exercise.
     * (Kids, don't do this at home)
     */
    static class DummyProcess extends Process {

        private final long pid;
        private final ProcessTool tool;

        DummyProcess(long pid, ProcessTool tool) {
            this.pid = pid;
            this.tool = tool;
        }

        @Override
        public OutputStream getOutputStream() {
            return null; // DANGER. This cannot be implemented for non-child process.
        }

        @Override
        public InputStream getInputStream() {
            return null; // DANGER. This cannot be implemented for non-child process.
        }

        @Override
        public InputStream getErrorStream() {
            return null; // DANGER. This cannot be implemented for non-child process.
        }

        @Override
        public int waitFor() throws InterruptedException {
            // Very sub-optimal implementation
            boolean isPidPresent = isPidStillThere();
            while (isPidPresent) {
                Thread.sleep(500);
                isPidPresent = isPidStillThere();
            }
            return 0;
        }

        @Override
        public int exitValue() {
            // For example, this is dangerous, as Process.isAlive() will call this, and determine process is not alive.
            // Also, it cannot be implemented correctly, it's not possible to tell what was exit value.
            // At best we could throw IllegalThreadStateException when process is still alive.
            return 0;
        }

        @Override
        public void destroy() {
            try {
                tool.killProcess(pid);
            } catch (IOException e) {
                throw new RuntimeException("Failed to kill process " + pid, e);
            }
        }

        private boolean isPidStillThere() {
            return tool.isPidStillThere(pid);
        }
    }

}
Hugues M.
  • 19,846
  • 6
  • 37
  • 65
  • Thanks for the explanation, especially about Java 8 vs. 9 and `ProcessHandle`. The only part I don't get is the one about subclassing or wrapping `java.lang.Process` and what is that is not useful, but even if I got that I wouldn't know how to "link" that instance to the actual running process anyway. I phrased my question around an instance of `java.lang.Process` only because I don't know better and I never worked with processes before; I am not committed to `Process`, I could use any other class that would do the job. I'll see if switching to Java 9 is an option and I'll study your example. – SantiBailors Jul 13 '17 at 07:01
  • 1
    To "attach" to an already running process, you have to first *find it*. In the example above, I show how to find one by its name, so in your case you would do `Process p = getDummyProcess("Chromedriver.exe")`, it would find the first process that matches this executable name. -- The only problem about wrapping `Process` (which I did as an exercise) is that it's an incomplete implementation, so, if that instance is passed around some other code might be tempted to use the methods that won't work. It would be better to use `findWithTaskList` and `isPidStillThere` directly. – Hugues M. Jul 13 '17 at 07:38
  • 1
    I've reworked the example to extract a reusable `WindowsProcessTool`, and make `DummyProcess` less attractive to copy-pasters, as it is dangerous :) – Hugues M. Jul 13 '17 at 11:57
  • Awesome, thanks. I'm following the advice of just waiting a couple of days before accepting answers but by following your code and explanation I think I understood the whole subject much better. I plan to use your code, and given the subject I was already prepared for OS-dependent solutions so for now I'll just throw an "OS not supported" exception if not on Windows, until we use Java 9 or I decide to write equivalent versions for other OSs. – SantiBailors Jul 13 '17 at 12:17
  • Oh, I was under the impression you needed only Windows. I've added an implementation for Linux as an example. For a mac it would be easy to adapt, but I don't have one with me right now. (oh, and yes, sure, wait a few days to accept of course) – Hugues M. Jul 13 '17 at 12:37
  • 1
    No, I will also need the same for Mac and Linux later, but the important thing is that I now understand much more about the subject and I have your code that I can use and I expect to be able to adapt it to other OSs. – SantiBailors Jul 13 '17 at 12:48
  • 1
    I think recent macs have `pgrep` too, but for `isPidStillThere` you'll need to use `ps` as there is no equivalent of `/proc/{pid}` to check for. – Hugues M. Jul 13 '17 at 13:08