diff --git a/pom.xml b/pom.xml
index 625f8f3b7..50137abb3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -177,6 +177,16 @@
javax-websocket-server-impl
${jetty.version}
+
+ org.eclipse.jetty
+ jetty-client
+ ${jetty.version}
+
+
+ org.eclipse.jetty.websocket
+ websocket-client
+ ${jetty.version}
+
org.slf4j
slf4j-api
diff --git a/tools/cli/src/main/java/org/teavm/cli/TeaVMDevServerRunner.java b/tools/cli/src/main/java/org/teavm/cli/TeaVMDevServerRunner.java
index fdec2f02e..e3ba94731 100644
--- a/tools/cli/src/main/java/org/teavm/cli/TeaVMDevServerRunner.java
+++ b/tools/cli/src/main/java/org/teavm/cli/TeaVMDevServerRunner.java
@@ -28,7 +28,6 @@ import org.teavm.tooling.ConsoleTeaVMToolLog;
public final class TeaVMDevServerRunner {
private static Options options = new Options();
- private ConsoleTeaVMToolLog log = new ConsoleTeaVMToolLog(false);
private DevServer devServer;
private CommandLine commandLine;
@@ -71,7 +70,7 @@ public final class TeaVMDevServerRunner {
options.addOption(OptionBuilder
.withDescription("display indicator on web page")
.withLongOpt("indicator")
- .create());
+ .create('i'));
options.addOption(OptionBuilder
.withDescription("automatically reload page when compilation completes")
.withLongOpt("auto-reload")
@@ -80,6 +79,18 @@ public final class TeaVMDevServerRunner {
.withDescription("display more messages on server log")
.withLongOpt("verbose")
.create('v'));
+ options.addOption(OptionBuilder
+ .withArgName("URL")
+ .hasArg()
+ .withDescription("delegate requests to URL")
+ .withLongOpt("proxy-url")
+ .create());
+ options.addOption(OptionBuilder
+ .withArgName("path")
+ .hasArg()
+ .withDescription("delegate requests from path")
+ .withLongOpt("proxy-path")
+ .create());
}
private TeaVMDevServerRunner(CommandLine commandLine) {
@@ -103,7 +114,6 @@ public final class TeaVMDevServerRunner {
TeaVMDevServerRunner runner = new TeaVMDevServerRunner(commandLine);
runner.parseArguments();
- runner.setUp();
runner.runAll();
}
@@ -124,6 +134,13 @@ public final class TeaVMDevServerRunner {
}
}
+ if (commandLine.hasOption("proxy-url")) {
+ devServer.setProxyUrl(commandLine.getOptionValue("proxy-url"));
+ }
+ if (commandLine.hasOption("proxy-path")) {
+ devServer.setProxyPath(commandLine.getOptionValue("proxy-path"));
+ }
+
String[] args = commandLine.getArgs();
if (args.length != 1) {
System.err.println("Unexpected arguments");
@@ -154,10 +171,6 @@ public final class TeaVMDevServerRunner {
}
}
- private void setUp() {
- devServer.setLog(log);
- }
-
private void runAll() {
devServer.start();
}
diff --git a/tools/devserver/pom.xml b/tools/devserver/pom.xml
index d6ae692d8..e76166256 100644
--- a/tools/devserver/pom.xml
+++ b/tools/devserver/pom.xml
@@ -75,6 +75,10 @@
org.eclipse.jetty.websocket
javax-websocket-server-impl
+
+ org.eclipse.jetty.websocket
+ websocket-client
+
javax.servlet
javax.servlet-api
@@ -86,6 +90,12 @@
jackson-databind
true
+
+
+ org.apache.httpcomponents
+ httpclient
+ 4.5.6
+
diff --git a/tools/devserver/src/main/java/org/teavm/devserver/CodeServlet.java b/tools/devserver/src/main/java/org/teavm/devserver/CodeServlet.java
index 5c4eee0d9..192c1e1c2 100644
--- a/tools/devserver/src/main/java/org/teavm/devserver/CodeServlet.java
+++ b/tools/devserver/src/main/java/org/teavm/devserver/CodeServlet.java
@@ -25,13 +25,19 @@ import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
+import java.nio.channels.Channels;
+import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Enumeration;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -40,11 +46,26 @@ import java.util.function.Supplier;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.util.InputStreamContentProvider;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.websocket.api.UpgradeRequest;
+import org.eclipse.jetty.websocket.api.UpgradeResponse;
+import org.eclipse.jetty.websocket.api.WebSocketBehavior;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.eclipse.jetty.websocket.client.io.UpgradeListener;
+import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.teavm.backend.javascript.JavaScriptTarget;
import org.teavm.cache.InMemoryMethodNodeCache;
import org.teavm.cache.InMemoryProgramCache;
@@ -69,17 +90,25 @@ import org.teavm.vm.TeaVMProgressListener;
public class CodeServlet extends HttpServlet {
private static final Supplier EMPTY_CONTENT = () -> null;
+ private WebSocketServletFactory wsFactory;
private String mainClass;
private String[] classPath;
private String fileName = "classes.js";
private String pathToFile = "/";
+ private String indicatorWsPath;
private List sourcePath = new ArrayList<>();
private TeaVMToolLog log = new EmptyTeaVMToolLog();
private boolean indicator;
private boolean automaticallyReloaded;
private int port;
private int debugPort;
+ private String proxyUrl;
+ private String proxyPath = "/";
+ private String proxyHost;
+ private String proxyProtocol;
+ private int proxyPort;
+ private String proxyBaseUrl;
private Map> sourceFileCache = new HashMap<>();
@@ -95,7 +124,7 @@ public class CodeServlet extends HttpServlet {
private final Map content = new HashMap<>();
private MemoryBuildTarget buildTarget = new MemoryBuildTarget();
- private final Set wsEndpoints = new LinkedHashSet<>();
+ private final Set progressHandlers = new LinkedHashSet<>();
private final Object statusLock = new Object();
private volatile boolean cancelRequested;
private boolean compiling;
@@ -103,10 +132,15 @@ public class CodeServlet extends HttpServlet {
private boolean waiting;
private Thread buildThread;
private List listeners = new ArrayList<>();
+ private HttpClient httpClient;
+ private WebSocketClient wsClient = new WebSocketClient();
public CodeServlet(String mainClass, String[] classPath) {
this.mainClass = mainClass;
this.classPath = classPath.clone();
+
+ httpClient = new HttpClient();
+ httpClient.setFollowRedirects(false);
}
public void setFileName(String fileName) {
@@ -114,13 +148,7 @@ public class CodeServlet extends HttpServlet {
}
public void setPathToFile(String pathToFile) {
- if (!pathToFile.endsWith("/")) {
- pathToFile += "/";
- }
- if (!pathToFile.startsWith("/")) {
- pathToFile = "/" + pathToFile;
- }
- this.pathToFile = pathToFile;
+ this.pathToFile = normalizePath(pathToFile);
}
public List getSourcePath() {
@@ -147,9 +175,17 @@ public class CodeServlet extends HttpServlet {
this.automaticallyReloaded = automaticallyReloaded;
}
- public void addWsEndpoint(CodeWsEndpoint endpoint) {
- synchronized (wsEndpoints) {
- wsEndpoints.add(endpoint);
+ public void setProxyUrl(String proxyUrl) {
+ this.proxyUrl = proxyUrl;
+ }
+
+ public void setProxyPath(String proxyPath) {
+ this.proxyPath = normalizePath(proxyPath);
+ }
+
+ public void addProgressHandler(ProgressHandler handler) {
+ synchronized (progressHandlers) {
+ progressHandlers.add(handler);
}
double progress;
@@ -160,12 +196,12 @@ public class CodeServlet extends HttpServlet {
progress = this.progress;
}
- endpoint.progress(progress);
+ handler.progress(progress);
}
- public void removeWsEndpoint(CodeWsEndpoint endpoint) {
- synchronized (wsEndpoints) {
- wsEndpoints.remove(endpoint);
+ public void removeProgressHandler(ProgressHandler handler) {
+ synchronized (progressHandlers) {
+ progressHandlers.remove(handler);
}
}
@@ -201,20 +237,73 @@ public class CodeServlet extends HttpServlet {
}
@Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ public void init(ServletConfig config) throws ServletException {
+ super.init(config);
+
+ if (proxyUrl != null) {
+ try {
+ httpClient.start();
+ wsClient.start();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ URL url = new URL(proxyUrl);
+ proxyPort = url.getPort();
+ proxyHost = proxyPort != 80 ? url.getHost() + ":" + proxyPort : url.getHost();
+ proxyProtocol = url.getProtocol();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(proxyProtocol).append("://").append(proxyHost);
+ proxyBaseUrl = sb.toString();
+ } catch (MalformedURLException e) {
+ log.warning("Could not extract host from URL: " + proxyUrl, e);
+ }
+ }
+
+ indicatorWsPath = pathToFile + fileName + ".ws";
+ WebSocketPolicy wsPolicy = new WebSocketPolicy(WebSocketBehavior.SERVER);
+ wsFactory = WebSocketServletFactory.Loader.load(config.getServletContext(), wsPolicy);
+ wsFactory.setCreator((req, resp) -> {
+ ProxyWsClient proxyClient = (ProxyWsClient) req.getHttpServletRequest().getAttribute("teavm.ws.client");
+ if (proxyClient == null) {
+ return new CodeWsEndpoint(this);
+ } else {
+ ProxyWsClient proxy = new ProxyWsClient();
+ proxy.setTarget(proxyClient);
+ proxyClient.setTarget(proxy);
+ return proxy;
+ }
+ });
+ try {
+ wsFactory.start();
+ } catch (Exception e) {
+ throw new ServletException(e);
+ }
+ }
+
+ @Override
+ protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String path = req.getPathInfo();
if (path != null) {
log.debug("Serving " + path);
if (!path.startsWith("/")) {
path = "/" + path;
}
- if (path.startsWith(pathToFile) && path.length() > pathToFile.length()) {
+ if (req.getMethod().equals("GET") && path.startsWith(pathToFile) && path.length() > pathToFile.length()) {
String fileName = path.substring(pathToFile.length());
if (fileName.startsWith("src/")) {
if (serveSourceFile(fileName.substring("src/".length()), resp)) {
log.debug("File " + path + " served as source file");
return;
}
+ } else if (path.equals(indicatorWsPath)) {
+ if (wsFactory.isUpgradeRequest(req, resp)) {
+ if (wsFactory.acceptWebSocket(req, resp) || resp.isCommitted()) {
+ return;
+ }
+ }
} else {
byte[] fileContent;
boolean firstTime;
@@ -236,15 +325,216 @@ public class CodeServlet extends HttpServlet {
}
}
}
+
+ if (proxyUrl != null && path.startsWith(proxyPath)) {
+ if (wsFactory.isUpgradeRequest(req, resp)) {
+ proxyWebSocket(req, resp, path);
+ } else {
+ proxy(req, resp, path);
+ }
+ return;
+ }
}
log.debug("File " + path + " not found");
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
+ private void proxy(HttpServletRequest req, HttpServletResponse resp, String path) throws IOException {
+ AsyncContext async = req.startAsync();
+
+ String relPath = path.substring(proxyPath.length());
+ StringBuilder sb = new StringBuilder(proxyUrl);
+ if (!relPath.isEmpty() && !proxyUrl.endsWith("/")) {
+ sb.append("/");
+ }
+ sb.append(relPath);
+
+ if (req.getQueryString() != null) {
+ sb.append("?").append(req.getQueryString());
+ }
+ log.debug("Trying to serve '" + relPath + "' from '" + sb + "'");
+
+ Request proxyReq = httpClient.newRequest(sb.toString());
+ proxyReq.method(req.getMethod());
+ copyRequestHeaders(req, proxyReq::header);
+
+ proxyReq.content(new InputStreamContentProvider(req.getInputStream()));
+ HeaderSender headerSender = new HeaderSender(resp);
+
+ proxyReq.onResponseContent((response, responseContent) -> {
+ headerSender.send(response);
+ try {
+ WritableByteChannel channel = Channels.newChannel(resp.getOutputStream());
+ while (responseContent.remaining() > 0) {
+ channel.write(responseContent);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ proxyReq.send(result -> {
+ headerSender.send(result.getResponse());
+ async.complete();
+ });
+ }
+
+ class HeaderSender {
+ final HttpServletResponse resp;
+ boolean sent;
+
+ HeaderSender(HttpServletResponse resp) {
+ this.resp = resp;
+ }
+
+ void send(Response response) {
+ if (sent) {
+ return;
+ }
+
+ sent = true;
+ resp.setStatus(response.getStatus());
+
+ for (HttpField field : response.getHeaders()) {
+ if (field.getName().toLowerCase().equals("location")) {
+ String value = field.getValue();
+ if (value.startsWith(proxyUrl)) {
+ String relLocation = value.substring(proxyUrl.length());
+ resp.addHeader(field.getName(), "http://localhost:" + port + proxyPath + relLocation);
+ continue;
+ }
+ }
+ resp.addHeader(field.getName(), field.getValue());
+ }
+ }
+ }
+
+ private void proxyWebSocket(HttpServletRequest req, HttpServletResponse resp, String path) throws IOException {
+ AsyncContext async = req.startAsync();
+
+ String relPath = path.substring(proxyPath.length());
+ StringBuilder sb = new StringBuilder(proxyProtocol.equals("http") ? "ws" : "wss").append("://");
+ sb.append(proxyHost);
+ if (!relPath.isEmpty()) {
+ sb.append("/");
+ }
+ sb.append(relPath);
+ if (req.getQueryString() != null) {
+ sb.append("?").append(req.getQueryString());
+ }
+ URI uri;
+ try {
+ uri = new URI(sb.toString());
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+
+ ProxyWsClient client = new ProxyWsClient();
+ req.setAttribute("teavm.ws.client", client);
+ ClientUpgradeRequest proxyReq = new ClientUpgradeRequest();
+ proxyReq.setMethod(req.getMethod());
+ Map> headers = new LinkedHashMap<>();
+ copyRequestHeaders(req, (key, value) -> headers.computeIfAbsent(key, k -> new ArrayList<>()).add(value));
+ proxyReq.setHeaders(headers);
+
+ wsClient.connect(client, uri, proxyReq, new UpgradeListener() {
+ @Override
+ public void onHandshakeRequest(UpgradeRequest request) {
+ }
+
+ @Override
+ public void onHandshakeResponse(UpgradeResponse response) {
+ resp.setStatus(response.getStatusCode());
+ for (String header : response.getHeaderNames()) {
+ switch (header.toLowerCase()) {
+ case "connection":
+ case "date":
+ case "sec-websocket-accept":
+ case "upgrade":
+ continue;
+ }
+ for (String value : response.getHeaders(header)) {
+ resp.addHeader(header, value);
+ }
+ }
+
+ try {
+ wsFactory.acceptWebSocket(req, resp);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ async.complete();
+ }
+ });
+ }
+
+ private void copyRequestHeaders(HttpServletRequest req, HeaderConsumer proxyReq) {
+ Enumeration headers = req.getHeaderNames();
+ while (headers.hasMoreElements()) {
+ String header = headers.nextElement();
+ String headerLower = header.toLowerCase();
+ switch (headerLower) {
+ case "host":
+ if (proxyHost != null) {
+ proxyReq.header(header, proxyHost);
+ continue;
+ }
+ break;
+ case "origin":
+ if (proxyBaseUrl != null) {
+ String origin = req.getHeader(header);
+ if (origin.equals("http://localhost:" + port)) {
+ proxyReq.header(header, proxyBaseUrl);
+ continue;
+ }
+ }
+ break;
+ case "referer": {
+ String referer = req.getHeader(header);
+ String localUrl = "http://localhost:" + port + "/";
+ if (referer.startsWith(localUrl)) {
+ String relReferer = referer.substring(localUrl.length());
+ proxyReq.header(header, proxyUrl + relReferer);
+ continue;
+ }
+ break;
+ }
+ case "connection":
+ case "upgrade":
+ case "user-agent":
+ case "sec-websocket-key":
+ case "sec-websocket-version":
+ case "sec-websocket-extensions":
+ case "accept-encoding":
+ continue;
+ }
+ Enumeration values = req.getHeaders(header);
+ while (values.hasMoreElements()) {
+ proxyReq.header(header, values.nextElement());
+ }
+ }
+ }
+
@Override
public void destroy() {
super.destroy();
+ try {
+ wsFactory.stop();
+ } catch (Exception e) {
+ log.warning("Error stopping WebSocket server", e);
+ }
+ if (proxyUrl != null) {
+ try {
+ httpClient.stop();
+ } catch (Exception e) {
+ log.warning("Error stopping HTTP client", e);
+ }
+ try {
+ wsClient.stop();
+ } catch (Exception e) {
+ log.warning("Error stopping WebSocket client", e);
+ }
+ }
stopped = true;
synchronized (statusLock) {
if (waiting) {
@@ -602,13 +892,13 @@ public class CodeServlet extends HttpServlet {
this.progress = progress;
}
- CodeWsEndpoint[] endpoints;
- synchronized (wsEndpoints) {
- endpoints = wsEndpoints.toArray(new CodeWsEndpoint[0]);
+ ProgressHandler[] handlers;
+ synchronized (progressHandlers) {
+ handlers = progressHandlers.toArray(new ProgressHandler[0]);
}
- for (CodeWsEndpoint endpoint : endpoints) {
- endpoint.progress(progress);
+ for (ProgressHandler handler : handlers) {
+ handler.progress(progress);
}
for (DevServerListener listener : listeners) {
@@ -624,13 +914,13 @@ public class CodeServlet extends HttpServlet {
compiling = false;
}
- CodeWsEndpoint[] endpoints;
- synchronized (wsEndpoints) {
- endpoints = wsEndpoints.toArray(new CodeWsEndpoint[0]);
+ ProgressHandler[] handlers;
+ synchronized (progressHandlers) {
+ handlers = progressHandlers.toArray(new ProgressHandler[0]);
}
- for (CodeWsEndpoint endpoint : endpoints) {
- endpoint.complete(success);
+ for (ProgressHandler handler : handlers) {
+ handler.complete(success);
}
}
@@ -725,4 +1015,14 @@ public class CodeServlet extends HttpServlet {
return TeaVMProgressFeedback.CONTINUE;
}
}
+
+ static String normalizePath(String path) {
+ if (!path.endsWith("/")) {
+ path += "/";
+ }
+ if (!path.startsWith("/")) {
+ path = "/" + path;
+ }
+ return path;
+ }
}
diff --git a/tools/devserver/src/main/java/org/teavm/devserver/CodeWsEndpoint.java b/tools/devserver/src/main/java/org/teavm/devserver/CodeWsEndpoint.java
index 278cad802..163b5ff75 100644
--- a/tools/devserver/src/main/java/org/teavm/devserver/CodeWsEndpoint.java
+++ b/tools/devserver/src/main/java/org/teavm/devserver/CodeWsEndpoint.java
@@ -15,43 +15,50 @@
*/
package org.teavm.devserver;
-import javax.websocket.OnClose;
-import javax.websocket.OnOpen;
-import javax.websocket.Session;
-import javax.websocket.server.ServerEndpoint;
+import java.util.HashMap;
+import java.util.Map;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
-@ServerEndpoint("/")
+@WebSocket
public class CodeWsEndpoint {
- private Session session;
+ private Map progressHandlerMap = new HashMap<>();
private CodeServlet servlet;
- @OnOpen
+ public CodeWsEndpoint(CodeServlet servlet) {
+ this.servlet = servlet;
+ }
+
+ @OnWebSocketConnect
public void open(Session session) {
- this.session = session;
- servlet = (CodeServlet) session.getUserProperties().get("teavm.servlet");
- if (servlet != null) {
- servlet.addWsEndpoint(this);
- }
+ ProgressHandlerImpl progressHandler = new ProgressHandlerImpl(session);
+ progressHandlerMap.put(session, progressHandler);
+ servlet.addProgressHandler(progressHandler);
}
- @OnClose
- public void close() {
- if (servlet != null) {
- servlet.removeWsEndpoint(this);
- }
- servlet = null;
- session = null;
+ @OnWebSocketClose
+ public void close(Session session, int code, String reason) {
+ ProgressHandlerImpl handler = progressHandlerMap.remove(session);
+ servlet.removeProgressHandler(handler);
}
- public void progress(double value) {
- if (session != null) {
- session.getAsyncRemote().sendText("{ \"command\": \"compiling\", \"progress\": " + value + " }");
- }
- }
+ static class ProgressHandlerImpl implements ProgressHandler {
+ Session session;
- public void complete(boolean success) {
- if (session != null) {
- session.getAsyncRemote().sendText("{ \"command\": \"complete\", \"success\": " + success + " }");
+ ProgressHandlerImpl(Session session) {
+ this.session = session;
+ }
+
+ @Override
+ public void progress(double value) {
+ session.getRemote().sendStringByFuture("{ \"command\": \"compiling\", \"progress\": " + value + " }");
+ }
+
+ @Override
+ public void complete(boolean success) {
+ session.getRemote().sendStringByFuture("{ \"command\": \"complete\", \"success\": " + success + " }");
}
}
}
diff --git a/tools/devserver/src/main/java/org/teavm/devserver/DevServer.java b/tools/devserver/src/main/java/org/teavm/devserver/DevServer.java
index a30a58a6a..1588d1cf0 100644
--- a/tools/devserver/src/main/java/org/teavm/devserver/DevServer.java
+++ b/tools/devserver/src/main/java/org/teavm/devserver/DevServer.java
@@ -16,20 +16,11 @@
package org.teavm.devserver;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
-import javax.websocket.Decoder;
-import javax.websocket.Encoder;
-import javax.websocket.Extension;
-import javax.websocket.server.ServerContainer;
-import javax.websocket.server.ServerEndpointConfig;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
-import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
import org.teavm.tooling.TeaVMToolLog;
public class DevServer {
@@ -47,6 +38,8 @@ public class DevServer {
private Server server;
private int port = 9090;
private int debugPort;
+ private String proxyUrl;
+ private String proxyPath = "/";
public void setMainClass(String mainClass) {
this.mainClass = mainClass;
@@ -90,6 +83,14 @@ public class DevServer {
this.reloadedAutomatically = reloadedAutomatically;
}
+ public void setProxyUrl(String proxyUrl) {
+ this.proxyUrl = proxyUrl;
+ }
+
+ public void setProxyPath(String proxyPath) {
+ this.proxyPath = proxyPath;
+ }
+
public List getSourcePath() {
return sourcePath;
}
@@ -128,14 +129,16 @@ public class DevServer {
servlet.setAutomaticallyReloaded(reloadedAutomatically);
servlet.setPort(port);
servlet.setDebugPort(debugPort);
+ servlet.setProxyUrl(proxyUrl);
+ servlet.setProxyPath(proxyPath);
for (DevServerListener listener : listeners) {
servlet.addListener(listener);
}
- context.addServlet(new ServletHolder(servlet), "/*");
+ ServletHolder servletHolder = new ServletHolder(servlet);
+ servletHolder.setAsyncSupported(true);
+ context.addServlet(servletHolder, "/*");
try {
- ServerContainer wscontainer = WebSocketServerContainerInitializer.configureContext(context);
- wscontainer.addEndpoint(new DevServerEndpointConfig(servlet));
server.start();
server.join();
} catch (Exception e) {
@@ -152,52 +155,4 @@ public class DevServer {
server = null;
servlet = null;
}
-
- private class DevServerEndpointConfig implements ServerEndpointConfig {
- private Map userProperties = new HashMap<>();
-
- public DevServerEndpointConfig(CodeServlet servlet) {
- userProperties.put("teavm.servlet", servlet);
- }
-
- @Override
- public List> getDecoders() {
- return Collections.emptyList();
- }
-
- @Override
- public List> getEncoders() {
- return Collections.emptyList();
- }
-
- @Override
- public Map getUserProperties() {
- return userProperties;
- }
-
- @Override
- public Configurator getConfigurator() {
- return null;
- }
-
- @Override
- public Class> getEndpointClass() {
- return CodeWsEndpoint.class;
- }
-
- @Override
- public List getExtensions() {
- return Collections.emptyList();
- }
-
- @Override
- public String getPath() {
- return pathToFile + fileName + ".ws";
- }
-
- @Override
- public List getSubprotocols() {
- return Collections.emptyList();
- }
- }
}
diff --git a/tools/devserver/src/main/java/org/teavm/devserver/HeaderConsumer.java b/tools/devserver/src/main/java/org/teavm/devserver/HeaderConsumer.java
new file mode 100644
index 000000000..74d3bd65f
--- /dev/null
+++ b/tools/devserver/src/main/java/org/teavm/devserver/HeaderConsumer.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018 Alexey Andreev.
+ *
+ * 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 org.teavm.devserver;
+
+interface HeaderConsumer {
+ void header(String key, String value);
+}
diff --git a/tools/devserver/src/main/java/org/teavm/devserver/ProgressHandler.java b/tools/devserver/src/main/java/org/teavm/devserver/ProgressHandler.java
new file mode 100644
index 000000000..86ca72e72
--- /dev/null
+++ b/tools/devserver/src/main/java/org/teavm/devserver/ProgressHandler.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2018 Alexey Andreev.
+ *
+ * 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 org.teavm.devserver;
+
+public interface ProgressHandler {
+ void complete(boolean success);
+
+ void progress(double value);
+}
diff --git a/tools/devserver/src/main/java/org/teavm/devserver/ProxyWsClient.java b/tools/devserver/src/main/java/org/teavm/devserver/ProxyWsClient.java
new file mode 100644
index 000000000..61f89cfed
--- /dev/null
+++ b/tools/devserver/src/main/java/org/teavm/devserver/ProxyWsClient.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018 Alexey Andreev.
+ *
+ * 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 org.teavm.devserver;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+
+@WebSocket
+public class ProxyWsClient {
+ private Session session;
+ private ProxyWsClient target;
+ private boolean closed;
+ private List> pendingMessages = new ArrayList<>();
+
+ public void setTarget(ProxyWsClient target) {
+ if (this.target != null) {
+ throw new IllegalStateException();
+ }
+ this.target = target;
+ flush();
+ target.flush();
+ }
+
+ @OnWebSocketConnect
+ public void connect(Session session) {
+ this.session = session;
+ flush();
+ if (target != null) {
+ target.flush();
+ }
+ }
+
+ @OnWebSocketClose
+ public void close(int code, String reason) {
+ closed = true;
+ if (!target.closed) {
+ target.closed = true;
+ session.close(code, reason);
+ }
+ }
+
+ @OnWebSocketMessage
+ public void onMessage(byte[] buf, int offset, int length) {
+ send(t -> t.session.getRemote().sendBytesByFuture(ByteBuffer.wrap(buf, offset, length)));
+ }
+
+ @OnWebSocketMessage
+ public void onMessage(String text) {
+ send(t -> t.session.getRemote().sendStringByFuture(text));
+ }
+
+ private void send(Consumer message) {
+ if (target == null || target.session == null || !target.session.isOpen()) {
+ if (pendingMessages != null) {
+ pendingMessages.add(message);
+ }
+ } else {
+ message.accept(target);
+ }
+ }
+
+ private void flush() {
+ if (pendingMessages == null || target == null || target.session == null || !target.session.isOpen()) {
+ return;
+ }
+ for (Consumer message : pendingMessages) {
+ message.accept(target);
+ }
+ pendingMessages = null;
+ }
+}
diff --git a/tools/idea/plugin/src/main/java/org/teavm/idea/devserver/DevServerConfiguration.java b/tools/idea/plugin/src/main/java/org/teavm/idea/devserver/DevServerConfiguration.java
index e332883b0..a67ccf2f7 100644
--- a/tools/idea/plugin/src/main/java/org/teavm/idea/devserver/DevServerConfiguration.java
+++ b/tools/idea/plugin/src/main/java/org/teavm/idea/devserver/DevServerConfiguration.java
@@ -27,4 +27,6 @@ public class DevServerConfiguration {
public String pathToFile;
public String fileName;
public int debugPort;
+ public String proxyUrl;
+ public String proxyPath;
}
diff --git a/tools/idea/plugin/src/main/java/org/teavm/idea/devserver/DevServerRunner.java b/tools/idea/plugin/src/main/java/org/teavm/idea/devserver/DevServerRunner.java
index fc8c652d5..e1de02d8d 100644
--- a/tools/idea/plugin/src/main/java/org/teavm/idea/devserver/DevServerRunner.java
+++ b/tools/idea/plugin/src/main/java/org/teavm/idea/devserver/DevServerRunner.java
@@ -108,7 +108,7 @@ public class DevServerRunner extends UnicastRemoteObject implements DevServerMan
public static void main(String[] args) throws Exception {
DevServer server = new DevServer();
- server.setLog(new ConsoleTeaVMToolLog(true));
+ server.setLog(new ConsoleTeaVMToolLog(false));
server.setMainClass(args[0]);
List classPath = new ArrayList<>();
for (int i = 1; i < args.length; ++i) {
@@ -137,6 +137,12 @@ public class DevServerRunner extends UnicastRemoteObject implements DevServerMan
case "-P":
server.setDebugPort(Integer.parseInt(args[++i]));
break;
+ case "-proxy-url":
+ server.setProxyUrl(args[++i]);
+ break;
+ case "-proxy-path":
+ server.setProxyPath(args[++i]);
+ break;
}
}
server.setClassPath(classPath.toArray(new String[0]));
@@ -196,6 +202,15 @@ public class DevServerRunner extends UnicastRemoteObject implements DevServerMan
arguments.add(Integer.toString(options.debugPort));
}
+ if (options.proxyUrl != null && !options.proxyUrl.isEmpty()) {
+ arguments.add("-proxy-url");
+ arguments.add(options.proxyUrl);
+ }
+ if (options.proxyPath != null) {
+ arguments.add("-proxy-path");
+ arguments.add(options.proxyPath);
+ }
+
ProcessBuilder builder = new ProcessBuilder(arguments.toArray(new String[0]));
Process process = builder.start();
BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream(),
diff --git a/tools/idea/plugin/src/main/java/org/teavm/idea/devserver/TeaVMDevServerConfiguration.java b/tools/idea/plugin/src/main/java/org/teavm/idea/devserver/TeaVMDevServerConfiguration.java
index c1b2e6b7e..ced8298b6 100644
--- a/tools/idea/plugin/src/main/java/org/teavm/idea/devserver/TeaVMDevServerConfiguration.java
+++ b/tools/idea/plugin/src/main/java/org/teavm/idea/devserver/TeaVMDevServerConfiguration.java
@@ -46,6 +46,8 @@ public class TeaVMDevServerConfiguration extends ModuleBasedConfiguration