From bbd02b0067fdb09f04f9c671a7ef96f39953bb61 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Wed, 13 Mar 2024 16:20:45 +0100 Subject: [PATCH] gradle: implement dev server task --- .idea/checkstyle-idea.xml | 1 - .../teavm/common/json/JsonNumericValue.java | 1 - .../org/teavm/common/json/JsonParser.java | 7 + .../java/org/teavm/common/json/JsonValue.java | 4 + .../common/json/JsonValueParserVisitor.java | 25 +- samples/hello/build.gradle.kts | 19 + .../org/teavm/samples/pi/PiCalculator.java | 1 + .../c/incremental/IncrementalCBuilder.java | 3 +- tools/cli/build.gradle.kts | 1 + .../cli/devserver/JsonCommandReader.java | 45 ++ .../cli/devserver/JsonCommandWriter.java | 234 +++++++ .../{ => devserver}/TeaVMDevServerRunner.java | 88 ++- .../teavm/tooling/builder/BuildResult.java | 7 - .../builder/InProcessBuildStrategy.java | 32 +- .../tooling/builder/RemoteBuildStrategy.java | 16 - .../tooling/builder/SimpleBuildResult.java | 26 +- .../org/teavm/tooling/daemon/BuildDaemon.java | 5 - .../tooling/daemon/RemoteBuildResponse.java | 5 - .../teavm/tooling/util/FileSystemWatcher.java | 16 +- .../java/org/teavm/devserver/CodeServlet.java | 96 ++- .../java/org/teavm/devserver/DevServer.java | 37 +- .../org/teavm/devserver/indicator.js | 60 +- tools/gradle/build.gradle.kts | 2 + .../org/teavm/gradle/TeaVMExtensionImpl.java | 36 +- .../java/org/teavm/gradle/TeaVMPlugin.java | 133 ++-- .../api/TeaVMDevServerConfiguration.java | 34 + .../gradle/api/TeaVMJSConfiguration.java | 9 + .../teavm/gradle/tasks/DevServerManager.java | 68 ++ .../gradle/tasks/JavaScriptDevServerTask.java | 144 +++++ .../gradle/tasks/ProjectDevServerManager.java | 589 ++++++++++++++++++ .../tasks/StopJavaScriptDevServerTask.java | 29 + .../org/teavm/gradle/tasks/TeaVMTask.java | 1 - tools/ide-deps/build.gradle.kts | 8 +- tools/idea/build.gradle.kts | 3 +- .../teavm/idea/devserver/DevServerRunner.java | 5 +- 35 files changed, 1583 insertions(+), 207 deletions(-) create mode 100644 tools/cli/src/main/java/org/teavm/cli/devserver/JsonCommandReader.java create mode 100644 tools/cli/src/main/java/org/teavm/cli/devserver/JsonCommandWriter.java rename tools/cli/src/main/java/org/teavm/cli/{ => devserver}/TeaVMDevServerRunner.java (67%) create mode 100644 tools/gradle/src/main/java/org/teavm/gradle/api/TeaVMDevServerConfiguration.java create mode 100644 tools/gradle/src/main/java/org/teavm/gradle/tasks/DevServerManager.java create mode 100644 tools/gradle/src/main/java/org/teavm/gradle/tasks/JavaScriptDevServerTask.java create mode 100644 tools/gradle/src/main/java/org/teavm/gradle/tasks/ProjectDevServerManager.java create mode 100644 tools/gradle/src/main/java/org/teavm/gradle/tasks/StopJavaScriptDevServerTask.java diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml index 6f3de1980..1fe460e06 100644 --- a/.idea/checkstyle-idea.xml +++ b/.idea/checkstyle-idea.xml @@ -3,7 +3,6 @@ 8.41.1 JavaOnlyWithTests - diff --git a/core/src/main/java/org/teavm/common/json/JsonNumericValue.java b/core/src/main/java/org/teavm/common/json/JsonNumericValue.java index 3aef24903..2461598b8 100644 --- a/core/src/main/java/org/teavm/common/json/JsonNumericValue.java +++ b/core/src/main/java/org/teavm/common/json/JsonNumericValue.java @@ -16,5 +16,4 @@ package org.teavm.common.json; public abstract class JsonNumericValue extends JsonValue { - public abstract double asNumber(); } diff --git a/core/src/main/java/org/teavm/common/json/JsonParser.java b/core/src/main/java/org/teavm/common/json/JsonParser.java index be3a3b3e4..0e8013637 100644 --- a/core/src/main/java/org/teavm/common/json/JsonParser.java +++ b/core/src/main/java/org/teavm/common/json/JsonParser.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.Reader; import java.util.HashSet; import java.util.Set; +import java.util.function.Consumer; public class JsonParser { private JsonConsumer consumer; @@ -32,6 +33,8 @@ public class JsonParser { } public void parse(Reader reader) throws IOException { + lineNumber = 0; + columnNumber = 0; lastChar = reader.read(); skipWhitespaces(reader); if (lastChar == -1) { @@ -412,4 +415,8 @@ public class JsonParser { private static boolean isDigit(int c) { return c >= '0' && c <= '9'; } + + public static JsonParser ofValue(Consumer consumer) { + return new JsonParser(new JsonVisitingConsumer(JsonValueParserVisitor.create(consumer))); + } } diff --git a/core/src/main/java/org/teavm/common/json/JsonValue.java b/core/src/main/java/org/teavm/common/json/JsonValue.java index c0f8cee9c..8fabc3ca0 100644 --- a/core/src/main/java/org/teavm/common/json/JsonValue.java +++ b/core/src/main/java/org/teavm/common/json/JsonValue.java @@ -27,4 +27,8 @@ public abstract class JsonValue { public long asIntNumber() { throw new IllegalStateException(); } + + public double asNumber() { + throw new IllegalStateException(); + } } diff --git a/core/src/main/java/org/teavm/common/json/JsonValueParserVisitor.java b/core/src/main/java/org/teavm/common/json/JsonValueParserVisitor.java index 344fc71df..dcb8ba0a7 100644 --- a/core/src/main/java/org/teavm/common/json/JsonValueParserVisitor.java +++ b/core/src/main/java/org/teavm/common/json/JsonValueParserVisitor.java @@ -18,6 +18,8 @@ package org.teavm.common.json; import java.util.function.Consumer; public abstract class JsonValueParserVisitor extends JsonAllErrorVisitor { + private JsonValue deferred; + public abstract void consume(JsonValue value); public static JsonValueParserVisitor create(Consumer consumer) { @@ -32,7 +34,7 @@ public abstract class JsonValueParserVisitor extends JsonAllErrorVisitor { @Override public JsonVisitor object(JsonErrorReporter reporter) { var jsonObject = new JsonObjectValue(); - consume(jsonObject); + deferred = jsonObject; return new JsonAllErrorVisitor() { @Override public JsonVisitor property(JsonErrorReporter reporter, String name) { @@ -46,19 +48,22 @@ public abstract class JsonValueParserVisitor extends JsonAllErrorVisitor { }; } + @Override + public void end(JsonErrorReporter reporter) { + super.end(reporter); + var value = deferred; + deferred = null; + consume(value); + } + @Override public JsonVisitor array(JsonErrorReporter reporter) { var jsonArray = new JsonArrayValue(); - consume(jsonArray); - return new JsonAllErrorVisitor() { + deferred = jsonArray; + return new JsonValueParserVisitor() { @Override - public JsonVisitor array(JsonErrorReporter reporter) { - return new JsonValueParserVisitor() { - @Override - public void consume(JsonValue value) { - jsonArray.add(value); - } - }; + public void consume(JsonValue value) { + jsonArray.add(value); } }; } diff --git a/samples/hello/build.gradle.kts b/samples/hello/build.gradle.kts index 725f36003..948edeac8 100644 --- a/samples/hello/build.gradle.kts +++ b/samples/hello/build.gradle.kts @@ -22,9 +22,17 @@ plugins { id("org.teavm") } +configurations { + create("teavmCli") + create("teavmClasslib") +} + dependencies { teavm(teavm.libs.jsoApis) compileOnly("jakarta.servlet:jakarta.servlet-api:6.0.0") + + "teavmCli"("org.teavm:teavm-cli:0.10.0-SNAPSHOT") + "teavmClasslib"("org.teavm:teavm-classlib:0.10.0-SNAPSHOT") } teavm.js { @@ -33,3 +41,14 @@ teavm.js { sourceMap = true sourceFilePolicy = SourceFilePolicy.LINK_LOCAL_FILES } + +tasks.register("runCli") { + classpath(configurations["teavmCli"]) + mainClass = "org.teavm.cli.devserver.TeaVMDevServerRunner" + args = listOf("--json-interface", "--no-watch", "-p", + layout.buildDirectory.dir("classes/java/teavm").get().asFile.absolutePath, + ) + configurations["teavmClasslib"].flatMap { listOf("-p", it.absolutePath) } + listOf( + "--", "org.teavm.samples.hello.Client" + ) + println(args) +} \ No newline at end of file diff --git a/samples/pi/src/main/java/org/teavm/samples/pi/PiCalculator.java b/samples/pi/src/main/java/org/teavm/samples/pi/PiCalculator.java index 78b57a389..9b94c8c05 100644 --- a/samples/pi/src/main/java/org/teavm/samples/pi/PiCalculator.java +++ b/samples/pi/src/main/java/org/teavm/samples/pi/PiCalculator.java @@ -24,6 +24,7 @@ public final class PiCalculator { } public static void main(String[] args) { + System.out.println("hello1"); var start = System.currentTimeMillis(); int n = Integer.parseInt(args[0]); int j = 0; diff --git a/tools/c-incremental/src/main/java/org/teavm/tooling/c/incremental/IncrementalCBuilder.java b/tools/c-incremental/src/main/java/org/teavm/tooling/c/incremental/IncrementalCBuilder.java index 3ed74b82e..bf950cd11 100644 --- a/tools/c-incremental/src/main/java/org/teavm/tooling/c/incremental/IncrementalCBuilder.java +++ b/tools/c-incremental/src/main/java/org/teavm/tooling/c/incremental/IncrementalCBuilder.java @@ -26,7 +26,6 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -530,7 +529,7 @@ public class IncrementalCBuilder { } private void fireBuildComplete(TeaVM vm) { - SimpleBuildResult result = new SimpleBuildResult(vm, Collections.emptyList()); + SimpleBuildResult result = new SimpleBuildResult(vm); for (BuilderListener listener : listeners) { listener.compilationComplete(result); } diff --git a/tools/cli/build.gradle.kts b/tools/cli/build.gradle.kts index 2bebcfed6..cab747786 100644 --- a/tools/cli/build.gradle.kts +++ b/tools/cli/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(project(":tools:devserver")) implementation(project(":tools:c-incremental")) implementation(libs.commons.cli) + implementation(libs.jetty.server) runtimeOnly(project(":classlib")) runtimeOnly(project(":metaprogramming:impl")) diff --git a/tools/cli/src/main/java/org/teavm/cli/devserver/JsonCommandReader.java b/tools/cli/src/main/java/org/teavm/cli/devserver/JsonCommandReader.java new file mode 100644 index 000000000..f99d2efa3 --- /dev/null +++ b/tools/cli/src/main/java/org/teavm/cli/devserver/JsonCommandReader.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 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.cli.devserver; + +import java.util.function.Consumer; +import org.teavm.common.json.JsonValue; +import org.teavm.devserver.DevServer; + +public class JsonCommandReader implements Consumer { + private DevServer devServer; + + public JsonCommandReader(DevServer devServer) { + this.devServer = devServer; + } + + @Override + public void accept(JsonValue jsonValue) { + var obj = jsonValue.asObject(); + var type = obj.get("type").asString(); + switch (type) { + case "build": + devServer.buildProject(); + break; + case "cancel": + devServer.cancelBuild(); + break; + case "stop": + System.exit(0); + break; + } + } +} diff --git a/tools/cli/src/main/java/org/teavm/cli/devserver/JsonCommandWriter.java b/tools/cli/src/main/java/org/teavm/cli/devserver/JsonCommandWriter.java new file mode 100644 index 000000000..40d88ffb2 --- /dev/null +++ b/tools/cli/src/main/java/org/teavm/cli/devserver/JsonCommandWriter.java @@ -0,0 +1,234 @@ +/* + * Copyright 2024 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.cli.devserver; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import org.eclipse.jetty.util.log.Logger; +import org.teavm.common.JsonUtil; +import org.teavm.devserver.DevServerListener; +import org.teavm.diagnostics.DefaultProblemTextConsumer; +import org.teavm.tooling.TeaVMProblemRenderer; +import org.teavm.tooling.TeaVMToolLog; +import org.teavm.tooling.builder.BuildResult; + +public class JsonCommandWriter implements TeaVMToolLog, DevServerListener, Logger { + private PrintWriter writer = new PrintWriter(System.out, false, StandardCharsets.UTF_8); + + @Override + public void info(String text) { + writeMessage("info", text, null); + } + + @Override + public void debug(String text) { + writeMessage("debug", text, null); + } + + @Override + public void warning(String text) { + writeMessage("warning", text, null); + } + + @Override + public void error(String text) { + writeMessage("error", text, null); + } + + @Override + public void info(String text, Throwable e) { + writeMessage("info", text, e); + } + + @Override + public void debug(String text, Throwable e) { + writeMessage("debug", text, e); + } + + @Override + public void warning(String text, Throwable e) { + writeMessage("warning", text, e); + } + + @Override + public void error(String text, Throwable e) { + writeMessage("error", text, e); + } + + @Override + public String getName() { + return "dev-server"; + } + + @Override + public void warn(String s, Object... objects) { + writeMessage("warning", format(s, objects), null); + } + + @Override + public void warn(Throwable throwable) { + writeMessage("warning", "", throwable); + } + + @Override + public void warn(String s, Throwable throwable) { + writeMessage("warning", s, throwable); + } + + @Override + public void info(String s, Object... objects) { + writeMessage("info", format(s, objects), null); + } + + @Override + public void info(Throwable throwable) { + writeMessage("info", "", throwable); + } + + @Override + public boolean isDebugEnabled() { + return true; + } + + @Override + public void setDebugEnabled(boolean b) { + } + + @Override + public void debug(String s, Object... objects) { + writeMessage("debug", format(s, objects), null); + } + + @Override + public void debug(String s, long l) { + writeMessage("debug", format(s, new Object[] { l }), null); + } + + @Override + public void debug(Throwable throwable) { + writeMessage("debug", "", null); + } + + @Override + public Logger getLogger(String s) { + return this; + } + + @Override + public void ignore(Throwable throwable) { + } + + private String format(String message, Object[] args) { + var index = 0; + var sb = new StringBuilder(); + for (var i = 0; i < args.length; ++i) { + var next = message.indexOf("{}", index); + if (next < 0) { + break; + } + sb.append(message, index, next); + sb.append(args[i]); + index = next + 2; + } + sb.append(message, index, message.length()); + return sb.toString(); + } + + private synchronized void writeMessage(String level, String message, Throwable throwable) { + try { + writer.append("{\"type\":\"log\",\"level\":\"").append(level).append("\",\"message\":\""); + JsonUtil.writeEscapedString(writer, message); + writer.append("\""); + if (throwable != null) { + writer.append(",\"throwable\":\""); + var throwableBuffer = new StringWriter(); + var throwableWriter = new PrintWriter(throwableBuffer); + throwable.printStackTrace(throwableWriter); + JsonUtil.writeEscapedString(writer, throwableBuffer.toString()); + writer.append("\""); + } + writer.append("}"); + writer.println(); + writer.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public synchronized void compilationStarted() { + writer.append("{\"type\":\"compilation-started\"}"); + writer.println(); + writer.flush(); + } + + @Override + public synchronized void compilationProgress(double progress) { + writer.append("{\"type\":\"compilation-progress\",\"progress\":").append(String.valueOf(progress)) + .append("}"); + writer.println(); + writer.flush(); + } + + @Override + public synchronized void compilationComplete(BuildResult result) { + var consumer = new DefaultProblemTextConsumer(); + try { + writer.append("{\"type\":\"compilation-complete\""); + if (result != null && !result.getProblems().getProblems().isEmpty()) { + writer.append(",\"problems\":["); + for (var i = 0; i < result.getProblems().getProblems().size(); ++i) { + if (i > 0) { + writer.append(","); + } + var problem = result.getProblems().getProblems().get(i); + writer.append("{\"severity\":"); + switch (problem.getSeverity()) { + case ERROR: + writer.append("\"error\""); + break; + case WARNING: + writer.append("\"warning\""); + break; + } + writer.append(",\"location\":\""); + var sb = new StringBuilder(); + TeaVMProblemRenderer.renderCallStack(result.getCallGraph(), problem.getLocation(), sb); + JsonUtil.writeEscapedString(writer, sb.toString()); + writer.append("\",\"message\":\""); + problem.render(consumer); + JsonUtil.writeEscapedString(writer, consumer.getText()); + writer.append("\"}"); + } + writer.append("]"); + } + writer.append("}"); + writer.println(); + writer.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public synchronized void compilationCancelled() { + writer.append("{\"type\":\"compilation-cancelled\"}"); + writer.println(); + writer.flush(); + } +} diff --git a/tools/cli/src/main/java/org/teavm/cli/TeaVMDevServerRunner.java b/tools/cli/src/main/java/org/teavm/cli/devserver/TeaVMDevServerRunner.java similarity index 67% rename from tools/cli/src/main/java/org/teavm/cli/TeaVMDevServerRunner.java rename to tools/cli/src/main/java/org/teavm/cli/devserver/TeaVMDevServerRunner.java index 6feb20380..ff75518ea 100644 --- a/tools/cli/src/main/java/org/teavm/cli/TeaVMDevServerRunner.java +++ b/tools/cli/src/main/java/org/teavm/cli/devserver/TeaVMDevServerRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Alexey Andreev. + * Copyright 2024 Alexey Andreev. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.teavm.cli; +package org.teavm.cli.devserver; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.List; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; @@ -23,6 +29,8 @@ import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; +import org.eclipse.jetty.util.log.Log; +import org.teavm.common.json.JsonParser; import org.teavm.devserver.DevServer; import org.teavm.tooling.ConsoleTeaVMToolLog; @@ -30,6 +38,7 @@ public final class TeaVMDevServerRunner { private static Options options = new Options(); private DevServer devServer; private CommandLine commandLine; + private JsonCommandWriter jsonWriter; static { setupOptions(); @@ -56,10 +65,21 @@ public final class TeaVMDevServerRunner { .build()); options.addOption(Option.builder("s") .argName("sourcepath") - .hasArg() + .hasArgs() .desc("source path (either directory or jar file which contains source code)") .longOpt("sourcepath") .build()); + options.addOption(Option.builder() + .argName("classnames") + .hasArgs() + .desc("list of classes that should be preserved during the build (e.g. to use with reflection)") + .longOpt("preserved-classes") + .build()); + options.addOption(Option.builder() + .valueSeparator() + .hasArgs() + .longOpt("property") + .build()); options.addOption(Option.builder() .argName("number") .hasArg() @@ -94,6 +114,14 @@ public final class TeaVMDevServerRunner { .desc("delegate requests from path") .longOpt("proxy-path") .build()); + options.addOption(Option.builder() + .desc("don't watch file system changes") + .longOpt("no-watch") + .build()); + options.addOption(Option.builder() + .desc("JSON interface over stdout") + .longOpt("json-interface") + .build()); } private TeaVMDevServerRunner(CommandLine commandLine) { @@ -117,7 +145,16 @@ public final class TeaVMDevServerRunner { TeaVMDevServerRunner runner = new TeaVMDevServerRunner(commandLine); runner.parseArguments(); - runner.runAll(); + runner.devServer.start(); + if (runner.jsonWriter != null) { + runner.readStdinCommands(); + } else { + try { + runner.devServer.awaitServer(); + } catch (InterruptedException e) { + // do nothing + } + } } private void parseArguments() { @@ -128,7 +165,6 @@ public final class TeaVMDevServerRunner { devServer.setIndicator(commandLine.hasOption("indicator")); devServer.setDeobfuscateStack(commandLine.hasOption("deobfuscate-stack")); devServer.setReloadedAutomatically(commandLine.hasOption("auto-reload")); - devServer.setLog(new ConsoleTeaVMToolLog(commandLine.hasOption('v'))); if (commandLine.hasOption("port")) { try { devServer.setPort(Integer.parseInt(commandLine.getOptionValue("port"))); @@ -138,12 +174,29 @@ public final class TeaVMDevServerRunner { } } + var properties = commandLine.getOptionProperties("property"); + for (var property : properties.stringPropertyNames()) { + devServer.getProperties().put(property, properties.getProperty(property)); + } + + if (commandLine.hasOption("preserved-classes")) { + devServer.getPreservedClasses().addAll(List.of(commandLine.getOptionValues("preserved-classes"))); + } + if (commandLine.hasOption("proxy-url")) { devServer.setProxyUrl(commandLine.getOptionValue("proxy-url")); } if (commandLine.hasOption("proxy-path")) { devServer.setProxyPath(commandLine.getOptionValue("proxy-path")); } + if (commandLine.hasOption("no-watch")) { + devServer.setFileSystemWatched(false); + } + if (commandLine.hasOption("json-interface")) { + setupJsonInterface(devServer); + } else { + devServer.setLog(new ConsoleTeaVMToolLog(commandLine.hasOption('v'))); + } String[] args = commandLine.getArgs(); if (args.length != 1) { @@ -175,8 +228,29 @@ public final class TeaVMDevServerRunner { } } - private void runAll() { - devServer.start(); + private void setupJsonInterface(DevServer devServer) { + jsonWriter = new JsonCommandWriter(); + devServer.setLog(jsonWriter); + devServer.addListener(jsonWriter); + devServer.setCompileOnStartup(false); + devServer.setLogBuildErrors(false); + Log.setLog(jsonWriter); + } + + private void readStdinCommands() { + var commandReader = new JsonCommandReader(devServer); + var parser = JsonParser.ofValue(commandReader); + try (var reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) { + while (true) { + var command = reader.readLine(); + if (command == null) { + break; + } + parser.parse(new StringReader(command)); + } + } catch (IOException e) { + throw new RuntimeException(e); + } } private static void printUsage() { diff --git a/tools/core/src/main/java/org/teavm/tooling/builder/BuildResult.java b/tools/core/src/main/java/org/teavm/tooling/builder/BuildResult.java index f068d66ac..cff14d0a5 100644 --- a/tools/core/src/main/java/org/teavm/tooling/builder/BuildResult.java +++ b/tools/core/src/main/java/org/teavm/tooling/builder/BuildResult.java @@ -15,7 +15,6 @@ */ package org.teavm.tooling.builder; -import java.util.Collection; import org.teavm.callgraph.CallGraph; import org.teavm.diagnostics.ProblemProvider; @@ -23,10 +22,4 @@ public interface BuildResult { CallGraph getCallGraph(); ProblemProvider getProblems(); - - Collection getUsedResources(); - - Collection getClasses(); - - Collection getGeneratedFiles(); } diff --git a/tools/core/src/main/java/org/teavm/tooling/builder/InProcessBuildStrategy.java b/tools/core/src/main/java/org/teavm/tooling/builder/InProcessBuildStrategy.java index 0ef888670..9d76a2961 100644 --- a/tools/core/src/main/java/org/teavm/tooling/builder/InProcessBuildStrategy.java +++ b/tools/core/src/main/java/org/teavm/tooling/builder/InProcessBuildStrategy.java @@ -22,10 +22,8 @@ import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.Properties; -import java.util.stream.Collectors; import org.teavm.backend.javascript.JSModuleType; import org.teavm.backend.wasm.render.WasmBinaryVersion; import org.teavm.callgraph.CallGraph; @@ -287,12 +285,8 @@ public class InProcessBuildStrategy implements BuildStrategy { throw new BuildException(e); } - var generatedFiles = tool.getGeneratedFiles().stream() - .map(File::getAbsolutePath) - .collect(Collectors.toSet()); - return new InProcessBuildResult(tool.getDependencyInfo().getCallGraph(), - tool.getProblemProvider(), tool.getClasses(), tool.getUsedResources(), generatedFiles); + tool.getProblemProvider()); } private URLClassLoader buildClassLoader() { @@ -310,17 +304,10 @@ public class InProcessBuildStrategy implements BuildStrategy { static class InProcessBuildResult implements BuildResult { private CallGraph callGraph; private ProblemProvider problemProvider; - private Collection classes; - private Collection usedResources; - private Collection generatedFiles; - InProcessBuildResult(CallGraph callGraph, ProblemProvider problemProvider, - Collection classes, Collection usedResources, Collection generatedFiles) { + InProcessBuildResult(CallGraph callGraph, ProblemProvider problemProvider) { this.callGraph = callGraph; this.problemProvider = problemProvider; - this.classes = classes; - this.usedResources = usedResources; - this.generatedFiles = generatedFiles; } @Override @@ -332,20 +319,5 @@ public class InProcessBuildStrategy implements BuildStrategy { public ProblemProvider getProblems() { return problemProvider; } - - @Override - public Collection getClasses() { - return classes; - } - - @Override - public Collection getUsedResources() { - return usedResources; - } - - @Override - public Collection getGeneratedFiles() { - return generatedFiles; - } } } diff --git a/tools/core/src/main/java/org/teavm/tooling/builder/RemoteBuildStrategy.java b/tools/core/src/main/java/org/teavm/tooling/builder/RemoteBuildStrategy.java index eaeec7a0f..07c662022 100644 --- a/tools/core/src/main/java/org/teavm/tooling/builder/RemoteBuildStrategy.java +++ b/tools/core/src/main/java/org/teavm/tooling/builder/RemoteBuildStrategy.java @@ -17,7 +17,6 @@ package org.teavm.tooling.builder; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; -import java.util.Collection; import java.util.List; import java.util.Properties; import org.teavm.backend.javascript.JSModuleType; @@ -246,21 +245,6 @@ public class RemoteBuildStrategy implements BuildStrategy { public ProblemProvider getProblems() { return problems; } - - @Override - public Collection getUsedResources() { - return response.usedResources; - } - - @Override - public Collection getClasses() { - return response.classes; - } - - @Override - public Collection getGeneratedFiles() { - return response.generatedFiles; - } }; } diff --git a/tools/core/src/main/java/org/teavm/tooling/builder/SimpleBuildResult.java b/tools/core/src/main/java/org/teavm/tooling/builder/SimpleBuildResult.java index 1343e1f28..931765fd6 100644 --- a/tools/core/src/main/java/org/teavm/tooling/builder/SimpleBuildResult.java +++ b/tools/core/src/main/java/org/teavm/tooling/builder/SimpleBuildResult.java @@ -15,21 +15,15 @@ */ package org.teavm.tooling.builder; -import java.util.Collection; -import java.util.List; import org.teavm.callgraph.CallGraph; import org.teavm.diagnostics.ProblemProvider; -import org.teavm.tooling.InstructionLocationReader; import org.teavm.vm.TeaVM; public class SimpleBuildResult implements BuildResult { private TeaVM vm; - private List generatedFiles; - private Collection usedResources; - public SimpleBuildResult(TeaVM vm, List generatedFiles) { + public SimpleBuildResult(TeaVM vm) { this.vm = vm; - this.generatedFiles = generatedFiles; } @Override @@ -41,22 +35,4 @@ public class SimpleBuildResult implements BuildResult { public ProblemProvider getProblems() { return vm.getProblemProvider(); } - - @Override - public Collection getUsedResources() { - if (usedResources == null) { - usedResources = InstructionLocationReader.extractUsedResources(vm); - } - return usedResources; - } - - @Override - public Collection getClasses() { - return vm.getClasses(); - } - - @Override - public Collection getGeneratedFiles() { - return generatedFiles; - } } diff --git a/tools/core/src/main/java/org/teavm/tooling/daemon/BuildDaemon.java b/tools/core/src/main/java/org/teavm/tooling/daemon/BuildDaemon.java index 77c3d383e..4ce05395f 100644 --- a/tools/core/src/main/java/org/teavm/tooling/daemon/BuildDaemon.java +++ b/tools/core/src/main/java/org/teavm/tooling/daemon/BuildDaemon.java @@ -186,11 +186,6 @@ public class BuildDaemon extends UnicastRemoteObject implements RemoteBuildServi response.callGraph = tool.getDependencyInfo().getCallGraph(); response.problems.addAll(tool.getProblemProvider().getProblems()); response.severeProblems.addAll(tool.getProblemProvider().getSevereProblems()); - response.classes.addAll(tool.getClasses()); - response.usedResources.addAll(tool.getUsedResources()); - response.generatedFiles.addAll(tool.getGeneratedFiles().stream() - .map(File::getAbsolutePath) - .collect(Collectors.toSet())); } return response; diff --git a/tools/core/src/main/java/org/teavm/tooling/daemon/RemoteBuildResponse.java b/tools/core/src/main/java/org/teavm/tooling/daemon/RemoteBuildResponse.java index dc68e2683..9d32827fd 100644 --- a/tools/core/src/main/java/org/teavm/tooling/daemon/RemoteBuildResponse.java +++ b/tools/core/src/main/java/org/teavm/tooling/daemon/RemoteBuildResponse.java @@ -17,9 +17,7 @@ package org.teavm.tooling.daemon; import java.io.Serializable; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import org.teavm.callgraph.CallGraph; import org.teavm.diagnostics.Problem; @@ -27,8 +25,5 @@ public class RemoteBuildResponse implements Serializable { public CallGraph callGraph; public final List problems = new ArrayList<>(); public final List severeProblems = new ArrayList<>(); - public final Set usedResources = new HashSet<>(); - public final Set classes = new HashSet<>(); - public final Set generatedFiles = new HashSet<>(); public Throwable exception; } diff --git a/tools/core/src/main/java/org/teavm/tooling/util/FileSystemWatcher.java b/tools/core/src/main/java/org/teavm/tooling/util/FileSystemWatcher.java index 6676206f8..b1f4bde8a 100644 --- a/tools/core/src/main/java/org/teavm/tooling/util/FileSystemWatcher.java +++ b/tools/core/src/main/java/org/teavm/tooling/util/FileSystemWatcher.java @@ -98,16 +98,22 @@ public class FileSystemWatcher { return !changedFiles.isEmpty() || pollNow(); } + public void pollChanges() throws IOException { + while (pollNow()) { + // continue polling + } + } + public void waitForChange(int timeout) throws InterruptedException, IOException { if (!hasChanges()) { take(); } - while (poll(timeout)) { - // continue polling - } - while (pollNow()) { - // continue polling + if (timeout > 0) { + while (poll(timeout)) { + // continue polling + } } + pollChanges(); } public List grabChangedFiles() { 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 a24cac187..88e8e0583 100644 --- a/tools/devserver/src/main/java/org/teavm/devserver/CodeServlet.java +++ b/tools/devserver/src/main/java/org/teavm/devserver/CodeServlet.java @@ -86,6 +86,7 @@ import org.teavm.parsing.resource.ResourceClassHolderMapper; import org.teavm.tooling.EmptyTeaVMToolLog; import org.teavm.tooling.TeaVMProblemRenderer; import org.teavm.tooling.TeaVMToolLog; +import org.teavm.tooling.builder.BuildResult; import org.teavm.tooling.builder.SimpleBuildResult; import org.teavm.tooling.util.FileSystemWatcher; import org.teavm.vm.MemoryBuildTarget; @@ -119,6 +120,8 @@ public class CodeServlet extends HttpServlet { private String proxyProtocol; private int proxyPort; private String proxyBaseUrl; + private Map properties = new LinkedHashMap<>(); + private List preservedClasses = new ArrayList<>(); private Map> sourceFileCache = new HashMap<>(); @@ -148,6 +151,9 @@ public class CodeServlet extends HttpServlet { private InMemorySymbolTable fileSymbolTable = new InMemorySymbolTable(); private InMemorySymbolTable variableSymbolTable = new InMemorySymbolTable(); private ReferenceCache referenceCache = new ReferenceCache(); + private boolean fileSystemWatched = true; + private boolean compileOnStartup = true; + private boolean logBuildErrors = true; public CodeServlet(String mainClass, String[] classPath) { this.mainClass = mainClass; @@ -201,6 +207,26 @@ public class CodeServlet extends HttpServlet { this.proxyPath = normalizePath(proxyPath); } + public void setFileSystemWatched(boolean fileSystemWatched) { + this.fileSystemWatched = fileSystemWatched; + } + + public void setCompileOnStartup(boolean compileOnStartup) { + this.compileOnStartup = compileOnStartup; + } + + public List getPreservedClasses() { + return preservedClasses; + } + + public Map getProperties() { + return properties; + } + + public void setLogBuildErrors(boolean logBuildErrors) { + this.logBuildErrors = logBuildErrors; + } + public void addProgressHandler(ProgressHandler handler) { synchronized (progressHandlers) { progressHandlers.add(handler); @@ -241,9 +267,13 @@ public class CodeServlet extends HttpServlet { } public void buildProject() { - synchronized (statusLock) { - if (waiting) { - buildThread.interrupt(); + if (buildThread == null) { + runCompilerThread(); + } else { + synchronized (statusLock) { + if (waiting) { + buildThread.interrupt(); + } } } } @@ -617,7 +647,7 @@ public class CodeServlet extends HttpServlet { } stopped = true; synchronized (statusLock) { - if (waiting) { + if (buildThread != null && waiting) { buildThread.interrupt(); } } @@ -626,7 +656,13 @@ public class CodeServlet extends HttpServlet { @Override public void init() throws ServletException { super.init(); - Thread thread = new Thread(this::runTeaVM); + if (compileOnStartup) { + runCompilerThread(); + } + } + + private void runCompilerThread() { + var thread = new Thread(this::runTeaVM); thread.setName("TeaVM compiler"); thread.start(); buildThread = thread; @@ -726,8 +762,13 @@ public class CodeServlet extends HttpServlet { try { initBuilder(); + var hasJob = true; while (!stopped) { - buildOnce(); + if (hasJob) { + buildOnce(); + } else { + emptyBuild(); + } if (stopped) { break; @@ -737,11 +778,22 @@ public class CodeServlet extends HttpServlet { synchronized (statusLock) { waiting = true; } - watcher.waitForChange(750); + if (fileSystemWatched) { + watcher.waitForChange(750); + log.info("Changes detected. Recompiling."); + } else { + while (true) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + break; + } + } + watcher.pollChanges(); + } synchronized (statusLock) { waiting = false; } - log.info("Changes detected. Recompiling."); } catch (InterruptedException e) { if (stopped) { break; @@ -760,6 +812,7 @@ public class CodeServlet extends HttpServlet { } classSource.evict(staleClasses); + hasJob = !staleClasses.isEmpty(); } log.info("Build process stopped"); } catch (Throwable e) { @@ -836,6 +889,10 @@ public class CodeServlet extends HttpServlet { vm.setProgressListener(progressListener); vm.setProgramCache(programCache); vm.installPlugins(); + for (var className : preservedClasses) { + vm.preserveType(className); + } + vm.getProperties().putAll(properties); vm.setLastKnownClasses(lastReachedClasses); vm.setEntryPoint(mainClass); @@ -850,6 +907,12 @@ public class CodeServlet extends HttpServlet { postBuild(vm, startTime); } + private void emptyBuild() { + fireBuildStarted(); + log.info("No files changed, nothing to do"); + fireBuildCompleteWithResult(null); + } + private ClassReaderSource packClasses(ClassReaderSource source, Collection classNames) { MemoryCachedClassReaderSource packedSource = createCachedSource(); packedSource.setProvider(source::get); @@ -912,7 +975,6 @@ public class CodeServlet extends HttpServlet { private void postBuild(TeaVM vm, long startTime) { if (!vm.wasCancelled()) { log.info("Recompiled stale methods: " + programCache.getPendingItemsCount()); - fireBuildComplete(vm); if (vm.getProblemProvider().getSevereProblems().isEmpty()) { log.info("Build complete successfully"); saveNewResult(); @@ -926,7 +988,10 @@ public class CodeServlet extends HttpServlet { reportCompilationComplete(false); } printStats(vm, startTime); - TeaVMProblemRenderer.describeProblems(vm, log); + if (logBuildErrors) { + TeaVMProblemRenderer.describeProblems(vm, log); + } + fireBuildComplete(vm); } else { log.info("Build cancelled"); fireBuildCancelled(); @@ -1056,9 +1121,12 @@ public class CodeServlet extends HttpServlet { } private void fireBuildComplete(TeaVM vm) { - SimpleBuildResult result = new SimpleBuildResult(vm, new ArrayList<>(buildTarget.getNames())); - for (DevServerListener listener : listeners) { - listener.compilationComplete(result); + fireBuildCompleteWithResult(new SimpleBuildResult(vm)); + } + + private void fireBuildCompleteWithResult(BuildResult buildResult) { + for (var listener : listeners) { + listener.compilationComplete(buildResult); } } @@ -1089,7 +1157,7 @@ public class CodeServlet extends HttpServlet { @Override public TeaVMProgressFeedback progressReached(int progress) { - if (indicator) { + if (indicator || !listeners.isEmpty()) { int current = start + Math.min(progress, phaseLimit) * (end - start) / phaseLimit; if (current != last) { if (current - last > 10 || System.currentTimeMillis() - lastTime > 100) { 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 cf3cb5124..5537252e8 100644 --- a/tools/devserver/src/main/java/org/teavm/devserver/DevServer.java +++ b/tools/devserver/src/main/java/org/teavm/devserver/DevServer.java @@ -16,7 +16,9 @@ package org.teavm.devserver; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.servlet.ServletContextHandler; @@ -32,9 +34,14 @@ public class DevServer { private boolean indicator; private boolean deobfuscateStack; private boolean reloadedAutomatically; + private boolean fileSystemWatched = true; private TeaVMToolLog log; private CodeServlet servlet; private List listeners = new ArrayList<>(); + private Map properties = new LinkedHashMap<>(); + private List preservedClasses = new ArrayList<>(); + private boolean compileOnStartup; + private boolean logBuildErrors = true; private Server server; private int port = 9090; @@ -88,6 +95,10 @@ public class DevServer { this.reloadedAutomatically = reloadedAutomatically; } + public void setFileSystemWatched(boolean fileSystemWatched) { + this.fileSystemWatched = fileSystemWatched; + } + public void setProxyUrl(String proxyUrl) { this.proxyUrl = proxyUrl; } @@ -100,6 +111,18 @@ public class DevServer { return sourcePath; } + public void setCompileOnStartup(boolean compileOnStartup) { + this.compileOnStartup = compileOnStartup; + } + + public List getPreservedClasses() { + return preservedClasses; + } + + public Map getProperties() { + return properties; + } + public void invalidateCache() { servlet.invalidateCache(); } @@ -116,6 +139,10 @@ public class DevServer { listeners.add(listener); } + public void setLogBuildErrors(boolean logBuildErrors) { + this.logBuildErrors = logBuildErrors; + } + public void start() { server = new Server(); ServerConnector connector = new ServerConnector(server); @@ -138,6 +165,11 @@ public class DevServer { servlet.setDebugPort(debugPort); servlet.setProxyUrl(proxyUrl); servlet.setProxyPath(proxyPath); + servlet.setFileSystemWatched(fileSystemWatched); + servlet.setCompileOnStartup(compileOnStartup); + servlet.setLogBuildErrors(logBuildErrors); + servlet.getProperties().putAll(properties); + servlet.getPreservedClasses().addAll(preservedClasses); for (DevServerListener listener : listeners) { servlet.addListener(listener); } @@ -147,12 +179,15 @@ public class DevServer { try { server.start(); - server.join(); } catch (Exception e) { throw new RuntimeException(e); } } + public void awaitServer() throws InterruptedException { + server.join(); + } + public void stop() { try { server.stop(); diff --git a/tools/devserver/src/main/resources/org/teavm/devserver/indicator.js b/tools/devserver/src/main/resources/org/teavm/devserver/indicator.js index 9e21dcce0..044cffab7 100644 --- a/tools/devserver/src/main/resources/org/teavm/devserver/indicator.js +++ b/tools/devserver/src/main/resources/org/teavm/devserver/indicator.js @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function (window) { - var boot = BOOT_FLAG; - var reload = RELOAD_FLAG; - var indicatorVisible = INDICATOR_FLAG; - var debugPort = DEBUG_PORT; - var deobfuscate = DEOBFUSCATE_FLAG; - var fileName = FILE_NAME; - var pathToFile = PATH_TO_FILE; +(function () { + let boot = BOOT_FLAG; + let reload = RELOAD_FLAG; + let indicatorVisible = INDICATOR_FLAG; + let debugPort = DEBUG_PORT; + let deobfuscate = DEOBFUSCATE_FLAG; + let fileName = FILE_NAME; + let pathToFile = PATH_TO_FILE; function createWebSocket() { return new WebSocket("ws://WS_PATH"); @@ -28,18 +28,18 @@ function createIndicator() { function createMainElement() { - var element = document.createElement("div"); + let element = document.createElement("div"); element.style.position = "fixed"; element.style.left = "0"; element.style.bottom = "0"; element.style.backgroundColor = "black"; element.style.color = "white"; - element.style.opacity = 0.4; + element.style.opacity = "0.4"; element.style.padding = "5px"; element.style.fontSize = "18px"; element.style.fontWeight = "bold"; element.style.pointerEvents = "none"; - element.style.zIndex = 1000; + element.style.zIndex = "1000"; element.style.display = "none"; return element; } @@ -49,7 +49,7 @@ } function createProgressElements() { - var element = document.createElement("span"); + const element = document.createElement("span"); element.style.display = "none"; element.style.marginLeft = "10px"; element.style.width = "150px"; @@ -61,7 +61,7 @@ element.style.backgroundColor = "white"; element.style.position = "relative"; - var progress = document.createElement("span"); + const progress = document.createElement("span"); progress.style.display = "block"; progress.style.position = "absolute"; progress.style.left = "0"; @@ -80,9 +80,9 @@ }; } - var container = createMainElement(); - var label = createLabelElement(); - var progress = createProgressElements(); + const container = createMainElement(); + const label = createLabelElement(); + const progress = createProgressElements(); container.appendChild(label); container.appendChild(progress.container); @@ -92,7 +92,7 @@ progress: progress, timer: void 0, - show: function(text, timeout) { + show(text, timeout) { this.container.style.display = "block"; this.label.innerText = text; if (this.timer) { @@ -118,7 +118,7 @@ }; } - var indicator = createIndicator(); + let indicator = createIndicator(); function onLoad() { document.body.appendChild(indicator.container); } @@ -137,15 +137,15 @@ } if (typeof main === 'function') { - var oldMain = main; + let oldMain = main; main = function() { - var args = arguments; - window.$teavm_deobfuscator_callback = function() { + const args = arguments; + window.$teavm_deobfuscator_callback = () => { oldMain.apply(window, args); }; - var elem = document.createElement("script"); + const elem = document.createElement("script"); elem.src = pathToFile + fileName + ".deobfuscator.js"; - elem.onload = function() { + elem.onload = () => { $teavm_deobfuscator([pathToFile + fileName + ".teavmdbg", pathToFile + fileName]); }; document.head.append(elem); @@ -165,9 +165,9 @@ main(); } - var ws = createWebSocket(); + let ws = createWebSocket(); ws.onmessage = function(event) { - var message = JSON.parse(event.data); + const message = JSON.parse(event.data); switch (message.command) { case "compiling": indicator.show("Compiling..."); @@ -177,9 +177,9 @@ if (message.success) { indicator.show("Compilation complete", 10); if (reload) { - window.location.reload(true); + window.location.reload(); } else if (boot) { - var scriptElem = document.createElement("script"); + const scriptElem = document.createElement("script"); scriptElem.src = pathToFile + fileName; scriptElem.onload = startMain; document.head.appendChild(scriptElem); @@ -197,12 +197,12 @@ } if (debugPort > 0) { - var connected = false; + let connected = false; function connectDebugAgent(event) { if (event.source !== window) { return; } - var data = event.data; + const data = event.data; if (typeof data.teavmDebuggerRequest !== "undefined" && !connected) { connected = true; window.postMessage({teavmDebugger: {port: debugPort}}, "*"); @@ -211,4 +211,4 @@ window.addEventListener("message", connectDebugAgent); window.postMessage({teavmDebugger: {port: debugPort}}, "*"); } -})(this); \ No newline at end of file +})(); \ No newline at end of file diff --git a/tools/gradle/build.gradle.kts b/tools/gradle/build.gradle.kts index c07fc25a5..19388ff54 100644 --- a/tools/gradle/build.gradle.kts +++ b/tools/gradle/build.gradle.kts @@ -74,6 +74,7 @@ val createConfig by tasks.registering { val jsoImpl = findArtifactCoordinates(":jso:impl") val metaprogrammingImpl = findArtifactCoordinates(":metaprogramming:impl") val tools = findArtifactCoordinates(":tools:core") + val cli = findArtifactCoordinates(":tools:cli") val junit = findArtifactCoordinates(":tools:junit") doLast { val file = File(baseDir, "org/teavm/gradle/config/ArtifactCoordinates.java") @@ -93,6 +94,7 @@ val createConfig by tasks.registering { public static final String JUNIT = "$junit"; public static final String TOOLS = "$tools"; + public static final String CLI = "$cli"; private ArtifactCoordinates() { } diff --git a/tools/gradle/src/main/java/org/teavm/gradle/TeaVMExtensionImpl.java b/tools/gradle/src/main/java/org/teavm/gradle/TeaVMExtensionImpl.java index 418e6c7ae..9bc4d5b6d 100644 --- a/tools/gradle/src/main/java/org/teavm/gradle/TeaVMExtensionImpl.java +++ b/tools/gradle/src/main/java/org/teavm/gradle/TeaVMExtensionImpl.java @@ -16,6 +16,7 @@ package org.teavm.gradle; import groovy.lang.Closure; +import javax.inject.Inject; import org.gradle.api.Action; import org.gradle.api.Project; import org.gradle.api.model.ObjectFactory; @@ -24,6 +25,7 @@ import org.teavm.gradle.api.OptimizationLevel; import org.teavm.gradle.api.SourceFilePolicy; import org.teavm.gradle.api.TeaVMCConfiguration; import org.teavm.gradle.api.TeaVMCommonConfiguration; +import org.teavm.gradle.api.TeaVMDevServerConfiguration; import org.teavm.gradle.api.TeaVMExtension; import org.teavm.gradle.api.TeaVMJSConfiguration; import org.teavm.gradle.api.TeaVMWasiConfiguration; @@ -38,7 +40,7 @@ class TeaVMExtensionImpl extends TeaVMBaseExtensionImpl implements TeaVMExtensio TeaVMExtensionImpl(Project project, ObjectFactory objectFactory) { super(project, objectFactory); - js = objectFactory.newInstance(TeaVMJSConfiguration.class); + js = objectFactory.newInstance(JsConfigImpl.class); wasm = objectFactory.newInstance(TeaVMWasmConfiguration.class); wasi = objectFactory.newInstance(TeaVMWasiConfiguration.class); c = objectFactory.newInstance(TeaVMCConfiguration.class); @@ -72,6 +74,14 @@ class TeaVMExtensionImpl extends TeaVMBaseExtensionImpl implements TeaVMExtensio js.getSourceFilePolicy().convention(property("js.sourceFilePolicy") .map(SourceFilePolicy::valueOf) .orElse(SourceFilePolicy.DO_NOTHING)); + js.getDevServer().getStackDeobfuscated().convention(property("js.devServer.stackDeobfuscated") + .map(Boolean::parseBoolean)); + js.getDevServer().getIndicator().convention(property("js.devServer.indicator").map(Boolean::parseBoolean)); + js.getDevServer().getAutoReload().convention(property("js.devServer.autoReload").map(Boolean::parseBoolean)); + js.getDevServer().getPort().convention(property("js.devServer.port").map(Integer::parseInt)); + js.getDevServer().getProxyUrl().convention(property("js.devServer.proxy.url")); + js.getDevServer().getProxyPath().convention(property("js.devServer.proxy.path")); + js.getDevServer().getProcessMemory().convention(property("js.devServer.memory").map(Integer::parseInt)); } private void setupWasmDefaults() { @@ -199,4 +209,28 @@ class TeaVMExtensionImpl extends TeaVMBaseExtensionImpl implements TeaVMExtensio target.getOutOfProcess().convention(source.getOutOfProcess()); target.getProcessMemory().convention(source.getProcessMemory()); } + + static abstract class JsConfigImpl implements TeaVMJSConfiguration { + private TeaVMDevServerConfiguration devServer; + + @Inject + public JsConfigImpl(Project project) { + devServer = project.getObjects().newInstance(TeaVMDevServerConfiguration.class); + } + + @Override + public void devServer(Action action) { + action.execute(devServer); + } + + @Override + public TeaVMDevServerConfiguration getDevServer() { + return devServer; + } + + @Override + public void devServer(Closure action) { + action.rehydrate(getDevServer(), action.getOwner(), action.getThisObject()).call(); + } + } } diff --git a/tools/gradle/src/main/java/org/teavm/gradle/TeaVMPlugin.java b/tools/gradle/src/main/java/org/teavm/gradle/TeaVMPlugin.java index a4f9609fe..819ffbe02 100644 --- a/tools/gradle/src/main/java/org/teavm/gradle/TeaVMPlugin.java +++ b/tools/gradle/src/main/java/org/teavm/gradle/TeaVMPlugin.java @@ -26,6 +26,7 @@ import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.component.ModuleComponentIdentifier; import org.gradle.api.artifacts.component.ProjectComponentIdentifier; import org.gradle.api.artifacts.result.ResolvedDependencyResult; +import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DuplicatesStrategy; import org.gradle.api.model.ObjectFactory; import org.gradle.api.plugins.JavaPlugin; @@ -40,17 +41,22 @@ import org.teavm.gradle.tasks.GenerateCTask; import org.teavm.gradle.tasks.GenerateJavaScriptTask; import org.teavm.gradle.tasks.GenerateWasiTask; import org.teavm.gradle.tasks.GenerateWasmTask; +import org.teavm.gradle.tasks.JavaScriptDevServerTask; +import org.teavm.gradle.tasks.StopJavaScriptDevServerTask; import org.teavm.gradle.tasks.TeaVMTask; public class TeaVMPlugin implements Plugin { public static final String EXTENSION_NAME = "teavm"; public static final String SOURCE_SET_NAME = "teavm"; public static final String JS_TASK_NAME = "generateJavaScript"; + public static final String JS_DEV_SERVER_TASK_NAME = "javaScriptDevServer"; + public static final String STOP_JS_DEV_SERVER_TASK_NAME = "stopJavaScriptDevServer"; public static final String WASM_TASK_NAME = "generateWasm"; public static final String WASI_TASK_NAME = "generateWasi"; public static final String C_TASK_NAME = "generateC"; public static final String CONFIGURATION_NAME = "teavm"; public static final String CLASSPATH_CONFIGURATION_NAME = "teavmClasspath"; + public static final String TASK_GROUP = "TeaVM"; private ObjectFactory objectFactory; @Inject @@ -105,7 +111,11 @@ public class TeaVMPlugin implements Plugin { private void registerTasks(Project project) { var compilerConfig = project.getConfigurations().detachedConfiguration( project.getDependencies().create(ArtifactCoordinates.TOOLS)); + var cliConfig = project.getConfigurations().detachedConfiguration( + project.getDependencies().create(ArtifactCoordinates.CLI)); registerJsTask(project, compilerConfig); + registerJsDevServerTask(project, cliConfig); + registerStopJsDevServerTask(project); registerWasmTask(project, compilerConfig); registerWasiTask(project, compilerConfig); registerCTask(project, compilerConfig); @@ -125,47 +135,44 @@ public class TeaVMPlugin implements Plugin { task.getSourceFilePolicy().convention(js.getSourceFilePolicy()); task.getMaxTopLevelNames().convention(js.getMaxTopLevelNames()); - task.getSourceFiles().from(project.provider(() -> { - var result = new ArrayList(); - addSourceDirs(project, result); - return result; - })); - task.getSourceFiles().from(project.provider(() -> { - var dependencies = project.getConfigurations() - .getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME) - .getIncoming() - .getResolutionResult() - .getAllDependencies(); + setupSources(task.getSourceFiles(), project); + }); + } - var result = new ArrayList(); - for (var dependencyResult : dependencies) { - if (!(dependencyResult instanceof ResolvedDependencyResult)) { - continue; - } - var id = ((ResolvedDependencyResult) dependencyResult).getSelected().getId(); - if (id instanceof ProjectComponentIdentifier) { - var path = ((ProjectComponentIdentifier) id).getProjectPath(); - var refProject = project.getRootProject().findProject(path); - if (refProject != null) { - addSourceDirs(refProject, result); - } - } else if (id instanceof ModuleComponentIdentifier) { - var moduleId = (ModuleComponentIdentifier) id; - var sourcesDep = project.getDependencies().create(Map.of( - "group", moduleId.getGroup(), - "name", moduleId.getModuleIdentifier().getName(), - "version", moduleId.getVersion(), - "classifier", "sources" - )); - var tmpConfig = project.getConfigurations().detachedConfiguration(sourcesDep); - tmpConfig.setTransitive(false); - if (!tmpConfig.getResolvedConfiguration().hasError()) { - result.addAll(tmpConfig.getResolvedConfiguration().getLenientConfiguration().getFiles()); - } - } - } - return result; - })); + private void registerJsDevServerTask(Project project, Configuration configuration) { + var extension = project.getExtensions().getByType(TeaVMExtension.class); + project.getTasks().create(JS_DEV_SERVER_TASK_NAME, JavaScriptDevServerTask.class, task -> { + var js = extension.getJs(); + task.setGroup(TASK_GROUP); + task.getMainClass().convention(js.getMainClass()); + task.getClasspath().from(task.getProject().getConfigurations().getByName(CLASSPATH_CONFIGURATION_NAME)); + task.getPreservedClasses().addAll(js.getPreservedClasses()); + task.getProcessMemory().convention(js.getDevServer().getProcessMemory()); + task.getProperties().putAll(js.getProperties()); + task.getServerClasspath().from(configuration); + task.getTargetFilePath().convention(js.getRelativePathInOutputDir()); + task.getTargetFileName().convention(js.getTargetFileName()); + task.getStackDeobfuscated().convention(js.getDevServer().getStackDeobfuscated()); + task.getIndicator().convention(js.getDevServer().getIndicator()); + task.getAutoReload().convention(js.getDevServer().getAutoReload()); + task.getPort().convention(js.getDevServer().getPort()); + task.getProxyUrl().convention(js.getDevServer().getProxyUrl()); + task.getProxyPath().convention(js.getDevServer().getProxyPath()); + task.getProcessMemory().convention(js.getDevServer().getProcessMemory()); + + var sourceSets = project.getExtensions().findByType(SourceSetContainer.class); + if (sourceSets != null) { + task.getClasspath().from(sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput()); + task.getClasspath().from(sourceSets.getByName(SOURCE_SET_NAME).getOutput()); + } + + setupSources(task.getSourceFiles(), project); + }); + } + + private void registerStopJsDevServerTask(Project project) { + project.getTasks().create(STOP_JS_DEV_SERVER_TASK_NAME, StopJavaScriptDevServerTask.class, task -> { + task.setGroup(TASK_GROUP); }); } @@ -270,5 +277,51 @@ public class TeaVMPlugin implements Plugin { task.getClasspath().from(sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput()); task.getClasspath().from(sourceSets.getByName(SOURCE_SET_NAME).getOutput()); } + + task.setGroup(TASK_GROUP); + } + + private void setupSources(ConfigurableFileCollection sources, Project project) { + sources.from(project.provider(() -> { + var result = new ArrayList(); + addSourceDirs(project, result); + return result; + })); + sources.from(project.provider(() -> { + var dependencies = project.getConfigurations() + .getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME) + .getIncoming() + .getResolutionResult() + .getAllDependencies(); + + var result = new ArrayList(); + for (var dependencyResult : dependencies) { + if (!(dependencyResult instanceof ResolvedDependencyResult)) { + continue; + } + var id = ((ResolvedDependencyResult) dependencyResult).getSelected().getId(); + if (id instanceof ProjectComponentIdentifier) { + var path = ((ProjectComponentIdentifier) id).getProjectPath(); + var refProject = project.getRootProject().findProject(path); + if (refProject != null) { + addSourceDirs(refProject, result); + } + } else if (id instanceof ModuleComponentIdentifier) { + var moduleId = (ModuleComponentIdentifier) id; + var sourcesDep = project.getDependencies().create(Map.of( + "group", moduleId.getGroup(), + "name", moduleId.getModuleIdentifier().getName(), + "version", moduleId.getVersion(), + "classifier", "sources" + )); + var tmpConfig = project.getConfigurations().detachedConfiguration(sourcesDep); + tmpConfig.setTransitive(false); + if (!tmpConfig.getResolvedConfiguration().hasError()) { + result.addAll(tmpConfig.getResolvedConfiguration().getLenientConfiguration().getFiles()); + } + } + } + return result; + })); } } diff --git a/tools/gradle/src/main/java/org/teavm/gradle/api/TeaVMDevServerConfiguration.java b/tools/gradle/src/main/java/org/teavm/gradle/api/TeaVMDevServerConfiguration.java new file mode 100644 index 000000000..b7eb236d7 --- /dev/null +++ b/tools/gradle/src/main/java/org/teavm/gradle/api/TeaVMDevServerConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 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.gradle.api; + +import org.gradle.api.provider.Property; + +public interface TeaVMDevServerConfiguration { + Property getStackDeobfuscated(); + + Property getIndicator(); + + Property getAutoReload(); + + Property getPort(); + + Property getProxyUrl(); + + Property getProxyPath(); + + Property getProcessMemory(); +} diff --git a/tools/gradle/src/main/java/org/teavm/gradle/api/TeaVMJSConfiguration.java b/tools/gradle/src/main/java/org/teavm/gradle/api/TeaVMJSConfiguration.java index eb29f4429..bef08d294 100644 --- a/tools/gradle/src/main/java/org/teavm/gradle/api/TeaVMJSConfiguration.java +++ b/tools/gradle/src/main/java/org/teavm/gradle/api/TeaVMJSConfiguration.java @@ -15,6 +15,9 @@ */ package org.teavm.gradle.api; +import groovy.lang.Closure; +import groovy.lang.DelegatesTo; +import org.gradle.api.Action; import org.gradle.api.provider.Property; public interface TeaVMJSConfiguration extends TeaVMWebConfiguration { @@ -33,4 +36,10 @@ public interface TeaVMJSConfiguration extends TeaVMWebConfiguration { Property getSourceFilePolicy(); Property getMaxTopLevelNames(); + + TeaVMDevServerConfiguration getDevServer(); + + void devServer(Action action); + + void devServer(@DelegatesTo(TeaVMDevServerConfiguration.class) Closure action); } diff --git a/tools/gradle/src/main/java/org/teavm/gradle/tasks/DevServerManager.java b/tools/gradle/src/main/java/org/teavm/gradle/tasks/DevServerManager.java new file mode 100644 index 000000000..1c3447060 --- /dev/null +++ b/tools/gradle/src/main/java/org/teavm/gradle/tasks/DevServerManager.java @@ -0,0 +1,68 @@ +/* + * Copyright 2024 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.gradle.tasks; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.gradle.api.Project; +import org.gradle.api.invocation.Gradle; + +public final class DevServerManager { + private static DevServerManager instance; + private final ConcurrentMap projectManagers = new ConcurrentHashMap<>(); + + private DevServerManager() { + } + + public ProjectDevServerManager getProjectManager(String path) { + return projectManagers.computeIfAbsent(path, this::createProjectManager); + } + + private ProjectDevServerManager createProjectManager(String path) { + return new ProjectDevServerManager(); + } + + public void cleanup(Gradle gradle) { + var allProjectPaths = new HashSet(); + collectProjects(gradle.getRootProject(), allProjectPaths); + var keysToRemove = new HashSet<>(projectManagers.keySet()); + keysToRemove.removeAll(allProjectPaths); + for (var path : keysToRemove) { + var pm = projectManagers.remove(path); + if (pm != null) { + pm.stop(gradle.getRootProject().getLogger()); + } + } + } + + public static DevServerManager instance() { + if (instance == null) { + instance = new DevServerManager(); + } + return instance; + } + + private static void collectProjects(Project project, Set collector) { + if (!collector.add(project.getPath())) { + return; + } + for (var child : project.getChildProjects().values()) { + collectProjects(child, collector); + } + } +} diff --git a/tools/gradle/src/main/java/org/teavm/gradle/tasks/JavaScriptDevServerTask.java b/tools/gradle/src/main/java/org/teavm/gradle/tasks/JavaScriptDevServerTask.java new file mode 100644 index 000000000..f72e7fb94 --- /dev/null +++ b/tools/gradle/src/main/java/org/teavm/gradle/tasks/JavaScriptDevServerTask.java @@ -0,0 +1,144 @@ +/* + * Copyright 2024 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.gradle.tasks; + +import java.io.IOException; +import javax.inject.Inject; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.TaskAction; +import org.gradle.internal.logging.progress.ProgressLoggerFactory; + +public abstract class JavaScriptDevServerTask extends DefaultTask { + @Classpath + public abstract ConfigurableFileCollection getClasspath(); + + @Input + @Optional + public abstract Property getTargetFileName(); + + @Input + @Optional + public abstract Property getTargetFilePath(); + + @Input + @Optional + public abstract MapProperty getProperties(); + + @Input + @Optional + public abstract ListProperty getPreservedClasses(); + + @Input + public abstract Property getMainClass(); + + @Input + @Optional + public abstract Property getStackDeobfuscated(); + + @Input + @Optional + public abstract Property getIndicator(); + + @Input + @Optional + public abstract Property getPort(); + + @InputFiles + public abstract ConfigurableFileCollection getSourceFiles(); + + @Input + @Optional + public abstract Property getAutoReload(); + + @Input + @Optional + public abstract Property getProxyUrl(); + + @Input + @Optional + public abstract Property getProxyPath(); + + @Input + @Optional + public abstract Property getProcessMemory(); + + @Classpath + public abstract ConfigurableFileCollection getServerClasspath(); + + @Internal + public abstract Property getServerDebugPort(); + + @Inject + protected abstract ProgressLoggerFactory getProgressLoggerFactory(); + + @TaskAction + public void compileInCodeServer() throws IOException { + var codeServerManager = DevServerManager.instance(); + codeServerManager.cleanup(getProject().getGradle()); + var pm = codeServerManager.getProjectManager(getProject().getPath()); + + pm.setClasspath(getClasspath().getFiles()); + pm.setSources(getSourceFiles().getFiles()); + if (getTargetFileName().isPresent()) { + pm.setTargetFileName(getTargetFileName().get()); + } + + if (getTargetFilePath().isPresent()) { + pm.setTargetFilePath(getTargetFilePath().get()); + } + + pm.setProperties(getProperties().get()); + pm.setPreservedClasses(getPreservedClasses().get()); + + pm.setServerClasspath(getServerClasspath().getFiles()); + pm.setMainClass(getMainClass().get()); + + pm.setStackDeobfuscated(!getStackDeobfuscated().isPresent() || getStackDeobfuscated().get()); + pm.setIndicator(getIndicator().isPresent() && getIndicator().get()); + pm.setAutoReload(getAutoReload().isPresent() && getAutoReload().get()); + + if (getPort().isPresent()) { + pm.setPort(getPort().get()); + } + if (getProxyUrl().isPresent()) { + pm.setProxyUrl(getProxyUrl().get()); + } + if (getProxyPath().isPresent()) { + pm.setProxyPath(getProxyPath().get()); + } + + if (getProcessMemory().isPresent()) { + pm.setProcessMemory(getProcessMemory().get()); + } + if (getServerDebugPort().isPresent()) { + pm.setDebugPort(getServerDebugPort().get()); + } + + var progress = getProgressLoggerFactory().newOperation(getClass()); + progress.start("Compilation", getName()); + pm.runBuild(getLogger(), progress); + progress.completed(); + } +} diff --git a/tools/gradle/src/main/java/org/teavm/gradle/tasks/ProjectDevServerManager.java b/tools/gradle/src/main/java/org/teavm/gradle/tasks/ProjectDevServerManager.java new file mode 100644 index 000000000..f744af229 --- /dev/null +++ b/tools/gradle/src/main/java/org/teavm/gradle/tasks/ProjectDevServerManager.java @@ -0,0 +1,589 @@ +/* + * Copyright 2024 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.gradle.tasks; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.stream.Collectors; +import org.gradle.api.GradleException; +import org.gradle.api.logging.Logger; +import org.gradle.internal.logging.progress.ProgressLogger; +import org.teavm.common.json.JsonArrayValue; +import org.teavm.common.json.JsonObjectValue; +import org.teavm.common.json.JsonParser; +import org.teavm.common.json.JsonValue; + +public class ProjectDevServerManager { + private Set serverClasspath = new LinkedHashSet<>(); + private Set classpath = new LinkedHashSet<>(); + private String targetFileName; + private String targetFilePath; + private Map properties = new LinkedHashMap<>(); + private Set preservedClasses = new LinkedHashSet<>(); + private String mainClass; + private boolean stackDeobfuscated; + private boolean indicator; + private int port; + private Set sources = new HashSet<>(); + private boolean autoReload; + private String proxyUrl; + private String proxyPath; + private int processMemory; + private int debugPort; + + private Process process; + private Thread processKillHook; + private Thread commandInputThread; + private Thread stderrThread; + private BufferedWriter commandOutput; + private JsonParser jsonParser; + private BlockingQueue eventQueue = new LinkedBlockingQueue<>(); + private boolean eventQueueDone; + private Logger logger; + private ProgressLogger progressLogger; + + private Set runningServerClasspath = new HashSet<>(); + private Set runningClasspath = new HashSet<>(); + private String runningTargetFileName; + private String runningTargetFilePath; + private Map runningProperties = new HashMap<>(); + private Set runningPreservedClasses = new HashSet<>(); + private String runningMainClass; + private boolean runningStackDeobfuscated; + private boolean runningIndicator; + private int runningPort; + private Set runningSources = new HashSet<>(); + private boolean runningAutoReload; + private String runningProxyUrl; + private String runningProxyPath; + private int runningProcessMemory; + private int runningDebugPort; + + ProjectDevServerManager() { + jsonParser = JsonParser.ofValue(this::parseCommand); + } + + public void setServerClasspath(Set serverClasspath) { + this.serverClasspath.clear(); + this.serverClasspath.addAll(serverClasspath); + } + + public void setClasspath(Set classpath) { + this.classpath.clear(); + this.classpath.addAll(classpath); + } + + public void setProperties(Map properties) { + this.properties.clear(); + this.properties.putAll(properties); + } + + public void setPreservedClasses(Collection preservedClasses) { + this.preservedClasses.clear(); + this.preservedClasses.addAll(preservedClasses); + } + + public void setTargetFileName(String targetFileName) { + this.targetFileName = targetFileName; + } + + public void setTargetFilePath(String targetFilePath) { + this.targetFilePath = targetFilePath; + } + + public void setMainClass(String mainClass) { + this.mainClass = mainClass; + } + + public void setStackDeobfuscated(boolean stackDeobfuscated) { + this.stackDeobfuscated = stackDeobfuscated; + } + + public void setIndicator(boolean indicator) { + this.indicator = indicator; + } + + public void setPort(int port) { + this.port = port; + } + + public void setSources(Set sources) { + this.sources.clear(); + this.sources.addAll(sources); + } + + public void setAutoReload(boolean autoReload) { + this.autoReload = autoReload; + } + + public void setProxyUrl(String proxyUrl) { + this.proxyUrl = proxyUrl; + } + + public void setProxyPath(String proxyPath) { + this.proxyPath = proxyPath; + } + + public void setProcessMemory(int processMemory) { + this.processMemory = processMemory; + } + + public void setDebugPort(int debugPort) { + this.debugPort = debugPort; + } + + public void runBuild(Logger logger, ProgressLogger progressLogger) throws IOException { + restartIfNecessary(logger); + try { + schedule(() -> { + try { + commandOutput.write("{\"type\":\"build\"}\n"); + commandOutput.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } catch (InterruptedException e) { + return; + } + processQueue(logger, progressLogger); + } + + private void processQueue(Logger logger, ProgressLogger progressLogger) { + eventQueueDone = false; + this.logger = logger; + this.progressLogger = progressLogger; + var stoppedUnexpectedly = new boolean[1]; + var processMonitorThread = new Thread(() -> { + try { + process.waitFor(); + schedule(() -> { + stoppedUnexpectedly[0] = true; + }); + stopEventQueue(); + } catch (InterruptedException e) { + // do nothing + } + }); + processMonitorThread.setDaemon(true); + processMonitorThread.setName("Dev server process crash monitor"); + processMonitorThread.start(); + try { + while (!eventQueueDone || !eventQueue.isEmpty()) { + Runnable command; + try { + command = eventQueue.take(); + } catch (InterruptedException e) { + break; + } + command.run(); + } + if (stoppedUnexpectedly[0]) { + logger.error("Dev server process stopped unexpectedly"); + throw new GradleException(); + } + } finally { + this.logger = null; + this.progressLogger = null; + processMonitorThread.interrupt(); + } + } + + private void restartIfNecessary(Logger logger) throws IOException { + if (process != null && !checkProcess()) { + logger.info("Changes detected in TeaVM development server config, restarting server"); + stop(logger); + } + if (process == null || !process.isAlive()) { + start(logger); + } + } + + public void stop(Logger logger) { + if (process != null) { + logger.info("Stopping TeaVM development server, PID = {}", process.pid()); + if (process.isAlive()) { + try { + commandOutput.write("{\"type\":\"stop\"}\n"); + commandOutput.flush(); + } catch (IOException e) { + process.destroy(); + } + try { + process.waitFor(); + } catch (InterruptedException e) { + // do nothing + } + } else { + logger.info("Process was dead"); + } + process = null; + Runtime.getRuntime().removeShutdownHook(processKillHook); + processKillHook = null; + commandInputThread.interrupt(); + commandInputThread = null; + stderrThread.interrupt(); + stderrThread = null; + commandOutput = null; + } else { + logger.info("No development server running, doing nothing"); + } + } + + private void start(Logger logger) throws IOException { + logger.info("Starting TeaVM development server"); + + var pb = new ProcessBuilder(); + pb.command(getBuilderCommand().toArray(new String[0])); + + process = pb.start(); + processKillHook = new Thread(() -> process.destroy()); + Runtime.getRuntime().addShutdownHook(processKillHook); + commandOutput = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8)); + + commandInputThread = new Thread(this::readCommandsFromProcess); + commandInputThread.setName("TeaVM development server command reader"); + commandInputThread.setDaemon(true); + commandInputThread.start(); + + stderrThread = new Thread(this::readStderrFromProcess); + stderrThread.setName("TeaVM development server stderr reader"); + stderrThread.setDaemon(true); + stderrThread.start(); + + logger.info("Development server started"); + } + + private void readCommandsFromProcess() { + try (var input = new BufferedReader(new InputStreamReader(process.getInputStream(), + StandardCharsets.UTF_8))) { + while (!Thread.currentThread().isInterrupted()) { + var command = input.readLine(); + if (command == null) { + break; + } + schedule(() -> readCommand(command)); + } + } catch (IOException e) { + try { + stopEventQueue(); + if (logger != null) { + logger.error("IO error occurred reading stdout of development server process", e); + } + } catch (InterruptedException e2) { + if (logger != null) { + logger.info("Development server process input thread interrupted"); + } + } + } catch (InterruptedException e) { + if (logger != null) { + logger.info("Development server process input thread interrupted"); + } + } + } + + private void readStderrFromProcess() { + try (var input = new BufferedReader(new InputStreamReader(process.getErrorStream(), + StandardCharsets.UTF_8))) { + while (!Thread.currentThread().isInterrupted()) { + var line = input.readLine(); + if (line == null) { + break; + } + schedule(() -> logger.warn("server stderr: {}", line)); + } + } catch (IOException e) { + if (logger != null) { + logger.error("IO error occurred reading stderr of development server process", e); + } + } catch (InterruptedException e) { + if (logger != null) { + logger.info("Development server process input thread interrupted"); + } + } + } + + private void stopEventQueue() throws InterruptedException { + schedule(() -> eventQueueDone = true); + } + + private void schedule(Runnable command) throws InterruptedException { + eventQueue.put(command); + } + + private void readCommand(String command) { + try { + jsonParser.parse(new StringReader(command)); + } catch (IOException e) { + // This should not happen + throw new RuntimeException(e); + } catch (RuntimeException e) { + throw new RuntimeException("Error reading command: " + command, e); + } + } + + private void parseCommand(JsonValue command) { + var obj = (JsonObjectValue) command; + var type = obj.get("type").asString(); + try { + switch (type) { + case "log": + logCommand(obj); + break; + case "compilation-started": + // do nothing + break; + case "compilation-progress": + progressCommand(obj); + break; + case "compilation-complete": + completeCommand(obj); + break; + case "compilation-cancelled": + stopEventQueue(); + break; + } + } catch (InterruptedException e) { + // do nothing + } + } + + private void logCommand(JsonObjectValue command) throws InterruptedException { + if (logger == null) { + return; + } + + var level = command.get("level").asString(); + var message = command.get("message").asString(); + var throwable = command.get("throwable"); + if (throwable != null) { + message += "\n" + throwable.asString(); + } + var messageToReport = message; + switch (level) { + case "debug": + schedule(() -> logger.debug(messageToReport)); + break; + case "info": + schedule(() -> logger.info(messageToReport)); + break; + case "warning": + schedule(() -> logger.warn(messageToReport)); + break; + case "error": + schedule(() -> logger.error(messageToReport)); + break; + } + } + + private void progressCommand(JsonObjectValue command) throws InterruptedException { + if (progressLogger == null) { + return; + } + var progress = command.get("progress").asNumber(); + var roundedResult = (int) (progress * 1000 + 5) / 10; + var result = Math.min(100, roundedResult / 10.0); + schedule(() -> progressLogger.progress(result + " %")); + } + + private void completeCommand(JsonObjectValue command) throws InterruptedException { + var problemsJson = command.get("problems"); + if (problemsJson != null && logger != null) { + reportProblems((JsonArrayValue) problemsJson); + } + stopEventQueue(); + } + + private void reportProblems(JsonArrayValue json) throws InterruptedException { + var hasSevere = false; + for (var i = 0; i < json.size(); ++i) { + var problem = json.get(i).asObject(); + var severity = problem.get("severity").asString(); + var sb = new StringBuilder(); + sb.append(problem.get("message").asString()); + sb.append(problem.get("location").asString()); + var message = sb.toString(); + switch (severity) { + case "error": + hasSevere = true; + schedule(() -> logger.error(message)); + break; + case "warning": + schedule(() -> logger.warn(message)); + break; + } + } + if (hasSevere) { + schedule(() -> { + throw new GradleException("Errors occurred during TeaVM build"); + }); + } + } + + private List getBuilderCommand() { + var command = new ArrayList(); + + var javaHome = System.getProperty("java.home"); + var javaExec = javaHome + "/bin/java"; + if (System.getProperty("os.name").toLowerCase().startsWith("windows")) { + javaExec += ".exe"; + } + command.add(javaExec); + + if (!serverClasspath.isEmpty()) { + command.add("-cp"); + command.add(serverClasspath.stream() + .map(File::getAbsolutePath) + .collect(Collectors.joining(File.pathSeparator))); + } + runningServerClasspath.clear(); + runningServerClasspath.addAll(serverClasspath); + + if (processMemory != 0) { + command.add("-Xmx" + processMemory + "m"); + } + runningProcessMemory = processMemory; + + if (debugPort != 0) { + command.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,quiet=y,address=*:" + debugPort); + } + runningDebugPort = debugPort; + + command.add("org.teavm.cli.devserver.TeaVMDevServerRunner"); + command.add("--json-interface"); + command.add("--no-watch"); + + if (targetFileName != null) { + command.add("--targetfile"); + command.add(targetFileName); + } + runningTargetFileName = targetFileName; + + if (targetFilePath != null) { + command.add("--targetdir"); + command.add(targetFilePath); + } + runningTargetFilePath = targetFilePath; + + if (!classpath.isEmpty()) { + command.add("--classpath"); + command.addAll(classpath.stream() + .map(File::getAbsolutePath) + .collect(Collectors.toList())); + } + runningClasspath.clear(); + runningClasspath.addAll(classpath); + + if (!sources.isEmpty()) { + command.add("--sourcepath"); + command.addAll(sources.stream() + .map(File::getAbsolutePath) + .collect(Collectors.toList())); + } + runningSources.clear(); + runningSources.addAll(sources); + + if (port != 0) { + command.add("--port"); + command.add(String.valueOf(port)); + } + runningPort = port; + + if (indicator) { + command.add("--indicator"); + } + runningIndicator = indicator; + + if (stackDeobfuscated) { + command.add("--deobfuscate-stack"); + } + runningStackDeobfuscated = stackDeobfuscated; + + if (autoReload) { + command.add("--auto-reload"); + } + runningAutoReload = autoReload; + + if (proxyUrl != null) { + command.add("--proxy-url"); + command.add(proxyUrl); + } + runningProxyUrl = proxyUrl; + + if (proxyPath != null) { + command.add("--proxy-path"); + command.add(proxyPath); + } + runningProxyPath = proxyPath; + + for (var entry : properties.entrySet()) { + command.add("--property"); + command.add(entry.getKey() + "=" + entry.getValue()); + } + runningProperties.clear(); + runningProperties.putAll(properties); + + if (!preservedClasses.isEmpty()) { + command.add("--preserved-classes"); + command.addAll(preservedClasses); + } + runningPreservedClasses.clear(); + runningPreservedClasses.addAll(preservedClasses); + + command.add("--"); + command.add(mainClass); + runningMainClass = mainClass; + + return command; + } + + private boolean checkProcess() { + return Objects.equals(serverClasspath, runningServerClasspath) + && Objects.equals(classpath, runningClasspath) + && Objects.equals(targetFileName, runningTargetFileName) + && Objects.equals(targetFilePath, runningTargetFilePath) + && Objects.equals(properties, runningProperties) + && Objects.equals(preservedClasses, runningPreservedClasses) + && Objects.equals(mainClass, runningMainClass) + && stackDeobfuscated == runningStackDeobfuscated + && indicator == runningIndicator + && port == runningPort + && Objects.equals(sources, runningSources) + && autoReload == runningAutoReload + && Objects.equals(proxyUrl, runningProxyUrl) + && Objects.equals(proxyPath, runningProxyPath) + && processMemory == runningProcessMemory + && debugPort == runningDebugPort; + } +} diff --git a/tools/gradle/src/main/java/org/teavm/gradle/tasks/StopJavaScriptDevServerTask.java b/tools/gradle/src/main/java/org/teavm/gradle/tasks/StopJavaScriptDevServerTask.java new file mode 100644 index 000000000..d9a24d799 --- /dev/null +++ b/tools/gradle/src/main/java/org/teavm/gradle/tasks/StopJavaScriptDevServerTask.java @@ -0,0 +1,29 @@ +/* + * Copyright 2024 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.gradle.tasks; + +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.TaskAction; + +public abstract class StopJavaScriptDevServerTask extends DefaultTask { + @TaskAction + public void stopServer() { + var codeServerManager = DevServerManager.instance(); + codeServerManager.cleanup(getProject().getGradle()); + var pm = codeServerManager.getProjectManager(getProject().getPath()); + pm.stop(getLogger()); + } +} diff --git a/tools/gradle/src/main/java/org/teavm/gradle/tasks/TeaVMTask.java b/tools/gradle/src/main/java/org/teavm/gradle/tasks/TeaVMTask.java index c5106e51e..6098fc5f4 100644 --- a/tools/gradle/src/main/java/org/teavm/gradle/tasks/TeaVMTask.java +++ b/tools/gradle/src/main/java/org/teavm/gradle/tasks/TeaVMTask.java @@ -52,7 +52,6 @@ import org.teavm.vm.TeaVMProgressListener; public abstract class TeaVMTask extends DefaultTask { public TeaVMTask() { - setGroup("TeaVM"); getDebugInformation().convention(false); getTargetFileName().convention("bundle"); getOptimization().convention(OptimizationLevel.BALANCED); diff --git a/tools/ide-deps/build.gradle.kts b/tools/ide-deps/build.gradle.kts index 50e181708..898f76216 100644 --- a/tools/ide-deps/build.gradle.kts +++ b/tools/ide-deps/build.gradle.kts @@ -22,10 +22,10 @@ plugins { description = "All-in one JAR file that used by IDE plugins" dependencies { - implementation(project(path = ":tools:core")) - implementation(project(path = ":tools:devserver")) - implementation(project(path = ":classlib")) - implementation(project(path = ":tools:chrome-rdp")) + api(project(":tools:core")) + api(project(":tools:devserver")) + api(project(":classlib")) + api(project(":tools:chrome-rdp")) } tasks.shadowJar { diff --git a/tools/idea/build.gradle.kts b/tools/idea/build.gradle.kts index bac2eb0f7..6ade3055f 100644 --- a/tools/idea/build.gradle.kts +++ b/tools/idea/build.gradle.kts @@ -32,7 +32,8 @@ intellij { } dependencies { - implementation(project(path = ":tools:ide-deps", configuration = "shadow").setTransitive(false)) + compileOnly(project(":tools:ide-deps")) + runtimeOnly(project(path = ":tools:ide-deps", configuration = "shadow").setTransitive(false)) } tasks { diff --git a/tools/idea/src/main/java/org/teavm/idea/devserver/DevServerRunner.java b/tools/idea/src/main/java/org/teavm/idea/devserver/DevServerRunner.java index 61f00e2b1..887dcc563 100644 --- a/tools/idea/src/main/java/org/teavm/idea/devserver/DevServerRunner.java +++ b/tools/idea/src/main/java/org/teavm/idea/devserver/DevServerRunner.java @@ -153,6 +153,7 @@ public class DevServerRunner extends UnicastRemoteObject implements DevServerMan DevServerRunner daemon = new DevServerRunner(server); System.out.println(PORT_MESSAGE_PREFIX + daemon.port); server.start(); + server.awaitServer(); try { daemon.registry.unbind(ID); @@ -327,7 +328,9 @@ public class DevServerRunner extends UnicastRemoteObject implements DevServerMan @Override public void compilationComplete(BuildResult buildResult) { DevServerBuildResult result = new DevServerBuildResult(); - result.problems.addAll(buildResult.getProblems().getProblems()); + if (buildResult != null) { + result.problems.addAll(buildResult.getProblems().getProblems()); + } for (DevServerManagerListener listener : getListeners()) { try { listener.compilationComplete(result);