Reputation: 113
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?
Upvotes: 6
Views: 4589
Reputation: 113
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:
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.Kernel32.INSTANCE.OpenProcess
method allows us to create HANDLE
objects from the pids.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.
Upvotes: 4