From 4a1fcaf1246fc82c4b429e75df2ad1214dcc4383 Mon Sep 17 00:00:00 2001 From: Krishnan Mahadevan Date: Sat, 14 Jul 2018 13:52:11 +0530 Subject: [PATCH 1/2] A Hub API that can query all running sessions. Closes #6070 The API can be accessed via http:///grid/api/sessions. Both GET and POST are supported and result in the same behavior. A typical response can look like below (intentionally trimmed the capabilities to reduce verbosity) { "proxies": [ { "proxyId": "http://192.168.1.6:5555", "proxyRemoteHost": "http://192.168.1.6:5555", "sessions": { "status": 0, "value": [ { "id": "df8aa16d-1d52-2e43-b584-2f9df586b0f3", "capabilities": {} }, { "id": "f85f5641-e6de-3642-913a-a45aafdb85ad", "capabilities": {} } ] } }, { "proxyId": "http://192.168.1.6:5556", "proxyRemoteHost": "http://192.168.1.6:5556", "sessions": { "status": 0, "value": [ { "id": "f4df4fbd-96ee-e042-ab43-40a33698d1e9", "capabilities": {} } ] } } ], "success": true } --- java/server/src/org/openqa/grid/web/Hub.java | 2 + .../grid/web/servlet/NodeSessionsServlet.java | 154 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 java/server/src/org/openqa/grid/web/servlet/NodeSessionsServlet.java diff --git a/java/server/src/org/openqa/grid/web/Hub.java b/java/server/src/org/openqa/grid/web/Hub.java index 98c3d701ee75e..9983f47c03574 100644 --- a/java/server/src/org/openqa/grid/web/Hub.java +++ b/java/server/src/org/openqa/grid/web/Hub.java @@ -27,6 +27,7 @@ import org.openqa.grid.web.servlet.HubStatusServlet; import org.openqa.grid.web.servlet.HubW3CStatusServlet; import org.openqa.grid.web.servlet.LifecycleServlet; +import org.openqa.grid.web.servlet.NodeSessionsServlet; import org.openqa.grid.web.servlet.ProxyStatusServlet; import org.openqa.grid.web.servlet.RegistrationServlet; import org.openqa.grid.web.servlet.ResourceServlet; @@ -130,6 +131,7 @@ private void addDefaultServlets(ServletContextHandler handler) { handler.addServlet(DriverServlet.class.getName(), "/selenium-server/driver/*"); handler.addServlet(ProxyStatusServlet.class.getName(), "/grid/api/proxy/*"); + handler.addServlet(NodeSessionsServlet.class.getName(), "/grid/api/sessions/*"); handler.addServlet(HubStatusServlet.class.getName(), "/grid/api/hub/*"); diff --git a/java/server/src/org/openqa/grid/web/servlet/NodeSessionsServlet.java b/java/server/src/org/openqa/grid/web/servlet/NodeSessionsServlet.java new file mode 100644 index 0000000000000..627ad66a1e6a1 --- /dev/null +++ b/java/server/src/org/openqa/grid/web/servlet/NodeSessionsServlet.java @@ -0,0 +1,154 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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 org.openqa.grid.web.servlet; + +import static com.google.common.net.HttpHeaders.CONTENT_TYPE; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.io.ByteStreams; +import com.google.common.net.MediaType; + +import org.openqa.grid.common.exception.GridException; +import org.openqa.grid.internal.GridRegistry; +import org.openqa.grid.internal.RemoteProxy; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.json.JsonException; +import org.openqa.selenium.json.JsonOutput; +import org.openqa.selenium.remote.http.HttpMethod; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.HttpResponse; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.Writer; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * API to query all the sessions that are currently running in the hub. + */ +public class NodeSessionsServlet extends RegistryBasedServlet { + + private final Json json = new Json(); + + public NodeSessionsServlet() { + this(null); + } + + public NodeSessionsServlet(GridRegistry registry) { + super(registry); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { + process(rsp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + if (req.getInputStream() != null) { + process(resp); + } else { + process(resp); + } + } + + protected void process(HttpServletResponse response) throws IOException { + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.setStatus(200); + Map proxies = new TreeMap<>(); + try (Writer writer = response.getWriter(); JsonOutput out = json.newOutput(writer)) { + proxies.put("success", true); + proxies.put("proxies", extractSessionsFromAllProxies()); + out.write(proxies); + } + } + + private List> extractSessionsFromAllProxies() { + List> results = new LinkedList<>(); + List proxies = getRegistry().getAllProxies().getBusyProxies(); + for (RemoteProxy proxy : proxies) { + Map sessionsInProxy = new TreeMap<>(extractSessionInfo(proxy)); + if (sessionsInProxy.isEmpty()) { + continue; + } + Map res = new TreeMap<>(); + res.put("proxyId", proxy.getId()); + res.put("proxyRemoteHost", proxy.getRemoteHost().toString()); + res.put("sessions", sessionsInProxy); + results.add(res); + } + return results; + } + + private static Map extractSessionInfo(RemoteProxy proxy) { + try { + URL url = proxy.getRemoteHost(); + HttpRequest req = new HttpRequest(HttpMethod.GET, "/wd/hub/sessions"); + HttpResponse rsp = proxy.getHttpClient(url).execute(req); + try (InputStream in = new ByteArrayInputStream(asBytes(rsp)); + Reader reader = new InputStreamReader(in, getContentEncoding(rsp))) { + Object body = new Json().newInput(reader).read(Object.class); + if (body instanceof Map) { + return (Map) body; + } + } catch (JsonException e) { + // Nothing to do --- poorly formed payload. + } + } catch (IOException e) { + throw new GridException(e.getMessage()); + } + return new TreeMap<>(); + } + + private static byte[] asBytes(HttpResponse rsp) { + InputStream stream = rsp.consumeContentStream(); + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + ByteStreams.copy(stream, bos); + return bos.toByteArray(); + } catch (IOException e) { + throw new GridException(e.getMessage()); + } + } + + private static Charset getContentEncoding(HttpResponse rsp) { + Charset charset = UTF_8; + try { + String contentType = rsp.getHeader(CONTENT_TYPE); + if (contentType != null) { + MediaType mediaType = MediaType.parse(contentType); + charset = mediaType.charset().or(UTF_8); + } + } catch (IllegalArgumentException ignored) { + // Do nothing. + } + return charset; + } +} From 5f591bd6410750ff64959ae4223bbf8af5edc06b Mon Sep 17 00:00:00 2001 From: Krishnan Mahadevan Date: Mon, 16 Jul 2018 09:29:19 +0530 Subject: [PATCH 2/2] Fixing code review comments. --- .../grid/web/servlet/NodeSessionsServlet.java | 63 ++------ .../org/openqa/grid/e2e/GridE2ETests.java | 2 + .../e2e/misc/GridListActiveSessionsTest.java | 140 ++++++++++++++++++ .../org/openqa/grid/e2e/node/SmokeTest.java | 46 +++--- 4 files changed, 184 insertions(+), 67 deletions(-) create mode 100644 java/server/test/org/openqa/grid/e2e/misc/GridListActiveSessionsTest.java diff --git a/java/server/src/org/openqa/grid/web/servlet/NodeSessionsServlet.java b/java/server/src/org/openqa/grid/web/servlet/NodeSessionsServlet.java index 627ad66a1e6a1..4089271e97186 100644 --- a/java/server/src/org/openqa/grid/web/servlet/NodeSessionsServlet.java +++ b/java/server/src/org/openqa/grid/web/servlet/NodeSessionsServlet.java @@ -17,31 +17,24 @@ package org.openqa.grid.web.servlet; -import static com.google.common.net.HttpHeaders.CONTENT_TYPE; -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.google.common.io.ByteStreams; -import com.google.common.net.MediaType; - import org.openqa.grid.common.exception.GridException; import org.openqa.grid.internal.GridRegistry; import org.openqa.grid.internal.RemoteProxy; import org.openqa.selenium.json.Json; import org.openqa.selenium.json.JsonException; +import org.openqa.selenium.json.JsonInput; import org.openqa.selenium.json.JsonOutput; import org.openqa.selenium.remote.http.HttpMethod; import org.openqa.selenium.remote.http.HttpRequest; import org.openqa.selenium.remote.http.HttpResponse; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.Writer; import java.net.URL; -import java.nio.charset.Charset; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -72,11 +65,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOE @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { - if (req.getInputStream() != null) { - process(resp); - } else { - process(resp); - } + process(resp); } protected void process(HttpServletResponse response) throws IOException { @@ -84,7 +73,8 @@ protected void process(HttpServletResponse response) throws IOException { response.setCharacterEncoding("UTF-8"); response.setStatus(200); Map proxies = new TreeMap<>(); - try (Writer writer = response.getWriter(); JsonOutput out = json.newOutput(writer)) { + try (Writer writer = response.getWriter(); + JsonOutput out = json.newOutput(writer)) { proxies.put("success", true); proxies.put("proxies", extractSessionsFromAllProxies()); out.write(proxies); @@ -95,30 +85,29 @@ private List> extractSessionsFromAllProxies() { List> results = new LinkedList<>(); List proxies = getRegistry().getAllProxies().getBusyProxies(); for (RemoteProxy proxy : proxies) { + Map res = new TreeMap<>(); + res.put("id", proxy.getId()); + res.put("remoteHost", proxy.getRemoteHost().toString()); Map sessionsInProxy = new TreeMap<>(extractSessionInfo(proxy)); if (sessionsInProxy.isEmpty()) { - continue; + sessionsInProxy.put("success", false); } - Map res = new TreeMap<>(); - res.put("proxyId", proxy.getId()); - res.put("proxyRemoteHost", proxy.getRemoteHost().toString()); res.put("sessions", sessionsInProxy); results.add(res); } return results; } - private static Map extractSessionInfo(RemoteProxy proxy) { + private Map extractSessionInfo(RemoteProxy proxy) { try { URL url = proxy.getRemoteHost(); HttpRequest req = new HttpRequest(HttpMethod.GET, "/wd/hub/sessions"); HttpResponse rsp = proxy.getHttpClient(url).execute(req); - try (InputStream in = new ByteArrayInputStream(asBytes(rsp)); - Reader reader = new InputStreamReader(in, getContentEncoding(rsp))) { - Object body = new Json().newInput(reader).read(Object.class); - if (body instanceof Map) { - return (Map) body; - } + + try (InputStream in = new ByteArrayInputStream(rsp.getContent()); + Reader reader = new InputStreamReader(in, rsp.getContentEncoding()); + JsonInput jsonReader = json.newInput(reader)){ + return jsonReader.read(Json.MAP_TYPE); } catch (JsonException e) { // Nothing to do --- poorly formed payload. } @@ -127,28 +116,4 @@ private static Map extractSessionInfo(RemoteProxy proxy) { } return new TreeMap<>(); } - - private static byte[] asBytes(HttpResponse rsp) { - InputStream stream = rsp.consumeContentStream(); - try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { - ByteStreams.copy(stream, bos); - return bos.toByteArray(); - } catch (IOException e) { - throw new GridException(e.getMessage()); - } - } - - private static Charset getContentEncoding(HttpResponse rsp) { - Charset charset = UTF_8; - try { - String contentType = rsp.getHeader(CONTENT_TYPE); - if (contentType != null) { - MediaType mediaType = MediaType.parse(contentType); - charset = mediaType.charset().or(UTF_8); - } - } catch (IllegalArgumentException ignored) { - // Do nothing. - } - return charset; - } } diff --git a/java/server/test/org/openqa/grid/e2e/GridE2ETests.java b/java/server/test/org/openqa/grid/e2e/GridE2ETests.java index 88b53219218ac..b67e0ea08e626 100644 --- a/java/server/test/org/openqa/grid/e2e/GridE2ETests.java +++ b/java/server/test/org/openqa/grid/e2e/GridE2ETests.java @@ -22,6 +22,7 @@ import org.openqa.grid.e2e.misc.ConfigInheritanceTest; import org.openqa.grid.e2e.misc.Grid1HeartbeatTest; import org.openqa.grid.e2e.misc.GridDistributionTest; +import org.openqa.grid.e2e.misc.GridListActiveSessionsTest; import org.openqa.grid.e2e.misc.GridSerializeExceptionTest; import org.openqa.grid.e2e.misc.GridViaCommandLineTest; import org.openqa.grid.e2e.misc.HubRestart; @@ -58,6 +59,7 @@ NodeTimeOutTest.class, SmokeTest.class, // slow WebDriverPriorityDemo.class, + GridListActiveSessionsTest.class }) public class GridE2ETests { } diff --git a/java/server/test/org/openqa/grid/e2e/misc/GridListActiveSessionsTest.java b/java/server/test/org/openqa/grid/e2e/misc/GridListActiveSessionsTest.java new file mode 100644 index 0000000000000..2b6db3300b8ed --- /dev/null +++ b/java/server/test/org/openqa/grid/e2e/misc/GridListActiveSessionsTest.java @@ -0,0 +1,140 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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 org.openqa.grid.e2e.misc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.openqa.grid.e2e.node.SmokeTest; +import org.openqa.grid.web.Hub; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.json.JsonInput; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.remote.RemoteWebDriver; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +@SuppressWarnings("unchecked") +public class GridListActiveSessionsTest { + + @Test + public void testNoSessions() throws Exception { + runTest(1, 0, map -> { + List> proxies = extractProxies(map); + assertTrue("No sessions data should be found", proxies.isEmpty()); + }); + } + + @Test + public void testOneSessionWithSingleProxy() throws Exception { + runTestForMultipleSessions(1, 1, 1); + } + + @Test + public void testMultipleSessionsWithSingleProxy() throws Exception { + runTestForMultipleSessions(1, 2, 2); + } + + @Test + public void testMultipleSessionsWithMultipleProxies() throws Exception { + runTestForMultipleSessions(2, 2, 1); + } + + private void runTestForMultipleSessions(int howManyNodes, int howManySessions, + int expectedSessionsPerProxy) throws Exception { + runTest(howManyNodes, + howManySessions, + map -> { + List> proxies = extractProxies(map); + assertEquals("Number of proxies", howManyNodes, proxies.size()); + for (Map proxy : proxies) { + Map sessions = (Map) proxy.get("sessions"); + List values = (List) sessions.get("value"); + assertEquals("Sessions per proxy", expectedSessionsPerProxy, + values.size()); + } + }); + } + + private void runTest(int nodesCount, int howMany, + Consumer> assertions) throws Exception { + Hub hub = null; + List drivers = new ArrayList<>(); + try { + hub = SmokeTest.prepareTestGrid(DesiredCapabilities.chrome(), nodesCount); + drivers = createSession(howMany, hub); + Map sessions = getSessions(hub); + assertions.accept(sessions); + } finally { + drivers.forEach(RemoteWebDriver::quit); + if (hub != null) { + hub.stop(); + } + } + } + + private List createSession(int howMany, Hub hub) { + List drivers = new ArrayList<>(); + if (howMany == 0) { + return drivers; + } + URL url; + try { + url = new URL("http://" + hub.getUrl().getHost() + ":" + + hub.getUrl().getPort() + "/wd/hub"); + } catch (MalformedURLException e) { + return new ArrayList<>(); + } + for (int i = 0; i < howMany; i++) { + drivers.add(new RemoteWebDriver(url, new ChromeOptions())); + } + return drivers; + + } + + private Map getSessions(Hub hub) throws IOException { + String url = String.format("http://%s:%d/grid/api/sessions", hub.getUrl().getHost(), + hub.getUrl().getPort()); + URL grid = new URL(url); + URLConnection connection = grid.openConnection(); + try (InputStream in = connection.getInputStream(); + JsonInput input = new Json().newInput(new BufferedReader(new InputStreamReader(in)))) { + return input.read(Json.MAP_TYPE); + + } + } + + private List> extractProxies(Map map) { + boolean success = Boolean.parseBoolean(map.get("success").toString()); + assertTrue("Status should be true", success); + return (List>) map.get("proxies"); + } + +} diff --git a/java/server/test/org/openqa/grid/e2e/node/SmokeTest.java b/java/server/test/org/openqa/grid/e2e/node/SmokeTest.java index 8502e25415348..5916756361eb1 100644 --- a/java/server/test/org/openqa/grid/e2e/node/SmokeTest.java +++ b/java/server/test/org/openqa/grid/e2e/node/SmokeTest.java @@ -40,24 +40,7 @@ public class SmokeTest { @Before public void prepare() throws Exception { - - hub = GridTestHelper.getHub(); - - SelfRegisteringRemote remote = - GridTestHelper.getRemoteWithoutCapabilities(hub.getUrl(), GridRole.NODE); - remote.addBrowser(GridTestHelper.getDefaultBrowserCapability(), 1); - - DesiredCapabilities capabilities = DesiredCapabilities.htmlUnit(); - capabilities.setCapability(RegistrationRequest.SELENIUM_PROTOCOL,SeleniumProtocol.WebDriver); - - remote.addBrowser(capabilities, 1); - - remote.setRemoteServer(new SeleniumServer(remote.getConfiguration())); - remote.startRemoteServer(); - - remote.getConfiguration().timeout = -1; - remote.sendRegistrationRequest(); - RegistryTestHelper.waitForNode(hub.getRegistry(), 1); + hub = prepareTestGrid(); } @Test @@ -79,4 +62,31 @@ public void browserOnWebDriver() { public void stop() { hub.stop(); } + + public static Hub prepareTestGrid() throws Exception { + return prepareTestGrid(DesiredCapabilities.htmlUnit(),1); + } + + public static Hub prepareTestGrid(DesiredCapabilities caps, int nodesCount) throws Exception { + Hub hub = GridTestHelper.getHub(); + for (int i = 1; i <= nodesCount; i++) { + + SelfRegisteringRemote remote = + GridTestHelper.getRemoteWithoutCapabilities(hub.getUrl(), GridRole.NODE); + remote.addBrowser(caps, 1); + + DesiredCapabilities capabilities = new DesiredCapabilities(caps); + caps.setCapability(RegistrationRequest.SELENIUM_PROTOCOL, SeleniumProtocol.WebDriver); + + remote.addBrowser(capabilities, 1); + + remote.setRemoteServer(new SeleniumServer(remote.getConfiguration())); + remote.startRemoteServer(); + + remote.getConfiguration().timeout = -1; + remote.sendRegistrationRequest(); + RegistryTestHelper.waitForNode(hub.getRegistry(), i); + } + return hub; + } }