From 9a32c51ede56d70c806171af110903763f7a5ab4 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 23 Oct 2023 17:25:56 +0200 Subject: [PATCH] Dev mode: RuntimeUpdatesProcessor#checkForFileChange() refactoring --- .../HotDeploymentWatchedFileBuildItem.java | 4 +- .../dev/RuntimeUpdatesProcessor.java | 286 +++++++++++------- 2 files changed, 184 insertions(+), 106 deletions(-) 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 ca9973d05a6c2..4c6f953dd15d6 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 @@ -14,8 +14,8 @@ * * The location may be: * *

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 90efe5480d3f1..0a92c1909a8fe 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 @@ -332,7 +332,7 @@ private void periodicTestCompile() { } Set filesChanges = new HashSet<>(checkForFileChange(s -> s.getTest().orElse(null), test)); filesChanges.addAll(checkForFileChange(DevModeContext.ModuleInfo::getMain, test)); - boolean fileRestartNeeded = filesChanges.stream().anyMatch(test::isWatchedFileRestartNeeded); + boolean fileRestartNeeded = filesChanges.stream().anyMatch(test::isRestartNeeded); ClassScanResult merged = ClassScanResult.merge(changedTestClassResult, changedApp); if (fileRestartNeeded) { @@ -462,12 +462,12 @@ public boolean doScan(boolean userInitiated, boolean forceRestart) { main, false); Set filesChanged = checkForFileChange(DevModeContext.ModuleInfo::getMain, main); - boolean fileRestartNeeded = forceRestart || filesChanged.stream().anyMatch(main::isWatchedFileRestartNeeded); + boolean fileRestartNeeded = forceRestart || filesChanged.stream().anyMatch(main::isRestartNeeded); boolean instrumentationChange = false; List changedFilesForRestart = new ArrayList<>(); if (fileRestartNeeded) { - filesChanged.stream().filter(main::isWatchedFileRestartNeeded).map(Paths::get) + filesChanged.stream().filter(main::isRestartNeeded).map(Paths::get) .forEach(changedFilesForRestart::add); } changedFilesForRestart.addAll(changedClassResults.getChangedClasses()); @@ -867,6 +867,23 @@ Set checkForFileChange() { return checkForFileChange(DevModeContext.ModuleInfo::getMain, main); } + /** + * Returns the set of modified files. + *

+ * The returned set may contain: + *

+ * + * @param cuf + * @param timestampSet + * @return the set of modified files + */ Set checkForFileChange(Function cuf, TimestampSet timestampSet) { Set ret = new HashSet<>(); @@ -901,14 +918,13 @@ Set checkForFileChange(Function seen = new HashSet<>(moduleResources); try { for (Path root : roots) { - //since the stream is Closeable, use a try with resources so the underlying iterator is closed try (final Stream walk = Files.walk(root)) { walk.forEach(path -> { try { Path relative = root.relativize(path); Path target = outputDir.resolve(relative); seen.remove(target); - if (!timestampSet.watchedFileTimestamps.containsKey(path)) { + if (!timestampSet.watchedPaths.containsKey(path)) { moduleResources.add(target); if (!Files.exists(target) || Files.getLastModifiedTime(target).toMillis() < Files .getLastModifiedTime(path).toMillis()) { @@ -916,7 +932,9 @@ Set checkForFileChange(Function checkForFileChange(Function watchedRoots = roots; - if (isAbsolute) { - // absolute files are assumed to be read directly from the project root. - // They therefore do not get copied to, and deleted from, the outputdir. - watchedRoots = List.of(Path.of("/")); - } - if (watchedRoots.isEmpty()) { - // this compilation unit has no resource roots, and therefore can not have this file + for (WatchedPath watchedPath : timestampSet.watchedPaths.values()) { + boolean isAbsolute = watchedPath.isAbsolute(); + if (!isAbsolute && roots.stream().noneMatch(watchedPath.filePath::startsWith)) { + // The watched path does not come from the current compilation unit continue; } boolean pathCurrentlyExisting = false; boolean pathPreviouslyExisting = false; - for (Path root : watchedRoots) { - Path file = root.resolve(watchedFilePath); - if (Files.exists(file)) { - pathCurrentlyExisting = true; - try { - long value = Files.getLastModifiedTime(file).toMillis(); - Long existing = timestampSet.watchedFileTimestamps.get(file); - //existing can be null when running tests - //as there is both normal and test resources, but only one set of watched timestampts - if (existing != null && value > existing) { - 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) { - try { - Thread.sleep(200); - } catch (InterruptedException e) { - //ignore - } + if (Files.exists(watchedPath.filePath)) { + pathCurrentlyExisting = true; + try { + long current = Files.getLastModifiedTime(watchedPath.filePath).toMillis(); + long last = watchedPath.lastModified; + if (current > last) { + // Use either the absolute path or the OS-agnostic path to match the HotDeploymentWatchedFileBuildItem + ret.add(isAbsolute ? watchedPath.filePath.toString() : watchedPath.getOSAgnosticMatchPath()); + //a write can be a 'truncate' + 'write' + //if the file is empty we may be seeing the middle of a write + if (Files.size(watchedPath.filePath) == 0) { + try { + Thread.sleep(200); + } catch (InterruptedException e) { + //ignore } - //re-read, as we may have read the original TS if the middle of - //a truncate+write, even if the write had completed by the time - //we read the size - value = Files.getLastModifiedTime(file).toMillis(); - - log.infof("File change detected: %s", file); - if (!isAbsolute && doCopy && !Files.isDirectory(file)) { - Path target = outputDir.resolve(watchedFilePath); - byte[] data = Files.readAllBytes(file); - try (FileOutputStream out = new FileOutputStream(target.toFile())) { - out.write(data); - } + } + //re-read, as we may have read the original TS if the middle of + //a truncate+write, even if the write had completed by the time + //we read the size + current = Files.getLastModifiedTime(watchedPath.filePath).toMillis(); + + log.infof("File change detected: %s", watchedPath.filePath); + if (!isAbsolute && doCopy && !Files.isDirectory(watchedPath.filePath)) { + Path target = outputDir.resolve(watchedPath.matchPath); + byte[] data = Files.readAllBytes(watchedPath.filePath); + try (FileOutputStream out = new FileOutputStream(target.toFile())) { + out.write(data); } - timestampSet.watchedFileTimestamps.put(file, value); } - } catch (IOException e) { - throw new UncheckedIOException(e); + watchedPath.lastModified = current; } - } else { - Long prevValue = timestampSet.watchedFileTimestamps.put(file, 0L); - pathPreviouslyExisting = pathPreviouslyExisting || (prevValue != null && prevValue > 0); + } catch (IOException e) { + throw new UncheckedIOException(e); } + } else { + long prevValue = watchedPath.lastModified; + watchedPath.lastModified = 0L; + pathPreviouslyExisting = prevValue > 0; } if (!pathCurrentlyExisting) { if (pathPreviouslyExisting) { - ret.add(watchedFilePath.toString()); + // Use either the absolute path or the OS-agnostic path to match the HotDeploymentWatchedFileBuildItem + ret.add(isAbsolute ? watchedPath.filePath.toString() : watchedPath.getOSAgnosticMatchPath()); } - if (!isAbsolute) { - Path target = outputDir.resolve(watchedFilePath); + Path target = outputDir.resolve(watchedPath.matchPath); try { FileUtil.deleteIfExists(target); } catch (IOException e) { @@ -1087,8 +1096,6 @@ public RuntimeUpdatesProcessor setWatchedFilePaths(Map watchedF 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()), watchedFilePredicates); } return this; @@ -1097,9 +1104,10 @@ public RuntimeUpdatesProcessor setWatchedFilePaths(Map watchedF private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map watchedFilePaths, TimestampSet timestamps, Function> cuf, List, Boolean>> watchedFilePredicates) { + timestamps.watchedFilePaths = watchedFilePaths; - Map extraWatchedFilePaths = new HashMap<>(); - Set watchedRootPaths = new HashSet<>(); + timestamps.watchedFilePredicates = watchedFilePredicates; + for (DevModeContext.ModuleInfo module : context.getAllModules()) { List compilationUnits = cuf.apply(module); for (DevModeContext.CompilationUnit unit : compilationUnits) { @@ -1111,55 +1119,69 @@ private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map } rootPaths = PathList.of(Path.of(rootPath)); } - for (Path root : rootPaths) { - // First find all matching paths from the root + final List roots = rootPaths.stream() + .filter(Files::exists) + .filter(Files::isReadable) + .collect(Collectors.toList()); + for (Path root : roots) { + Set watchedRootPaths = new HashSet<>(); + // First find all matching paths from all roots 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 + // For example /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); + // We need to match the OS-agnostic path + String relativePathStr = toOSAgnosticPathStr(relativePath.toString()); + Boolean restart = watchedFilePaths.get(relativePathStr); + if (restart == null) { + restart = watchedFilePredicates.stream().filter(p -> p.getKey().test(relativePathStr)) + .map(Entry::getValue).findFirst().orElse(null); + } + if (restart != null) { + log.debugf("Watch %s from: %s", relativePath, root); watchedRootPaths.add(relativePathStr); - putLastModifiedTime(path, relativePath, timestamps); + putLastModifiedTime(path, relativePath, restart, 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()) { + // Then process glob patterns + for (Entry e : watchedFilePaths.entrySet()) { + String watchedFilePath = e.getKey(); Path path = Paths.get(watchedFilePath); - if (path.isAbsolute()) { - if (Files.exists(path)) { - log.debugf("Watch %s", path); - putLastModifiedTime(path, path, timestamps); - } - } else if (!watchedRootPaths.contains(watchedFilePath) && maybeGlobPattern(watchedFilePath)) { + if (!path.isAbsolute() && !watchedRootPaths.contains(e.getKey()) && 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); + for (WatchedPath extra : expandGlobPattern(root, resolvedPath, watchedFilePath, e.getValue())) { + timestamps.watchedPaths.put(extra.filePath, extra); } } } } } } - timestamps.watchedFilePaths.putAll(extraWatchedFilePaths); + + // Finally process watched absolute paths + for (Entry e : watchedFilePaths.entrySet()) { + String watchedFilePath = e.getKey(); + Path path = Paths.get(watchedFilePath); + if (path.isAbsolute()) { + log.debugf("Watch %s", path); + if (Files.exists(path)) { + putLastModifiedTime(path, path, e.getValue(), timestamps); + } else { + // The watched file does not exist yet but we still need to keep track of this path + timestamps.watchedPaths.put(path, new WatchedPath(path, path, e.getValue(), -1)); + } + } + } + + log.debugf("Watched paths: %s", timestamps.watchedPaths.values()); return this; } @@ -1167,10 +1189,10 @@ private boolean maybeGlobPattern(String path) { return path.contains("*") || path.contains("?"); } - private void putLastModifiedTime(Path filePath, Path keyPath, TimestampSet timestamps) { + private void putLastModifiedTime(Path path, Path relativePath, boolean restart, TimestampSet timestamps) { try { - FileTime lastModifiedTime = Files.getLastModifiedTime(filePath); - timestamps.watchedFileTimestamps.put(keyPath, lastModifiedTime.toMillis()); + FileTime lastModifiedTime = Files.getLastModifiedTime(path); + timestamps.watchedPaths.put(path, new WatchedPath(path, relativePath, restart, lastModifiedTime.toMillis())); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -1210,9 +1232,9 @@ public void close() throws IOException { } } - private Map expandGlobPattern(Path root, Path path, String pattern) { + private List expandGlobPattern(Path root, Path path, String pattern, boolean restart) { PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + path.toString()); - Map files = new HashMap<>(); + List files = new ArrayList<>(); try { Files.walkFileTree(root, new SimpleFileVisitor() { @Override @@ -1220,7 +1242,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { if (pathMatcher.matches(file)) { Path relativePath = root.relativize(file); log.debugf("Glob pattern [%s] matched %s from %s", pattern, relativePath, root); - files.put(relativePath, attrs.lastModifiedTime().toMillis()); + files.add(new WatchedPath(file, relativePath, restart, attrs.lastModifiedTime().toMillis())); } return FileVisitResult.CONTINUE; } @@ -1261,25 +1283,30 @@ public boolean isLiveReloadEnabled() { } static class TimestampSet { - final Map watchedFileTimestamps = new ConcurrentHashMap<>(); final Map classFileChangeTimeStamps = new ConcurrentHashMap<>(); final Map classFilePathToSourceFilePath = new ConcurrentHashMap<>(); - // file path -> isRestartNeeded - private volatile Map watchedFilePaths = Collections.emptyMap(); - volatile List, Boolean>> watchedFilePredicates = Collections.emptyList(); + volatile Map watchedPaths = new ConcurrentHashMap<>(); + + // The current paths and predicates from all HotDeploymentWatchedFileBuildItems + volatile Map watchedFilePaths; + volatile List, Boolean>> watchedFilePredicates; public void merge(TimestampSet other) { - watchedFileTimestamps.putAll(other.watchedFileTimestamps); classFileChangeTimeStamps.putAll(other.classFileChangeTimeStamps); classFilePathToSourceFilePath.putAll(other.classFilePathToSourceFilePath); - Map newVal = new HashMap<>(watchedFilePaths); - newVal.putAll(other.watchedFilePaths); - watchedFilePaths = newVal; - // The list of predicates should be effectively immutable - watchedFilePredicates = other.watchedFilePredicates; + Map newVal = new HashMap<>(watchedPaths); + newVal.putAll(other.watchedPaths); + watchedPaths = newVal; } - boolean isWatchedFileRestartNeeded(String changedFile) { + boolean isRestartNeeded(String changedFile) { + // First try to match all existing watched paths + for (WatchedPath path : watchedPaths.values()) { + if (path.matches(changedFile)) { + return path.restartNeeded; + } + } + // Then try to match a new file that was added to a resource root Boolean ret = watchedFilePaths.get(changedFile); if (ret == null) { ret = false; @@ -1291,6 +1318,57 @@ boolean isWatchedFileRestartNeeded(String changedFile) { } return ret; } + + } + + private static class WatchedPath { + + final Path filePath; + + // Used to match a HotDeploymentWatchedFileBuildItem + final Path matchPath; + + // Last modification time or -1 if the file does not exist + volatile long lastModified; + + // HotDeploymentWatchedFileBuildItem.restartNeeded + final boolean restartNeeded; + + private WatchedPath(Path path, Path relativePath, boolean restartNeeded, long lastModified) { + this.filePath = path; + this.matchPath = relativePath; + this.restartNeeded = restartNeeded; + this.lastModified = lastModified; + } + + private boolean matches(String changedFile) { + return isAbsolute() ? filePath.toString().equals(changedFile) : getOSAgnosticMatchPath().equals(changedFile); + } + + private String getOSAgnosticMatchPath() { + return toOSAgnosticPathStr(matchPath.toString()); + } + + private boolean isAbsolute() { + return matchPath.isAbsolute(); + } + + @Override + public String toString() { + return "WatchedPath [matchPath=" + matchPath + ", filePath=" + filePath + ", restartNeeded=" + restartNeeded + "]"; + } + + } + + /** + * @param path + * @return an OS-agnostic path; {@code /} is used as a separator + */ + private static String toOSAgnosticPathStr(String path) { + if (File.separatorChar != '/') { + path = path.replace(File.separatorChar, '/'); + } + return path; } public String[] getCommandLineArgs() {