Skip to content

Commit 5d27b59

Browse files
committed
Fix negative maxDepth in Jetty 12.1 fullScan hot-reload scanner
- Resolves IllegalArgumentException on startup by setting scan depth to Integer.MAX_VALUE (which represents unlimited depth in Jetty 12.1) instead of the invalid negative -1 value. - Applies the fix to both EE8 and EE11 configurations to ensure both support scanning deeply nested directories. - Added e2e test (DevAppServerFullScanTest) that verifies the server boots without exceptions and successfully reloads when a nested resource file (> 3 levels deep) changes. - Verified that on the original code, the EE8 server crashes on startup, and the EE11 server fails to trigger reloads for changes nested deeper than 3 directory levels.
1 parent 9a616f7 commit 5d27b59

4 files changed

Lines changed: 114 additions & 5 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.appengine.tools.development;
17+
18+
import com.google.common.collect.ImmutableList;
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.util.List;
22+
import org.junit.Before;
23+
import org.junit.Test;
24+
import org.junit.runner.RunWith;
25+
import org.junit.runners.Parameterized;
26+
27+
@RunWith(Parameterized.class)
28+
public class DevAppServerFullScanTest extends DevAppServerTestBase {
29+
30+
public DevAppServerFullScanTest(String runtimeVersion, String jettyVersion, String jakartaVersion) {
31+
super(runtimeVersion, jettyVersion, jakartaVersion);
32+
}
33+
34+
private File appDir;
35+
36+
@Before
37+
public void setUpClass() throws IOException, InterruptedException {
38+
appDir =
39+
Boolean.getBoolean("appengine.use.EE10") || Boolean.getBoolean("appengine.use.EE11")
40+
? createApp("allinone_jakarta")
41+
: createApp("allinone");
42+
setUpClass(appDir);
43+
}
44+
45+
@Override
46+
protected List<String> getExtraJvmArgs() {
47+
return ImmutableList.of("-Dappengine.fullscan.seconds=1");
48+
}
49+
50+
@Test
51+
public void testFullScanStartAndReload() throws Exception {
52+
// Basic request to ensure server is running and hot reload scanner starts up fine
53+
executeHttpGet("/?memcache_loops=1&memcache_size=1", "Running memcache for 1 loops with value size 1\nCache hits: 1\nCache misses: 0\n", RESPONSE_200);
54+
55+
// Clear logs to ensure we only search for the subsequent reload event
56+
serverLogs.clear();
57+
58+
// Touch web.xml to trigger a reload
59+
File webXml = new File(appDir, "WEB-INF/web.xml");
60+
com.google.common.truth.Truth.assertThat(webXml.exists()).isTrue();
61+
long oldLastModified = webXml.lastModified();
62+
long newTime = oldLastModified + 2000;
63+
boolean modified = webXml.setLastModified(newTime);
64+
com.google.common.truth.Truth.assertThat(modified).isTrue();
65+
66+
// Verify that the hot-reload scanner initiates a reload
67+
boolean reloaded = awaitLogContains("A file has changed, reloading the web application.", 10);
68+
com.google.common.truth.Truth.assertThat(reloaded).isTrue();
69+
70+
// Clear logs again to test a deeply nested resource file (depth > 3)
71+
serverLogs.clear();
72+
73+
File nestedClassFile = new File(appDir, "WEB-INF/classes/allinone/deeper/package/test/Dummy.properties");
74+
nestedClassFile.getParentFile().mkdirs();
75+
java.nio.file.Files.write(nestedClassFile.toPath(), new byte[]{0, 1, 2, 3});
76+
77+
// Verify that the hot-reload scanner initiates a reload for the deeply nested resource file
78+
boolean nestedReloaded = awaitLogContains("A file has changed, reloading the web application.", 10);
79+
com.google.common.truth.Truth.assertThat(nestedReloaded).isTrue();
80+
}
81+
}

