Allow to delegate requests to dev server to another server

This commit is contained in:
Alexey Andreev 2018-12-27 12:22:30 +03:00
parent c2697dce88
commit 97a1db1b79
14 changed files with 611 additions and 127 deletions

10
pom.xml
View File

@ -177,6 +177,16 @@
<artifactId>javax-websocket-server-impl</artifactId> <artifactId>javax-websocket-server-impl</artifactId>
<version>${jetty.version}</version> <version>${jetty.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-client</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>

View File

@ -28,7 +28,6 @@ import org.teavm.tooling.ConsoleTeaVMToolLog;
public final class TeaVMDevServerRunner { public final class TeaVMDevServerRunner {
private static Options options = new Options(); private static Options options = new Options();
private ConsoleTeaVMToolLog log = new ConsoleTeaVMToolLog(false);
private DevServer devServer; private DevServer devServer;
private CommandLine commandLine; private CommandLine commandLine;
@ -71,7 +70,7 @@ public final class TeaVMDevServerRunner {
options.addOption(OptionBuilder options.addOption(OptionBuilder
.withDescription("display indicator on web page") .withDescription("display indicator on web page")
.withLongOpt("indicator") .withLongOpt("indicator")
.create()); .create('i'));
options.addOption(OptionBuilder options.addOption(OptionBuilder
.withDescription("automatically reload page when compilation completes") .withDescription("automatically reload page when compilation completes")
.withLongOpt("auto-reload") .withLongOpt("auto-reload")
@ -80,6 +79,18 @@ public final class TeaVMDevServerRunner {
.withDescription("display more messages on server log") .withDescription("display more messages on server log")
.withLongOpt("verbose") .withLongOpt("verbose")
.create('v')); .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) { private TeaVMDevServerRunner(CommandLine commandLine) {
@ -103,7 +114,6 @@ public final class TeaVMDevServerRunner {
TeaVMDevServerRunner runner = new TeaVMDevServerRunner(commandLine); TeaVMDevServerRunner runner = new TeaVMDevServerRunner(commandLine);
runner.parseArguments(); runner.parseArguments();
runner.setUp();
runner.runAll(); 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(); String[] args = commandLine.getArgs();
if (args.length != 1) { if (args.length != 1) {
System.err.println("Unexpected arguments"); System.err.println("Unexpected arguments");
@ -154,10 +171,6 @@ public final class TeaVMDevServerRunner {
} }
} }
private void setUp() {
devServer.setLog(log);
}
private void runAll() { private void runAll() {
devServer.start(); devServer.start();
} }

View File

@ -75,6 +75,10 @@
<groupId>org.eclipse.jetty.websocket</groupId> <groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>javax-websocket-server-impl</artifactId> <artifactId>javax-websocket-server-impl</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-client</artifactId>
</dependency>
<dependency> <dependency>
<groupId>javax.servlet</groupId> <groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId> <artifactId>javax.servlet-api</artifactId>
@ -86,6 +90,12 @@
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -25,13 +25,19 @@ import java.io.OutputStreamWriter;
import java.io.Reader; import java.io.Reader;
import java.io.Writer; import java.io.Writer;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader; import java.net.URLClassLoader;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -40,11 +46,26 @@ import java.util.function.Supplier;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import javax.servlet.AsyncContext;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils; 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.backend.javascript.JavaScriptTarget;
import org.teavm.cache.InMemoryMethodNodeCache; import org.teavm.cache.InMemoryMethodNodeCache;
import org.teavm.cache.InMemoryProgramCache; import org.teavm.cache.InMemoryProgramCache;
@ -69,17 +90,25 @@ import org.teavm.vm.TeaVMProgressListener;
public class CodeServlet extends HttpServlet { public class CodeServlet extends HttpServlet {
private static final Supplier<InputStream> EMPTY_CONTENT = () -> null; private static final Supplier<InputStream> EMPTY_CONTENT = () -> null;
private WebSocketServletFactory wsFactory;
private String mainClass; private String mainClass;
private String[] classPath; private String[] classPath;
private String fileName = "classes.js"; private String fileName = "classes.js";
private String pathToFile = "/"; private String pathToFile = "/";
private String indicatorWsPath;
private List<String> sourcePath = new ArrayList<>(); private List<String> sourcePath = new ArrayList<>();
private TeaVMToolLog log = new EmptyTeaVMToolLog(); private TeaVMToolLog log = new EmptyTeaVMToolLog();
private boolean indicator; private boolean indicator;
private boolean automaticallyReloaded; private boolean automaticallyReloaded;
private int port; private int port;
private int debugPort; private int debugPort;
private String proxyUrl;
private String proxyPath = "/";
private String proxyHost;
private String proxyProtocol;
private int proxyPort;
private String proxyBaseUrl;
private Map<String, Supplier<InputStream>> sourceFileCache = new HashMap<>(); private Map<String, Supplier<InputStream>> sourceFileCache = new HashMap<>();
@ -95,7 +124,7 @@ public class CodeServlet extends HttpServlet {
private final Map<String, byte[]> content = new HashMap<>(); private final Map<String, byte[]> content = new HashMap<>();
private MemoryBuildTarget buildTarget = new MemoryBuildTarget(); private MemoryBuildTarget buildTarget = new MemoryBuildTarget();
private final Set<CodeWsEndpoint> wsEndpoints = new LinkedHashSet<>(); private final Set<ProgressHandler> progressHandlers = new LinkedHashSet<>();
private final Object statusLock = new Object(); private final Object statusLock = new Object();
private volatile boolean cancelRequested; private volatile boolean cancelRequested;
private boolean compiling; private boolean compiling;
@ -103,10 +132,15 @@ public class CodeServlet extends HttpServlet {
private boolean waiting; private boolean waiting;
private Thread buildThread; private Thread buildThread;
private List<DevServerListener> listeners = new ArrayList<>(); private List<DevServerListener> listeners = new ArrayList<>();
private HttpClient httpClient;
private WebSocketClient wsClient = new WebSocketClient();
public CodeServlet(String mainClass, String[] classPath) { public CodeServlet(String mainClass, String[] classPath) {
this.mainClass = mainClass; this.mainClass = mainClass;
this.classPath = classPath.clone(); this.classPath = classPath.clone();
httpClient = new HttpClient();
httpClient.setFollowRedirects(false);
} }
public void setFileName(String fileName) { public void setFileName(String fileName) {
@ -114,13 +148,7 @@ public class CodeServlet extends HttpServlet {
} }
public void setPathToFile(String pathToFile) { public void setPathToFile(String pathToFile) {
if (!pathToFile.endsWith("/")) { this.pathToFile = normalizePath(pathToFile);
pathToFile += "/";
}
if (!pathToFile.startsWith("/")) {
pathToFile = "/" + pathToFile;
}
this.pathToFile = pathToFile;
} }
public List<String> getSourcePath() { public List<String> getSourcePath() {
@ -147,9 +175,17 @@ public class CodeServlet extends HttpServlet {
this.automaticallyReloaded = automaticallyReloaded; this.automaticallyReloaded = automaticallyReloaded;
} }
public void addWsEndpoint(CodeWsEndpoint endpoint) { public void setProxyUrl(String proxyUrl) {
synchronized (wsEndpoints) { this.proxyUrl = proxyUrl;
wsEndpoints.add(endpoint); }
public void setProxyPath(String proxyPath) {
this.proxyPath = normalizePath(proxyPath);
}
public void addProgressHandler(ProgressHandler handler) {
synchronized (progressHandlers) {
progressHandlers.add(handler);
} }
double progress; double progress;
@ -160,12 +196,12 @@ public class CodeServlet extends HttpServlet {
progress = this.progress; progress = this.progress;
} }
endpoint.progress(progress); handler.progress(progress);
} }
public void removeWsEndpoint(CodeWsEndpoint endpoint) { public void removeProgressHandler(ProgressHandler handler) {
synchronized (wsEndpoints) { synchronized (progressHandlers) {
wsEndpoints.remove(endpoint); progressHandlers.remove(handler);
} }
} }
@ -201,20 +237,73 @@ public class CodeServlet extends HttpServlet {
} }
@Override @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(); String path = req.getPathInfo();
if (path != null) { if (path != null) {
log.debug("Serving " + path); log.debug("Serving " + path);
if (!path.startsWith("/")) { if (!path.startsWith("/")) {
path = "/" + path; 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()); String fileName = path.substring(pathToFile.length());
if (fileName.startsWith("src/")) { if (fileName.startsWith("src/")) {
if (serveSourceFile(fileName.substring("src/".length()), resp)) { if (serveSourceFile(fileName.substring("src/".length()), resp)) {
log.debug("File " + path + " served as source file"); log.debug("File " + path + " served as source file");
return; return;
} }
} else if (path.equals(indicatorWsPath)) {
if (wsFactory.isUpgradeRequest(req, resp)) {
if (wsFactory.acceptWebSocket(req, resp) || resp.isCommitted()) {
return;
}
}
} else { } else {
byte[] fileContent; byte[] fileContent;
boolean firstTime; 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"); log.debug("File " + path + " not found");
resp.setStatus(HttpServletResponse.SC_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<String, List<String>> 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<String> 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<String> values = req.getHeaders(header);
while (values.hasMoreElements()) {
proxyReq.header(header, values.nextElement());
}
}
}
@Override @Override
public void destroy() { public void destroy() {
super.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; stopped = true;
synchronized (statusLock) { synchronized (statusLock) {
if (waiting) { if (waiting) {
@ -602,13 +892,13 @@ public class CodeServlet extends HttpServlet {
this.progress = progress; this.progress = progress;
} }
CodeWsEndpoint[] endpoints; ProgressHandler[] handlers;
synchronized (wsEndpoints) { synchronized (progressHandlers) {
endpoints = wsEndpoints.toArray(new CodeWsEndpoint[0]); handlers = progressHandlers.toArray(new ProgressHandler[0]);
} }
for (CodeWsEndpoint endpoint : endpoints) { for (ProgressHandler handler : handlers) {
endpoint.progress(progress); handler.progress(progress);
} }
for (DevServerListener listener : listeners) { for (DevServerListener listener : listeners) {
@ -624,13 +914,13 @@ public class CodeServlet extends HttpServlet {
compiling = false; compiling = false;
} }
CodeWsEndpoint[] endpoints; ProgressHandler[] handlers;
synchronized (wsEndpoints) { synchronized (progressHandlers) {
endpoints = wsEndpoints.toArray(new CodeWsEndpoint[0]); handlers = progressHandlers.toArray(new ProgressHandler[0]);
} }
for (CodeWsEndpoint endpoint : endpoints) { for (ProgressHandler handler : handlers) {
endpoint.complete(success); handler.complete(success);
} }
} }
@ -725,4 +1015,14 @@ public class CodeServlet extends HttpServlet {
return TeaVMProgressFeedback.CONTINUE; return TeaVMProgressFeedback.CONTINUE;
} }
} }
static String normalizePath(String path) {
if (!path.endsWith("/")) {
path += "/";
}
if (!path.startsWith("/")) {
path = "/" + path;
}
return path;
}
} }

View File

@ -15,43 +15,50 @@
*/ */
package org.teavm.devserver; package org.teavm.devserver;
import javax.websocket.OnClose; import java.util.HashMap;
import javax.websocket.OnOpen; import java.util.Map;
import javax.websocket.Session; import org.eclipse.jetty.websocket.api.Session;
import javax.websocket.server.ServerEndpoint; 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 { public class CodeWsEndpoint {
private Session session; private Map<Session, ProgressHandlerImpl> progressHandlerMap = new HashMap<>();
private CodeServlet servlet; private CodeServlet servlet;
@OnOpen public CodeWsEndpoint(CodeServlet servlet) {
this.servlet = servlet;
}
@OnWebSocketConnect
public void open(Session session) { public void open(Session session) {
ProgressHandlerImpl progressHandler = new ProgressHandlerImpl(session);
progressHandlerMap.put(session, progressHandler);
servlet.addProgressHandler(progressHandler);
}
@OnWebSocketClose
public void close(Session session, int code, String reason) {
ProgressHandlerImpl handler = progressHandlerMap.remove(session);
servlet.removeProgressHandler(handler);
}
static class ProgressHandlerImpl implements ProgressHandler {
Session session;
ProgressHandlerImpl(Session session) {
this.session = session; this.session = session;
servlet = (CodeServlet) session.getUserProperties().get("teavm.servlet");
if (servlet != null) {
servlet.addWsEndpoint(this);
}
}
@OnClose
public void close() {
if (servlet != null) {
servlet.removeWsEndpoint(this);
}
servlet = null;
session = null;
} }
@Override
public void progress(double value) { public void progress(double value) {
if (session != null) { session.getRemote().sendStringByFuture("{ \"command\": \"compiling\", \"progress\": " + value + " }");
session.getAsyncRemote().sendText("{ \"command\": \"compiling\", \"progress\": " + value + " }");
}
} }
@Override
public void complete(boolean success) { public void complete(boolean success) {
if (session != null) { session.getRemote().sendStringByFuture("{ \"command\": \"complete\", \"success\": " + success + " }");
session.getAsyncRemote().sendText("{ \"command\": \"complete\", \"success\": " + success + " }");
} }
} }
} }

View File

@ -16,20 +16,11 @@
package org.teavm.devserver; package org.teavm.devserver;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List; 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.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
import org.teavm.tooling.TeaVMToolLog; import org.teavm.tooling.TeaVMToolLog;
public class DevServer { public class DevServer {
@ -47,6 +38,8 @@ public class DevServer {
private Server server; private Server server;
private int port = 9090; private int port = 9090;
private int debugPort; private int debugPort;
private String proxyUrl;
private String proxyPath = "/";
public void setMainClass(String mainClass) { public void setMainClass(String mainClass) {
this.mainClass = mainClass; this.mainClass = mainClass;
@ -90,6 +83,14 @@ public class DevServer {
this.reloadedAutomatically = reloadedAutomatically; this.reloadedAutomatically = reloadedAutomatically;
} }
public void setProxyUrl(String proxyUrl) {
this.proxyUrl = proxyUrl;
}
public void setProxyPath(String proxyPath) {
this.proxyPath = proxyPath;
}
public List<String> getSourcePath() { public List<String> getSourcePath() {
return sourcePath; return sourcePath;
} }
@ -128,14 +129,16 @@ public class DevServer {
servlet.setAutomaticallyReloaded(reloadedAutomatically); servlet.setAutomaticallyReloaded(reloadedAutomatically);
servlet.setPort(port); servlet.setPort(port);
servlet.setDebugPort(debugPort); servlet.setDebugPort(debugPort);
servlet.setProxyUrl(proxyUrl);
servlet.setProxyPath(proxyPath);
for (DevServerListener listener : listeners) { for (DevServerListener listener : listeners) {
servlet.addListener(listener); servlet.addListener(listener);
} }
context.addServlet(new ServletHolder(servlet), "/*"); ServletHolder servletHolder = new ServletHolder(servlet);
servletHolder.setAsyncSupported(true);
context.addServlet(servletHolder, "/*");
try { try {
ServerContainer wscontainer = WebSocketServerContainerInitializer.configureContext(context);
wscontainer.addEndpoint(new DevServerEndpointConfig(servlet));
server.start(); server.start();
server.join(); server.join();
} catch (Exception e) { } catch (Exception e) {
@ -152,52 +155,4 @@ public class DevServer {
server = null; server = null;
servlet = null; servlet = null;
} }
private class DevServerEndpointConfig implements ServerEndpointConfig {
private Map<String, Object> userProperties = new HashMap<>();
public DevServerEndpointConfig(CodeServlet servlet) {
userProperties.put("teavm.servlet", servlet);
}
@Override
public List<Class<? extends Decoder>> getDecoders() {
return Collections.emptyList();
}
@Override
public List<Class<? extends Encoder>> getEncoders() {
return Collections.emptyList();
}
@Override
public Map<String, Object> getUserProperties() {
return userProperties;
}
@Override
public Configurator getConfigurator() {
return null;
}
@Override
public Class<?> getEndpointClass() {
return CodeWsEndpoint.class;
}
@Override
public List<Extension> getExtensions() {
return Collections.emptyList();
}
@Override
public String getPath() {
return pathToFile + fileName + ".ws";
}
@Override
public List<String> getSubprotocols() {
return Collections.emptyList();
}
}
} }

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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<Consumer<ProxyWsClient>> 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<ProxyWsClient> 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<ProxyWsClient> message : pendingMessages) {
message.accept(target);
}
pendingMessages = null;
}
}

View File

@ -27,4 +27,6 @@ public class DevServerConfiguration {
public String pathToFile; public String pathToFile;
public String fileName; public String fileName;
public int debugPort; public int debugPort;
public String proxyUrl;
public String proxyPath;
} }

View File

@ -108,7 +108,7 @@ public class DevServerRunner extends UnicastRemoteObject implements DevServerMan
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
DevServer server = new DevServer(); DevServer server = new DevServer();
server.setLog(new ConsoleTeaVMToolLog(true)); server.setLog(new ConsoleTeaVMToolLog(false));
server.setMainClass(args[0]); server.setMainClass(args[0]);
List<String> classPath = new ArrayList<>(); List<String> classPath = new ArrayList<>();
for (int i = 1; i < args.length; ++i) { for (int i = 1; i < args.length; ++i) {
@ -137,6 +137,12 @@ public class DevServerRunner extends UnicastRemoteObject implements DevServerMan
case "-P": case "-P":
server.setDebugPort(Integer.parseInt(args[++i])); server.setDebugPort(Integer.parseInt(args[++i]));
break; break;
case "-proxy-url":
server.setProxyUrl(args[++i]);
break;
case "-proxy-path":
server.setProxyPath(args[++i]);
break;
} }
} }
server.setClassPath(classPath.toArray(new String[0])); server.setClassPath(classPath.toArray(new String[0]));
@ -196,6 +202,15 @@ public class DevServerRunner extends UnicastRemoteObject implements DevServerMan
arguments.add(Integer.toString(options.debugPort)); 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])); ProcessBuilder builder = new ProcessBuilder(arguments.toArray(new String[0]));
Process process = builder.start(); Process process = builder.start();
BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream(), BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream(),

View File

@ -46,6 +46,8 @@ public class TeaVMDevServerConfiguration extends ModuleBasedConfiguration<RunCon
private boolean indicator = true; private boolean indicator = true;
private boolean automaticallyReloaded; private boolean automaticallyReloaded;
private int maxHeap = 1024; private int maxHeap = 1024;
private String proxyUrl = "";
private String proxyPath = "";
public TeaVMDevServerConfiguration( public TeaVMDevServerConfiguration(
@NotNull RunConfigurationModule configurationModule, @NotNull RunConfigurationModule configurationModule,
@ -172,4 +174,24 @@ public class TeaVMDevServerConfiguration extends ModuleBasedConfiguration<RunCon
public void setMaxHeap(int maxHeap) { public void setMaxHeap(int maxHeap) {
this.maxHeap = maxHeap; this.maxHeap = maxHeap;
} }
@Property
@Tag
public String getProxyUrl() {
return proxyUrl;
}
public void setProxyUrl(String proxyUrl) {
this.proxyUrl = proxyUrl;
}
@Property
@Tag
public String getProxyPath() {
return proxyPath;
}
public void setProxyPath(String proxyPath) {
this.proxyPath = proxyPath;
}
} }

View File

@ -81,6 +81,8 @@ public class TeaVMDevServerRunState implements RunProfileState {
config.autoReload = configuration.isAutomaticallyReloaded(); config.autoReload = configuration.isAutomaticallyReloaded();
config.mainClass = configuration.getMainClass(); config.mainClass = configuration.getMainClass();
config.maxHeap = configuration.getMaxHeap(); config.maxHeap = configuration.getMaxHeap();
config.proxyUrl = configuration.getProxyUrl();
config.proxyPath = configuration.getProxyPath();
if (executor.getId().equals(DefaultDebugExecutor.EXECUTOR_ID)) { if (executor.getId().equals(DefaultDebugExecutor.EXECUTOR_ID)) {
config.debugPort = choosePort(); config.debugPort = choosePort();

View File

@ -50,6 +50,8 @@ public class TeaVMDevServerSettingsPanel extends JPanel {
private JCheckBox indicatorField; private JCheckBox indicatorField;
private JCheckBox autoReloadField; private JCheckBox autoReloadField;
private JFormattedTextField maxHeapField; private JFormattedTextField maxHeapField;
private JTextField proxyUrlField;
private JTextField proxyPathField;
public TeaVMDevServerSettingsPanel(Project project) { public TeaVMDevServerSettingsPanel(Project project) {
moduleField = new ModuleDescriptionsComboBox(); moduleField = new ModuleDescriptionsComboBox();
@ -77,6 +79,9 @@ public class TeaVMDevServerSettingsPanel extends JPanel {
autoReloadField = new JCheckBox("Reload page automatically:"); autoReloadField = new JCheckBox("Reload page automatically:");
maxHeapField = new JFormattedTextField(new DecimalFormat("#0")); maxHeapField = new JFormattedTextField(new DecimalFormat("#0"));
proxyUrlField = new JTextField();
proxyPathField = new JTextField();
initLayout(); initLayout();
} }
@ -117,6 +122,12 @@ public class TeaVMDevServerSettingsPanel extends JPanel {
add(new JLabel("Server heap limit:"), labelConstraints); add(new JLabel("Server heap limit:"), labelConstraints);
add(maxHeapField, constraints); add(maxHeapField, constraints);
add(new JLabel("Proxy URL:"), labelConstraints);
add(proxyUrlField, constraints);
add(new JLabel("Proxy from path:"), labelConstraints);
add(proxyPathField, constraints);
} }
public void load(TeaVMDevServerConfiguration configuration) { public void load(TeaVMDevServerConfiguration configuration) {
@ -129,21 +140,25 @@ public class TeaVMDevServerSettingsPanel extends JPanel {
autoReloadField.setSelected(configuration.isAutomaticallyReloaded()); autoReloadField.setSelected(configuration.isAutomaticallyReloaded());
maxHeapField.setText(Integer.toString(configuration.getMaxHeap())); maxHeapField.setText(Integer.toString(configuration.getMaxHeap()));
portField.setText(Integer.toString(configuration.getPort())); portField.setText(Integer.toString(configuration.getPort()));
proxyUrlField.setText(configuration.getProxyUrl());
proxyPathField.setText(configuration.getProxyPath());
} }
public void save(TeaVMDevServerConfiguration configuration) { public void save(TeaVMDevServerConfiguration configuration) {
configuration.setMainClass(mainClassField.getText()); configuration.setMainClass(mainClassField.getText().trim());
moduleSelector.applyTo(configuration); moduleSelector.applyTo(configuration);
configuration.setJdkPath(jrePathEditor.getJrePathOrName()); configuration.setJdkPath(jrePathEditor.getJrePathOrName());
configuration.setFileName(fileNameField.getText()); configuration.setFileName(fileNameField.getText().trim());
configuration.setPathToFile(pathToFileField.getText()); configuration.setPathToFile(pathToFileField.getText().trim());
configuration.setIndicator(indicatorField.isSelected()); configuration.setIndicator(indicatorField.isSelected());
configuration.setAutomaticallyReloaded(autoReloadField.isSelected()); configuration.setAutomaticallyReloaded(autoReloadField.isSelected());
if (!maxHeapField.getText().isEmpty()) { if (!maxHeapField.getText().trim().isEmpty()) {
configuration.setMaxHeap(Integer.parseInt(maxHeapField.getText())); configuration.setMaxHeap(Integer.parseInt(maxHeapField.getText()));
} }
if (!portField.getText().isEmpty()) { if (!portField.getText().trim().isEmpty()) {
configuration.setPort(Integer.parseInt(portField.getText())); configuration.setPort(Integer.parseInt(portField.getText()));
} }
configuration.setProxyUrl(proxyUrlField.getText().trim());
configuration.setProxyPath(proxyPathField.getText().trim());
} }
} }