All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.teavm.devserver.CodeServlet Maven / Gradle / Ivy

/*
 *  Copyright 2018 Alexey Andreev.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.teavm.devserver;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import javax.servlet.AsyncContext;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.util.InputStreamContentProvider;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.client.io.UpgradeListener;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.teavm.backend.javascript.JSModuleType;
import org.teavm.backend.javascript.JavaScriptTarget;
import org.teavm.cache.InMemoryMethodNodeCache;
import org.teavm.cache.InMemoryProgramCache;
import org.teavm.cache.InMemorySymbolTable;
import org.teavm.cache.MemoryCachedClassReaderSource;
import org.teavm.debugging.information.DebugInformation;
import org.teavm.debugging.information.DebugInformationBuilder;
import org.teavm.dependency.FastDependencyAnalyzer;
import org.teavm.model.ClassHolder;
import org.teavm.model.ClassReader;
import org.teavm.model.ClassReaderSource;
import org.teavm.model.PreOptimizingClassHolderSource;
import org.teavm.model.ReferenceCache;
import org.teavm.parsing.ClasspathResourceMapper;
import org.teavm.parsing.resource.ClasspathResourceReader;
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;
import org.teavm.vm.TeaVM;
import org.teavm.vm.TeaVMBuilder;
import org.teavm.vm.TeaVMOptimizationLevel;
import org.teavm.vm.TeaVMPhase;
import org.teavm.vm.TeaVMProgressFeedback;
import org.teavm.vm.TeaVMProgressListener;

public class CodeServlet extends HttpServlet {
    private static final Supplier EMPTY_CONTENT = () -> null;
    private WebSocketServletFactory wsFactory;

    private String mainClass;
    private String[] classPath;
    private String fileName = "classes.js";
    private String pathToFile = "/";
    private String indicatorWsPath;
    private String deobfuscatorPath;
    private List sourcePath = new ArrayList<>();
    private TeaVMToolLog log = new EmptyTeaVMToolLog();
    private boolean indicator;
    private boolean deobfuscateStack;
    private boolean automaticallyReloaded;
    private int port;
    private int debugPort;
    private String proxyUrl;
    private String proxyPath = "/";
    private String proxyHost;
    private String proxyProtocol;
    private int proxyPort;
    private String proxyBaseUrl;
    private Map properties = new LinkedHashMap<>();
    private List preservedClasses = new ArrayList<>();
    private JSModuleType jsModuleType;

    private Map> sourceFileCache = new HashMap<>();

    private volatile boolean stopped;
    private FileSystemWatcher watcher;
    private MemoryCachedClassReaderSource classSource;
    private InMemoryProgramCache programCache;
    private InMemoryMethodNodeCache astCache;
    private int lastReachedClasses;
    private boolean firstTime = true;

    private final Object contentLock = new Object();
    private final Map content = new HashMap<>();
    private MemoryBuildTarget buildTarget = new MemoryBuildTarget();

    private final Set progressHandlers = new LinkedHashSet<>();
    private final Object statusLock = new Object();
    private volatile boolean cancelRequested;
    private boolean compiling;
    private double progress;
    private boolean waiting;
    private Thread buildThread;
    private List listeners = new ArrayList<>();
    private HttpClient httpClient;
    private WebSocketClient wsClient = new WebSocketClient();
    private InMemorySymbolTable symbolTable = new InMemorySymbolTable();
    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;
        this.classPath = classPath != null ? classPath.clone() : new String[0];

        httpClient = new HttpClient();
        httpClient.setFollowRedirects(false);
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public void setPathToFile(String pathToFile) {
        this.pathToFile = normalizePath(pathToFile);
    }

    public List getSourcePath() {
        return sourcePath;
    }

    public void setLog(TeaVMToolLog log) {
        this.log = log;
    }

    public void setIndicator(boolean indicator) {
        this.indicator = indicator;
    }

    public void setDeobfuscateStack(boolean deobfuscateStack) {
        this.deobfuscateStack = deobfuscateStack;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public void setDebugPort(int debugPort) {
        this.debugPort = debugPort;
    }

    public void setAutomaticallyReloaded(boolean automaticallyReloaded) {
        this.automaticallyReloaded = automaticallyReloaded;
    }

    public void setProxyUrl(String proxyUrl) {
        this.proxyUrl = proxyUrl;
    }

    public void setProxyPath(String proxyPath) {
        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 setJsModuleType(JSModuleType jsModuleType) {
        this.jsModuleType = jsModuleType;
    }

    public void setLogBuildErrors(boolean logBuildErrors) {
        this.logBuildErrors = logBuildErrors;
    }

    public void addProgressHandler(ProgressHandler handler) {
        synchronized (progressHandlers) {
            progressHandlers.add(handler);
        }

        double progress;
        synchronized (statusLock) {
            if (!compiling) {
                return;
            }
            progress = this.progress;
        }

        handler.progress(progress);
    }

    public void removeProgressHandler(ProgressHandler handler) {
        synchronized (progressHandlers) {
            progressHandlers.remove(handler);
        }
    }

    public void addListener(DevServerListener listener) {
        listeners.add(listener);
    }

    public void invalidateCache() {
        synchronized (statusLock) {
            if (compiling) {
                return;
            }
            astCache.invalidate();
            programCache.invalidate();
            classSource.invalidate();
            symbolTable.invalidate();
            fileSymbolTable.invalidate();
        }
    }

    public void buildProject() {
        if (buildThread == null) {
            runCompilerThread();
        } else {
            synchronized (statusLock) {
                if (waiting) {
                    buildThread.interrupt();
                }
            }
        }
    }

    public void cancelBuild() {
        synchronized (statusLock) {
            if (compiling) {
                cancelRequested = true;
            }
        }
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);

        if (proxyUrl != null) {
            try {
                httpClient.start();
                wsClient.start();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

            try {
                URL url = new URL(proxyUrl);
                proxyPort = url.getPort();
                proxyHost = proxyPort >= 0 ? url.getHost() + ":" + proxyPort : url.getHost();
                proxyProtocol = url.getProtocol();

                StringBuilder sb = new StringBuilder();
                sb.append(proxyProtocol).append("://").append(proxyHost);
                proxyBaseUrl = sb.toString();
            } catch (MalformedURLException e) {
                log.warning("Could not extract host from URL: " + proxyUrl, e);
            }
        }

        indicatorWsPath = pathToFile + fileName + ".ws";
        deobfuscatorPath = pathToFile + fileName + ".deobfuscator.js";
        WebSocketPolicy wsPolicy = new WebSocketPolicy(WebSocketBehavior.SERVER);
        wsFactory = WebSocketServletFactory.Loader.load(config.getServletContext(), wsPolicy);
        wsFactory.setCreator((req, resp) -> {
            ProxyWsClient proxyClient = (ProxyWsClient) req.getHttpServletRequest().getAttribute("teavm.ws.client");
            if (proxyClient == null) {
                return new CodeWsEndpoint(this);
            } else {
                ProxyWsClient proxy = new ProxyWsClient();
                proxy.setTarget(proxyClient);
                proxyClient.setTarget(proxy);
                return proxy;
            }
        });
        try {
            wsFactory.start();
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String path = req.getRequestURI();
        if (path != null) {
            log.debug("Serving " + path);
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            if ((req.getMethod().equals("GET") || req.getMethod().equals("OPTIONS"))
                    && path.startsWith(pathToFile) && path.length() > pathToFile.length()) {
                boolean hasBody = req.getMethod().equals("GET");
                String fileName = path.substring(pathToFile.length());
                if (fileName.startsWith("src/")) {
                    if (serveSourceFile(fileName.substring("src/".length()), req, resp, hasBody)) {
                        log.debug("File " + path + " served as source file");
                        return;
                    }
                } else if (path.equals(indicatorWsPath)) {
                    if (wsFactory.isUpgradeRequest(req, resp)) {
                        if (wsFactory.acceptWebSocket(req, resp) || resp.isCommitted()) {
                            return;
                        }
                    }
                } else if (path.equals(deobfuscatorPath)) {
                    serveDeobfuscator(req, resp, hasBody);
                    return;
                } else {
                    byte[] fileContent;
                    boolean firstTime;
                    synchronized (contentLock) {
                        fileContent = content.get(fileName);
                        firstTime = this.firstTime;
                    }
                    if (fileContent != null) {
                        resp.setStatus(hasBody ? HttpServletResponse.SC_OK : HttpServletResponse.SC_NO_CONTENT);
                        resp.setCharacterEncoding("UTF-8");
                        allowOrigin(req, resp);
                        if (!hasBody) {
                            resp.setHeader("Access-Control-Allow-Methods", "GET");
                        } else {
                            resp.setContentType(chooseContentType(fileName));
                            noCache(resp);
                            resp.getOutputStream().write(fileContent);
                        }
                        resp.getOutputStream().flush();
                        log.debug("File " + path + " served as generated file");
                        return;
                    } else if (fileName.equals(this.fileName) && indicator && firstTime) {
                        serveBootFile(req, resp, hasBody);
                        return;
                    }
                }
            }

            if (proxyUrl != null && path.startsWith(proxyPath)) {
                if (wsFactory.isUpgradeRequest(req, resp)) {
                    proxyWebSocket(req, resp, path);
                } else {
                    proxy(req, resp, path);
                }
                return;
            }
        }

        log.debug("File " + path + " not found");
        resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
    }

    private String chooseContentType(String name) {
        if (name.endsWith(".js")) {
            return "application/javascript";
        } else if (name.endsWith(".js.map")) {
            return "application/json";
        } else if (name.endsWith(".teavmdbg")) {
            return "application/octet-stream";
        } else {
            return "text/plain";
        }
    }

    private void serveDeobfuscator(HttpServletRequest req, HttpServletResponse resp, boolean hasBody)
            throws IOException {
        ClassLoader loader = CodeServlet.class.getClassLoader();
        resp.setStatus(hasBody ? HttpServletResponse.SC_OK : HttpServletResponse.SC_NO_CONTENT);
        allowOrigin(req, resp);
        if (!hasBody) {
            resp.setHeader("Access-Control-Allow-Methods", "GET");
        } else {
            resp.setCharacterEncoding("UTF-8");
            resp.setContentType("application/javascript");
            noCache(resp);
            try (InputStream input = loader.getResourceAsStream("teavm/devserver/deobfuscator.js")) {
                IOUtils.copy(input, resp.getOutputStream());
            }
        }
        resp.getOutputStream().flush();
    }

    private void proxy(HttpServletRequest req, HttpServletResponse resp, String path) throws IOException {
        AsyncContext async = req.startAsync();

        String relPath = path.substring(proxyPath.length());
        StringBuilder sb = new StringBuilder(proxyUrl);
        if (!relPath.isEmpty() && !proxyUrl.endsWith("/")) {
            sb.append("/");
        }
        sb.append(relPath);

        if (req.getQueryString() != null) {
            sb.append("?").append(req.getQueryString());
        }
        log.debug("Trying to serve '" + relPath + "' from '" + sb + "'");

        Request proxyReq = httpClient.newRequest(sb.toString());
        proxyReq.method(req.getMethod());
        copyRequestHeaders(req, proxyReq::header);

        proxyReq.content(new InputStreamContentProvider(req.getInputStream()));
        HeaderSender headerSender = new HeaderSender(resp);

        proxyReq.onResponseContent((response, responseContent) -> {
            headerSender.send(response);
            try {
                WritableByteChannel channel = Channels.newChannel(resp.getOutputStream());
                while (responseContent.remaining() > 0) {
                    channel.write(responseContent);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
        proxyReq.send(result -> {
            headerSender.send(result.getResponse());
            async.complete();
        });
    }

    class HeaderSender {
        final HttpServletResponse resp;
        boolean sent;

        HeaderSender(HttpServletResponse resp) {
            this.resp = resp;
        }

        void send(Response response) {
            if (sent) {
                return;
            }

            sent = true;
            resp.setStatus(response.getStatus());

            int length = -1;
            boolean isGzip = false;
            for (HttpField field : response.getHeaders()) {
                String name = field.getName().toLowerCase();
                if (name.equals("location")) {
                    String value = field.getValue();
                    if (value.startsWith(proxyUrl)) {
                        String relLocation = value.substring(proxyUrl.length());
                        resp.addHeader(field.getName(), "http://localhost:" + port + proxyPath + relLocation);
                        continue;
                    }
                }
                if (name.equals("content-encoding")) {
                    isGzip = true;
                    continue;
                } else if (name.equals("content-length")) {
                    try {
                        length = Integer.parseInt(field.getValue());
                    } catch (NumberFormatException e) {
                        // do nothing
                    }
                    continue;
                }
                resp.addHeader(field.getName(), field.getValue());
            }

            if (length > 0 && !isGzip) {
                resp.addHeader("Content-Length", String.valueOf(length));
            }
        }
    }

    private void proxyWebSocket(HttpServletRequest req, HttpServletResponse resp, String path) throws IOException {
        AsyncContext async = req.startAsync();

        String relPath = path.substring(proxyPath.length());
        StringBuilder sb = new StringBuilder(proxyProtocol.equals("http") ? "ws" : "wss").append("://");
        sb.append(proxyHost);
        if (!relPath.isEmpty()) {
            sb.append("/");
        }
        sb.append(relPath);
        if (req.getQueryString() != null) {
            sb.append("?").append(req.getQueryString());
        }
        URI uri;
        try {
            uri = new URI(sb.toString());
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        ProxyWsClient client = new ProxyWsClient();
        req.setAttribute("teavm.ws.client", client);
        ClientUpgradeRequest proxyReq = new ClientUpgradeRequest();
        proxyReq.setMethod(req.getMethod());
        Map> headers = new LinkedHashMap<>();
        copyRequestHeaders(req, (key, value) -> headers.computeIfAbsent(key, k -> new ArrayList<>()).add(value));
        proxyReq.setHeaders(headers);

        wsClient.connect(client, uri, proxyReq, new UpgradeListener() {
            @Override
            public void onHandshakeRequest(UpgradeRequest request) {
            }

            @Override
            public void onHandshakeResponse(UpgradeResponse response) {
                resp.setStatus(response.getStatusCode());
                for (String header : response.getHeaderNames()) {
                    switch (header.toLowerCase()) {
                        case "connection":
                        case "date":
                        case "sec-websocket-accept":
                        case "upgrade":
                            continue;
                    }
                    for (String value : response.getHeaders(header)) {
                        resp.addHeader(header, value);
                    }
                }

                try {
                    wsFactory.acceptWebSocket(req, resp);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
                async.complete();
            }
        });
    }

    private void copyRequestHeaders(HttpServletRequest req, HeaderConsumer proxyReq) {
        Enumeration headers = req.getHeaderNames();
        while (headers.hasMoreElements()) {
            String header = headers.nextElement();
            String headerLower = header.toLowerCase();
            switch (headerLower) {
                case "host":
                    if (proxyHost != null) {
                        proxyReq.header(header, proxyHost);
                        continue;
                    }
                    break;
                case "origin":
                    if (proxyBaseUrl != null) {
                        String origin = req.getHeader(header);
                        if (origin.equals("http://localhost:" + port)) {
                            proxyReq.header(header, proxyBaseUrl);
                            continue;
                        }
                    }
                    break;
                case "referer": {
                    String referer = req.getHeader(header);
                    String localUrl = "http://localhost:" + port + "/";
                    if (referer.startsWith(localUrl)) {
                        String relReferer = referer.substring(localUrl.length());
                        proxyReq.header(header, proxyUrl + relReferer);
                        continue;
                    }
                    break;
                }
                case "connection":
                case "upgrade":
                case "user-agent":
                case "sec-websocket-key":
                case "sec-websocket-version":
                case "sec-websocket-extensions":
                case "accept-encoding":
                    continue;
            }
            Enumeration values = req.getHeaders(header);
            while (values.hasMoreElements()) {
                proxyReq.header(header, values.nextElement());
            }
        }
    }

    @Override
    public void destroy() {
        super.destroy();
        try {
            wsFactory.stop();
        } catch (Exception e) {
            log.warning("Error stopping WebSocket server", e);
        }
        if (proxyUrl != null) {
            try {
                httpClient.stop();
            } catch (Exception e) {
                log.warning("Error stopping HTTP client", e);
            }
            try {
                wsClient.stop();
            } catch (Exception e) {
                log.warning("Error stopping WebSocket client", e);
            }
        }
        stopped = true;
        synchronized (statusLock) {
            if (buildThread != null && waiting) {
                buildThread.interrupt();
            }
        }
    }

    @Override
    public void init() throws ServletException {
        super.init();
        if (compileOnStartup) {
            runCompilerThread();
        }
    }

    private void runCompilerThread() {
        var thread = new Thread(this::runTeaVM);
        thread.setName("TeaVM compiler");
        thread.start();
        buildThread = thread;
    }

    private boolean serveSourceFile(String fileName, HttpServletRequest req, HttpServletResponse resp,
            boolean hasBody) throws IOException {
        try (InputStream stream = sourceFileCache.computeIfAbsent(fileName, this::findSourceFile).get()) {
            if (stream == null) {
                return false;
            }

            resp.setStatus(hasBody ? HttpServletResponse.SC_OK : HttpServletResponse.SC_NO_CONTENT);
            resp.setCharacterEncoding("UTF-8");
            allowOrigin(req, resp);
            if (!hasBody) {
                resp.setHeader("Access-Control-Allow-Methods", "GET");
            } else {
                resp.setContentType("text/plain");
                noCache(resp);
                IOUtils.copy(stream, resp.getOutputStream());
            }
            resp.getOutputStream().flush();
            return true;
        }
    }

    private Supplier findSourceFile(String fileName) {
        for (String element : sourcePath) {
            File sourceFile = new File(element);
            if (sourceFile.isFile()) {
                Supplier result = findSourceFileInZip(sourceFile, fileName);
                if (result != null) {
                    return result;
                }
            } else if (sourceFile.isDirectory()) {
                File result = new File(sourceFile, fileName);
                if (result.exists()) {
                    return () -> {
                        try {
                            return new FileInputStream(result);
                        } catch (FileNotFoundException e) {
                            return null;
                        }
                    };
                }
            }
        }

        return EMPTY_CONTENT;
    }

    private Supplier findSourceFileInZip(File zipFile, String fileName) {
        try (ZipFile zip = new ZipFile(zipFile)) {
            ZipEntry entry = zip.getEntry(fileName);
            if (entry == null) {
                return null;
            }
            return () -> {
                try {
                    ZipInputStream input = new ZipInputStream(new FileInputStream(zipFile));
                    while (true) {
                        ZipEntry e = input.getNextEntry();
                        if (e == null) {
                            return null;
                        }
                        if (e.getName().equals(fileName)) {
                            return input;
                        }
                    }
                } catch (IOException e) {
                    return null;
                }
            };
        } catch (IOException e) {
            return null;
        }
    }

    private void serveBootFile(HttpServletRequest req, HttpServletResponse resp, boolean hasBody) throws IOException {
        resp.setStatus(hasBody ? HttpServletResponse.SC_OK : HttpServletResponse.SC_NO_CONTENT);
        resp.setCharacterEncoding("UTF-8");
        allowOrigin(req, resp);
        if (!hasBody) {
            resp.setHeader("Access-Control-Allow-Methods", "GET");
        } else {
            resp.setContentType("text/plain");
            noCache(resp);
            resp.getWriter().write("function main() { }\n");
            resp.getWriter().write(getIndicatorScript(true));
        }
        resp.getWriter().flush();
        log.debug("Served boot file");
    }

    private void runTeaVM() {
        try {
            initBuilder();

            var hasJob = true;
            while (!stopped) {
                if (hasJob) {
                    buildOnce();
                } else {
                    emptyBuild();
                }

                if (stopped) {
                    break;
                }

                try {
                    synchronized (statusLock) {
                        waiting = true;
                    }
                    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;
                    }
                } catch (InterruptedException e) {
                    if (stopped) {
                        break;
                    }
                    log.info("Build triggered by user");
                }

                List staleClasses = getChangedClasses(watcher.grabChangedFiles());
                if (staleClasses.size() > 15) {
                    List displayedStaleClasses = staleClasses.subList(0, 10);
                    log.debug("Following classes changed (" + staleClasses.size() + "): "
                            + String.join(", ", displayedStaleClasses) + " and more...");
                } else {
                    log.debug("Following classes changed (" + staleClasses.size() + "): "
                            + String.join(", ", staleClasses));
                }

                classSource.evict(staleClasses);
                hasJob = !staleClasses.isEmpty();
            }
            log.info("Build process stopped");
        } catch (Throwable e) {
            log.error("Compile server crashed", e);
        } finally {
            shutdownBuilder();
        }
    }

    private void initBuilder() throws IOException {
        watcher = new FileSystemWatcher(classPath);

        classSource = createCachedSource();
        astCache = new InMemoryMethodNodeCache(referenceCache, symbolTable, fileSymbolTable, variableSymbolTable);
        programCache = new InMemoryProgramCache(referenceCache, symbolTable, fileSymbolTable, variableSymbolTable);
    }

    private MemoryCachedClassReaderSource createCachedSource() {
        return new MemoryCachedClassReaderSource(referenceCache, symbolTable, fileSymbolTable,
                variableSymbolTable);
    }

    private void shutdownBuilder() {
        try {
            watcher.dispose();
        } catch (IOException e) {
            log.debug("Exception caught", e);
        }
        classSource = null;
        watcher = null;
        astCache = null;
        programCache = null;
        synchronized (content) {
            content.clear();
        }
        buildTarget.clear();

        log.info("Build thread complete");
    }

    private void buildOnce() {
        fireBuildStarted();
        reportProgress(0);

        DebugInformationBuilder debugInformationBuilder = new DebugInformationBuilder(referenceCache);
        ClassLoader classLoader = initClassLoader();
        ClasspathResourceReader reader = new ClasspathResourceReader(classLoader);
        ResourceClassHolderMapper rawMapper = new ResourceClassHolderMapper(reader, referenceCache);
        Function classPathMapper = new ClasspathResourceMapper(classLoader, referenceCache,
                rawMapper);
        classSource.setProvider(name -> PreOptimizingClassHolderSource.optimize(classPathMapper, name));

        long startTime = System.currentTimeMillis();
        JavaScriptTarget jsTarget = new JavaScriptTarget();

        TeaVM vm = new TeaVMBuilder(jsTarget)
                .setReferenceCache(referenceCache)
                .setClassLoader(classLoader)
                .setClassSource(classSource)
                .setDependencyAnalyzerFactory(FastDependencyAnalyzer::new)
                .setClassSourcePacker(this::packClasses)
                .setStrict(true)
                .setObfuscated(false)
                .build();

        jsTarget.setStackTraceIncluded(true);
        jsTarget.setObfuscated(false);
        jsTarget.setAstCache(astCache);
        jsTarget.setDebugEmitter(debugInformationBuilder);
        if (jsModuleType != null) {
            jsTarget.setModuleType(jsModuleType);
        }
        jsTarget.setStrict(true);
        vm.setOptimizationLevel(TeaVMOptimizationLevel.SIMPLE);
        vm.setCacheStatus(classSource);
        vm.addVirtualMethods(m -> true);
        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);

        log.info("Starting build");
        progressListener.last = 0;
        progressListener.lastTime = System.currentTimeMillis();
        vm.build(buildTarget, fileName);
        addIndicator();
        generateDebug(debugInformationBuilder);

        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);
        for (String className : classNames) {
            packedSource.populate(className);
        }
        packedSource.setProvider(null);
        return packedSource;
    }

    private void addIndicator() {
        String script = getIndicatorScript(false);
        try (Writer writer = new OutputStreamWriter(buildTarget.appendToResource(fileName), StandardCharsets.UTF_8)) {
            writer.append("\n");
            writer.append(script);
        } catch (IOException e) {
            throw new RuntimeException("IO error occurred writing debug information", e);
        }
    }

    private String getIndicatorScript(boolean boot) {
        try (Reader reader = new InputStreamReader(CodeServlet.class.getResourceAsStream("indicator.js"),
                StandardCharsets.UTF_8)) {
            String script = IOUtils.toString(reader);
            script = script.substring(script.indexOf("*/") + 2);
            script = script.replace("WS_PATH", "localhost:" + port + pathToFile + fileName + ".ws");
            script = script.replace("BOOT_FLAG", Boolean.toString(boot));
            script = script.replace("RELOAD_FLAG", Boolean.toString(automaticallyReloaded));
            script = script.replace("INDICATOR_FLAG", Boolean.toString(indicator));
            script = script.replace("DEBUG_PORT", Integer.toString(debugPort));
            script = script.replace("FILE_NAME", "\"" + fileName + "\"");
            script = script.replace("PATH_TO_FILE", "\"http://localhost:" + port + pathToFile + "\"");
            script = script.replace("DEOBFUSCATE_FLAG", String.valueOf(deobfuscateStack));
            return script;
        } catch (IOException e) {
            throw new RuntimeException("IO error occurred writing debug information", e);
        }
    }

    private void generateDebug(DebugInformationBuilder debugInformationBuilder) {
        try {
            DebugInformation debugInformation = debugInformationBuilder.getDebugInformation();
            String sourceMapName = fileName + ".map";

            try (Writer writer = new OutputStreamWriter(buildTarget.appendToResource(fileName),
                    StandardCharsets.UTF_8)) {
                writer.append("\n//# sourceMappingURL=" + sourceMapName);
            }

            try (Writer writer = new OutputStreamWriter(buildTarget.createResource(sourceMapName),
                    StandardCharsets.UTF_8)) {
                debugInformation.writeAsSourceMaps(writer, "src", fileName);
            }
            debugInformation.write(buildTarget.createResource(fileName + ".teavmdbg"));
        } catch (IOException e) {
            throw new RuntimeException("IO error occurred writing debug information", e);
        }
    }

    private void postBuild(TeaVM vm, long startTime) {
        if (!vm.wasCancelled()) {
            log.info("Recompiled stale methods: " + programCache.getPendingItemsCount());
            if (vm.getProblemProvider().getSevereProblems().isEmpty()) {
                log.info("Build complete successfully");
                saveNewResult();
                lastReachedClasses = vm.getDependencyInfo().getReachableClasses().size();
                classSource.commit();
                programCache.commit();
                astCache.commit();
                reportCompilationComplete(true);
            } else {
                log.info("Build complete with errors");
                reportCompilationComplete(false);
            }
            printStats(vm, startTime);
            if (logBuildErrors) {
                TeaVMProblemRenderer.describeProblems(vm, log);
            }
            fireBuildComplete(vm);
        } else {
            log.info("Build cancelled");
            fireBuildCancelled();
        }

        astCache.discard();
        programCache.discard();
        buildTarget.clear();
        cancelRequested = false;
    }

    private void printStats(TeaVM vm, long startTime) {
        if (vm.getWrittenClasses() != null) {
            int classCount = vm.getWrittenClasses().getClassNames().size();
            int methodCount = 0;
            for (String className : vm.getWrittenClasses().getClassNames()) {
                ClassReader cls = vm.getWrittenClasses().get(className);
                methodCount += cls.getMethods().size();
            }

            log.info("Classes compiled: " + classCount);
            log.info("Methods compiled: " + methodCount);
        }

        log.info("Compilation took " + (System.currentTimeMillis() - startTime) + " ms");
    }

    private void saveNewResult() {
        synchronized (contentLock) {
            firstTime = false;
            content.clear();
            for (String name : buildTarget.getNames()) {
                content.put(name, buildTarget.getContent(name));
            }
        }
    }

    private List getChangedClasses(Collection changedFiles) {
        List result = new ArrayList<>();
        String[] prefixes = Arrays.stream(classPath).map(s -> s.replace('\\', '/')).toArray(String[]::new);

        for (File file : changedFiles) {
            String path = file.getPath().replace('\\', '/');
            if (!path.endsWith(".class")) {
                continue;
            }

            String prefix = Arrays.stream(prefixes)
                    .filter(path::startsWith)
                    .findFirst()
                    .orElse("");
            int start = prefix.length();
            if (start < path.length() && path.charAt(start) == '/') {
                ++start;
            }

            path = path.substring(start, path.length() - ".class".length()).replace('/', '.');
            result.add(path);
        }

        return result;
    }

    private ClassLoader initClassLoader() {
        URL[] urls = new URL[classPath.length];
        try {
            for (int i = 0; i < classPath.length; i++) {
                urls[i] = new File(classPath[i]).toURI().toURL();
            }
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
        return new URLClassLoader(urls, CodeServlet.class.getClassLoader());
    }

    private void reportProgress(double progress) {
        synchronized (statusLock) {
            if (compiling && this.progress == progress) {
                return;
            }
            compiling = true;
            this.progress = progress;
        }

        ProgressHandler[] handlers;
        synchronized (progressHandlers) {
            handlers = progressHandlers.toArray(new ProgressHandler[0]);
        }

        for (ProgressHandler handler : handlers) {
            handler.progress(progress);
        }

        for (DevServerListener listener : listeners) {
            listener.compilationProgress(progress);
        }
    }

    private void reportCompilationComplete(boolean success) {
        synchronized (statusLock) {
            if (!compiling) {
                return;
            }
            compiling = false;
        }

        ProgressHandler[] handlers;
        synchronized (progressHandlers) {
            handlers = progressHandlers.toArray(new ProgressHandler[0]);
        }

        for (ProgressHandler handler : handlers) {
            handler.complete(success);
        }
    }

    private void fireBuildStarted() {
        for (DevServerListener listener : listeners) {
            listener.compilationStarted();
        }
    }

    private void fireBuildCancelled() {
        for (DevServerListener listener : listeners) {
            listener.compilationCancelled();
        }
    }

    private void fireBuildComplete(TeaVM vm) {
        fireBuildCompleteWithResult(new SimpleBuildResult(vm));
    }

    private void fireBuildCompleteWithResult(BuildResult buildResult) {
        for (var listener : listeners) {
            listener.compilationComplete(buildResult);
        }
    }

    private final ProgressListenerImpl progressListener = new ProgressListenerImpl();

    class ProgressListenerImpl implements TeaVMProgressListener {
        private int start;
        private int end;
        private int phaseLimit;
        private int last;
        private long lastTime;

        @Override
        public TeaVMProgressFeedback phaseStarted(TeaVMPhase phase, int count) {
            switch (phase) {
                case DEPENDENCY_ANALYSIS:
                    start = 0;
                    end = 500;
                    break;
                case COMPILING:
                    start = 500;
                    end = 1000;
                    break;
            }
            phaseLimit = Math.max(1, count);
            return progressReached(0);
        }

        @Override
        public TeaVMProgressFeedback progressReached(int progress) {
            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) {
                        lastTime = System.currentTimeMillis();
                        last = current;
                        reportProgress(current / 10.0);
                    }
                }
            }
            return getResult();
        }

        private TeaVMProgressFeedback getResult() {
            if (cancelRequested) {
                log.info("Trying to cancel compilation due to user request");
                return TeaVMProgressFeedback.CANCEL;
            }

            if (stopped) {
                log.info("Trying to cancel compilation due to server stopping");
                return TeaVMProgressFeedback.CANCEL;
            }

            try {
                if (watcher.hasChanges()) {
                    log.info("Changes detected, cancelling build");
                    return TeaVMProgressFeedback.CANCEL;
                }
            } catch (IOException e) {
                log.info("IO error occurred", e);
                return TeaVMProgressFeedback.CANCEL;
            }

            return TeaVMProgressFeedback.CONTINUE;
        }
    }

    static String normalizePath(String path) {
        if (!path.endsWith("/")) {
            path += "/";
        }
        if (!path.startsWith("/")) {
            path = "/" + path;
        }
        return path;
    }

    static void noCache(HttpServletResponse response) {
        response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
    }

    static void allowOrigin(HttpServletRequest req, HttpServletResponse resp) {
        String origin = req.getHeader("Origin");
        if (origin != null) {
            resp.setHeader("Access-Control-Allow-Origin", origin);
            resp.setHeader("Vary", "Origin");
        } else {
            resp.setHeader("Access-Control-Allow-Origin", "*");
        }
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Private-Network", "true");
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy