/
AbstractDevServerRunner.java
790 lines (696 loc) · 28.5 KB
/
AbstractDevServerRunner.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
/*
* Copyright 2000-2021 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 com.vaadin.base.devserver;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.HttpURLConnection;
import java.net.ServerSocket;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.regex.Pattern;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.vaadin.base.devserver.DevServerOutputTracker.Result;
import com.vaadin.base.devserver.stats.DevModeUsageStatistics;
import com.vaadin.base.devserver.stats.StatisticsConstants;
import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.internal.BrowserLiveReload;
import com.vaadin.flow.internal.BrowserLiveReloadAccessor;
import com.vaadin.flow.internal.DevModeHandler;
import com.vaadin.flow.internal.UrlUtil;
import com.vaadin.flow.server.ExecutionFailedException;
import com.vaadin.flow.server.HandlerHelper;
import com.vaadin.flow.server.InitParameters;
import com.vaadin.flow.server.StaticFileServer;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinResponse;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.frontend.FrontendTools;
import com.vaadin.flow.server.frontend.FrontendToolsSettings;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.server.startup.ApplicationConfiguration;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.vaadin.base.devserver.stats.StatisticsConstants.EVENT_LIVE_RELOAD;
/**
* Deals with most details of starting a frontend development server or
* connecting to an existing one.
* <p>
* This class is meant to be used during developing time.
* <p>
* For internal use only. May be renamed or removed in a future release.
*/
public abstract class AbstractDevServerRunner implements DevModeHandler {
private static final String START_FAILURE = "Couldn't start dev server because";
private static final String DEV_SERVER_HOST = "http://127.0.0.1";
private static final String FAILED_MSG = "\n------------------ Frontend compilation failed. ------------------\n\n";
private static final String SUCCEED_MSG = "\n----------------- Frontend compiled successfully. -----------------\n\n";
private static final String START = "\n------------------ Starting Frontend compilation. ------------------\n";
private static final String LOG_START = "Running {} to compile frontend resources. This may take a moment, please stand by...";
/**
* If after this time in millisecs, the pattern was not found, we unlock the
* process and continue. It might happen if the dev server changes their
* output.
*/
private static final String DEFAULT_TIMEOUT_FOR_PATTERN = "60000";
/**
* UUID system property for identifying JVM restart.
*/
private static final String DEV_SERVER_PORTFILE_UUID_PROPERTY = "vaadin.frontend.devserver.portfile.uuid";
// webpack dev-server allows " character if passed through, need to
// explicitly check requests for it
private static final Pattern WEBPACK_ILLEGAL_CHAR_PATTERN = Pattern
.compile("\"|%22");
private static final int DEFAULT_BUFFER_SIZE = 32 * 1024;
private static final int DEFAULT_TIMEOUT = 120 * 1000;
private final File npmFolder;
private volatile int port;
private final AtomicReference<Process> devServerProcess = new AtomicReference<>();
private final boolean reuseDevServer;
private final File devServerPortFile;
private AtomicBoolean isDevServerFailedToStart = new AtomicBoolean();
private transient BrowserLiveReload liveReload;
private final CompletableFuture<Void> devServerStartFuture;
private final AtomicReference<DevServerWatchDog> watchDog = new AtomicReference<>();
private boolean usingAlreadyStartedProcess = false;
private ApplicationConfiguration applicationConfiguration;
private String failedOutput = null;
/**
* Craete an instance that waits for the given task to complete before
* starting or connecting to the server.
*
* @param lookup
* a lookup instance
* @param runningPort
* the port that a dev server is already running on or 0 to start
* a new server
* @param npmFolder
* the project root
* @param waitFor
* the task to wait for before running the server.
*/
protected AbstractDevServerRunner(Lookup lookup, int runningPort,
File npmFolder, CompletableFuture<Void> waitFor) {
this.npmFolder = npmFolder;
port = runningPort;
applicationConfiguration = lookup
.lookup(ApplicationConfiguration.class);
reuseDevServer = applicationConfiguration.reuseDevServer();
devServerPortFile = getDevServerPortFile(npmFolder);
BrowserLiveReloadAccessor liveReloadAccess = lookup
.lookup(BrowserLiveReloadAccessor.class);
liveReload = liveReloadAccess != null
? liveReloadAccess
.getLiveReload(applicationConfiguration.getContext())
: null;
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();
};
devServerStartFuture = waitFor.whenCompleteAsync(action);
}
private void runOnFutureComplete() {
try {
doStartDevModeServer();
} catch (ExecutionFailedException exception) {
getLogger().error(null, exception);
throw new CompletionException(exception);
}
}
private void doStartDevModeServer() throws ExecutionFailedException {
// If port is defined, means that the dev server is already running
if (port > 0) {
if (!checkConnection()) {
throw new IllegalStateException(String.format(
"%s %s port '%d' is defined but it's not working properly",
getServerName(), START_FAILURE, port));
}
reuseExistingPort(port);
return;
}
port = getRunningDevServerPort(npmFolder);
if (port > 0) {
if (checkConnection()) {
reuseExistingPort(port);
return;
} else {
getLogger().warn(String.format(
"%s port '%d' is defined but it's not working properly. Using a new free port...",
getServerName(), port));
port = 0;
}
}
// here the port == 0
validateFiles();
long start = System.nanoTime();
getLogger().info("Starting " + getServerName());
watchDog.set(new DevServerWatchDog());
// Look for a free port
port = getFreePort();
// save the port immediately before start a dev server, see #8981
saveRunningDevServerPort();
try {
Process process = doStartDevServer();
devServerProcess.set(process);
if (!isRunning()) {
throw new IllegalStateException(
getServerName() + " exited prematurely");
}
long ms = (System.nanoTime() - start) / 1000000;
getLogger().info("Started {}. Time: {}ms", getServerName(), ms);
DevModeUsageStatistics.collectEvent(
StatisticsConstants.EVENT_DEV_SERVER_START_PREFIX
+ getServerName(),
ms);
} finally {
if (devServerProcess.get() == null) {
removeRunningDevServerPort();
}
}
}
/**
* Validates that the needed server binary and config file(s) are available.
*
* @throws ExecutionFailedException
* if there is a problem
*/
protected void validateFiles() throws ExecutionFailedException {
assert getPort() == 0;
// Skip checks if we have a dev server already running
File binary = getServerBinary();
File config = getServerConfig();
if (!getProjectRoot().exists()) {
getLogger().warn("No project folder '{}' exists", getProjectRoot());
throw new ExecutionFailedException(START_FAILURE
+ " the target execution folder doesn't exist.");
}
if (!binary.exists()) {
getLogger().warn("'{}' doesn't exist. Did you run `npm install`?",
binary);
throw new ExecutionFailedException(String.format(
"%s '%s' doesn't exist. `npm install` has not run or failed.",
START_FAILURE, binary));
} else if (!binary.canExecute()) {
getLogger().warn(
" '{}' is not an executable. Did you run `npm install`?",
binary);
throw new ExecutionFailedException(String.format(
"%s '%s' is not an executable."
+ " `npm install` has not run or failed.",
START_FAILURE, binary));
}
if (!config.canRead()) {
getLogger().warn(
"{} configuration '{}' is not found or is not readable.",
getServerName(), config);
throw new ExecutionFailedException(
String.format("%s '%s' doesn't exist or is not readable.",
START_FAILURE, config));
}
}
/**
* Gets the binary that starts the dev server.
*/
protected abstract File getServerBinary();
/**
* Gets the main configuration file for the dev server.
*/
protected abstract File getServerConfig();
/**
* Gets the name of the dev server for outputting to the user and
* statistics.
*/
protected abstract String getServerName();
/**
* Gets the commands to run to start the dev server.
*
* @param nodeExec
* the path to the node binary
*/
protected abstract List<String> getServerStartupCommand(String nodeExec);
/**
* Defines the environment variables to use when starting the dev server.
*
* @param frontendTools
* frontend tools metadata
* @param environment
* the environment variables to use
*/
protected void updateServerStartupEnvironment(FrontendTools frontendTools,
Map<String, String> environment) {
environment.put("watchDogPort",
Integer.toString(getWatchDog().getWatchDogPort()));
}
/**
* Gets a pattern to match with the output to determine that the server has
* started successfully.
*/
protected abstract Pattern getServerSuccessPattern();
/**
* Gets a pattern to match with the output to determine that the server has
* failed to start.
*/
protected abstract Pattern getServerFailurePattern();
/**
* Starts the dev server and returns the started process.
*
* @return the started process or {@code null} if no process was started
*/
protected Process doStartDevServer() {
ApplicationConfiguration config = getApplicationConfiguration();
ProcessBuilder processBuilder = new ProcessBuilder()
.directory(getProjectRoot());
boolean useHomeNodeExec = config.getBooleanProperty(
InitParameters.REQUIRE_HOME_NODE_EXECUTABLE, false);
boolean nodeAutoUpdate = config
.getBooleanProperty(InitParameters.NODE_AUTO_UPDATE, false);
boolean useGlobalPnpm = config.getBooleanProperty(
InitParameters.SERVLET_PARAMETER_GLOBAL_PNPM, false);
FrontendToolsSettings settings = new FrontendToolsSettings(
getProjectRoot().getAbsolutePath(),
() -> FrontendUtils.getVaadinHomeDirectory().getAbsolutePath());
settings.setForceAlternativeNode(useHomeNodeExec);
settings.setAutoUpdate(nodeAutoUpdate);
settings.setUseGlobalPnpm(useGlobalPnpm);
FrontendTools tools = new FrontendTools(settings);
tools.validateNodeAndNpmVersion();
String nodeExec = null;
if (useHomeNodeExec) {
nodeExec = tools.forceAlternativeNodeExecutable();
} else {
nodeExec = tools.getNodeExecutable();
}
List<String> command = getServerStartupCommand(nodeExec);
FrontendUtils.console(FrontendUtils.GREEN, START);
if (getLogger().isDebugEnabled()) {
getLogger().debug(FrontendUtils.commandToString(
getProjectRoot().getAbsolutePath(), command));
}
processBuilder.command(command);
Map<String, String> environment = processBuilder.environment();
updateServerStartupEnvironment(tools, environment);
try {
Process process = processBuilder.redirectErrorStream(true).start();
/*
* We only can save the dev server process 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 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));
DevServerOutputTracker outputTracker = new DevServerOutputTracker(
process.getInputStream(), getServerSuccessPattern(),
getServerFailurePattern(), this::onDevServerCompilation);
outputTracker.find();
getLogger().info(LOG_START, getServerName());
int timeout = Integer.parseInt(config.getStringProperty(
InitParameters.SERVLET_PARAMETER_DEVMODE_WEBPACK_TIMEOUT,
DEFAULT_TIMEOUT_FOR_PATTERN));
outputTracker.awaitFirstMatch(timeout);
return process;
} catch (IOException e) {
getLogger().error(
"Failed to start the " + getServerName() + " process", e);
} catch (InterruptedException e) {
getLogger().debug(
getServerName() + " process start has been interrupted", e);
}
return null;
}
/**
* Called whenever the dev server output matche the success or failure
* pattern.
*/
protected void onDevServerCompilation(Result result) {
if (result.isSuccess()) {
FrontendUtils.console(FrontendUtils.GREEN, SUCCEED_MSG);
failedOutput = null;
} else {
FrontendUtils.console(FrontendUtils.RED, FAILED_MSG);
failedOutput = result.getOutput();
}
}
@Override
public String getFailedOutput() {
return failedOutput;
}
/**
* Gets the server watch dog.
*
* @return the watch dog
*/
protected DevServerWatchDog getWatchDog() {
return watchDog.get();
}
/** Triggers live reload. */
protected void triggerLiveReload() {
if (liveReload != null) {
liveReload.reload();
DevModeUsageStatistics.collectEvent(EVENT_LIVE_RELOAD);
}
}
@Override
public File getProjectRoot() {
return npmFolder;
}
/**
* Gets the application configuration.
*
* @return the application configuration
*/
protected ApplicationConfiguration getApplicationConfiguration() {
return applicationConfiguration;
}
/**
* Check the connection to the dev server.
*
* @return {@code true} if the dev server is responding correctly,
* {@code false} otherwise
*/
protected boolean checkConnection() {
try {
HttpURLConnection connection = prepareConnection("/index.html",
"GET");
return connection.getResponseCode() == HttpURLConnection.HTTP_OK;
} catch (IOException e) {
getLogger().debug("Error checking dev server connection", e);
}
return false;
}
private static int getRunningDevServerPort(File npmFolder) {
int port = 0;
File portFile = getDevServerPortFile(npmFolder);
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;
}
/**
* Remove the running port from the vaadinContext and temporary file.
*/
private void removeRunningDevServerPort() {
FileUtils.deleteQuietly(devServerPortFile);
}
/**
* 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 the dev server", e);
}
}
/**
* Get the listening port of the dev server.
*
* @return the listening port
*/
public int getPort() {
return port;
}
private void reuseExistingPort(int port) {
getLogger().info("Reusing {} running at {}:{}", getServerName(),
DEV_SERVER_HOST, port);
this.usingAlreadyStartedProcess = true;
// Save running port for next usage
saveRunningDevServerPort();
watchDog.set(null);
}
private void saveRunningDevServerPort() {
try {
FileUtils.writeStringToFile(devServerPortFile, String.valueOf(port),
StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static File getDevServerPortFile(File npmFolder) {
// UUID changes between JVM restarts
String jvmUuid = System.getProperty(DEV_SERVER_PORTFILE_UUID_PROPERTY);
if (jvmUuid == null) {
jvmUuid = UUID.randomUUID().toString();
System.setProperty(DEV_SERVER_PORTFILE_UUID_PROPERTY, jvmUuid);
}
// Frontend path ensures uniqueness for multiple devmode apps running
// simultaneously
String frontendBuildPath = npmFolder.getAbsolutePath();
String uniqueUid = UUID.nameUUIDFromBytes(
(jvmUuid + frontendBuildPath).getBytes(StandardCharsets.UTF_8))
.toString();
return new File(System.getProperty("java.io.tmpdir"), uniqueUid);
}
/**
* Waits for the dev server to start.
* <p>
* Suspends the caller's thread until the dev mode server is started (or
* failed to start).
*/
public void waitForDevServer() {
devServerStartFuture.join();
}
boolean isRunning() {
Process process = devServerProcess.get();
return (process != null && process.isAlive())
|| usingAlreadyStartedProcess;
}
@Override
public void stop() {
if (reuseDevServer) {
return;
}
try {
// The most reliable way to stop the dev server is
// by informing it to exit. We have implemented
// a listener that handles the stop command via HTTP and exits.
prepareConnection("/stop", "GET").getResponseCode();
} catch (IOException e) {
getLogger().debug(
getServerName() + " does not support the `/stop` command.",
e);
}
DevServerWatchDog watchDogInstance = watchDog.get();
if (watchDogInstance != null) {
watchDogInstance.stop();
}
Process process = devServerProcess.get();
if (process != null && process.isAlive()) {
process.destroy();
}
devServerProcess.set(null);
usingAlreadyStartedProcess = false;
removeRunningDevServerPort();
}
@Override
public HttpURLConnection prepareConnection(String path, String method)
throws IOException {
// path should have been checked at this point for any outside requests
URL uri = new URL(DEV_SERVER_HOST + ":" + getPort() + path);
HttpURLConnection connection = (HttpURLConnection) uri.openConnection();
connection.setRequestMethod(method);
connection.setReadTimeout(DEFAULT_TIMEOUT);
connection.setConnectTimeout(DEFAULT_TIMEOUT);
return connection;
}
@Override
public boolean handleRequest(VaadinSession session, VaadinRequest request,
VaadinResponse response) throws IOException {
if (devServerStartFuture.isDone()) {
// The server has started, check for any exceptions in the startup
// process
try {
devServerStartFuture.getNow(null);
} catch (CompletionException exception) {
isDevServerFailedToStart.set(true);
throw getCause(exception);
}
if (request.getHeader("X-DevModePoll") != null) {
// Avoid creating a UI that is thrown away for polling requests
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("Ready");
return true;
}
return false;
} else {
if (request.getHeader("X-DevModePoll") == null) {
// The initial request while the dev server is starting
InputStream inputStream = AbstractDevServerRunner.class
.getResourceAsStream("dev-mode-not-ready.html");
IOUtils.copy(inputStream, response.getOutputStream());
} else {
// A polling request while the server is starting
response.getWriter().write("Pending");
}
response.setContentType("text/html;charset=utf-8");
response.setHeader("X-DevModePending", "true");
return true;
}
}
/**
* Serve a file by proxying to the dev server.
* <p>
* Note: it considers the {@link HttpServletRequest#getPathInfo} that will
* be the path passed to the 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 the dev server returned a not found, true otherwise
* @throws IOException
* in the case something went wrong like connection refused
*/
@Override
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 the dev server config,
// a valid request for the dev server should start with '/VAADIN/'
String requestFilename = request.getPathInfo();
if (HandlerHelper.isPathUnsafe(requestFilename)
|| WEBPACK_ILLEGAL_CHAR_PATTERN.matcher(requestFilename)
.find()) {
getLogger().info("Blocked attempt to access file: {}",
requestFilename);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return true;
}
// Redirect theme source request
if (StaticFileServer.APP_THEME_PATTERN.matcher(requestFilename)
.find()) {
requestFilename = "/VAADIN/static" + requestFilename;
}
if (requestFilename.equals("") || requestFilename.equals("/")) {
// Index file must be handled by IndexHtmlRequestHandler
return false;
}
String devServerRequestPath = UrlUtil.encodeURI(requestFilename);
if (request.getQueryString() != null) {
devServerRequestPath += "?" + request.getQueryString();
}
HttpURLConnection connection = prepareConnection(devServerRequestPath,
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 from {} {}", getServerName(),
connection.getURL());
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
getLogger().debug("Resource not served by {} {}", getServerName(),
devServerRequestPath);
// the dev server cannot access the resource, return false so Flow
// can
// handle it
return false;
}
getLogger().debug("Served resource by {}: {} {}", getServerName(),
responseCode, devServerRequestPath);
// Copies response headers
connection.getHeaderFields().forEach((header, values) -> {
if (header != null) {
if ("Transfer-Encoding".equals(header)) {
return;
}
response.addHeader(header, values.get(0));
}
});
if (responseCode == HttpURLConnection.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 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);
}
}
protected 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() {
return LoggerFactory.getLogger(AbstractDevServerRunner.class);
}
}