diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/HotDeploymentWatchedFileBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/HotDeploymentWatchedFileBuildItem.java index 359c546612e6f..ca9973d05a6c2 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/HotDeploymentWatchedFileBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/HotDeploymentWatchedFileBuildItem.java @@ -9,8 +9,15 @@ * {@link io.quarkus.bootstrap.devmode.DependenciesFilter#getReloadableModules(io.quarkus.bootstrap.model.ApplicationModel) * reloadable module} that, if modified, may result in a hot redeployment when in the dev mode. *

- * A file may be identified with an exact location or a matching predicate. See {@link Builder#setLocation(String)} and + * A file may be identified with an location or a matching predicate. See {@link Builder#setLocation(String)} and * {@link Builder#setLocationPredicate(Predicate)}. + * + * The location may be: + *

*

* If multiple build items match the same file then the final value of {@code restartNeeded} is computed as a logical OR of all * the {@link #isRestartNeeded()} values. diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java index d256e61e2b500..90efe5480d3f1 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java @@ -944,9 +944,8 @@ Set checkForFileChange(Function watchedRoots = roots; if (isAbsolute) { // absolute files are assumed to be read directly from the project root. @@ -961,7 +960,7 @@ Set checkForFileChange(Function checkForFileChange(Function existing) { - ret.add(watchedFilePath); + ret.add(file.toString()); //a write can be a 'truncate' + 'write' //if the file is empty we may be seeing the middle of a write if (Files.size(file) == 0) { @@ -1004,7 +1003,7 @@ Set checkForFileChange(Function watchedF List, Boolean>> watchedFilePredicates, boolean isTest) { if (isTest) { setWatchedFilePathsInternal(watchedFilePaths, test, - s -> s.getTest().isPresent() ? asList(s.getTest().get(), s.getMain()) : singletonList(s.getMain())); + s -> s.getTest().isPresent() ? asList(s.getTest().get(), s.getMain()) : singletonList(s.getMain()), + watchedFilePredicates); } else { main.watchedFileTimestamps.clear(); main.watchedFilePredicates = watchedFilePredicates; - setWatchedFilePathsInternal(watchedFilePaths, main, s -> singletonList(s.getMain())); + setWatchedFilePathsInternal(watchedFilePaths, main, s -> singletonList(s.getMain()), watchedFilePredicates); } return this; } private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map watchedFilePaths, - TimestampSet timestamps, Function> cuf) { + TimestampSet timestamps, Function> cuf, + List, Boolean>> watchedFilePredicates) { timestamps.watchedFilePaths = watchedFilePaths; Map extraWatchedFilePaths = new HashMap<>(); + Set watchedRootPaths = new HashSet<>(); for (DevModeContext.ModuleInfo module : context.getAllModules()) { List compilationUnits = cuf.apply(module); for (DevModeContext.CompilationUnit unit : compilationUnits) { @@ -1110,24 +1112,48 @@ private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map rootPaths = PathList.of(Path.of(rootPath)); } for (Path root : rootPaths) { - for (String path : watchedFilePaths.keySet()) { - Path config = root.resolve(path); - if (config.toFile().exists()) { - try { - FileTime lastModifiedTime = Files.getLastModifiedTime(config); - timestamps.watchedFileTimestamps.put(config, lastModifiedTime.toMillis()); - } catch (IOException e) { - throw new UncheckedIOException(e); + // First find all matching paths from the root + try (final Stream walk = Files.walk(root)) { + walk.forEach(path -> { + if (path.equals(root)) { + return; + } + // Use the relative path to match the watched file + // /some/more/complex/path/src/main/resources/foo/bar.txt -> foo/bar.txt + Path relativePath = root.relativize(path); + String relativePathStr = relativePath.toString(); + if (watchedFilePaths.containsKey(relativePathStr) + || watchedFilePredicates.stream().anyMatch(e -> e.getKey().test(relativePathStr))) { + log.debugf("Watch %s from: %s", relativePathStr, root); + watchedRootPaths.add(relativePathStr); + putLastModifiedTime(path, relativePath, timestamps); + } + }); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + // Then process watched paths that are not matched with a path from a resource root, + // for example absolute paths and glob patterns + for (String watchedFilePath : watchedFilePaths.keySet()) { + Path path = Paths.get(watchedFilePath); + if (path.isAbsolute()) { + if (Files.exists(path)) { + log.debugf("Watch %s", path); + putLastModifiedTime(path, path, timestamps); } - } else { - timestamps.watchedFileTimestamps.put(config, 0L); - Map extraWatchedFileTimestamps = expandGlobPattern(root, config); - timestamps.watchedFileTimestamps.putAll(extraWatchedFileTimestamps); - for (Path extraPath : extraWatchedFileTimestamps.keySet()) { - extraWatchedFilePaths.put(root.relativize(extraPath).toString(), - timestamps.watchedFilePaths.get(path)); + } else if (!watchedRootPaths.contains(watchedFilePath) && maybeGlobPattern(watchedFilePath)) { + Path resolvedPath = root.resolve(watchedFilePath); + Map extraWatchedFileTimestamps = expandGlobPattern(root, resolvedPath, watchedFilePath); + if (!extraWatchedFileTimestamps.isEmpty()) { + timestamps.watchedFileTimestamps.put(resolvedPath, 0L); + timestamps.watchedFileTimestamps.putAll(extraWatchedFileTimestamps); + for (Path extraPath : extraWatchedFileTimestamps.keySet()) { + extraWatchedFilePaths.put(extraPath.toString(), + timestamps.watchedFilePaths.get(watchedFilePath)); + } + timestamps.watchedFileTimestamps.putAll(extraWatchedFileTimestamps); } - timestamps.watchedFileTimestamps.putAll(extraWatchedFileTimestamps); } } } @@ -1137,6 +1163,19 @@ private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map return this; } + private boolean maybeGlobPattern(String path) { + return path.contains("*") || path.contains("?"); + } + + private void putLastModifiedTime(Path filePath, Path keyPath, TimestampSet timestamps) { + try { + FileTime lastModifiedTime = Files.getLastModifiedTime(filePath); + timestamps.watchedFileTimestamps.put(keyPath, lastModifiedTime.toMillis()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + public void addHotReplacementSetup(HotReplacementSetup service) { hotReplacementSetup.add(service); } @@ -1171,15 +1210,17 @@ public void close() throws IOException { } } - private Map expandGlobPattern(Path root, Path configFile) { - PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + configFile.toString()); + private Map expandGlobPattern(Path root, Path path, String pattern) { + PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + path.toString()); Map files = new HashMap<>(); try { Files.walkFileTree(root, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { if (pathMatcher.matches(file)) { - files.put(file, attrs.lastModifiedTime().toMillis()); + Path relativePath = root.relativize(file); + log.debugf("Glob pattern [%s] matched %s from %s", pattern, relativePath, root); + files.put(relativePath, attrs.lastModifiedTime().toMillis()); } return FileVisitResult.CONTINUE; } diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AdditionalWatchedResourcesDevModeTest.java b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AdditionalWatchedResourcesDevModeTest.java index 9bd310ec7badf..2f4a3d23c2c0a 100644 --- a/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AdditionalWatchedResourcesDevModeTest.java +++ b/integration-tests/devmode/src/test/java/io/quarkus/test/reload/AdditionalWatchedResourcesDevModeTest.java @@ -74,6 +74,10 @@ public void globWatch() { TEST.modifyResourceFile(SAMPLE_FILE, oldSource -> MODIFIED); RestAssured.get("/content/{name}", SAMPLE_FILE).then().body(is(MODIFIED)); + + TEST.modifyResourceFile(SAMPLE_FILE, oldSource -> INITIAL); + + RestAssured.get("/content/{name}", SAMPLE_FILE).then().body(is(INITIAL)); } @Test