‎e2etests/devappservertests/src/test/java/com/google/appengine/tools/development/DevAppServerTestBase.java‎

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public abstract class DevAppServerTestBase {
4848
int jettyPort;
4949
private Process runtimeProc;
5050
private CountDownLatch serverStarted;
51+
protected final List<String> serverLogs = java.util.Collections.synchronizedList(new ArrayList<>());
5152

5253
static final int NUMBER_OF_RETRIES = 5;
5354

@@ -177,6 +178,7 @@ public void setUpClass(File appDir) throws IOException, InterruptedException {
177178
runtimeArgs.add("-Dappengine.use.EE10=" + System.getProperty("appengine.use.EE10"));
178179
runtimeArgs.add("-Dappengine.use.EE11=" + System.getProperty("appengine.use.EE11"));
179180
runtimeArgs.add("-Dappengine.use.jetty121=" + System.getProperty("appengine.use.jetty121"));
181+
runtimeArgs.addAll(getExtraJvmArgs());
180182
runtimeArgs.add("-cp");
181183
runtimeArgs.add(TOOLS_JAR);
182184
runtimeArgs.add("com.google.appengine.tools.development.DevAppServerMain");
@@ -185,10 +187,19 @@ public void setUpClass(File appDir) throws IOException, InterruptedException {
185187
runtimeArgs.add("--allow_remote_shutdown"); // Keep as used in Maven plugin
186188
runtimeArgs.add("--disable_update_check"); // Keep, as used in Maven plugin
187189

190+
runtimeArgs.addAll(getExtraAppArgs());
188191
runtimeArgs.add(appDir.toString());
189192
createRuntime(ImmutableList.copyOf(runtimeArgs), ImmutableMap.of(), jettyPort);
190193
}
191194

195+
protected List<String> getExtraJvmArgs() {
196+
return ImmutableList.of();
197+
}
198+
199+
protected List<String> getExtraAppArgs() {
200+
return ImmutableList.of();
201+
}
202+
192203
void createRuntime(
193204
ImmutableList<String> runtimeArgs,
194205
ImmutableMap<String, String> extraEnvironmentEntries,
@@ -221,8 +232,8 @@ private Process launchRuntime(
221232
pb.environment().putAll(extraEnvironmentEntries);
222233
Process process = pb.start();
223234

224-
OutputPump outPump = new OutputPump(process.getInputStream(), serverStarted);
225-
OutputPump errPump = new OutputPump(process.getErrorStream(), serverStarted);
235+
OutputPump outPump = new OutputPump(process.getInputStream(), serverStarted, serverLogs);
236+
OutputPump errPump = new OutputPump(process.getErrorStream(), serverStarted, serverLogs);
226237
new Thread(outPump).start();
227238
new Thread(errPump).start();
228239
if (!serverStarted.await(120, TimeUnit.SECONDS)) {
@@ -284,12 +295,28 @@ void executeHttpGetWithRetriesContains(
284295
assertThat(retCode).isEqualTo(expectedReturnCode);
285296
}
286297

298+
protected boolean awaitLogContains(String expected, int timeoutSeconds) throws InterruptedException {
299+
for (int i = 0; i < timeoutSeconds * 10; i++) {
300+
synchronized (serverLogs) {
301+
for (String logLine : serverLogs) {
302+
if (logLine.contains(expected)) {
303+
return true;
304+
}
305+
}
306+
}
307+
Thread.sleep(100);
308+
}
309+
return false;
310+
}
311+
287312
private static class OutputPump implements Runnable {
288313
private final BufferedReader stream;
289314
private final CountDownLatch serverStarted;
315+
private final List<String> serverLogs;
290316

291-
public OutputPump(InputStream instream, CountDownLatch serverStarted) {
317+
public OutputPump(InputStream instream, CountDownLatch serverStarted, List<String> serverLogs) {
292318
this.serverStarted = serverStarted;
319+
this.serverLogs = serverLogs;
293320
this.stream = new BufferedReader(new InputStreamReader(instream, UTF_8));
294321
}
295322

@@ -299,6 +326,7 @@ public void run() {
299326
try {
300327
while ((line = stream.readLine()) != null) {
301328
System.out.println(line);
329+
serverLogs.add(line);
302330
if (line.contains("INFO: Dev App Server is now running")) {
303331
serverStarted.countDown();
304332
}

‎runtime/local_jetty121/src/main/java/com/google/appengine/tools/development/jetty/JettyContainerService.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ private void fullWebAppScanner(int interval) throws IOException {
513513
scanner.setScanInterval(interval);
514514
scanner.setScanDirs(scanList);
515515
scanner.setReportExistingFilesOnStartup(false);
516-
scanner.setScanDepth(-1); // -1 means unlimited depth.
516+
scanner.setScanDepth(Integer.MAX_VALUE); // Integer.MAX_VALUE means unlimited depth.
517517

518518
scanner.addListener(
519519
new Scanner.BulkListener() {

‎runtime/local_jetty121_ee11/src/main/java/com/google/appengine/tools/development/jetty/ee11/JettyContainerService.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ private void fullWebAppScanner(int interval) throws IOException {
507507
scanner.setScanInterval(interval);
508508
scanner.setScanDirs(scanList);
509509
scanner.setReportExistingFilesOnStartup(false);
510-
scanner.setScanDepth(3);
510+
scanner.setScanDepth(Integer.MAX_VALUE); // Integer.MAX_VALUE means unlimited depth.
511511

512512
scanner.addListener(
513513
new Scanner.BulkListener() {

0 commit comments

Comments
 (0)