| /* | |
| * 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 Production 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; | |
| } | |
| } | |
| } |