Initial import of other dev-mode handlers

This commit is contained in:
Robinson 2021-08-31 21:54:30 -06:00
parent e94239a658
commit ed263d34b1
No known key found for this signature in database
GPG Key ID: 8E7DB78588BD6F5C
6 changed files with 1121 additions and 36 deletions

View File

@ -4,6 +4,7 @@ import com.vaadin.flow.server.Constants
import com.vaadin.flow.server.InitParameters
import com.vaadin.flow.server.VaadinServlet
import com.vaadin.flow.server.frontend.FrontendUtils
import dorkbox.vaadin.devMode.DevModeInitializer
import dorkbox.vaadin.undertow.*
import dorkbox.vaadin.util.CallingClass
import dorkbox.vaadin.util.ahoCorasick.DoubleArrayTrie
@ -502,7 +503,7 @@ class VaadinApplication() {
// instead of the default, we load **OUR** dev-mode initializer.
// The vaadin one is super buggy for custom environments
servletBuilder.addServletContainerInitializer(
ServletContainerInitializerInfo(dorkbox.vaadin.DevModeInitializer::class.java, classSet))
ServletContainerInitializerInfo(DevModeInitializer::class.java, classSet))
}
} else {
// do not load the dev-mode initializer for production mode

View File

@ -1,9 +1,18 @@
package dorkbox.vaadin
package dorkbox.vaadin.devMode
import com.vaadin.flow.server.frontend.scanner.ClassFinder
import java.util.*
import javax.servlet.annotation.HandlesTypes
/**
* THIS IS COPIED DIRECTLY FROM VAADIN 14.6.8 (flow 2.4.6)
*
* CHANGES FROM DEFAULT ARE MANAGED AS DIFFERENT REVISIONS.
*
* The initial commit is exactly as-is from vaadin.
*
* This file is NOT extensible/configurable AT-ALL, so this is required...
*/
internal class DevModeClassFinder(classes: Set<Class<*>?>?) : ClassFinder.DefaultClassFinder(classes) {
companion object {
private val APPLICABLE_CLASS_NAMES = Collections.unmodifiableSet(calculateApplicableClassNames())

View File

@ -0,0 +1,862 @@
/*
* Copyright 2000-2020 Vaadin Ltd.
*
* 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 dorkbox.vaadin.devMode;
import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_DEVMODE_WEBPACK_ERROR_PATTERN;
import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_DEVMODE_WEBPACK_OPTIONS;
import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_DEVMODE_WEBPACK_SUCCESS_PATTERN;
import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_DEVMODE_WEBPACK_TIMEOUT;
import static com.vaadin.flow.server.frontend.FrontendUtils.GREEN;
import static com.vaadin.flow.server.frontend.FrontendUtils.RED;
import static com.vaadin.flow.server.frontend.FrontendUtils.YELLOW;
import static com.vaadin.flow.server.frontend.FrontendUtils.commandToString;
import static com.vaadin.flow.server.frontend.FrontendUtils.console;
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
import static java.net.HttpURLConnection.HTTP_OK;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.net.HttpURLConnection;
import java.net.ServerSocket;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vaadin.flow.function.DeploymentConfiguration;
import com.vaadin.flow.internal.BrowserLiveReload;
import com.vaadin.flow.internal.Pair;
import com.vaadin.flow.server.ExecutionFailedException;
import com.vaadin.flow.server.InitParameters;
import com.vaadin.flow.server.RequestHandler;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinResponse;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.communication.StreamRequestHandler;
import com.vaadin.flow.server.frontend.FrontendTools;
import com.vaadin.flow.server.frontend.FrontendUtils;
/**
* THIS IS COPIED DIRECTLY FROM VAADIN 14.6.8 (flow 2.4.6)
*
* CHANGES FROM DEFAULT ARE MANAGED AS DIFFERENT REVISIONS.
*
* The initial commit is exactly as-is from vaadin.
*
* This file is NOT extensible/configurable AT-ALL, so this is required...
*
*
*
* Handles getting resources from <code>webpack-dev-server</code>.
* <p>
* This class is meant to be used during developing time. For a production mode
* site <code>webpack</code> generates the static bundles that will be served
* directly from the servlet (using a default servlet if such exists) or through
* a stand alone static file server.
*
* By default it keeps updated npm dependencies and node imports before running
* webpack server
*
* @since 2.0
*/
public final class DevModeHandler implements RequestHandler {
private static final String START_FAILURE = "Couldn't start dev server because";
private static final AtomicReference<DevModeHandler> atomicHandler = new AtomicReference<>();
// It's not possible to know whether webpack is ready unless reading output
// messages. When webpack finishes, it writes either a `Compiled` or a
// `Failed` in the last line
private static final String DEFAULT_OUTPUT_PATTERN = ": Compiled.";
private static final String DEFAULT_ERROR_PATTERN = ": Failed to compile.";
private static final String FAILED_MSG = "\n------------------ Frontend compilation failed. -----------------";
private static final String SUCCEED_MSG = "\n----------------- Frontend compiled successfully. -----------------";
private static final String START = "\n------------------ Starting Frontend compilation. ------------------\n";
private static final String END = "\n------------------------- Webpack stopped -------------------------\n";
private static final String LOG_START = "Running webpack to compile frontend resources. This may take a moment, please stand by...";
private static final String LOG_END = "Started webpack-dev-server. Time: {}ms";
// If after this time in millisecs, the pattern was not found, we unlock the
// process and continue. It might happen if webpack changes their output
// without advise.
private static final String DEFAULT_TIMEOUT_FOR_PATTERN = "60000";
private static final int DEFAULT_BUFFER_SIZE = 32 * 1024;
private static final int DEFAULT_TIMEOUT = 120 * 1000;
private static final String WEBPACK_HOST = "http://localhost";
private boolean notified = false;
private volatile String failedOutput;
private AtomicBoolean isDevServerFailedToStart = new AtomicBoolean();
private transient BrowserLiveReload liveReload;
/**
* The local installation path of the webpack-dev-server node script.
*/
public static final String WEBPACK_SERVER = "node_modules/webpack-dev-server/bin/webpack-dev-server.js";
private volatile int port;
private final AtomicReference<Process> webpackProcess = new AtomicReference<>();
private final boolean reuseDevServer;
private final AtomicReference<DevServerWatchDog> watchDog = new AtomicReference<>();
private final CompletableFuture<Void> devServerStartFuture;
private final File npmFolder;
private DevModeHandler(DeploymentConfiguration config, int runningPort,
File npmFolder, CompletableFuture<Void> waitFor) {
this.npmFolder = Objects.requireNonNull(npmFolder);
port = runningPort;
reuseDevServer = config.reuseDevServer();
// Check whether executor is provided by the caller (framework)
Object service = config.getInitParameters().get(Executor.class);
BiConsumer<Void, ? super Throwable> action = (value, exception) -> {
// this will throw an exception if an exception has been thrown by
// the waitFor task
waitFor.getNow(null);
runOnFutureComplete(config);
};
if (service instanceof Executor) {
// if there is an executor use it to run the task
devServerStartFuture = waitFor.whenCompleteAsync(action,
(Executor) service);
} else {
devServerStartFuture = waitFor.whenCompleteAsync(action);
}
}
/**
* Start the dev mode handler if none has been started yet.
*
* @param configuration
* deployment configuration
* @param npmFolder
* folder with npm configuration files
* @param waitFor
* a completable future whose execution result needs to be
* available to start the webpack dev server
*
* @return the instance in case everything is alright, null otherwise
*/
public static DevModeHandler start(DeploymentConfiguration configuration,
File npmFolder, CompletableFuture<Void> waitFor) {
return start(0, configuration, npmFolder, waitFor);
}
/**
* Start the dev mode handler if none has been started yet.
*
* @param runningPort
* port on which Webpack is listening.
* @param configuration
* deployment configuration
* @param npmFolder
* folder with npm configuration files
* @param waitFor
* a completable future whose execution result needs to be
* available to start the webpack dev server
*
* @return the instance in case everything is alright, null otherwise
*/
public static DevModeHandler start(int runningPort,
DeploymentConfiguration configuration, File npmFolder,
CompletableFuture<Void> waitFor) {
if (configuration.isProductionMode()
|| configuration.isCompatibilityMode()
|| !configuration.enableDevServer()) {
return null;
}
DevModeHandler handler = atomicHandler.get();
if (handler == null) {
handler = createInstance(runningPort, configuration, npmFolder,
waitFor);
atomicHandler.compareAndSet(null, handler);
}
return getDevModeHandler();
}
/**
* Get the instantiated DevModeHandler.
*
* @return devModeHandler or {@code null} if not started
*/
public static DevModeHandler getDevModeHandler() {
return atomicHandler.get();
}
@Override
public boolean handleRequest(VaadinSession session, VaadinRequest request,
VaadinResponse response) throws IOException {
if (devServerStartFuture.isDone()) {
try {
devServerStartFuture.getNow(null);
} catch (CompletionException exception) {
isDevServerFailedToStart.set(true);
throw getCause(exception);
}
return false;
} else {
InputStream inputStream = DevModeHandler.class
.getResourceAsStream("dev-mode-not-ready.html");
IOUtils.copy(inputStream, response.getOutputStream());
response.setContentType("text/html;charset=utf-8");
return true;
}
}
private RuntimeException getCause(Throwable exception) {
if (exception instanceof CompletionException) {
return getCause(exception.getCause());
} else if (exception instanceof RuntimeException) {
return (RuntimeException) exception;
} else {
return new IllegalStateException(exception);
}
}
/**
* Set the live reload service instance.
*
* @param liveReload
* the live reload instance
*/
public void setLiveReload(BrowserLiveReload liveReload) {
this.liveReload = liveReload;
}
/**
* Get the live reload service instance.
*
* @return the live reload instance
*/
public BrowserLiveReload getLiveReload() {
return liveReload;
}
private static DevModeHandler createInstance(int runningPort,
DeploymentConfiguration configuration, File npmFolder,
CompletableFuture<Void> waitFor) {
return new DevModeHandler(configuration, runningPort, npmFolder,
waitFor);
}
/**
* Returns true if it's a request that should be handled by webpack.
*
* @param request
* the servlet request
* @return true if the request should be forwarded to webpack
*/
public boolean isDevModeRequest(HttpServletRequest request) {
final String pathInfo = request.getPathInfo();
return pathInfo != null && pathInfo.matches(".+\\.js") && !pathInfo
.startsWith("/" + StreamRequestHandler.DYN_RES_PREFIX);
}
/**
* Serve a file by proxying to webpack.
* <p>
* Note: it considers the {@link HttpServletRequest#getPathInfo} that will
* be the path passed to the 'webpack-dev-server' which is running in the
* context root folder of the application.
* <p>
* Method returns {@code false} immediately if dev server failed on its
* startup.
*
* @param request
* the servlet request
* @param response
* the servlet response
* @return false if webpack returned a not found, true otherwise
* @throws IOException
* in the case something went wrong like connection refused
*/
public boolean serveDevModeRequest(HttpServletRequest request,
HttpServletResponse response) throws IOException {
// Do not serve requests if dev server starting or failed to start.
if (isDevServerFailedToStart.get() || !devServerStartFuture.isDone()) {
return false;
}
// Since we have 'publicPath=/VAADIN/' in webpack config,
// a valid request for webpack-dev-server should start with '/VAADIN/'
String requestFilename = request.getPathInfo();
if (HandlerHelper.isPathUnsafe(requestFilename)) {
getLogger().info("Blocked attempt to access file: {}",
requestFilename);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return true;
}
HttpURLConnection connection = prepareConnection(requestFilename,
request.getMethod());
// Copies all the headers from the original request
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String header = headerNames.nextElement();
connection.setRequestProperty(header,
// Exclude keep-alive
"Connect".equals(header) ? "close"
: request.getHeader(header));
}
// Send the request
getLogger().debug("Requesting resource to webpack {}",
connection.getURL());
int responseCode = connection.getResponseCode();
if (responseCode == HTTP_NOT_FOUND) {
getLogger().debug("Resource not served by webpack {}",
requestFilename);
// webpack cannot access the resource, return false so as flow can
// handle it
return false;
}
getLogger().debug("Served resource by webpack: {} {}", responseCode,
requestFilename);
// Copies response headers
connection.getHeaderFields().forEach((header, values) -> {
if (header != null) {
response.addHeader(header, values.get(0));
}
});
if (responseCode == HTTP_OK) {
// Copies response payload
writeStream(response.getOutputStream(),
connection.getInputStream());
} else if (responseCode < 400) {
response.setStatus(responseCode);
} else {
// Copies response code
response.sendError(responseCode);
}
// Close request to avoid issues in CI and Chrome
response.getOutputStream().close();
return true;
}
private boolean checkWebpackConnection() {
try {
prepareConnection("/", "GET").getResponseCode();
return true;
} catch (IOException e) {
getLogger().debug("Error checking webpack dev server connection",
e);
}
return false;
}
/**
* Prepare a HTTP connection against webpack-dev-server.
*
* @param path
* the file to request
* @param method
* the http method to use
* @return the connection
* @throws IOException
* on connection error
*/
public HttpURLConnection prepareConnection(String path, String method)
throws IOException {
URL uri = new URL(WEBPACK_HOST + ":" + getPort() + path);
HttpURLConnection connection = (HttpURLConnection) uri.openConnection();
connection.setRequestMethod(method);
connection.setReadTimeout(DEFAULT_TIMEOUT);
connection.setConnectTimeout(DEFAULT_TIMEOUT);
return connection;
}
private synchronized void doNotify() {
if (!notified) {
notified = true;
notifyAll(); // NOSONAR
}
}
// mirrors a stream to logger, and check whether a success or error pattern
// is found in the output.
private void logStream(InputStream input, Pattern success,
Pattern failure) {
Thread thread = new Thread(() -> {
BufferedReader reader = new BufferedReader(
new InputStreamReader(input, StandardCharsets.UTF_8));
try {
readLinesLoop(success, failure, reader);
} catch (IOException e) {
if ("Stream closed".equals(e.getMessage())) {
console(GREEN, END);
getLogger().debug("Exception when reading webpack output.",
e);
} else {
getLogger().error("Exception when reading webpack output.",
e);
}
}
// Process closed stream, means that it exited, notify
// DevModeHandler to continue
doNotify();
});
thread.setDaemon(true);
thread.setName("webpack");
thread.start();
}
private void readLinesLoop(Pattern success, Pattern failure,
BufferedReader reader) throws IOException {
StringBuilder output = getOutputBuilder();
Consumer<String> info = s -> getLogger()
.debug(String.format(GREEN, "{}"), s);
Consumer<String> error = s -> getLogger()
.error(String.format(RED, "{}"), s);
Consumer<String> warn = s -> getLogger()
.debug(String.format(YELLOW, "{}"), s);
Consumer<String> log = info;
for (String line; ((line = reader.readLine()) != null);) {
String cleanLine = line
// remove color escape codes for console
.replaceAll("\u001b\\[[;\\d]*m", "")
// remove babel query string which is confusing
.replaceAll("\\?babel-target=[\\w\\d]+", "");
// write each line read to logger, but selecting its correct level
log = line.contains("WARNING") ? warn
: line.contains("ERROR") ? error
: isInfo(line, cleanLine) ? info : log;
log.accept(cleanLine);
// Only store webpack errors to be shown in the browser.
if (log.equals(error)) {
// save output so as it can be used to alert user in browser.
output.append(cleanLine).append(System.lineSeparator());
}
boolean succeed = success.matcher(line).find();
boolean failed = failure.matcher(line).find();
// We found the success or failure pattern in stream
if (succeed || failed) {
log.accept(succeed ? SUCCEED_MSG : FAILED_MSG);
// save output in case of failure
failedOutput = failed ? output.toString() : null;
// reset output and logger for the next compilation
output = getOutputBuilder();
log = info;
// Notify DevModeHandler to continue
doNotify();
}
}
}
private boolean isInfo(String line, String cleanLine) {
return line.trim().isEmpty() || cleanLine.trim().startsWith("i");
}
private StringBuilder getOutputBuilder() {
StringBuilder output = new StringBuilder();
output.append(String.format("Webpack build failed with errors:%n"));
return output;
}
private void writeStream(ServletOutputStream outputStream,
InputStream inputStream) throws IOException {
final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int bytes;
while ((bytes = inputStream.read(buffer)) >= 0) {
outputStream.write(buffer, 0, bytes);
}
}
private static Logger getLogger() {
// Using an short prefix so as webpack output is more readable
return LoggerFactory.getLogger("dev-webpack");
}
/**
* Return webpack console output when a compilation error happened.
*
* @return console output if error or null otherwise.
*/
public String getFailedOutput() {
return failedOutput;
}
/**
* Remove the running port from the vaadinContext and temporary file.
*/
public void removeRunningDevServerPort() {
FileUtils.deleteQuietly(LazyDevServerPortFileInit.DEV_SERVER_PORT_FILE);
}
private void runOnFutureComplete(DeploymentConfiguration config) {
try {
doStartDevModeServer(config);
} catch (ExecutionFailedException exception) {
getLogger().error(null, exception);
throw new CompletionException(exception);
}
}
private void saveRunningDevServerPort() {
File portFile = LazyDevServerPortFileInit.DEV_SERVER_PORT_FILE;
try {
FileUtils.writeStringToFile(portFile, String.valueOf(port),
StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private void doStartDevModeServer(DeploymentConfiguration config)
throws ExecutionFailedException {
// If port is defined, means that webpack is already running
if (port > 0) {
if (!checkWebpackConnection()) {
throw new IllegalStateException(String.format(
"%s webpack-dev-server port '%d' is defined but it's not working properly",
START_FAILURE, port));
}
reuseExistingPort(port);
return;
}
port = getRunningDevServerPort();
if (port > 0) {
if (checkWebpackConnection()) {
reuseExistingPort(port);
return;
} else {
getLogger().warn(
"webpack-dev-server port '%d' is defined but it's not working properly. Using a new free port...",
port);
port = 0;
}
}
// here the port == 0
Pair<File, File> webPackFiles = validateFiles(npmFolder);
getLogger().info("Starting webpack-dev-server");
watchDog.set(new DevServerWatchDog());
// Look for a free port
port = getFreePort();
saveRunningDevServerPort();
boolean success = false;
try {
success = doStartWebpack(config, webPackFiles);
} finally {
if (!success) {
removeRunningDevServerPort();
}
}
}
private boolean doStartWebpack(DeploymentConfiguration config,
Pair<File, File> webPackFiles) {
ProcessBuilder processBuilder = new ProcessBuilder()
.directory(npmFolder);
FrontendTools tools = new FrontendTools(npmFolder.getAbsolutePath(),
() -> FrontendUtils.getVaadinHomeDirectory().getAbsolutePath());
tools.validateNodeAndNpmVersion();
boolean useHomeNodeExec = config.getBooleanProperty(
InitParameters.REQUIRE_HOME_NODE_EXECUTABLE, false);
String nodeExec = null;
if (useHomeNodeExec) {
nodeExec = tools.forceAlternativeNodeExecutable();
} else {
nodeExec = tools.getNodeExecutable();
}
List<String> command = makeCommands(config, webPackFiles.getFirst(),
webPackFiles.getSecond(), nodeExec);
console(GREEN, START);
if (getLogger().isDebugEnabled()) {
getLogger().debug(
commandToString(npmFolder.getAbsolutePath(), command));
}
long start = System.nanoTime();
processBuilder.command(command);
try {
webpackProcess.set(
processBuilder.redirectError(ProcessBuilder.Redirect.PIPE)
.redirectErrorStream(true).start());
// We only can save the webpackProcess reference the first time that
// the DevModeHandler is created. There is no way to store
// it in the servlet container, and we do not want to save it in the
// global JVM.
// We instruct the JVM to stop the webpack-dev-server daemon when
// the JVM stops, to avoid leaving daemons running in the system.
// NOTE: that in the corner case that the JVM crashes or it is
// killed
// the daemon will be kept running. But anyways it will also happens
// if the system was configured to be stop the daemon when the
// servlet context is destroyed.
Runtime.getRuntime().addShutdownHook(new Thread(this::stop));
Pattern succeed = Pattern.compile(config.getStringProperty(
SERVLET_PARAMETER_DEVMODE_WEBPACK_SUCCESS_PATTERN,
DEFAULT_OUTPUT_PATTERN));
Pattern failure = Pattern.compile(config.getStringProperty(
SERVLET_PARAMETER_DEVMODE_WEBPACK_ERROR_PATTERN,
DEFAULT_ERROR_PATTERN));
logStream(webpackProcess.get().getInputStream(), succeed, failure);
getLogger().info(LOG_START);
synchronized (this) {
this.wait(Integer.parseInt(config.getStringProperty( // NOSONAR
SERVLET_PARAMETER_DEVMODE_WEBPACK_TIMEOUT,
DEFAULT_TIMEOUT_FOR_PATTERN)));
}
if (!webpackProcess.get().isAlive()) {
throw new IllegalStateException("Webpack exited prematurely");
}
long ms = (System.nanoTime() - start) / 1000000;
getLogger().info(LOG_END, ms);
saveRunningDevServerPort();
return true;
} catch (IOException e) {
getLogger().error("Failed to start the webpack process", e);
} catch (InterruptedException e) {
getLogger().debug("Webpack process start has been interrupted", e);
}
return false;
}
private void reuseExistingPort(int port) {
getLogger().info("Reusing webpack-dev-server running at {}:{}",
WEBPACK_HOST, port);
// Save running port for next usage
saveRunningDevServerPort();
watchDog.set(null);
}
private List<String> makeCommands(DeploymentConfiguration config,
File webpack, File webpackConfig, String nodeExec) {
List<String> command = new ArrayList<>();
command.add(nodeExec);
command.add(webpack.getAbsolutePath());
command.add("--config");
command.add(webpackConfig.getAbsolutePath());
command.add("--port");
command.add(String.valueOf(port));
command.add("--watchDogPort=" + watchDog.get().getWatchDogPort());
command.addAll(Arrays.asList(config
.getStringProperty(SERVLET_PARAMETER_DEVMODE_WEBPACK_OPTIONS,
"-d --inline=false")
.split(" +")));
return command;
}
private Pair<File, File> validateFiles(File npmFolder)
throws ExecutionFailedException {
assert port == 0;
// Skip checks if we have a webpack-dev-server already running
File webpack = new File(npmFolder, WEBPACK_SERVER);
File webpackConfig = new File(npmFolder, FrontendUtils.WEBPACK_CONFIG);
if (!npmFolder.exists()) {
getLogger().warn("No project folder '{}' exists", npmFolder);
throw new ExecutionFailedException(START_FAILURE
+ " the target execution folder doesn't exist.");
}
if (!webpack.exists()) {
getLogger().warn("'{}' doesn't exist. Did you run `npm install`?",
webpack);
throw new ExecutionFailedException(String.format(
"%s '%s' doesn't exist. `npm install` has not run or failed.",
START_FAILURE, webpack));
} else if (!webpack.canExecute()) {
getLogger().warn(
" '{}' is not an executable. Did you run `npm install`?",
webpack);
throw new ExecutionFailedException(String.format(
"%s '%s' is not an executable."
+ " `npm install` has not run or failed.",
START_FAILURE, webpack));
}
if (!webpackConfig.canRead()) {
getLogger().warn(
"Webpack configuration '{}' is not found or is not readable.",
webpackConfig);
throw new ExecutionFailedException(
String.format("%s '%s' doesn't exist or is not readable.",
START_FAILURE, webpackConfig));
}
return new Pair<>(webpack, webpackConfig);
}
private static int getRunningDevServerPort() {
int port = 0;
File portFile = LazyDevServerPortFileInit.DEV_SERVER_PORT_FILE;
if (portFile.canRead()) {
try {
String portString = FileUtils
.readFileToString(portFile, StandardCharsets.UTF_8)
.trim();
if (!portString.isEmpty()) {
port = Integer.parseInt(portString);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
return port;
}
/**
* Returns an available tcp port in the system.
*
* @return a port number which is not busy
*/
static int getFreePort() {
try (ServerSocket s = new ServerSocket(0)) {
s.setReuseAddress(true);
return s.getLocalPort();
} catch (IOException e) {
throw new IllegalStateException(
"Unable to find a free port for running webpack", e);
}
}
/**
* Get the listening port of the 'webpack-dev-server'.
*
* @return the listening port of webpack
*/
public int getPort() {
return port;
}
/**
* Whether the 'webpack-dev-server' should be reused on servlet reload.
* Default true.
*
* @return true in case of reusing the server.
*/
public boolean reuseDevServer() {
return reuseDevServer;
}
/**
* Stop the webpack-dev-server.
*/
public void stop() {
if (atomicHandler.get() == null) {
return;
}
try {
// The most reliable way to stop the webpack-dev-server is
// by informing webpack to exit. We have implemented in webpack a
// a listener that handles the stop command via HTTP and exits.
prepareConnection("/stop", "GET").getResponseCode();
} catch (IOException e) {
getLogger().debug(
"webpack-dev-server does not support the `/stop` command.",
e);
}
DevServerWatchDog watchDogInstance = watchDog.get();
if (watchDogInstance != null) {
watchDogInstance.stop();
}
Process process = webpackProcess.get();
if (process != null && process.isAlive()) {
process.destroy();
}
atomicHandler.set(null);
removeRunningDevServerPort();
}
/**
* Waits for the dev server to start.
* <p>
* Suspends the caller's thread until the dev mode server is started (or
* failed to start).
*
* @see Thread#join()
*/
void join() {
devServerStartFuture.join();
}
private static final class LazyDevServerPortFileInit {
private static final File DEV_SERVER_PORT_FILE = createDevServerPortFile();
private static File createDevServerPortFile() {
try {
return File.createTempFile("flow-dev-server", "port");
} catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}
}
}

View File

@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package dorkbox.vaadin
package dorkbox.vaadin.devMode
import com.vaadin.flow.component.WebComponentExporter
import com.vaadin.flow.component.WebComponentExporterFactory
@ -44,7 +44,6 @@ import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException
import java.util.concurrent.Executor
@ -65,6 +64,8 @@ import javax.servlet.annotation.WebListener
* This file is NOT extensible/configurable AT-ALL, so this is required...
*
*
*
*
* Servlet initializer starting node updaters as well as the webpack-dev-mode
* server.
*
@ -140,14 +141,15 @@ class DevModeInitializer : ClassLoaderAwareServletContainerInitializer, Serializ
return
}
val baseDir = config.getStringProperty(FrontendUtils.PROJECT_BASEDIR, null) ?: baseDirectoryFallback
// these are BOTH set by VaadinApplication.kt
val baseDir = config.getStringProperty(FrontendUtils.PROJECT_BASEDIR, File("").absolutePath)
val generatedDir = System.getProperty(FrontendUtils.PARAM_GENERATED_DIR, FrontendUtils.DEFAULT_GENERATED_DIR)
val frontendFolder = config.getStringProperty(
FrontendUtils.PARAM_FRONTEND_DIR,
System.getProperty(FrontendUtils.PARAM_FRONTEND_DIR, FrontendUtils.DEFAULT_FRONTEND_DIR))
val builder = NodeTasks.Builder(DevModeClassFinder(classes), File(baseDir), File(generatedDir), File(frontendFolder))
log().info("Starting dev-mode updaters in {} folder.", builder.npmFolder)
@ -249,35 +251,6 @@ class DevModeInitializer : ClassLoaderAwareServletContainerInitializer, Serializ
return LoggerFactory.getLogger(DevModeInitializer::class.java)
}
/*
* Accept user.dir or cwd as a fallback only if the directory seems to be a
* Maven or Gradle project. Check to avoid cluttering server directories
* (see tickets #8249, #8403).
*/
private val baseDirectoryFallback: String
get() {
val baseDirCandidate = System.getProperty("user.dir", ".")
val path = Paths.get(baseDirCandidate)
return if (path.toFile().isDirectory
&& (path.resolve("pom.xml").toFile().exists()
|| path.resolve("build.gradle").toFile().exists())
) {
path.toString()
} else {
throw IllegalStateException(
String.format(
"Failed to determine project directory for dev mode. "
+ "Directory '%s' does not look like a Maven or "
+ "Gradle project. Ensure that you have run the "
+ "prepare-frontend Maven goal, which generates "
+ "'flow-build-info.json', prior to deploying your "
+ "application",
path.toString()
)
)
}
}
/*
* This method returns all folders of jar files having files in the
* META-INF/resources/frontend folder. We don't use URLClassLoader because

View File

@ -0,0 +1,129 @@
/*
* Copyright 2000-2020 Vaadin Ltd.
*
* 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 dorkbox.vaadin.devMode;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* THIS IS COPIED DIRECTLY FROM VAADIN 14.6.8 (flow 2.4.6)
*
* CHANGES FROM DEFAULT ARE MANAGED AS DIFFERENT REVISIONS.
*
* The initial commit is exactly as-is from vaadin.
*
* This file is NOT extensible/configurable AT-ALL, so this is required...
*
*
*
*
* Opens a server socket which is supposed to be opened until dev mode is active
* inside JVM.
* <p>
* If this socket is closed then there is no anymore Java "client" for the
* webpack dev server and it should be stopped.
*
* @author Vaadin Ltd
* @since 2.0
*/
class DevServerWatchDog {
private static class WatchDogServer implements Runnable {
private final ServerSocket server;
WatchDogServer() {
try {
server = new ServerSocket(0);
server.setSoTimeout(0);
if (getLogger().isDebugEnabled()) {
getLogger().debug("Watchdog server has started on port {}",
server.getLocalPort());
}
} catch (IOException e) {
throw new RuntimeException("Could not open a server socket", e);
}
}
@Override
public void run() {
while (!server.isClosed()) {
try {
Socket accept = server.accept();
accept.setSoTimeout(0);
enterReloadMessageReadLoop(accept);
} catch (IOException e) {
getLogger().debug(
"Error occurred during accept a connection", e);
}
}
}
void stop() {
if (server != null) {
try {
server.close();
} catch (IOException e) {
getLogger().debug(
"Error occurred during close the server socket", e);
}
}
}
private Logger getLogger() {
return LoggerFactory.getLogger(WatchDogServer.class);
}
private void enterReloadMessageReadLoop(Socket accept) throws IOException{
BufferedReader in = new BufferedReader(new InputStreamReader(
accept.getInputStream(), StandardCharsets.UTF_8));
String line;
while ((line = in.readLine()) != null) {
DevModeHandler devModeHandler = DevModeHandler
.getDevModeHandler();
if ("reload".equals(line) && devModeHandler != null
&& devModeHandler.getLiveReload() != null) {
devModeHandler.getLiveReload().reload();
}
}
}
}
private final WatchDogServer watchDogServer;
DevServerWatchDog() {
watchDogServer = new WatchDogServer();
Thread serverThread = new Thread(watchDogServer);
serverThread.setDaemon(true);
serverThread.start();
}
int getWatchDogPort() {
return watchDogServer.server.getLocalPort();
}
void stop() {
watchDogServer.stop();
}
}

View File

@ -0,0 +1,111 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package dorkbox.vaadin.devMode;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.function.BiConsumer;
import java.util.regex.Pattern;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinSession;
/**
* THIS IS COPIED DIRECTLY FROM VAADIN 14.6.8 (flow 2.4.6)
*
* CHANGES FROM DEFAULT ARE MANAGED AS DIFFERENT REVISIONS.
*
* The initial commit is exactly as-is from vaadin.
*
* This file is NOT extensible/configurable AT-ALL, so this is required...
*/
public class HandlerHelper implements Serializable {
// static final SystemMessages DEFAULT_SYSTEM_MESSAGES = new SystemMessages();
static final String UNSAFE_PATH_ERROR_MESSAGE_PATTERN = "Blocked attempt to access file: {}";
private static final Pattern PARENT_DIRECTORY_REGEX = Pattern.compile("(/|\\\\)\\.\\.(/|\\\\)?", 2);
private HandlerHelper() {
}
public static boolean isRequestType(VaadinRequest request, HandlerHelper.RequestType requestType) {
return requestType.getIdentifier().equals(request.getParameter("v-r"));
}
public static Locale findLocale(VaadinSession session, VaadinRequest request) {
if (session == null) {
session = VaadinSession.getCurrent();
}
Locale locale;
if (session != null) {
locale = session.getLocale();
if (locale != null) {
return locale;
}
}
if (request == null) {
request = VaadinService.getCurrentRequest();
}
if (request != null) {
locale = request.getLocale();
if (locale != null) {
return locale;
}
}
return Locale.getDefault();
}
public static void setResponseNoCacheHeaders(BiConsumer<String, String> headerSetter, BiConsumer<String, Long> longHeaderSetter) {
headerSetter.accept("Cache-Control", "no-cache, no-store");
headerSetter.accept("Pragma", "no-cache");
longHeaderSetter.accept("Expires", 0L);
}
public static String getCancelingRelativePath(String pathToCancel) {
StringBuilder sb = new StringBuilder(".");
for(int i = 1; i < pathToCancel.length(); ++i) {
if (pathToCancel.charAt(i) == '/') {
sb.append("/..");
}
}
return sb.toString();
}
static boolean isPathUnsafe(String path) {
try {
path = URLDecoder.decode(path, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException var2) {
throw new RuntimeException("An error occurred during decoding URL.", var2);
}
return PARENT_DIRECTORY_REGEX.matcher(path).find();
}
public static enum RequestType {
UIDL("uidl"),
HEARTBEAT("heartbeat"),
PUSH("push");
private String identifier;
private RequestType(String identifier) {
this.identifier = identifier;
}
public String getIdentifier() {
return this.identifier;
}
}
}