/*
* 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.jstestdriver;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.name.Named;
import com.google.jstestdriver.browser.BrowserCaptureEvent;
import com.google.jstestdriver.browser.BrowserReaper;
import com.google.jstestdriver.config.ExecutionType;
import com.google.jstestdriver.hooks.FileInfoScheme;
import com.google.jstestdriver.hooks.ServerListener;
import com.google.jstestdriver.model.HandlerPathPrefix;
import com.google.jstestdriver.server.JettyModule;
import com.google.jstestdriver.server.JstdTestCaseStore;
import com.google.jstestdriver.server.handlers.JstdHandlersModule;
import org.mortbay.component.LifeCycle;
import org.mortbay.component.LifeCycle.Listener;
import org.mortbay.jetty.Server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import java.util.Timer;
/**
* @author jeremiele@google.com (Jeremie Lenfant-Engelmann)
*/
public class JsTestDriverServerImpl implements JsTestDriverServer, Observer {
private static final Logger logger = LoggerFactory.getLogger(JsTestDriverServerImpl.class);
private Server server;
private final int port;
private final int sslPort;
private final CapturedBrowsers capturedBrowsers;
private final JstdTestCaseStore testCaseStore;
private final long browserTimeout;
private Timer timer;
private final HandlerPathPrefix handlerPrefix;
private final Set<ServerListener> listeners;
private final Set<FileInfoScheme> schemes;
private final ExecutionType executionType;
private final Boolean debug;
@Inject
public JsTestDriverServerImpl(@Assisted("port") int port,
@Assisted("sslPort") int sslPort,
@Assisted JstdTestCaseStore testCaseStore,
CapturedBrowsers capturedBrowsers,
@Named("browserTimeout") long browserTimeout,
@Named("serverHandlerPrefix") HandlerPathPrefix handlerPrefix,
Set<ServerListener> listeners,
Set<FileInfoScheme> schemes,
@Named("executionType") ExecutionType executionType,
@Named("debug") Boolean debug) {
this.port = port;
this.sslPort = sslPort;
this.capturedBrowsers = capturedBrowsers;
this.testCaseStore = testCaseStore;
this.browserTimeout = browserTimeout;
this.handlerPrefix = handlerPrefix;
this.listeners = listeners;
this.schemes = schemes;
this.executionType = executionType;
this.debug = debug;
initServer();
}
private void initServer() {
if (server != null) {
logger.warn("Attempt to start a started server");
} else {
// TODO(corysmith): move this to the normal guice injection scope.
capturedBrowsers.deleteObserver(this);
capturedBrowsers.addObserver(this);
server = Guice.createInjector(
new JettyModule(port, sslPort, handlerPrefix),
new JstdHandlersModule(capturedBrowsers,
testCaseStore,
browserTimeout,
handlerPrefix,
schemes,
executionType,
debug)).getInstance(Server.class);
server.addLifeCycleListener(new JettyLifeCycleLogger());
}
}
@Override
public void start() {
try {
initServer();
// TODO(corysmith): Move this to the constructor when we are injecting
// everything.
timer = new Timer(true);
timer.schedule(new BrowserReaper(capturedBrowsers), browserTimeout, browserTimeout);
server.start();
logger.info("Started the JsTD server on {} with execution type {}", port, executionType);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void stop() {
try {
timer.cancel();
if (server != null) {
server.stop();
server.join();
server = null;
}
logger.debug("Stopped the server.");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** Separate function for synchronization and thread handling. */
private void notifyListeners(ServerNotification<ServerListener> notification) {
for (ServerListener listener : listeners) {
notification.notify(listener);
}
}
/*
* (non-Javadoc)
*
* @see com.google.jstestdriver.JsTestDriverServer#isHealthy()
*/
@Override
public boolean isHealthy() {
String url = "http://127.0.0.1:" + port + handlerPrefix.prefixPath("/hello");
HttpURLConnection connection;
try {
connection = (HttpURLConnection) new URL(url).openConnection();
connection.connect();
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
return true;
}
logger.warn("Bad response code {} from server: {}", responseCode, connection.getContent());
return false;
} catch (MalformedURLException e) {
logger.warn("Bad url {}", e);
} catch (IOException e) {
logger.warn("Server not ready.", e);
}
return false;
}
private final class JettyLifeCycleLogger implements Listener {
@Override
public void lifeCycleStopping(LifeCycle arg0) {
logger.debug("Server stopping");
}
@Override
public void lifeCycleStopped(LifeCycle arg0) {
notifyListeners(STOPPED_NOTIFICATION);
logger.debug("Server stopped");
}
@Override
public void lifeCycleStarting(LifeCycle arg0) {
logger.debug("Server starting");
}
@Override
public void lifeCycleStarted(LifeCycle arg0) {
notifyListeners(STARTED_NOTIFICATION);
logger.debug("Server started");
}
@Override
public void lifeCycleFailure(LifeCycle arg0, Throwable arg1) {
logger.warn("Server failed", arg1);
}
}
/**
* Receives updates from the CapturedBrowsers.
*/
@Override
public void update(Observable o, Object arg) {
logger.debug("Server Event {}, {}" + o, arg);
// TODO(corysmith): Cleanup browser capture event.
BrowserCaptureEvent event = (BrowserCaptureEvent) arg;
final BrowserInfo info = event.getBrowser().getBrowserInfo();
switch (event.event) {
case CONNECTED:
notifyListeners(new BrowserCaptureNotification(info));
break;
case DISCONNECTED:
notifyListeners(new BrowserPanickedNotification(info));
break;
}
}
private static final class BrowserPanickedNotification implements ServerNotification<
ServerListener> {
private final BrowserInfo info;
private BrowserPanickedNotification(BrowserInfo info) {
this.info = info;
}
@Override
public void notify(ServerListener listener) {
listener.browserPanicked(info);
}
}
private static final class BrowserCaptureNotification implements ServerNotification<
ServerListener> {
private final BrowserInfo info;
private BrowserCaptureNotification(BrowserInfo info) {
this.info = info;
}
@Override
public void notify(ServerListener listener) {
listener.browserCaptured(info);
}
}
private static final ServerNotification<ServerListener> STARTED_NOTIFICATION =
new ServerNotification<ServerListener>() {
@Override
public void notify(ServerListener listener) {
listener.serverStarted();
}
};
private static final ServerNotification<ServerListener> STOPPED_NOTIFICATION =
new ServerNotification<ServerListener>() {
@Override
public void notify(ServerListener listener) {
listener.serverStopped();
}
};
/** Internal server notification. */
private static interface ServerNotification<T> {
public void notify(ServerListener listener);
}
}