From 74365b79a270b914c0dd3ed068070170705cf5a0 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 --- .../dev/RuntimeUpdatesProcessor.java | 199 ++++++++++-------- 1 file changed, 108 insertions(+), 91 deletions(-) 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 90efe5480d3f1f..601a3eecff6976 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,14 @@ Set checkForFileChange() { return checkForFileChange(DevModeContext.ModuleInfo::getMain, main); } + /** + * The returned set contains the value from the matched HotDeploymentWatchedFileBuildItem, i.e. the location or the relative + * path for predicates a glob patterns. + * + * @param cuf + * @param timestampSet + * @return the set of modified files + */ Set checkForFileChange(Function cuf, TimestampSet timestampSet) { Set ret = new HashSet<>(); @@ -901,14 +909,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()) { @@ -944,70 +951,59 @@ Set 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()) { + if (roots.stream().noneMatch(watchedPath.filePath::startsWith)) { + // The watched path does not come from the current compilation unit continue; } + boolean isAbsolute = watchedPath.matchPath.isAbsolute(); 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) { + ret.add(watchedPath.matchPath.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(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()); + ret.add(watchedPath.matchPath.toString()); } - if (!isAbsolute) { - Path target = outputDir.resolve(watchedFilePath); + Path target = outputDir.resolve(watchedPath.matchPath); try { FileUtil.deleteIfExists(target); } catch (IOException e) { @@ -1087,8 +1083,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,8 +1091,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<>(); + timestamps.watchedFilePredicates = watchedFilePredicates; + Set watchedRootPaths = new HashSet<>(); for (DevModeContext.ModuleInfo module : context.getAllModules()) { List compilationUnits = cuf.apply(module); @@ -1112,7 +1108,7 @@ private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map rootPaths = PathList.of(Path.of(rootPath)); } for (Path root : rootPaths) { - // First find all matching paths from the root + // First find all matching paths from all roots try (final Stream walk = Files.walk(root)) { walk.forEach(path -> { if (path.equals(root)) { @@ -1122,11 +1118,15 @@ private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map // /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))) { + 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", relativePathStr, root); watchedRootPaths.add(relativePathStr); - putLastModifiedTime(path, relativePath, timestamps); + putLastModifiedTime(path, relativePath, restart, timestamps); } }); } catch (IOException e) { @@ -1135,31 +1135,25 @@ private RuntimeUpdatesProcessor setWatchedFilePathsInternal(Map // 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()) { + 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); + putLastModifiedTime(path, path, e.getValue(), timestamps); } - } else if (!watchedRootPaths.contains(watchedFilePath) && maybeGlobPattern(watchedFilePath)) { + } else if (!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); + log.debugf("Watched paths: %s", timestamps.watchedPaths); return this; } @@ -1167,10 +1161,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 +1204,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 +1214,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 +1255,25 @@ 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 + Map newVal = new HashMap<>(watchedPaths); + newVal.putAll(other.watchedPaths); + watchedPaths = newVal; + watchedFilePaths = other.watchedFilePaths; watchedFilePredicates = other.watchedFilePredicates; } - boolean isWatchedFileRestartNeeded(String changedFile) { + boolean isRestartNeeded(String changedFile) { Boolean ret = watchedFilePaths.get(changedFile); if (ret == null) { ret = false; @@ -1291,6 +1285,29 @@ boolean isWatchedFileRestartNeeded(String changedFile) { } return ret; } + + } + + static class WatchedPath { + + final Path filePath; + + // Used to match a HotDeploymentWatchedFileBuildItem + final Path matchPath; + + // Last modification time + 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; + } + } public String[] getCommandLineArgs() {