| /* |
| * Copyright 2008 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.gwt.junit; |
| |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.UnableToCompleteException; |
| import com.google.gwt.junit.client.TimeoutException; |
| import com.google.gwt.junit.remote.BrowserManager; |
| |
| import java.io.IOException; |
| import java.net.InetSocketAddress; |
| import java.net.ServerSocket; |
| import java.net.Socket; |
| import java.net.SocketTimeoutException; |
| import java.rmi.Naming; |
| import java.rmi.server.RMISocketFactory; |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| /** |
| * Runs in web mode via browsers managed over RMI. This feature is experimental |
| * and is not officially supported. |
| */ |
| class RunStyleRemoteWeb extends RunStyle { |
| |
| static class RMISocketFactoryWithTimeouts extends RMISocketFactory { |
| private static boolean initialized; |
| |
| public static void init() throws IOException { |
| if (!initialized) { |
| RMISocketFactory.setSocketFactory(new RMISocketFactoryWithTimeouts()); |
| initialized = true; |
| } |
| } |
| |
| @Override |
| public ServerSocket createServerSocket(int port) throws IOException { |
| return RMISocketFactory.getDefaultSocketFactory().createServerSocket(port); |
| } |
| |
| @Override |
| public Socket createSocket(String host, int port) throws IOException { |
| Socket socket = new Socket(); |
| socket.connect(new InetSocketAddress(host, port), CONNECT_MS); |
| socket.setSoTimeout(RESPONSE_TIMEOUT_MS); |
| return socket; |
| } |
| } |
| |
| private static class RemoteBrowser { |
| /** |
| * Remote browser manager. |
| */ |
| private final BrowserManager manager; |
| |
| /** |
| * RMI URLs for the remote browser server. |
| */ |
| private final String rmiUrl; |
| /** |
| * Reference to the remote browser processes. |
| */ |
| private int token; |
| |
| public RemoteBrowser(BrowserManager manager, String rmiUrl) { |
| this.manager = manager; |
| this.rmiUrl = rmiUrl; |
| } |
| |
| public BrowserManager getManager() { |
| return manager; |
| } |
| |
| public String getRmiUrl() { |
| return rmiUrl; |
| } |
| |
| public int getToken() { |
| return token; |
| } |
| |
| public void setToken(int token) { |
| this.token = token; |
| } |
| } |
| |
| /** |
| * Registered as a shutdown hook to make sure that any browsers that were not |
| * finished are killed. |
| */ |
| private class ShutdownCb extends Thread { |
| |
| @Override |
| public synchronized void run() { |
| for (RemoteBrowser remoteBrowser : remoteBrowsers) { |
| int remoteToken = remoteBrowser.getToken(); |
| if (remoteToken > 0) { |
| try { |
| remoteBrowser.getManager().killBrowser(remoteToken); |
| } catch (Exception e) { |
| System.err.println("Error killing remote browser during shutdown: " |
| + remoteBrowser.getRmiUrl()); |
| e.printStackTrace(); |
| } |
| // We've done our best to kill it. Don't try anymore. |
| remoteBrowser.setToken(0); |
| } |
| } |
| } |
| } |
| |
| private static final int CONNECT_MS = 10000; |
| private static final int PING_KEEPALIVE_MS = 5000; |
| |
| // Larger values when debugging the unit test framework, so you |
| // don't get spurious timeouts. |
| // private static final int CONNECT_MS = 1000000; |
| // private static final int PING_KEEPALIVE_MS = 500000; |
| // private static final int RESPONSE_TIMEOUT_MS = 1000000; |
| |
| private static final int RESPONSE_TIMEOUT_MS = 10000; |
| |
| /** |
| * The list of hosts that were interrupted. |
| */ |
| private Set<String> interruptedHosts; |
| |
| private RemoteBrowser[] remoteBrowsers; |
| |
| /** |
| * A separate lock to control access to {@link #interruptedHosts}. This keeps |
| * the main thread calls into {@link #getInterruptedHosts()} from having to |
| * synchronized on the containing instance and potentially block on RPC calls. |
| * It is okay to take the {@link #wasInterruptedLock} while locking the |
| * containing instance; it is NOT okay to do the opposite or deadlock could |
| * occur. |
| */ |
| private final Object wasInterruptedLock = new Object(); |
| |
| /** |
| * @param shell the containing shell |
| */ |
| public RunStyleRemoteWeb(JUnitShell shell) { |
| super(shell); |
| } |
| |
| @Override |
| public String[] getInterruptedHosts() { |
| synchronized (wasInterruptedLock) { |
| if (interruptedHosts == null) { |
| return null; |
| } |
| return interruptedHosts.toArray(new String[interruptedHosts.size()]); |
| } |
| } |
| |
| @Override |
| public int initialize(String args) { |
| if (args == null || args.length() == 0) { |
| getLogger().log(TreeLogger.ERROR, |
| "RemoteWeb runstyle requires comma-separated RMI URLs"); |
| return -1; |
| } |
| String[] urls = args.split(","); |
| try { |
| RMISocketFactoryWithTimeouts.init(); |
| } catch (IOException e) { |
| getLogger().log(TreeLogger.ERROR, |
| "RemoteWeb: Error initializing RMISocketFactory", e); |
| return -1; |
| } |
| int numClients = urls.length; |
| BrowserManager[] browserManagers = new BrowserManager[numClients]; |
| for (int i = 0; i < numClients; ++i) { |
| long callStart = System.currentTimeMillis(); |
| try { |
| browserManagers[i] = (BrowserManager) Naming.lookup(urls[i]); |
| } catch (Exception e) { |
| String message = "RemoteWeb: Error connecting to browser manager at " |
| + urls[i]; |
| Throwable cause = e; |
| if (e.getCause() instanceof SocketTimeoutException) { |
| long elapsed = System.currentTimeMillis() - callStart; |
| message += " - Timeout " + elapsed |
| + "ms waiting to connect to browser manager."; |
| cause = e.getCause(); |
| } |
| getLogger().log(TreeLogger.ERROR, message, cause); |
| return -1; |
| } |
| } |
| synchronized (this) { |
| this.remoteBrowsers = new RemoteBrowser[browserManagers.length]; |
| for (int i = 0; i < browserManagers.length; ++i) { |
| remoteBrowsers[i] = new RemoteBrowser(browserManagers[i], urls[i]); |
| } |
| } |
| Runtime.getRuntime().addShutdownHook(new ShutdownCb()); |
| return numClients; |
| } |
| |
| @Override |
| public synchronized void launchModule(String moduleName) |
| throws UnableToCompleteException { |
| String url = shell.getModuleUrl(moduleName); |
| |
| for (RemoteBrowser remoteBrowser : remoteBrowsers) { |
| long callStart = System.currentTimeMillis(); |
| try { |
| int remoteToken = remoteBrowser.getToken(); |
| BrowserManager mgr = remoteBrowser.getManager(); |
| if (remoteToken != 0) { |
| mgr.killBrowser(remoteToken); |
| } |
| remoteToken = mgr.launchNewBrowser(url, PING_KEEPALIVE_MS); |
| remoteBrowser.setToken(remoteToken); |
| } catch (Exception e) { |
| Throwable cause = e.getCause(); |
| if (cause instanceof SocketTimeoutException) { |
| long elapsed = System.currentTimeMillis() - callStart; |
| getLogger().log( |
| TreeLogger.ERROR, |
| "Timeout: " + elapsed + "ms launching remote browser at: " |
| + remoteBrowser.getRmiUrl(), e.getCause()); |
| throw new UnableToCompleteException(); |
| } |
| getLogger().log(TreeLogger.ERROR, |
| "Error launching remote browser at " + remoteBrowser.getRmiUrl(), e); |
| throw new UnableToCompleteException(); |
| } |
| } |
| |
| Thread keepAliveThread = new Thread() { |
| @Override |
| public void run() { |
| do { |
| try { |
| Thread.sleep(1000); |
| } catch (InterruptedException ignored) { |
| } |
| } while (doKeepAlives()); |
| } |
| }; |
| keepAliveThread.setDaemon(true); |
| keepAliveThread.start(); |
| } |
| |
| private synchronized boolean doKeepAlives() { |
| for (RemoteBrowser remoteBrowser : remoteBrowsers) { |
| if (remoteBrowser.getToken() > 0) { |
| long callStart = System.currentTimeMillis(); |
| try { |
| remoteBrowser.getManager().keepAlive(remoteBrowser.getToken(), |
| PING_KEEPALIVE_MS); |
| } catch (Exception e) { |
| Throwable cause = e.getCause(); |
| String rmiUrl = remoteBrowser.getRmiUrl(); |
| if (cause instanceof SocketTimeoutException) { |
| long elapsed = System.currentTimeMillis() - callStart; |
| throw new TimeoutException("Timeout: " + elapsed |
| + "ms keeping alive remote browser at: " + rmiUrl, |
| e.getCause()); |
| } else if (e instanceof IllegalStateException) { |
| getLogger().log(TreeLogger.INFO, |
| "Browser at: " + rmiUrl + " already exited.", e); |
| } else { |
| getLogger().log(TreeLogger.ERROR, |
| "Error keeping alive remote browser at " + rmiUrl, e); |
| } |
| remoteBrowser.setToken(0); |
| synchronized (wasInterruptedLock) { |
| if (interruptedHosts == null) { |
| interruptedHosts = new HashSet<String>(); |
| } |
| interruptedHosts.add(remoteBrowser.getRmiUrl()); |
| } |
| break; |
| } |
| } |
| } |
| |
| synchronized (wasInterruptedLock) { |
| return interruptedHosts == null; |
| } |
| } |
| } |