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