Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to determine when the browser is closed?

I am automating a website (filling forms and clicking around) with Selenium Webdriver to save time for my users. I have encountered an annoying problem though:

Selenium does not seem to support any event listeners for the browsers themselves. When the browser is closed driver.quit() is not called and an unusable driver remains which throws various exceptions. Having no way to know when the browser is closed, I cannot create a new driver instance.

What I need is some way to notify my program when the browser is closed.

The user might close the browser for whatever reason which breaks my program. The user cannot run the automated tasks again without restarting the application. Knowing when the browser is closed would allow me to call driver.quit() and make a new driver instance if the user wants to run it again.

The problem is further complicated by the fact that when the browser dies the error thrown is not uniform across browsers. With Firefox I might get an UnreachableBrowserException, with Chrome NullPointer and WebDriverExceptions.

To clarify, I know how to close the driver and the browser, but I don't know when they are closed by external sources. Can this be done in Selenium 2 in a cross-browser way (and if yes, how) or do I need to find another way (like another library) to watch the browser window?

like image 637
Shantu Avatar asked Sep 29 '22 08:09

Shantu


1 Answers

I have solved the problem with the help of JNA (Java Native Access) 3.4 which was already included with Selenium. My target platform is Windows only, but it shouldn't take much work to make this cross-platform. I had to do the following:

  1. Collect all browser process IDs before launching WebDriver (this can be done with utilities like tasklist or powershell Get-Process). If you know for certain there will be no browser processes running before launching your application, this step can be omitted.
  2. Initialize the driver.
  3. Collect all browser processes again, then take the difference between the two.
  4. Using the code linked in LittlePanda's comment as a base, we can create a watcher service on a new thread. This will alert all listeners when all the processes have closed.
  5. The Kernel32.INSTANCE.OpenProcess method allows us to create HANDLE objects from the pids.
  6. With these handles we can have the thread wait until all processes are signaled with the Kernel32.INSTANCE.WaitForMultipleObjects method.

Here's the code I used so that it might help others:

public void startAutomation() throws IOException { 
        Set<Integer> pidsBefore = getBrowserPIDs(browserType);
        automator.initDriver(browserType); //calls new ChromeDriver() for example
        Set<Integer> pidsAfter = getBrowserPIDs(browserType);
        pidsAfter.removeAll(pidsBefore);

        ProcessGroupExitWatcher watcher = new ProcessGroupExitWatcher(pidsAfter);
        watcher.addProcessExitListener(new ProcessExitListener() {
            @Override
            public void processFinished() {
                if (automator != null) {
                    automator.closeDriver(); //calls driver.quit()
                    automator = null;
                }
            }
        });
        watcher.start();
        //do webdriver stuff
}

private Set<Integer> getBrowserPIDs(String browserType) throws IOException {
    Set<Integer> processIds = new HashSet<Integer>();

    //powershell was convenient, tasklist is probably safer but requires more parsing
    String cmd = "powershell get-process " + browserType + " | foreach { $_.id }";
    Process processes = Runtime.getRuntime().exec(cmd);
    processes.getOutputStream().close(); //otherwise powershell hangs
    BufferedReader input = new BufferedReader(new InputStreamReader(processes.getInputStream()));
    String line;

    while ((line = input.readLine()) != null) {
        processIds.add(Integer.parseInt(line));
    }
    input.close();

    return processIds;
}

And the code for the watcher:

/**
* Takes a <code>Set</code> of Process IDs and notifies all listeners when all
* of them have exited.<br>
*/
public class ProcessGroupExitWatcher extends Thread {
    private List<HANDLE> processHandles;
    private List<ProcessExitListener> listeners = new ArrayList<ProcessExitListener>();

    /**
     * Initializes the object and takes a set of pids and creates a list of
     * <CODE>HANDLE</CODE>s from them.
     *
     * @param processIds
     *           process id numbers of the processes to watch.
     * @see HANDLE
     */
    public ProcessGroupExitWatcher(Set<Integer> processIds) {
        processHandles = new ArrayList<HANDLE>(processIds.size());
        //create Handles from the process ids
        for (Integer pid : processIds) {
            processHandles.add(Kernel32.INSTANCE.OpenProcess(Kernel32.SYNCHRONIZE, false, pid)); //synchronize must be used
        }
    }

    public void run() {
        //blocks the thread until all handles are signaled
        Kernel32.INSTANCE.WaitForMultipleObjects(processHandles.size(), processHandles.toArray(new HANDLE[processHandles.size()]), true,
            Kernel32.INFINITE);
       for (ProcessExitListener listener : listeners) {
            listener.processFinished();
       }
    }

    /**
     * Adds the listener to the list of listeners who will be notified when all
     * processes have exited.
     *
     * @param listener
     */
     public void addProcessExitListener(ProcessExitListener listener) {
         listeners.add(listener);
     }

    /**
     * Removes the listener.
     * 
     * @param listener
     */
    public void removeProcessExitListener(ProcessExitListener listener) {
         listeners.remove(listener);
    }
}

NOTE: when using powershell this way, the user's profile scripts are executed. This can create unexpected output which breaks the above code. Because of this, using tasklist is much more recommended.

like image 198
Shantu Avatar answered Oct 03 '22 03:10

Shantu