/*
* Copyright 2012-2015 eBay Software Foundation and selendroid committers.
*
* 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 io.selendroid.standalone.server.grid;
import com.google.gson.Gson;
import io.selendroid.common.SelendroidCapabilities;
import io.selendroid.server.common.exceptions.SelendroidException;
import io.selendroid.standalone.SelendroidConfiguration;
import io.selendroid.standalone.server.model.SelendroidStandaloneDriver;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.openqa.selenium.remote.CapabilityType;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;
import static io.selendroid.standalone.server.model.SelendroidStandaloneDriver.APP_BASE_PACKAGE;
import static io.selendroid.standalone.server.model.SelendroidStandaloneDriver.APP_ID;
public class SelfRegisteringRemote {
private static final Logger log = Logger.getLogger(SelfRegisteringRemote.class.getName());
public static final String ANDROIDDRIVER_APP = "io.selendroid.androiddriver";
private SelendroidConfiguration config;
private SelendroidStandaloneDriver driver;
private boolean isFirstTime = true;
private final URL hub;
public SelfRegisteringRemote(SelendroidConfiguration config, SelendroidStandaloneDriver driver) {
this.config = config;
this.driver = driver;
this.hub = getGridHubUrl(config.getRegistrationUrl());
}
public void performRegistrationIfNotRegistered() {
try {
if (isFirstTime) {
performRegistration();
isFirstTime = false;
} else if (!isRegistered()) {
performRegistration();
}
} catch (Exception e) {
log.log(Level.SEVERE, "An error occurred while registering selendroid into grid hub.", e);
}
}
public void performRegistration() throws Exception {
String nodeConfigString = getNodeConfig().toString();
log.info("Registering Selendroid node with following config:\n" + nodeConfigString + "\n" +
"at Selenium Grid hub: " + hub.toExternalForm());
BasicHttpEntityEnclosingRequest request = new BasicHttpEntityEnclosingRequest("POST",
hub.toExternalForm());
request.setEntity(new StringEntity(nodeConfigString));
HttpResponse response = getHttpClient().execute(getHttpHost(), request);
if (response.getStatusLine().getStatusCode() != 200) {
throw new SelendroidException("Error sending the registration request. Response from server: " +
response.getStatusLine().toString());
}
}
// for testing purpose
protected HttpClient getHttpClient() {
return HttpClientBuilder.create().build();
}
/**
* Get the node configuration and capabilities for Grid registration
*
* @return The configuration
*/
private JSONObject getNodeConfig() {
JSONObject res = new JSONObject();
try {
res.put("class", "org.openqa.grid.common.RegistrationRequest");
res.put("configuration", getConfiguration());
JSONArray caps = new JSONArray();
JSONArray devices = driver.getSupportedDevices();
for (int i = 0; i < devices.length(); i++) {
JSONObject device = (JSONObject) devices.get(i);
for (int x = 0; x < driver.getSupportedApps().length(); x++) {
caps.put(getDeviceConfig(device, driver.getSupportedApps().getJSONObject(x)));
}
}
res.put("capabilities", caps);
} catch (JSONException e) {
throw new SelendroidException(e.getMessage(), e);
}
return res;
}
private JSONObject getDeviceConfig(final JSONObject device, final JSONObject supportedApp) throws JSONException {
JSONObject capa = new JSONObject(device, JSONObject.getNames(device));
// For each device, register as "android" for WebView tests and also selendroid if an aut is specified
if (ANDROIDDRIVER_APP.equals(supportedApp.get(APP_BASE_PACKAGE))) {
//it's possible the user does not want to register the device as capable to recieve webview tests
if (!config.isNoWebViewApp()) {
capa.put(CapabilityType.BROWSER_NAME, "android");
}
} else {
capa.put(CapabilityType.BROWSER_NAME, "selendroid");
capa.put(SelendroidCapabilities.AUT, supportedApp.get(APP_ID));
}
capa.put(CapabilityType.PLATFORM, "ANDROID");
capa.put(SelendroidCapabilities.PLATFORM_NAME, "android");
capa.put(CapabilityType.VERSION, device.getString(SelendroidCapabilities.PLATFORM_VERSION));
capa.put("maxInstances", config.getMaxInstances());
return capa;
}
/**
* Extracts the configuration.
*
* @return The configuration
* @throws JSONException On JSON errors.
*/
private JSONObject getConfiguration() throws JSONException {
JSONObject configuration = new JSONObject();
configuration.put("port", config.getPort());
configuration.put("register", true);
if (config.getProxy() != null) {
configuration.put("proxy", config.getProxy());
} else {
configuration.put("proxy", "org.openqa.grid.selenium.proxy.DefaultRemoteProxy");
}
configuration.put("role", "node");
configuration.put("registerCycle", config.getRegisterCycle());
if (config.getMaxSession() == 0) {
configuration.put("maxSession", driver.getSupportedDevices().length());
} else {
configuration.put("maxSession", config.getMaxSession());
}
configuration.put("browserTimeout", config.getSessionTimeoutMillis() / 1000);
configuration.put("cleanupCycle", config.getCleanupCycle());
configuration.put("timeout", config.getTimeout());
configuration.put("nodePolling", config.getNodePolling());
configuration.put("unregisterIfStillDownAfter", config.getUnregisterIfStillDownAfter());
configuration.put("downPollingLimit", config.getDownPollingLimit());
configuration.put("nodeStatusCheckTimeout", config.getNodeStatusCheckTimeout());
// adding hub details
configuration.put("hubHost", hub.getHost());
configuration.put("hubPort", hub.getPort());
// adding driver details
configuration.put("seleniumProtocol", "WebDriver");
configuration.put("host", config.getServerHost());
configuration.put("remoteHost", "http://" + config.getServerHost() + ":" + config.getPort());
return configuration;
}
protected boolean isRegistered() {
try {
URL hubProxyApiUrl = new URL(hub.getProtocol(), hub.getHost(), hub.getPort(), "/grid/api/proxy");
HttpResponse response = getHttpClient().execute(getHttpHost(), new BasicHttpRequest("GET",
hubProxyApiUrl.toExternalForm() + "?id=http://" + config.getServerHost() + ":" + config.getPort()));
if (response.getStatusLine().getStatusCode() != 200) {
throw new RuntimeException("Hub is down or not responding. Response was: " + response.getStatusLine().toString());
}
GridResponse gridResponse = new Gson().fromJson(EntityUtils.toString(response.getEntity()), GridResponse.class);
return gridResponse.success;
} catch (IOException e) {
throw new SelendroidException("Hub is down or not responding.", e);
}
}
private HttpHost getHttpHost() {
return new HttpHost(hub.getHost(), hub.getPort(), hub.getProtocol());
}
private URL getGridHubUrl(String connectionString) {
try {
return new URL(connectionString);
} catch (MalformedURLException e) {
throw new SelendroidException("Grid hub connection string cannot be parsed into URL", e);
}
}
private class GridResponse {
private boolean success;
}
}