gradle: implement dev server task

This commit is contained in:
Alexey Andreev 2024-03-13 16:20:45 +01:00
parent 7341fb38a6
commit bbd02b0067
35 changed files with 1583 additions and 207 deletions

View File

@ -3,7 +3,6 @@
<component name="CheckStyle-IDEA" serialisationVersion="2"> <component name="CheckStyle-IDEA" serialisationVersion="2">
<checkstyleVersion>8.41.1</checkstyleVersion> <checkstyleVersion>8.41.1</checkstyleVersion>
<scanScope>JavaOnlyWithTests</scanScope> <scanScope>JavaOnlyWithTests</scanScope>
<option name="thirdPartyClasspath" />
<option name="activeLocationIds"> <option name="activeLocationIds">
<option value="c65b2ab0-cb40-4423-befb-a37d514deee5" /> <option value="c65b2ab0-cb40-4423-befb-a37d514deee5" />
</option> </option>

View File

@ -16,5 +16,4 @@
package org.teavm.common.json; package org.teavm.common.json;
public abstract class JsonNumericValue extends JsonValue { public abstract class JsonNumericValue extends JsonValue {
public abstract double asNumber();
} }

View File

@ -19,6 +19,7 @@ import java.io.IOException;
import java.io.Reader; import java.io.Reader;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer;
public class JsonParser { public class JsonParser {
private JsonConsumer consumer; private JsonConsumer consumer;
@ -32,6 +33,8 @@ public class JsonParser {
} }
public void parse(Reader reader) throws IOException { public void parse(Reader reader) throws IOException {
lineNumber = 0;
columnNumber = 0;
lastChar = reader.read(); lastChar = reader.read();
skipWhitespaces(reader); skipWhitespaces(reader);
if (lastChar == -1) { if (lastChar == -1) {
@ -412,4 +415,8 @@ public class JsonParser {
private static boolean isDigit(int c) { private static boolean isDigit(int c) {
return c >= '0' && c <= '9'; return c >= '0' && c <= '9';
} }
public static JsonParser ofValue(Consumer<JsonValue> consumer) {
return new JsonParser(new JsonVisitingConsumer(JsonValueParserVisitor.create(consumer)));
}
} }

View File

@ -27,4 +27,8 @@ public abstract class JsonValue {
public long asIntNumber() { public long asIntNumber() {
throw new IllegalStateException(); throw new IllegalStateException();
} }
public double asNumber() {
throw new IllegalStateException();
}
} }

View File

@ -18,6 +18,8 @@ package org.teavm.common.json;
import java.util.function.Consumer; import java.util.function.Consumer;
public abstract class JsonValueParserVisitor extends JsonAllErrorVisitor { public abstract class JsonValueParserVisitor extends JsonAllErrorVisitor {
private JsonValue deferred;
public abstract void consume(JsonValue value); public abstract void consume(JsonValue value);
public static JsonValueParserVisitor create(Consumer<JsonValue> consumer) { public static JsonValueParserVisitor create(Consumer<JsonValue> consumer) {
@ -32,7 +34,7 @@ public abstract class JsonValueParserVisitor extends JsonAllErrorVisitor {
@Override @Override
public JsonVisitor object(JsonErrorReporter reporter) { public JsonVisitor object(JsonErrorReporter reporter) {
var jsonObject = new JsonObjectValue(); var jsonObject = new JsonObjectValue();
consume(jsonObject); deferred = jsonObject;
return new JsonAllErrorVisitor() { return new JsonAllErrorVisitor() {
@Override @Override
public JsonVisitor property(JsonErrorReporter reporter, String name) { public JsonVisitor property(JsonErrorReporter reporter, String name) {
@ -47,12 +49,17 @@ public abstract class JsonValueParserVisitor extends JsonAllErrorVisitor {
} }
@Override @Override
public JsonVisitor array(JsonErrorReporter reporter) { public void end(JsonErrorReporter reporter) {
var jsonArray = new JsonArrayValue(); super.end(reporter);
consume(jsonArray); var value = deferred;
return new JsonAllErrorVisitor() { deferred = null;
consume(value);
}
@Override @Override
public JsonVisitor array(JsonErrorReporter reporter) { public JsonVisitor array(JsonErrorReporter reporter) {
var jsonArray = new JsonArrayValue();
deferred = jsonArray;
return new JsonValueParserVisitor() { return new JsonValueParserVisitor() {
@Override @Override
public void consume(JsonValue value) { public void consume(JsonValue value) {
@ -60,8 +67,6 @@ public abstract class JsonValueParserVisitor extends JsonAllErrorVisitor {
} }
}; };
} }
};
}
@Override @Override
public void stringValue(JsonErrorReporter reporter, String value) { public void stringValue(JsonErrorReporter reporter, String value) {

View File

@ -22,9 +22,17 @@ plugins {
id("org.teavm") id("org.teavm")
} }
configurations {
create("teavmCli")
create("teavmClasslib")
}
dependencies { dependencies {
teavm(teavm.libs.jsoApis) teavm(teavm.libs.jsoApis)
compileOnly("jakarta.servlet:jakarta.servlet-api:6.0.0") 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 { teavm.js {
@ -33,3 +41,14 @@ teavm.js {
sourceMap = true sourceMap = true
sourceFilePolicy = SourceFilePolicy.LINK_LOCAL_FILES sourceFilePolicy = SourceFilePolicy.LINK_LOCAL_FILES
} }
tasks.register<JavaExec>("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)
}

View File

@ -24,6 +24,7 @@ public final class PiCalculator {
} }
public static void main(String[] args) { public static void main(String[] args) {
System.out.println("hello1");
var start = System.currentTimeMillis(); var start = System.currentTimeMillis();
int n = Integer.parseInt(args[0]); int n = Integer.parseInt(args[0]);
int j = 0; int j = 0;

View File

@ -26,7 +26,6 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -530,7 +529,7 @@ public class IncrementalCBuilder {
} }
private void fireBuildComplete(TeaVM vm) { private void fireBuildComplete(TeaVM vm) {
SimpleBuildResult result = new SimpleBuildResult(vm, Collections.emptyList()); SimpleBuildResult result = new SimpleBuildResult(vm);
for (BuilderListener listener : listeners) { for (BuilderListener listener : listeners) {
listener.compilationComplete(result); listener.compilationComplete(result);
} }

View File

@ -27,6 +27,7 @@ dependencies {
implementation(project(":tools:devserver")) implementation(project(":tools:devserver"))
implementation(project(":tools:c-incremental")) implementation(project(":tools:c-incremental"))
implementation(libs.commons.cli) implementation(libs.commons.cli)
implementation(libs.jetty.server)
runtimeOnly(project(":classlib")) runtimeOnly(project(":classlib"))
runtimeOnly(project(":metaprogramming:impl")) runtimeOnly(project(":metaprogramming:impl"))

View File

@ -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<JsonValue> {
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;
}
}
}

View File

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

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2018 Alexey Andreev. * Copyright 2024 Alexey Andreev.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 * See the License for the specific language governing permissions and
* limitations under the License. * 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.Arrays;
import java.util.List;
import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser; 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.Option;
import org.apache.commons.cli.Options; import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException; 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.devserver.DevServer;
import org.teavm.tooling.ConsoleTeaVMToolLog; import org.teavm.tooling.ConsoleTeaVMToolLog;
@ -30,6 +38,7 @@ public final class TeaVMDevServerRunner {
private static Options options = new Options(); private static Options options = new Options();
private DevServer devServer; private DevServer devServer;
private CommandLine commandLine; private CommandLine commandLine;
private JsonCommandWriter jsonWriter;
static { static {
setupOptions(); setupOptions();
@ -56,10 +65,21 @@ public final class TeaVMDevServerRunner {
.build()); .build());
options.addOption(Option.builder("s") options.addOption(Option.builder("s")
.argName("sourcepath") .argName("sourcepath")
.hasArg() .hasArgs()
.desc("source path (either directory or jar file which contains source code)") .desc("source path (either directory or jar file which contains source code)")
.longOpt("sourcepath") .longOpt("sourcepath")
.build()); .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() options.addOption(Option.builder()
.argName("number") .argName("number")
.hasArg() .hasArg()
@ -94,6 +114,14 @@ public final class TeaVMDevServerRunner {
.desc("delegate requests from path") .desc("delegate requests from path")
.longOpt("proxy-path") .longOpt("proxy-path")
.build()); .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) { private TeaVMDevServerRunner(CommandLine commandLine) {
@ -117,7 +145,16 @@ public final class TeaVMDevServerRunner {
TeaVMDevServerRunner runner = new TeaVMDevServerRunner(commandLine); TeaVMDevServerRunner runner = new TeaVMDevServerRunner(commandLine);
runner.parseArguments(); 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() { private void parseArguments() {
@ -128,7 +165,6 @@ public final class TeaVMDevServerRunner {
devServer.setIndicator(commandLine.hasOption("indicator")); devServer.setIndicator(commandLine.hasOption("indicator"));
devServer.setDeobfuscateStack(commandLine.hasOption("deobfuscate-stack")); devServer.setDeobfuscateStack(commandLine.hasOption("deobfuscate-stack"));
devServer.setReloadedAutomatically(commandLine.hasOption("auto-reload")); devServer.setReloadedAutomatically(commandLine.hasOption("auto-reload"));
devServer.setLog(new ConsoleTeaVMToolLog(commandLine.hasOption('v')));
if (commandLine.hasOption("port")) { if (commandLine.hasOption("port")) {
try { try {
devServer.setPort(Integer.parseInt(commandLine.getOptionValue("port"))); 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")) { if (commandLine.hasOption("proxy-url")) {
devServer.setProxyUrl(commandLine.getOptionValue("proxy-url")); devServer.setProxyUrl(commandLine.getOptionValue("proxy-url"));
} }
if (commandLine.hasOption("proxy-path")) { if (commandLine.hasOption("proxy-path")) {
devServer.setProxyPath(commandLine.getOptionValue("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(); String[] args = commandLine.getArgs();
if (args.length != 1) { if (args.length != 1) {
@ -175,8 +228,29 @@ public final class TeaVMDevServerRunner {
} }
} }
private void runAll() { private void setupJsonInterface(DevServer devServer) {
devServer.start(); 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() { private static void printUsage() {

View File

@ -15,7 +15,6 @@
*/ */
package org.teavm.tooling.builder; package org.teavm.tooling.builder;
import java.util.Collection;
import org.teavm.callgraph.CallGraph; import org.teavm.callgraph.CallGraph;
import org.teavm.diagnostics.ProblemProvider; import org.teavm.diagnostics.ProblemProvider;
@ -23,10 +22,4 @@ public interface BuildResult {
CallGraph getCallGraph(); CallGraph getCallGraph();
ProblemProvider getProblems(); ProblemProvider getProblems();
Collection<String> getUsedResources();
Collection<String> getClasses();
Collection<String> getGeneratedFiles();
} }

View File

@ -22,10 +22,8 @@ import java.net.URL;
import java.net.URLClassLoader; import java.net.URLClassLoader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Properties; import java.util.Properties;
import java.util.stream.Collectors;
import org.teavm.backend.javascript.JSModuleType; import org.teavm.backend.javascript.JSModuleType;
import org.teavm.backend.wasm.render.WasmBinaryVersion; import org.teavm.backend.wasm.render.WasmBinaryVersion;
import org.teavm.callgraph.CallGraph; import org.teavm.callgraph.CallGraph;
@ -287,12 +285,8 @@ public class InProcessBuildStrategy implements BuildStrategy {
throw new BuildException(e); throw new BuildException(e);
} }
var generatedFiles = tool.getGeneratedFiles().stream()
.map(File::getAbsolutePath)
.collect(Collectors.toSet());
return new InProcessBuildResult(tool.getDependencyInfo().getCallGraph(), return new InProcessBuildResult(tool.getDependencyInfo().getCallGraph(),
tool.getProblemProvider(), tool.getClasses(), tool.getUsedResources(), generatedFiles); tool.getProblemProvider());
} }
private URLClassLoader buildClassLoader() { private URLClassLoader buildClassLoader() {
@ -310,17 +304,10 @@ public class InProcessBuildStrategy implements BuildStrategy {
static class InProcessBuildResult implements BuildResult { static class InProcessBuildResult implements BuildResult {
private CallGraph callGraph; private CallGraph callGraph;
private ProblemProvider problemProvider; private ProblemProvider problemProvider;
private Collection<String> classes;
private Collection<String> usedResources;
private Collection<String> generatedFiles;
InProcessBuildResult(CallGraph callGraph, ProblemProvider problemProvider, InProcessBuildResult(CallGraph callGraph, ProblemProvider problemProvider) {
Collection<String> classes, Collection<String> usedResources, Collection<String> generatedFiles) {
this.callGraph = callGraph; this.callGraph = callGraph;
this.problemProvider = problemProvider; this.problemProvider = problemProvider;
this.classes = classes;
this.usedResources = usedResources;
this.generatedFiles = generatedFiles;
} }
@Override @Override
@ -332,20 +319,5 @@ public class InProcessBuildStrategy implements BuildStrategy {
public ProblemProvider getProblems() { public ProblemProvider getProblems() {
return problemProvider; return problemProvider;
} }
@Override
public Collection<String> getClasses() {
return classes;
}
@Override
public Collection<String> getUsedResources() {
return usedResources;
}
@Override
public Collection<String> getGeneratedFiles() {
return generatedFiles;
}
} }
} }

View File

@ -17,7 +17,6 @@ package org.teavm.tooling.builder;
import java.rmi.RemoteException; import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject; import java.rmi.server.UnicastRemoteObject;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Properties; import java.util.Properties;
import org.teavm.backend.javascript.JSModuleType; import org.teavm.backend.javascript.JSModuleType;
@ -246,21 +245,6 @@ public class RemoteBuildStrategy implements BuildStrategy {
public ProblemProvider getProblems() { public ProblemProvider getProblems() {
return problems; return problems;
} }
@Override
public Collection<String> getUsedResources() {
return response.usedResources;
}
@Override
public Collection<String> getClasses() {
return response.classes;
}
@Override
public Collection<String> getGeneratedFiles() {
return response.generatedFiles;
}
}; };
} }

View File

@ -15,21 +15,15 @@
*/ */
package org.teavm.tooling.builder; package org.teavm.tooling.builder;
import java.util.Collection;
import java.util.List;
import org.teavm.callgraph.CallGraph; import org.teavm.callgraph.CallGraph;
import org.teavm.diagnostics.ProblemProvider; import org.teavm.diagnostics.ProblemProvider;
import org.teavm.tooling.InstructionLocationReader;
import org.teavm.vm.TeaVM; import org.teavm.vm.TeaVM;
public class SimpleBuildResult implements BuildResult { public class SimpleBuildResult implements BuildResult {
private TeaVM vm; private TeaVM vm;
private List<String> generatedFiles;
private Collection<String> usedResources;
public SimpleBuildResult(TeaVM vm, List<String> generatedFiles) { public SimpleBuildResult(TeaVM vm) {
this.vm = vm; this.vm = vm;
this.generatedFiles = generatedFiles;
} }
@Override @Override
@ -41,22 +35,4 @@ public class SimpleBuildResult implements BuildResult {
public ProblemProvider getProblems() { public ProblemProvider getProblems() {
return vm.getProblemProvider(); return vm.getProblemProvider();
} }
@Override
public Collection<String> getUsedResources() {
if (usedResources == null) {
usedResources = InstructionLocationReader.extractUsedResources(vm);
}
return usedResources;
}
@Override
public Collection<String> getClasses() {
return vm.getClasses();
}
@Override
public Collection<String> getGeneratedFiles() {
return generatedFiles;
}
} }

View File

@ -186,11 +186,6 @@ public class BuildDaemon extends UnicastRemoteObject implements RemoteBuildServi
response.callGraph = tool.getDependencyInfo().getCallGraph(); response.callGraph = tool.getDependencyInfo().getCallGraph();
response.problems.addAll(tool.getProblemProvider().getProblems()); response.problems.addAll(tool.getProblemProvider().getProblems());
response.severeProblems.addAll(tool.getProblemProvider().getSevereProblems()); 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; return response;

View File

@ -17,9 +17,7 @@ package org.teavm.tooling.daemon;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import org.teavm.callgraph.CallGraph; import org.teavm.callgraph.CallGraph;
import org.teavm.diagnostics.Problem; import org.teavm.diagnostics.Problem;
@ -27,8 +25,5 @@ public class RemoteBuildResponse implements Serializable {
public CallGraph callGraph; public CallGraph callGraph;
public final List<Problem> problems = new ArrayList<>(); public final List<Problem> problems = new ArrayList<>();
public final List<Problem> severeProblems = new ArrayList<>(); public final List<Problem> severeProblems = new ArrayList<>();
public final Set<String> usedResources = new HashSet<>();
public final Set<String> classes = new HashSet<>();
public final Set<String> generatedFiles = new HashSet<>();
public Throwable exception; public Throwable exception;
} }

View File

@ -98,16 +98,22 @@ public class FileSystemWatcher {
return !changedFiles.isEmpty() || pollNow(); return !changedFiles.isEmpty() || pollNow();
} }
public void pollChanges() throws IOException {
while (pollNow()) {
// continue polling
}
}
public void waitForChange(int timeout) throws InterruptedException, IOException { public void waitForChange(int timeout) throws InterruptedException, IOException {
if (!hasChanges()) { if (!hasChanges()) {
take(); take();
} }
if (timeout > 0) {
while (poll(timeout)) { while (poll(timeout)) {
// continue polling // continue polling
} }
while (pollNow()) {
// continue polling
} }
pollChanges();
} }
public List<File> grabChangedFiles() { public List<File> grabChangedFiles() {

View File

@ -86,6 +86,7 @@ import org.teavm.parsing.resource.ResourceClassHolderMapper;
import org.teavm.tooling.EmptyTeaVMToolLog; import org.teavm.tooling.EmptyTeaVMToolLog;
import org.teavm.tooling.TeaVMProblemRenderer; import org.teavm.tooling.TeaVMProblemRenderer;
import org.teavm.tooling.TeaVMToolLog; import org.teavm.tooling.TeaVMToolLog;
import org.teavm.tooling.builder.BuildResult;
import org.teavm.tooling.builder.SimpleBuildResult; import org.teavm.tooling.builder.SimpleBuildResult;
import org.teavm.tooling.util.FileSystemWatcher; import org.teavm.tooling.util.FileSystemWatcher;
import org.teavm.vm.MemoryBuildTarget; import org.teavm.vm.MemoryBuildTarget;
@ -119,6 +120,8 @@ public class CodeServlet extends HttpServlet {
private String proxyProtocol; private String proxyProtocol;
private int proxyPort; private int proxyPort;
private String proxyBaseUrl; private String proxyBaseUrl;
private Map<String, String> properties = new LinkedHashMap<>();
private List<String> preservedClasses = new ArrayList<>();
private Map<String, Supplier<InputStream>> sourceFileCache = new HashMap<>(); private Map<String, Supplier<InputStream>> sourceFileCache = new HashMap<>();
@ -148,6 +151,9 @@ public class CodeServlet extends HttpServlet {
private InMemorySymbolTable fileSymbolTable = new InMemorySymbolTable(); private InMemorySymbolTable fileSymbolTable = new InMemorySymbolTable();
private InMemorySymbolTable variableSymbolTable = new InMemorySymbolTable(); private InMemorySymbolTable variableSymbolTable = new InMemorySymbolTable();
private ReferenceCache referenceCache = new ReferenceCache(); private ReferenceCache referenceCache = new ReferenceCache();
private boolean fileSystemWatched = true;
private boolean compileOnStartup = true;
private boolean logBuildErrors = true;
public CodeServlet(String mainClass, String[] classPath) { public CodeServlet(String mainClass, String[] classPath) {
this.mainClass = mainClass; this.mainClass = mainClass;
@ -201,6 +207,26 @@ public class CodeServlet extends HttpServlet {
this.proxyPath = normalizePath(proxyPath); this.proxyPath = normalizePath(proxyPath);
} }
public void setFileSystemWatched(boolean fileSystemWatched) {
this.fileSystemWatched = fileSystemWatched;
}
public void setCompileOnStartup(boolean compileOnStartup) {
this.compileOnStartup = compileOnStartup;
}
public List<String> getPreservedClasses() {
return preservedClasses;
}
public Map<String, String> getProperties() {
return properties;
}
public void setLogBuildErrors(boolean logBuildErrors) {
this.logBuildErrors = logBuildErrors;
}
public void addProgressHandler(ProgressHandler handler) { public void addProgressHandler(ProgressHandler handler) {
synchronized (progressHandlers) { synchronized (progressHandlers) {
progressHandlers.add(handler); progressHandlers.add(handler);
@ -241,12 +267,16 @@ public class CodeServlet extends HttpServlet {
} }
public void buildProject() { public void buildProject() {
if (buildThread == null) {
runCompilerThread();
} else {
synchronized (statusLock) { synchronized (statusLock) {
if (waiting) { if (waiting) {
buildThread.interrupt(); buildThread.interrupt();
} }
} }
} }
}
public void cancelBuild() { public void cancelBuild() {
synchronized (statusLock) { synchronized (statusLock) {
@ -617,7 +647,7 @@ public class CodeServlet extends HttpServlet {
} }
stopped = true; stopped = true;
synchronized (statusLock) { synchronized (statusLock) {
if (waiting) { if (buildThread != null && waiting) {
buildThread.interrupt(); buildThread.interrupt();
} }
} }
@ -626,7 +656,13 @@ public class CodeServlet extends HttpServlet {
@Override @Override
public void init() throws ServletException { public void init() throws ServletException {
super.init(); 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.setName("TeaVM compiler");
thread.start(); thread.start();
buildThread = thread; buildThread = thread;
@ -726,8 +762,13 @@ public class CodeServlet extends HttpServlet {
try { try {
initBuilder(); initBuilder();
var hasJob = true;
while (!stopped) { while (!stopped) {
if (hasJob) {
buildOnce(); buildOnce();
} else {
emptyBuild();
}
if (stopped) { if (stopped) {
break; break;
@ -737,11 +778,22 @@ public class CodeServlet extends HttpServlet {
synchronized (statusLock) { synchronized (statusLock) {
waiting = true; waiting = true;
} }
if (fileSystemWatched) {
watcher.waitForChange(750); watcher.waitForChange(750);
log.info("Changes detected. Recompiling.");
} else {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
watcher.pollChanges();
}
synchronized (statusLock) { synchronized (statusLock) {
waiting = false; waiting = false;
} }
log.info("Changes detected. Recompiling.");
} catch (InterruptedException e) { } catch (InterruptedException e) {
if (stopped) { if (stopped) {
break; break;
@ -760,6 +812,7 @@ public class CodeServlet extends HttpServlet {
} }
classSource.evict(staleClasses); classSource.evict(staleClasses);
hasJob = !staleClasses.isEmpty();
} }
log.info("Build process stopped"); log.info("Build process stopped");
} catch (Throwable e) { } catch (Throwable e) {
@ -836,6 +889,10 @@ public class CodeServlet extends HttpServlet {
vm.setProgressListener(progressListener); vm.setProgressListener(progressListener);
vm.setProgramCache(programCache); vm.setProgramCache(programCache);
vm.installPlugins(); vm.installPlugins();
for (var className : preservedClasses) {
vm.preserveType(className);
}
vm.getProperties().putAll(properties);
vm.setLastKnownClasses(lastReachedClasses); vm.setLastKnownClasses(lastReachedClasses);
vm.setEntryPoint(mainClass); vm.setEntryPoint(mainClass);
@ -850,6 +907,12 @@ public class CodeServlet extends HttpServlet {
postBuild(vm, startTime); postBuild(vm, startTime);
} }
private void emptyBuild() {
fireBuildStarted();
log.info("No files changed, nothing to do");
fireBuildCompleteWithResult(null);
}
private ClassReaderSource packClasses(ClassReaderSource source, Collection<? extends String> classNames) { private ClassReaderSource packClasses(ClassReaderSource source, Collection<? extends String> classNames) {
MemoryCachedClassReaderSource packedSource = createCachedSource(); MemoryCachedClassReaderSource packedSource = createCachedSource();
packedSource.setProvider(source::get); packedSource.setProvider(source::get);
@ -912,7 +975,6 @@ public class CodeServlet extends HttpServlet {
private void postBuild(TeaVM vm, long startTime) { private void postBuild(TeaVM vm, long startTime) {
if (!vm.wasCancelled()) { if (!vm.wasCancelled()) {
log.info("Recompiled stale methods: " + programCache.getPendingItemsCount()); log.info("Recompiled stale methods: " + programCache.getPendingItemsCount());
fireBuildComplete(vm);
if (vm.getProblemProvider().getSevereProblems().isEmpty()) { if (vm.getProblemProvider().getSevereProblems().isEmpty()) {
log.info("Build complete successfully"); log.info("Build complete successfully");
saveNewResult(); saveNewResult();
@ -926,7 +988,10 @@ public class CodeServlet extends HttpServlet {
reportCompilationComplete(false); reportCompilationComplete(false);
} }
printStats(vm, startTime); printStats(vm, startTime);
if (logBuildErrors) {
TeaVMProblemRenderer.describeProblems(vm, log); TeaVMProblemRenderer.describeProblems(vm, log);
}
fireBuildComplete(vm);
} else { } else {
log.info("Build cancelled"); log.info("Build cancelled");
fireBuildCancelled(); fireBuildCancelled();
@ -1056,9 +1121,12 @@ public class CodeServlet extends HttpServlet {
} }
private void fireBuildComplete(TeaVM vm) { private void fireBuildComplete(TeaVM vm) {
SimpleBuildResult result = new SimpleBuildResult(vm, new ArrayList<>(buildTarget.getNames())); fireBuildCompleteWithResult(new SimpleBuildResult(vm));
for (DevServerListener listener : listeners) { }
listener.compilationComplete(result);
private void fireBuildCompleteWithResult(BuildResult buildResult) {
for (var listener : listeners) {
listener.compilationComplete(buildResult);
} }
} }
@ -1089,7 +1157,7 @@ public class CodeServlet extends HttpServlet {
@Override @Override
public TeaVMProgressFeedback progressReached(int progress) { public TeaVMProgressFeedback progressReached(int progress) {
if (indicator) { if (indicator || !listeners.isEmpty()) {
int current = start + Math.min(progress, phaseLimit) * (end - start) / phaseLimit; int current = start + Math.min(progress, phaseLimit) * (end - start) / phaseLimit;
if (current != last) { if (current != last) {
if (current - last > 10 || System.currentTimeMillis() - lastTime > 100) { if (current - last > 10 || System.currentTimeMillis() - lastTime > 100) {

View File

@ -16,7 +16,9 @@
package org.teavm.devserver; package org.teavm.devserver;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
@ -32,9 +34,14 @@ public class DevServer {
private boolean indicator; private boolean indicator;
private boolean deobfuscateStack; private boolean deobfuscateStack;
private boolean reloadedAutomatically; private boolean reloadedAutomatically;
private boolean fileSystemWatched = true;
private TeaVMToolLog log; private TeaVMToolLog log;
private CodeServlet servlet; private CodeServlet servlet;
private List<DevServerListener> listeners = new ArrayList<>(); private List<DevServerListener> listeners = new ArrayList<>();
private Map<String, String> properties = new LinkedHashMap<>();
private List<String> preservedClasses = new ArrayList<>();
private boolean compileOnStartup;
private boolean logBuildErrors = true;
private Server server; private Server server;
private int port = 9090; private int port = 9090;
@ -88,6 +95,10 @@ public class DevServer {
this.reloadedAutomatically = reloadedAutomatically; this.reloadedAutomatically = reloadedAutomatically;
} }
public void setFileSystemWatched(boolean fileSystemWatched) {
this.fileSystemWatched = fileSystemWatched;
}
public void setProxyUrl(String proxyUrl) { public void setProxyUrl(String proxyUrl) {
this.proxyUrl = proxyUrl; this.proxyUrl = proxyUrl;
} }
@ -100,6 +111,18 @@ public class DevServer {
return sourcePath; return sourcePath;
} }
public void setCompileOnStartup(boolean compileOnStartup) {
this.compileOnStartup = compileOnStartup;
}
public List<String> getPreservedClasses() {
return preservedClasses;
}
public Map<String, String> getProperties() {
return properties;
}
public void invalidateCache() { public void invalidateCache() {
servlet.invalidateCache(); servlet.invalidateCache();
} }
@ -116,6 +139,10 @@ public class DevServer {
listeners.add(listener); listeners.add(listener);
} }
public void setLogBuildErrors(boolean logBuildErrors) {
this.logBuildErrors = logBuildErrors;
}
public void start() { public void start() {
server = new Server(); server = new Server();
ServerConnector connector = new ServerConnector(server); ServerConnector connector = new ServerConnector(server);
@ -138,6 +165,11 @@ public class DevServer {
servlet.setDebugPort(debugPort); servlet.setDebugPort(debugPort);
servlet.setProxyUrl(proxyUrl); servlet.setProxyUrl(proxyUrl);
servlet.setProxyPath(proxyPath); servlet.setProxyPath(proxyPath);
servlet.setFileSystemWatched(fileSystemWatched);
servlet.setCompileOnStartup(compileOnStartup);
servlet.setLogBuildErrors(logBuildErrors);
servlet.getProperties().putAll(properties);
servlet.getPreservedClasses().addAll(preservedClasses);
for (DevServerListener listener : listeners) { for (DevServerListener listener : listeners) {
servlet.addListener(listener); servlet.addListener(listener);
} }
@ -147,12 +179,15 @@ public class DevServer {
try { try {
server.start(); server.start();
server.join();
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
public void awaitServer() throws InterruptedException {
server.join();
}
public void stop() { public void stop() {
try { try {
server.stop(); server.stop();

View File

@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
(function (window) { (function () {
var boot = BOOT_FLAG; let boot = BOOT_FLAG;
var reload = RELOAD_FLAG; let reload = RELOAD_FLAG;
var indicatorVisible = INDICATOR_FLAG; let indicatorVisible = INDICATOR_FLAG;
var debugPort = DEBUG_PORT; let debugPort = DEBUG_PORT;
var deobfuscate = DEOBFUSCATE_FLAG; let deobfuscate = DEOBFUSCATE_FLAG;
var fileName = FILE_NAME; let fileName = FILE_NAME;
var pathToFile = PATH_TO_FILE; let pathToFile = PATH_TO_FILE;
function createWebSocket() { function createWebSocket() {
return new WebSocket("ws://WS_PATH"); return new WebSocket("ws://WS_PATH");
@ -28,18 +28,18 @@
function createIndicator() { function createIndicator() {
function createMainElement() { function createMainElement() {
var element = document.createElement("div"); let element = document.createElement("div");
element.style.position = "fixed"; element.style.position = "fixed";
element.style.left = "0"; element.style.left = "0";
element.style.bottom = "0"; element.style.bottom = "0";
element.style.backgroundColor = "black"; element.style.backgroundColor = "black";
element.style.color = "white"; element.style.color = "white";
element.style.opacity = 0.4; element.style.opacity = "0.4";
element.style.padding = "5px"; element.style.padding = "5px";
element.style.fontSize = "18px"; element.style.fontSize = "18px";
element.style.fontWeight = "bold"; element.style.fontWeight = "bold";
element.style.pointerEvents = "none"; element.style.pointerEvents = "none";
element.style.zIndex = 1000; element.style.zIndex = "1000";
element.style.display = "none"; element.style.display = "none";
return element; return element;
} }
@ -49,7 +49,7 @@
} }
function createProgressElements() { function createProgressElements() {
var element = document.createElement("span"); const element = document.createElement("span");
element.style.display = "none"; element.style.display = "none";
element.style.marginLeft = "10px"; element.style.marginLeft = "10px";
element.style.width = "150px"; element.style.width = "150px";
@ -61,7 +61,7 @@
element.style.backgroundColor = "white"; element.style.backgroundColor = "white";
element.style.position = "relative"; element.style.position = "relative";
var progress = document.createElement("span"); const progress = document.createElement("span");
progress.style.display = "block"; progress.style.display = "block";
progress.style.position = "absolute"; progress.style.position = "absolute";
progress.style.left = "0"; progress.style.left = "0";
@ -80,9 +80,9 @@
}; };
} }
var container = createMainElement(); const container = createMainElement();
var label = createLabelElement(); const label = createLabelElement();
var progress = createProgressElements(); const progress = createProgressElements();
container.appendChild(label); container.appendChild(label);
container.appendChild(progress.container); container.appendChild(progress.container);
@ -92,7 +92,7 @@
progress: progress, progress: progress,
timer: void 0, timer: void 0,
show: function(text, timeout) { show(text, timeout) {
this.container.style.display = "block"; this.container.style.display = "block";
this.label.innerText = text; this.label.innerText = text;
if (this.timer) { if (this.timer) {
@ -118,7 +118,7 @@
}; };
} }
var indicator = createIndicator(); let indicator = createIndicator();
function onLoad() { function onLoad() {
document.body.appendChild(indicator.container); document.body.appendChild(indicator.container);
} }
@ -137,15 +137,15 @@
} }
if (typeof main === 'function') { if (typeof main === 'function') {
var oldMain = main; let oldMain = main;
main = function() { main = function() {
var args = arguments; const args = arguments;
window.$teavm_deobfuscator_callback = function() { window.$teavm_deobfuscator_callback = () => {
oldMain.apply(window, args); oldMain.apply(window, args);
}; };
var elem = document.createElement("script"); const elem = document.createElement("script");
elem.src = pathToFile + fileName + ".deobfuscator.js"; elem.src = pathToFile + fileName + ".deobfuscator.js";
elem.onload = function() { elem.onload = () => {
$teavm_deobfuscator([pathToFile + fileName + ".teavmdbg", pathToFile + fileName]); $teavm_deobfuscator([pathToFile + fileName + ".teavmdbg", pathToFile + fileName]);
}; };
document.head.append(elem); document.head.append(elem);
@ -165,9 +165,9 @@
main(); main();
} }
var ws = createWebSocket(); let ws = createWebSocket();
ws.onmessage = function(event) { ws.onmessage = function(event) {
var message = JSON.parse(event.data); const message = JSON.parse(event.data);
switch (message.command) { switch (message.command) {
case "compiling": case "compiling":
indicator.show("Compiling..."); indicator.show("Compiling...");
@ -177,9 +177,9 @@
if (message.success) { if (message.success) {
indicator.show("Compilation complete", 10); indicator.show("Compilation complete", 10);
if (reload) { if (reload) {
window.location.reload(true); window.location.reload();
} else if (boot) { } else if (boot) {
var scriptElem = document.createElement("script"); const scriptElem = document.createElement("script");
scriptElem.src = pathToFile + fileName; scriptElem.src = pathToFile + fileName;
scriptElem.onload = startMain; scriptElem.onload = startMain;
document.head.appendChild(scriptElem); document.head.appendChild(scriptElem);
@ -197,12 +197,12 @@
} }
if (debugPort > 0) { if (debugPort > 0) {
var connected = false; let connected = false;
function connectDebugAgent(event) { function connectDebugAgent(event) {
if (event.source !== window) { if (event.source !== window) {
return; return;
} }
var data = event.data; const data = event.data;
if (typeof data.teavmDebuggerRequest !== "undefined" && !connected) { if (typeof data.teavmDebuggerRequest !== "undefined" && !connected) {
connected = true; connected = true;
window.postMessage({teavmDebugger: {port: debugPort}}, "*"); window.postMessage({teavmDebugger: {port: debugPort}}, "*");
@ -211,4 +211,4 @@
window.addEventListener("message", connectDebugAgent); window.addEventListener("message", connectDebugAgent);
window.postMessage({teavmDebugger: {port: debugPort}}, "*"); window.postMessage({teavmDebugger: {port: debugPort}}, "*");
} }
})(this); })();

View File

@ -74,6 +74,7 @@ val createConfig by tasks.registering {
val jsoImpl = findArtifactCoordinates(":jso:impl") val jsoImpl = findArtifactCoordinates(":jso:impl")
val metaprogrammingImpl = findArtifactCoordinates(":metaprogramming:impl") val metaprogrammingImpl = findArtifactCoordinates(":metaprogramming:impl")
val tools = findArtifactCoordinates(":tools:core") val tools = findArtifactCoordinates(":tools:core")
val cli = findArtifactCoordinates(":tools:cli")
val junit = findArtifactCoordinates(":tools:junit") val junit = findArtifactCoordinates(":tools:junit")
doLast { doLast {
val file = File(baseDir, "org/teavm/gradle/config/ArtifactCoordinates.java") 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 JUNIT = "$junit";
public static final String TOOLS = "$tools"; public static final String TOOLS = "$tools";
public static final String CLI = "$cli";
private ArtifactCoordinates() { private ArtifactCoordinates() {
} }

View File

@ -16,6 +16,7 @@
package org.teavm.gradle; package org.teavm.gradle;
import groovy.lang.Closure; import groovy.lang.Closure;
import javax.inject.Inject;
import org.gradle.api.Action; import org.gradle.api.Action;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.model.ObjectFactory; 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.SourceFilePolicy;
import org.teavm.gradle.api.TeaVMCConfiguration; import org.teavm.gradle.api.TeaVMCConfiguration;
import org.teavm.gradle.api.TeaVMCommonConfiguration; import org.teavm.gradle.api.TeaVMCommonConfiguration;
import org.teavm.gradle.api.TeaVMDevServerConfiguration;
import org.teavm.gradle.api.TeaVMExtension; import org.teavm.gradle.api.TeaVMExtension;
import org.teavm.gradle.api.TeaVMJSConfiguration; import org.teavm.gradle.api.TeaVMJSConfiguration;
import org.teavm.gradle.api.TeaVMWasiConfiguration; import org.teavm.gradle.api.TeaVMWasiConfiguration;
@ -38,7 +40,7 @@ class TeaVMExtensionImpl extends TeaVMBaseExtensionImpl implements TeaVMExtensio
TeaVMExtensionImpl(Project project, ObjectFactory objectFactory) { TeaVMExtensionImpl(Project project, ObjectFactory objectFactory) {
super(project, objectFactory); super(project, objectFactory);
js = objectFactory.newInstance(TeaVMJSConfiguration.class); js = objectFactory.newInstance(JsConfigImpl.class);
wasm = objectFactory.newInstance(TeaVMWasmConfiguration.class); wasm = objectFactory.newInstance(TeaVMWasmConfiguration.class);
wasi = objectFactory.newInstance(TeaVMWasiConfiguration.class); wasi = objectFactory.newInstance(TeaVMWasiConfiguration.class);
c = objectFactory.newInstance(TeaVMCConfiguration.class); c = objectFactory.newInstance(TeaVMCConfiguration.class);
@ -72,6 +74,14 @@ class TeaVMExtensionImpl extends TeaVMBaseExtensionImpl implements TeaVMExtensio
js.getSourceFilePolicy().convention(property("js.sourceFilePolicy") js.getSourceFilePolicy().convention(property("js.sourceFilePolicy")
.map(SourceFilePolicy::valueOf) .map(SourceFilePolicy::valueOf)
.orElse(SourceFilePolicy.DO_NOTHING)); .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() { private void setupWasmDefaults() {
@ -199,4 +209,28 @@ class TeaVMExtensionImpl extends TeaVMBaseExtensionImpl implements TeaVMExtensio
target.getOutOfProcess().convention(source.getOutOfProcess()); target.getOutOfProcess().convention(source.getOutOfProcess());
target.getProcessMemory().convention(source.getProcessMemory()); 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<TeaVMDevServerConfiguration> action) {
action.execute(devServer);
}
@Override
public TeaVMDevServerConfiguration getDevServer() {
return devServer;
}
@Override
public void devServer(Closure<?> action) {
action.rehydrate(getDevServer(), action.getOwner(), action.getThisObject()).call();
}
}
} }

View File

@ -26,6 +26,7 @@ import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier; import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.gradle.api.artifacts.component.ProjectComponentIdentifier; import org.gradle.api.artifacts.component.ProjectComponentIdentifier;
import org.gradle.api.artifacts.result.ResolvedDependencyResult; import org.gradle.api.artifacts.result.ResolvedDependencyResult;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DuplicatesStrategy; import org.gradle.api.file.DuplicatesStrategy;
import org.gradle.api.model.ObjectFactory; import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.JavaPlugin; 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.GenerateJavaScriptTask;
import org.teavm.gradle.tasks.GenerateWasiTask; import org.teavm.gradle.tasks.GenerateWasiTask;
import org.teavm.gradle.tasks.GenerateWasmTask; import org.teavm.gradle.tasks.GenerateWasmTask;
import org.teavm.gradle.tasks.JavaScriptDevServerTask;
import org.teavm.gradle.tasks.StopJavaScriptDevServerTask;
import org.teavm.gradle.tasks.TeaVMTask; import org.teavm.gradle.tasks.TeaVMTask;
public class TeaVMPlugin implements Plugin<Project> { public class TeaVMPlugin implements Plugin<Project> {
public static final String EXTENSION_NAME = "teavm"; public static final String EXTENSION_NAME = "teavm";
public static final String SOURCE_SET_NAME = "teavm"; public static final String SOURCE_SET_NAME = "teavm";
public static final String JS_TASK_NAME = "generateJavaScript"; 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 WASM_TASK_NAME = "generateWasm";
public static final String WASI_TASK_NAME = "generateWasi"; public static final String WASI_TASK_NAME = "generateWasi";
public static final String C_TASK_NAME = "generateC"; public static final String C_TASK_NAME = "generateC";
public static final String CONFIGURATION_NAME = "teavm"; public static final String CONFIGURATION_NAME = "teavm";
public static final String CLASSPATH_CONFIGURATION_NAME = "teavmClasspath"; public static final String CLASSPATH_CONFIGURATION_NAME = "teavmClasspath";
public static final String TASK_GROUP = "TeaVM";
private ObjectFactory objectFactory; private ObjectFactory objectFactory;
@Inject @Inject
@ -105,7 +111,11 @@ public class TeaVMPlugin implements Plugin<Project> {
private void registerTasks(Project project) { private void registerTasks(Project project) {
var compilerConfig = project.getConfigurations().detachedConfiguration( var compilerConfig = project.getConfigurations().detachedConfiguration(
project.getDependencies().create(ArtifactCoordinates.TOOLS)); project.getDependencies().create(ArtifactCoordinates.TOOLS));
var cliConfig = project.getConfigurations().detachedConfiguration(
project.getDependencies().create(ArtifactCoordinates.CLI));
registerJsTask(project, compilerConfig); registerJsTask(project, compilerConfig);
registerJsDevServerTask(project, cliConfig);
registerStopJsDevServerTask(project);
registerWasmTask(project, compilerConfig); registerWasmTask(project, compilerConfig);
registerWasiTask(project, compilerConfig); registerWasiTask(project, compilerConfig);
registerCTask(project, compilerConfig); registerCTask(project, compilerConfig);
@ -125,47 +135,44 @@ public class TeaVMPlugin implements Plugin<Project> {
task.getSourceFilePolicy().convention(js.getSourceFilePolicy()); task.getSourceFilePolicy().convention(js.getSourceFilePolicy());
task.getMaxTopLevelNames().convention(js.getMaxTopLevelNames()); task.getMaxTopLevelNames().convention(js.getMaxTopLevelNames());
task.getSourceFiles().from(project.provider(() -> { setupSources(task.getSourceFiles(), project);
var result = new ArrayList<File>(); });
addSourceDirs(project, result); }
return result;
}));
task.getSourceFiles().from(project.provider(() -> {
var dependencies = project.getConfigurations()
.getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME)
.getIncoming()
.getResolutionResult()
.getAllDependencies();
var result = new ArrayList<File>(); private void registerJsDevServerTask(Project project, Configuration configuration) {
for (var dependencyResult : dependencies) { var extension = project.getExtensions().getByType(TeaVMExtension.class);
if (!(dependencyResult instanceof ResolvedDependencyResult)) { project.getTasks().create(JS_DEV_SERVER_TASK_NAME, JavaScriptDevServerTask.class, task -> {
continue; 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());
} }
var id = ((ResolvedDependencyResult) dependencyResult).getSelected().getId();
if (id instanceof ProjectComponentIdentifier) { setupSources(task.getSourceFiles(), project);
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; private void registerStopJsDevServerTask(Project project) {
var sourcesDep = project.getDependencies().create(Map.of( project.getTasks().create(STOP_JS_DEV_SERVER_TASK_NAME, StopJavaScriptDevServerTask.class, task -> {
"group", moduleId.getGroup(), task.setGroup(TASK_GROUP);
"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;
}));
}); });
} }
@ -270,5 +277,51 @@ public class TeaVMPlugin implements Plugin<Project> {
task.getClasspath().from(sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput()); task.getClasspath().from(sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput());
task.getClasspath().from(sourceSets.getByName(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<File>();
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<File>();
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;
}));
} }
} }

View File

@ -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<Boolean> getStackDeobfuscated();
Property<Boolean> getIndicator();
Property<Boolean> getAutoReload();
Property<Integer> getPort();
Property<String> getProxyUrl();
Property<String> getProxyPath();
Property<Integer> getProcessMemory();
}

View File

@ -15,6 +15,9 @@
*/ */
package org.teavm.gradle.api; package org.teavm.gradle.api;
import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
import org.gradle.api.Action;
import org.gradle.api.provider.Property; import org.gradle.api.provider.Property;
public interface TeaVMJSConfiguration extends TeaVMWebConfiguration { public interface TeaVMJSConfiguration extends TeaVMWebConfiguration {
@ -33,4 +36,10 @@ public interface TeaVMJSConfiguration extends TeaVMWebConfiguration {
Property<SourceFilePolicy> getSourceFilePolicy(); Property<SourceFilePolicy> getSourceFilePolicy();
Property<Integer> getMaxTopLevelNames(); Property<Integer> getMaxTopLevelNames();
TeaVMDevServerConfiguration getDevServer();
void devServer(Action<TeaVMDevServerConfiguration> action);
void devServer(@DelegatesTo(TeaVMDevServerConfiguration.class) Closure<?> action);
} }

View File

@ -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<String, ProjectDevServerManager> 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<String>();
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<String> collector) {
if (!collector.add(project.getPath())) {
return;
}
for (var child : project.getChildProjects().values()) {
collectProjects(child, collector);
}
}
}

View File

@ -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<String> getTargetFileName();
@Input
@Optional
public abstract Property<String> getTargetFilePath();
@Input
@Optional
public abstract MapProperty<String, String> getProperties();
@Input
@Optional
public abstract ListProperty<String> getPreservedClasses();
@Input
public abstract Property<String> getMainClass();
@Input
@Optional
public abstract Property<Boolean> getStackDeobfuscated();
@Input
@Optional
public abstract Property<Boolean> getIndicator();
@Input
@Optional
public abstract Property<Integer> getPort();
@InputFiles
public abstract ConfigurableFileCollection getSourceFiles();
@Input
@Optional
public abstract Property<Boolean> getAutoReload();
@Input
@Optional
public abstract Property<String> getProxyUrl();
@Input
@Optional
public abstract Property<String> getProxyPath();
@Input
@Optional
public abstract Property<Integer> getProcessMemory();
@Classpath
public abstract ConfigurableFileCollection getServerClasspath();
@Internal
public abstract Property<Integer> 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();
}
}

View File

@ -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<File> serverClasspath = new LinkedHashSet<>();
private Set<File> classpath = new LinkedHashSet<>();
private String targetFileName;
private String targetFilePath;
private Map<String, String> properties = new LinkedHashMap<>();
private Set<String> preservedClasses = new LinkedHashSet<>();
private String mainClass;
private boolean stackDeobfuscated;
private boolean indicator;
private int port;
private Set<File> 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<Runnable> eventQueue = new LinkedBlockingQueue<>();
private boolean eventQueueDone;
private Logger logger;
private ProgressLogger progressLogger;
private Set<File> runningServerClasspath = new HashSet<>();
private Set<File> runningClasspath = new HashSet<>();
private String runningTargetFileName;
private String runningTargetFilePath;
private Map<String, String> runningProperties = new HashMap<>();
private Set<String> runningPreservedClasses = new HashSet<>();
private String runningMainClass;
private boolean runningStackDeobfuscated;
private boolean runningIndicator;
private int runningPort;
private Set<File> 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<File> serverClasspath) {
this.serverClasspath.clear();
this.serverClasspath.addAll(serverClasspath);
}
public void setClasspath(Set<File> classpath) {
this.classpath.clear();
this.classpath.addAll(classpath);
}
public void setProperties(Map<String, String> properties) {
this.properties.clear();
this.properties.putAll(properties);
}
public void setPreservedClasses(Collection<String> 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<File> 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<String> getBuilderCommand() {
var command = new ArrayList<String>();
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;
}
}

View File

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

View File

@ -52,7 +52,6 @@ import org.teavm.vm.TeaVMProgressListener;
public abstract class TeaVMTask extends DefaultTask { public abstract class TeaVMTask extends DefaultTask {
public TeaVMTask() { public TeaVMTask() {
setGroup("TeaVM");
getDebugInformation().convention(false); getDebugInformation().convention(false);
getTargetFileName().convention("bundle"); getTargetFileName().convention("bundle");
getOptimization().convention(OptimizationLevel.BALANCED); getOptimization().convention(OptimizationLevel.BALANCED);

View File

@ -22,10 +22,10 @@ plugins {
description = "All-in one JAR file that used by IDE plugins" description = "All-in one JAR file that used by IDE plugins"
dependencies { dependencies {
implementation(project(path = ":tools:core")) api(project(":tools:core"))
implementation(project(path = ":tools:devserver")) api(project(":tools:devserver"))
implementation(project(path = ":classlib")) api(project(":classlib"))
implementation(project(path = ":tools:chrome-rdp")) api(project(":tools:chrome-rdp"))
} }
tasks.shadowJar { tasks.shadowJar {

View File

@ -32,7 +32,8 @@ intellij {
} }
dependencies { 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 { tasks {

View File

@ -153,6 +153,7 @@ public class DevServerRunner extends UnicastRemoteObject implements DevServerMan
DevServerRunner daemon = new DevServerRunner(server); DevServerRunner daemon = new DevServerRunner(server);
System.out.println(PORT_MESSAGE_PREFIX + daemon.port); System.out.println(PORT_MESSAGE_PREFIX + daemon.port);
server.start(); server.start();
server.awaitServer();
try { try {
daemon.registry.unbind(ID); daemon.registry.unbind(ID);
@ -327,7 +328,9 @@ public class DevServerRunner extends UnicastRemoteObject implements DevServerMan
@Override @Override
public void compilationComplete(BuildResult buildResult) { public void compilationComplete(BuildResult buildResult) {
DevServerBuildResult result = new DevServerBuildResult(); DevServerBuildResult result = new DevServerBuildResult();
if (buildResult != null) {
result.problems.addAll(buildResult.getProblems().getProblems()); result.problems.addAll(buildResult.getProblems().getProblems());
}
for (DevServerManagerListener listener : getListeners()) { for (DevServerManagerListener listener : getListeners()) {
try { try {
listener.compilationComplete(result); listener.compilationComplete(result);