Java tutorial
/* * Copyright (C) 2015 End Point Corporation * Copyright (C) 2015 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.endpoint.lg.browser.service; import com.endpoint.lg.browser.service.BrowserWindow; import com.endpoint.lg.support.interactivespaces.ConfigurationHelper; import com.endpoint.lg.support.message.Window; import com.google.common.collect.Maps; import interactivespaces.activity.binary.NativeActivityRunner; import interactivespaces.activity.binary.NativeActivityRunnerFactory; import interactivespaces.activity.impl.BaseActivity; import interactivespaces.configuration.Configuration; import interactivespaces.InteractiveSpacesException; import interactivespaces.service.web.client.WebSocketClientService; import interactivespaces.system.InteractiveSpacesEnvironment; import interactivespaces.util.data.json.JsonMapper; import interactivespaces.util.process.NativeApplicationRunner; import interactivespaces.util.process.NativeApplicationRunnerCollection; import java.io.File; import java.io.InputStream; import java.io.IOException; import java.net.ServerSocket; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import org.apache.commons.logging.Log; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.HttpResponse; import org.apache.http.impl.client.DefaultHttpClient; import org.codehaus.jackson.map.ObjectMapper; /** * Class to manage one browser instance, with one window * * @author Josh Tolley <josh@endpoint.com> */ public class BrowserInstance { private static int MIN_DEBUG_PORT = 19000; private static int MAX_DEBUG_PORT = 20000; private static String INITIAL_URL = "http://localhost/0xDEADBEEF"; private NativeApplicationRunnerCollection runnerCollection; private NativeApplicationRunner runner; private Log log; private Configuration config; private WebSocketClientService webSocketClientService; private BaseActivity activity; private int debugPort = 0; private BrowserWindow window; private Window currentWindow; // The Window message we processed to get into our current state private boolean isAvailable; private boolean isStatic; BrowserInstance(BaseActivity act, Configuration cfg, Log lg, InteractiveSpacesEnvironment ise) { final File tmpdir = act.getActivityFilesystem().getTempDataDirectory(); String className; HttpResponse resp; int i = 0; isStatic = false; activity = act; log = lg; config = cfg; webSocketClientService = ise.getServiceRegistry().getService(WebSocketClientService.SERVICE_NAME); runnerCollection = new NativeApplicationRunnerCollection(ise, lg); runner = runnerCollection.newNativeApplicationRunner(); activity.addManagedResource(runnerCollection); debugPort = findDebugPort(); if (debugPort == 0) { getLog().error("Couldn't start browser instance, because I couldn't find a debug port"); return; } className = runBrowser(); while ((window == null || !window.isDebugConnected()) && i < 20) { i++; try { Thread.sleep(100); resp = getDebugHttp(debugPort); if (resp != null) connectDebugWS(debugPort, resp, className); } catch (InterruptedException e) { } } if (!runner.isRunning()) { throw new InteractiveSpacesException("Failed to run browser instance"); } if (window != null && !window.isDebugConnected()) { throw new InteractiveSpacesException("Failed to connect to the browser instance's debug socket"); } if (window == null) { throw new InteractiveSpacesException("Failed to create managed window"); } isAvailable = true; currentWindow = null; } /** * Attempts to get an HTTP debug response from a Chrome browser listening on the given debugPort */ private HttpResponse getDebugHttp(int debugPort) { HttpClient httpclient = new DefaultHttpClient(); HttpGet httpget; HttpResponse resp; httpget = new HttpGet("http://localhost:" + debugPort + "/json"); try { resp = httpclient.execute(httpget); getLog().debug("Response: " + resp); return resp; } catch (IOException e) { // Don't want to log all these. It's expected we'll fail several times before success // getLog().error("Exception connecting to browser debug port", e); return null; } } /** * Connects a websocket to each available browser window that isn't already connected */ private void connectDebugWS(int debugPort, HttpResponse resp, String className) { StringBuilder sb; InputStream is; byte[] buffer = new byte[2048]; int length; ObjectMapper om = new ObjectMapper(); try { sb = new StringBuilder(); is = resp.getEntity().getContent(); sb.append("{\"tabs\":"); while ((length = is.read(buffer)) != -1) { sb.append(new String(buffer, 0, length)); } sb.append("}"); is.close(); getLog().debug(sb); BrowserDebugInfo d = om.readValue(sb.toString(), BrowserDebugInfo.class); // Here we might have multiple tabs open. We need to be sure we // only connect to the visible one. We do this by checking the URL // for the one we requested when starting the instance. for (BrowserTabInfo t : d.tabs) { if (t.type.equals("page") && t.url.equals(INITIAL_URL)) { window = new BrowserWindow(t, activity, className, getLog(), webSocketClientService); break; } } } catch (IOException e) { getLog().error("Exception connecting to browser debug port", e); } } private String runBrowser() { Map<String, Object> runnerConfig = Maps.newHashMap(); String className = activity.getActivityFilesystem().getTempDataDirectory().getAbsolutePath() + "/" + UUID.randomUUID().toString().replace("-", ""); runnerConfig.put(NativeActivityRunner.EXECUTABLE_PATHNAME, config.getRequiredPropertyString("space.activity.lg.browser.service.chrome.path")); runnerConfig.put(NativeActivityRunner.EXECUTABLE_FLAGS, "--user-data-dir=" + className + " " + "--remote-debugging-port=" + debugPort + " " + getConfigArray("space.activity.lg.browser.service.chrome.flags") + " --class=" + className + " " + INITIAL_URL); // Is this useful? Initial testing didn't prove it did much good. // runner.setRestartStrategy(new LimitedRetryRestartStrategy(10, 100, 5000, getSpaceEnvironment())); runner.configure(runnerConfig); runnerCollection.addNativeApplicationRunner(runner); return className; } private String getConfigArray(String key) { return ConfigurationHelper.getConfigurationConcat(config, key, " "); } private Log getLog() { return log; } /** * Signals container that all existing browsers can be recycled, such as * when a new scene message comes in */ public void disable() { currentWindow = null; isAvailable = true; if (window != null) window.disableWindow(); } public Window getCurrentWindow() { return currentWindow; } public boolean isAvailable() { return isAvailable; } public boolean isStatic() { return isStatic; } public void navigate(BrowserStaticPageConf p) { isStatic = true; isAvailable = false; getLog().debug("Positioning browser window to " + p.x_coord + ", " + p.y_coord + ", with dimensions " + p.width + "x" + p.height + ", on viewport " + p.viewport + " and URL " + p.url); window.setViewport(p.viewport); window.positionWindow(p.width, p.height, p.x_coord, p.y_coord); window.enableWindow(); window.navigate(p.url); } /** * Responds to individual window commands */ @SuppressWarnings("unchecked") public void handleBrowserCommand(Window w) { boolean found = false; currentWindow = w; isAvailable = false; getLog().debug("Positioning browser window to " + w.x_coord + ", " + w.y_coord + ", with dimensions " + w.width + "x" + w.height); window.setViewport(w.presentation_viewport); window.positionWindow(w.width, w.height, w.x_coord, w.y_coord); window.enableWindow(); window.navigate(w.assets[0]); } /** * Finds an available debug port * * This could fail, if we find a port and something steals it before we get * a browser listening on it. This seems unlikely. Note that chromium * doesn't fail if its assigned debug port is unavailable (at least so far * as I can tell) */ private int findDebugPort() { int debugPort = 0; ServerSocket s = null; for (int i = MIN_DEBUG_PORT; i < MAX_DEBUG_PORT; i++) { try { s = new ServerSocket(i); s.setReuseAddress(true); debugPort = i; } catch (IOException e) { // Port isn't available getLog().debug("Port " + i + " isn't available"); } finally { try { if (s != null) { s.close(); } } catch (IOException e) { // s wasn't opened. Don't throw this } } if (debugPort != 0) break; } if (debugPort == 0) { getLog().error("Couldn't find unused debug port for new browser activity"); return 0; } getLog().debug("Found debug port " + debugPort + " for new browser instance"); return debugPort; } public void shutdown() { window.shutdown(); runner.shutdown(); } }