package com.intellij.javascript.karma.server; import com.google.common.base.Splitter; import com.google.common.collect.Maps; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.intellij.javascript.karma.KarmaConfig; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; import com.intellij.webcore.util.JsonUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; public class KarmaServerState { private static final Logger LOG = Logger.getInstance(KarmaServerState.class); private static final String BROWSER_CONNECTED_EVENT_TYPE = "browserConnected"; private static final String BROWSER_DISCONNECTED_EVENT_TYPE = "browserDisconnected"; private static final Pattern SERVER_PORT_LINE_PATTERN = Pattern.compile("Karma.+server started at http://[^:]+:(\\d+)/.*$"); private static final String[][] FAILED_TO_START_BROWSER_PATTERNS = new String[][] { {"ERROR [launcher]: No binary for ", " browser on your platform.\n"}, {"WARN [launcher]: Can not load \"", "\", it is not registered!\n"}, {"ERROR [launcher]: Cannot start ", "\n"} }; private final KarmaServer myServer; private final List<String> myOverriddenBrowsers; private final ConcurrentMap<String, CapturedBrowser> myCapturedBrowsers = Maps.newConcurrentMap(); private final AtomicInteger myBoundServerPort = new AtomicInteger(-1); private final AtomicBoolean myBrowsersReady = new AtomicBoolean(false); private final List<String> myFailedToStartBrowsers = ContainerUtil.createLockFreeCopyOnWriteList(); private volatile KarmaConfig myConfig; public KarmaServerState(@NotNull KarmaServer server, @NotNull File configurationFile) { myServer = server; myOverriddenBrowsers = parseBrowsers(server.getServerSettings().getBrowsers()); myServer.registerStreamEventHandler(new BrowserEventHandler(BROWSER_CONNECTED_EVENT_TYPE)); myServer.registerStreamEventHandler(new BrowserEventHandler(BROWSER_DISCONNECTED_EVENT_TYPE)); myServer.registerStreamEventHandler(new ConfigHandler(configurationFile)); } @Nullable private static List<String> parseBrowsers(@NotNull String browsersStr) { if (StringUtil.isEmptyOrSpaces(browsersStr)) { return null; } Splitter splitter = Splitter.on(',').trimResults().omitEmptyStrings(); return splitter.splitToList(browsersStr); } private void handleBrowsersChange(@NotNull String eventType, @NotNull String browserId, @NotNull String browserName, @Nullable Boolean autoCaptured) { if (BROWSER_CONNECTED_EVENT_TYPE.equals(eventType)) { boolean captured = ObjectUtils.notNull(autoCaptured, true); CapturedBrowser browser = new CapturedBrowser(browserName, browserId, captured); myCapturedBrowsers.put(browserId, browser); if (autoCaptured == Boolean.FALSE || canSetBrowsersReady()) { setBrowsersReady(); } } else { myCapturedBrowsers.remove(browserId); } } private boolean canSetBrowsersReady() { List<String> expectedBrowsers = myOverriddenBrowsers; if (expectedBrowsers == null) { KarmaConfig config = myConfig; if (config == null) { return true; } expectedBrowsers = config.getBrowsers(); } Set<String> expectedBrowserSet = ContainerUtil.newHashSet(expectedBrowsers); expectedBrowserSet.removeAll(myFailedToStartBrowsers); int autoCapturedBrowserCount = getAutoCapturedBrowserCount(); return autoCapturedBrowserCount > 0 && expectedBrowserSet.size() <= autoCapturedBrowserCount; } private int getAutoCapturedBrowserCount() { int res = 0; for (CapturedBrowser browser : myCapturedBrowsers.values()) { if (browser.isAutoCaptured()) { res++; } } return res; } private void setBrowsersReady() { if (myBrowsersReady.compareAndSet(false, true)) { myServer.fireOnBrowsersReady(); } } public boolean areBrowsersReady() { return myBrowsersReady.get(); } @NotNull public Collection<CapturedBrowser> getCapturedBrowsers() { return myCapturedBrowsers.values(); } public int getServerPort() { return myBoundServerPort.get(); } @Nullable public KarmaConfig getKarmaConfig() { return myConfig; } public void onStandardOutputLineAvailable(@NotNull String line) { int serverPort = myBoundServerPort.get(); if (serverPort == -1) { serverPort = parseServerPort(line); if (serverPort != -1 && myBoundServerPort.compareAndSet(-1, serverPort)) { myServer.fireOnPortBound(); } } if (!myBrowsersReady.get()) { String failedToStartBrowser = parseFailedToStartBrowser(line); if (failedToStartBrowser != null) { LOG.info("Browser " + failedToStartBrowser + " failed to start: " + line); myFailedToStartBrowsers.add(failedToStartBrowser); if (canSetBrowsersReady()) { setBrowsersReady(); } } } } @Nullable private static String parseFailedToStartBrowser(@NotNull String line) { for (String[] pattern : FAILED_TO_START_BROWSER_PATTERNS) { String failedToStartBrowser = getInnerSubstring(line, pattern[0], pattern[1]); if (failedToStartBrowser != null) { return failedToStartBrowser; } } return null; } private static int parseServerPort(@NotNull String text) { Matcher m = SERVER_PORT_LINE_PATTERN.matcher(text); if (m.find()) { String portStr = m.group(1); try { return Integer.parseInt(portStr); } catch (NumberFormatException e) { LOG.warn("Can't parse web server port from '" + text + "'"); } } return -1; } @Nullable private static String getInnerSubstring(@NotNull String str, @NotNull String prefix, @NotNull String suffix) { if (str.startsWith(prefix) && str.endsWith(suffix) && prefix.length() + suffix.length() <= str.length()) { return str.substring(prefix.length(), str.length() - suffix.length()); } return null; } private class BrowserEventHandler implements StreamEventHandler { private final String myEventType; private BrowserEventHandler(@NotNull String eventType) { myEventType = eventType; } @NotNull @Override public String getEventType() { return myEventType; } @Override public void handle(@NotNull JsonElement eventBody) { if (eventBody.isJsonObject()) { JsonObject event = eventBody.getAsJsonObject(); String id = JsonUtil.getChildAsString(event, "id"); String name = JsonUtil.getChildAsString(event, "name"); Boolean autoCaptured = JsonUtil.getChildAsBooleanObj(event, "isAutoCaptured"); if (id != null && name != null) { handleBrowsersChange(myEventType, id, name, autoCaptured); } else { LOG.warn("Illegal browser event. Type: " + myEventType + ", body: " + eventBody.toString()); } } } } private class ConfigHandler implements StreamEventHandler { private final File myConfigurationFileDir; public ConfigHandler(@NotNull File configurationFile) { myConfigurationFileDir = configurationFile.getParentFile(); } @NotNull @Override public String getEventType() { return "configFile"; } @Override public void handle(@NotNull JsonElement eventBody) { myConfig = KarmaConfig.parseFromJson(eventBody, myConfigurationFileDir); } } }