// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.util; import static org.eclipse.jetty.util.PathWatcher.PathWatchEventType.*; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.toolchain.test.OS; import org.eclipse.jetty.toolchain.test.TestingDir; import org.eclipse.jetty.util.PathWatcher.PathWatchEvent; import org.eclipse.jetty.util.PathWatcher.PathWatchEventType; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; @Ignore("Disabled due to behavioral differences in various FileSystems (hard to write a single testcase that works in all scenarios)") public class PathWatcherTest { public static class PathWatchEventCapture implements PathWatcher.Listener { public final static String FINISH_TAG = "#finished#.tag"; private static final Logger LOG = Log.getLogger(PathWatcherTest.PathWatchEventCapture.class); private final Path baseDir; /** * Map of relative paths seen, to their events seen (in order seen) */ public Map<String, List<PathWatchEventType>> events = new HashMap<>(); public int latchCount = 1; public CountDownLatch finishedLatch; private PathWatchEventType triggerType; private Path triggerPath; public PathWatchEventCapture(Path baseDir) { this.baseDir = baseDir; } public void reset() { finishedLatch = new CountDownLatch(latchCount); events.clear(); } @Override public void onPathWatchEvent(PathWatchEvent event) { synchronized (events) { //if triggered by path if (triggerPath != null) { if (triggerPath.equals(event.getPath()) && (event.getType() == triggerType)) { LOG.debug("Encountered finish trigger: {} on {}",event.getType(),event.getPath()); finishedLatch.countDown(); } } else if (finishedLatch != null) { finishedLatch.countDown(); } Path relativePath = this.baseDir.relativize(event.getPath()); String key = relativePath.toString().replace(File.separatorChar,'/'); List<PathWatchEventType> types = this.events.get(key); if (types == null) { types = new ArrayList<>(); } types.add(event.getType()); this.events.put(key,types); LOG.debug("Captured Event: {} | {}",event.getType(),key); } } /** * Validate the events seen match expectations. * <p> * Note: order of events is only important when looking at a specific file or directory. Events for multiple * files can overlap in ways that this assertion doesn't care about. * * @param expectedEvents * the events expected */ public void assertEvents(Map<String, PathWatchEventType[]> expectedEvents) { assertThat("Event match (file|diretory) count",this.events.size(),is(expectedEvents.size())); for (Map.Entry<String, PathWatchEventType[]> entry : expectedEvents.entrySet()) { String relativePath = entry.getKey(); PathWatchEventType[] expectedTypes = entry.getValue(); assertEvents(relativePath,expectedTypes); } } /** * Validate the events seen match expectations. * <p> * Note: order of events is only important when looking at a specific file or directory. Events for multiple * files can overlap in ways that this assertion doesn't care about. * * @param relativePath * the test relative path to look for * * @param expectedEvents * the events expected */ public void assertEvents(String relativePath, PathWatchEventType... expectedEvents) { synchronized (events) { List<PathWatchEventType> actualEvents = this.events.get(relativePath); assertThat("Events for path [" + relativePath + "]",actualEvents,contains(expectedEvents)); } } /** * Set the path and type that will trigger this capture to be finished * * @param triggerPath * the trigger path we look for to know that the capture is complete * @param triggerType * the trigger type we look for to know that the capture is complete */ public void setFinishTrigger(Path triggerPath, PathWatchEventType triggerType) { this.triggerPath = triggerPath; this.triggerType = triggerType; this.latchCount = 1; this.finishedLatch = new CountDownLatch(1); LOG.debug("Setting finish trigger {} for path {}",triggerType,triggerPath); } public void setFinishTrigger (int count) { latchCount = count; finishedLatch = new CountDownLatch(latchCount); } /** * Await the countdown latch on the finish trigger * * @param pathWatcher * the watcher instance we are waiting on * @throws IOException * if unable to create the finish tag file * @throws InterruptedException * if unable to await the finish of the run * @see #setFinishTrigger(Path, PathWatchEventType) */ public void awaitFinish(PathWatcher pathWatcher) throws IOException, InterruptedException { //assertThat("Trigger Path must be set",triggerPath,notNullValue()); //assertThat("Trigger Type must be set",triggerType,notNullValue()); double multiplier = 25.0; long awaitMillis = (long)((double)pathWatcher.getUpdateQuietTimeMillis() * multiplier); LOG.debug("Waiting for finish ({} ms)",awaitMillis); assertThat("Timed Out (" + awaitMillis + "ms) waiting for capture to finish",finishedLatch.await(awaitMillis,TimeUnit.MILLISECONDS),is(true)); LOG.debug("Finished capture"); } } private static void updateFile(Path path, String newContents) throws IOException { try (FileOutputStream out = new FileOutputStream(path.toFile())) { out.write(newContents.getBytes(StandardCharsets.UTF_8)); out.flush(); out.getChannel().force(true); out.getFD().sync(); } } /** * Update (optionally create) a file over time. * <p> * The file will be created in a slowed down fashion, over the time specified. * * @param path * the file to update / create * @param fileSize * the ultimate file size to create * @param timeDuration * the time duration to take to create the file (approximate, not 100% accurate) * @param timeUnit * the time unit to take to create the file * @throws IOException * if unable to write file * @throws InterruptedException * if sleep between writes was interrupted */ private void updateFileOverTime(Path path, int fileSize, int timeDuration, TimeUnit timeUnit) throws IOException, InterruptedException { // how long to sleep between writes int sleepMs = 100; // how many millis to spend writing entire file size long totalMs = timeUnit.toMillis(timeDuration); // how many write chunks to write int writeCount = (int)((int)totalMs / (int)sleepMs); // average chunk buffer int chunkBufLen = fileSize / writeCount; byte chunkBuf[] = new byte[chunkBufLen]; Arrays.fill(chunkBuf,(byte)'x'); try (FileOutputStream out = new FileOutputStream(path.toFile())) { int left = fileSize; while (left > 0) { int len = Math.min(left,chunkBufLen); out.write(chunkBuf,0,len); left -= chunkBufLen; out.flush(); out.getChannel().force(true); // Force file to actually write to disk. // Skipping any sort of filesystem caching of the write out.getFD().sync(); TimeUnit.MILLISECONDS.sleep(sleepMs); } } } /** * Sleep longer than the quiet time. * * @param pathWatcher * the path watcher to inspect for its quiet time * @throws InterruptedException * if unable to sleep */ private static void awaitQuietTime(PathWatcher pathWatcher) throws InterruptedException { double multiplier = 5.0; if (OS.IS_WINDOWS) { // Microsoft Windows filesystem is too slow for a lower multiplier multiplier = 6.0; } TimeUnit.MILLISECONDS.sleep((long)((double)pathWatcher.getUpdateQuietTimeMillis() * multiplier)); } private static final int KB = 1024; private static final int MB = KB * KB; @Rule public TestingDir testdir = new TestingDir(); @Test public void testConfig_ShouldRecurse_0() throws IOException { Path dir = testdir.getEmptyPathDir(); // Create a few directories Files.createDirectories(dir.resolve("a/b/c/d")); PathWatcher.Config config = new PathWatcher.Config(dir); config.setRecurseDepth(0); assertThat("Config.recurse[0].shouldRecurse[./a/b]",config.shouldRecurseDirectory(dir.resolve("a/b")),is(false)); assertThat("Config.recurse[0].shouldRecurse[./a]",config.shouldRecurseDirectory(dir.resolve("a")),is(false)); assertThat("Config.recurse[0].shouldRecurse[./]",config.shouldRecurseDirectory(dir),is(false)); } @Test public void testConfig_ShouldRecurse_1() throws IOException { Path dir = testdir.getEmptyPathDir(); // Create a few directories Files.createDirectories(dir.resolve("a/b/c/d")); PathWatcher.Config config = new PathWatcher.Config(dir); config.setRecurseDepth(1); assertThat("Config.recurse[1].shouldRecurse[./a/b]",config.shouldRecurseDirectory(dir.resolve("a/b")),is(false)); assertThat("Config.recurse[1].shouldRecurse[./a]",config.shouldRecurseDirectory(dir.resolve("a")),is(true)); assertThat("Config.recurse[1].shouldRecurse[./]",config.shouldRecurseDirectory(dir),is(true)); } @Test public void testConfig_ShouldRecurse_2() throws IOException { Path dir = testdir.getEmptyPathDir(); // Create a few directories Files.createDirectories(dir.resolve("a/b/c/d")); PathWatcher.Config config = new PathWatcher.Config(dir); config.setRecurseDepth(2); assertThat("Config.recurse[1].shouldRecurse[./a/b/c]",config.shouldRecurseDirectory(dir.resolve("a/b/c")),is(false)); assertThat("Config.recurse[1].shouldRecurse[./a/b]",config.shouldRecurseDirectory(dir.resolve("a/b")),is(true)); assertThat("Config.recurse[1].shouldRecurse[./a]",config.shouldRecurseDirectory(dir.resolve("a")),is(true)); assertThat("Config.recurse[1].shouldRecurse[./]",config.shouldRecurseDirectory(dir),is(true)); } @Test public void testConfig_ShouldRecurse_3() throws IOException { Path dir = testdir.getEmptyPathDir(); //Create some deep dirs Files.createDirectories(dir.resolve("a/b/c/d/e/f/g")); PathWatcher.Config config = new PathWatcher.Config(dir); config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); assertThat("Config.recurse[1].shouldRecurse[./a/b/c/d/g]",config.shouldRecurseDirectory(dir.resolve("a/b/c/d/g")),is(true)); assertThat("Config.recurse[1].shouldRecurse[./a/b/c/d/f]",config.shouldRecurseDirectory(dir.resolve("a/b/c/d/f")),is(true)); assertThat("Config.recurse[1].shouldRecurse[./a/b/c/d/e]",config.shouldRecurseDirectory(dir.resolve("a/b/c/d/e")),is(true)); assertThat("Config.recurse[1].shouldRecurse[./a/b/c/d]",config.shouldRecurseDirectory(dir.resolve("a/b/c/d")),is(true)); assertThat("Config.recurse[1].shouldRecurse[./a/b/c]",config.shouldRecurseDirectory(dir.resolve("a/b/c")),is(true)); assertThat("Config.recurse[1].shouldRecurse[./a/b]",config.shouldRecurseDirectory(dir.resolve("a/b")),is(true)); assertThat("Config.recurse[1].shouldRecurse[./a]",config.shouldRecurseDirectory(dir.resolve("a")),is(true)); assertThat("Config.recurse[1].shouldRecurse[./]",config.shouldRecurseDirectory(dir),is(true)); } @Test public void testRestart() throws Exception { Path dir = testdir.getEmptyPathDir(); Files.createDirectories(dir.resolve("b/c")); Files.createFile(dir.resolve("a.txt")); Files.createFile(dir.resolve("b.txt")); PathWatcher pathWatcher = new PathWatcher(); pathWatcher.setNotifyExistingOnStart(true); pathWatcher.setUpdateQuietTime(500,TimeUnit.MILLISECONDS); // Add listener PathWatchEventCapture capture = new PathWatchEventCapture(dir); capture.setFinishTrigger(2); pathWatcher.addListener(capture); PathWatcher.Config config = new PathWatcher.Config(dir); config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); config.addIncludeGlobRelative("*.txt"); pathWatcher.watch(config); try { pathWatcher.start(); // Let quiet time do its thing awaitQuietTime(pathWatcher); Map<String, PathWatchEventType[]> expected = new HashMap<>(); expected.put("a.txt",new PathWatchEventType[] {ADDED}); expected.put("b.txt",new PathWatchEventType[] {ADDED}); capture.assertEvents(expected); //stop it pathWatcher.stop(); capture.reset(); Thread.currentThread().sleep(1000); pathWatcher.start(); awaitQuietTime(pathWatcher); capture.assertEvents(expected); } finally { pathWatcher.stop(); } } /** * When starting up the PathWatcher, the events should occur * indicating files that are of interest that already exist * on the filesystem. * * @throws Exception * on test failure */ @Test public void testStartupFindFiles() throws Exception { Path dir = testdir.getEmptyPathDir(); // Files we are interested in Files.createFile(dir.resolve("foo.war")); Files.createDirectories(dir.resolve("bar/WEB-INF")); Files.createFile(dir.resolve("bar/WEB-INF/web.xml")); // Files we don't care about Files.createFile(dir.resolve("foo.war.backup")); Files.createFile(dir.resolve(".hidden.war")); Files.createDirectories(dir.resolve(".wat/WEB-INF")); Files.createFile(dir.resolve(".wat/huh.war")); Files.createFile(dir.resolve(".wat/WEB-INF/web.xml")); PathWatcher pathWatcher = new PathWatcher(); pathWatcher.setUpdateQuietTime(300,TimeUnit.MILLISECONDS); // Add listener PathWatchEventCapture capture = new PathWatchEventCapture(dir); pathWatcher.addListener(capture); // Add test dir configuration PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir); baseDirConfig.setRecurseDepth(2); baseDirConfig.addExcludeHidden(); baseDirConfig.addIncludeGlobRelative("*.war"); baseDirConfig.addIncludeGlobRelative("*/WEB-INF/web.xml"); pathWatcher.watch(baseDirConfig); try { pathWatcher.start(); // Let quiet time do its thing awaitQuietTime(pathWatcher); Map<String, PathWatchEventType[]> expected = new HashMap<>(); expected.put("bar/WEB-INF/web.xml",new PathWatchEventType[] { ADDED }); expected.put("foo.war",new PathWatchEventType[] { ADDED }); capture.assertEvents(expected); } finally { pathWatcher.stop(); } } @Test public void testGlobPattern () throws Exception { Path dir = testdir.getEmptyPathDir(); // Files we are interested in Files.createFile(dir.resolve("a.txt")); Files.createDirectories(dir.resolve("b/b.txt")); Files.createDirectories(dir.resolve("c/d")); Files.createFile(dir.resolve("c/d/d.txt")); Files.createFile(dir.resolve(".foo.txt")); // Files we don't care about Files.createFile(dir.resolve("txt.foo")); Files.createFile(dir.resolve("b/foo.xml")); PathWatcher pathWatcher = new PathWatcher(); pathWatcher.setUpdateQuietTime(300,TimeUnit.MILLISECONDS); // Add listener PathWatchEventCapture capture = new PathWatchEventCapture(dir); capture.setFinishTrigger(3); pathWatcher.addListener(capture); // Add test dir configuration PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir); baseDirConfig.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); baseDirConfig.addExcludeHidden(); baseDirConfig.addIncludeGlobRelative("**.txt"); pathWatcher.watch(baseDirConfig); try { pathWatcher.start(); // Let quiet time do its thing awaitQuietTime(pathWatcher); Map<String, PathWatchEventType[]> expected = new HashMap<>(); expected.put("a.txt",new PathWatchEventType[] { ADDED }); expected.put("b/b.txt",new PathWatchEventType[] { ADDED }); expected.put("c/d/d.txt",new PathWatchEventType[] { ADDED }); capture.assertEvents(expected); } finally { pathWatcher.stop(); } } @Test public void testDeployFiles_Update_Delete() throws Exception { Path dir = testdir.getEmptyPathDir(); // Files we are interested in Files.createFile(dir.resolve("foo.war")); Files.createDirectories(dir.resolve("bar/WEB-INF")); Files.createFile(dir.resolve("bar/WEB-INF/web.xml")); PathWatcher pathWatcher = new PathWatcher(); pathWatcher.setUpdateQuietTime(300,TimeUnit.MILLISECONDS); // Add listener PathWatchEventCapture capture = new PathWatchEventCapture(dir); capture.setFinishTrigger(5); pathWatcher.addListener(capture); // Add test dir configuration PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir); baseDirConfig.setRecurseDepth(2); baseDirConfig.addExcludeHidden(); baseDirConfig.addIncludeGlobRelative("*.war"); baseDirConfig.addIncludeGlobRelative("*/WEB-INF/web.xml"); pathWatcher.watch(baseDirConfig); try { pathWatcher.start(); // Pretend that startup occurred awaitQuietTime(pathWatcher); // Update web.xml Path webFile = dir.resolve("bar/WEB-INF/web.xml"); //capture.setFinishTrigger(webFile,MODIFIED); updateFile(webFile,"Hello Update"); // Delete war Files.delete(dir.resolve("foo.war")); // Add a another new war Files.createFile(dir.resolve("bar.war")); // Let capture complete capture.awaitFinish(pathWatcher); Map<String, PathWatchEventType[]> expected = new HashMap<>(); expected.put("bar/WEB-INF/web.xml",new PathWatchEventType[] { ADDED, MODIFIED }); expected.put("foo.war",new PathWatchEventType[] { ADDED, DELETED }); expected.put("bar.war",new PathWatchEventType[] { ADDED }); capture.assertEvents(expected); } finally { pathWatcher.stop(); } } @Test public void testDeployFiles_NewWar() throws Exception { Path dir = testdir.getEmptyPathDir(); // Files we are interested in Files.createFile(dir.resolve("foo.war")); Files.createDirectories(dir.resolve("bar/WEB-INF")); Files.createFile(dir.resolve("bar/WEB-INF/web.xml")); PathWatcher pathWatcher = new PathWatcher(); pathWatcher.setUpdateQuietTime(300,TimeUnit.MILLISECONDS); // Add listener PathWatchEventCapture capture = new PathWatchEventCapture(dir); pathWatcher.addListener(capture); // Add test dir configuration PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir); baseDirConfig.setRecurseDepth(2); baseDirConfig.addExcludeHidden(); baseDirConfig.addIncludeGlobRelative("*.war"); baseDirConfig.addIncludeGlobRelative("*/WEB-INF/web.xml"); pathWatcher.watch(baseDirConfig); try { pathWatcher.start(); // Pretend that startup occurred awaitQuietTime(pathWatcher); // New war added Path warFile = dir.resolve("hello.war"); capture.setFinishTrigger(warFile,MODIFIED); updateFile(warFile,"Hello Update"); // Let capture finish capture.awaitFinish(pathWatcher); Map<String, PathWatchEventType[]> expected = new HashMap<>(); expected.put("bar/WEB-INF/web.xml",new PathWatchEventType[] { ADDED }); expected.put("foo.war",new PathWatchEventType[] { ADDED }); expected.put("hello.war",new PathWatchEventType[] { ADDED, MODIFIED }); capture.assertEvents(expected); } finally { pathWatcher.stop(); } } /** * Pretend to add a new war file that is large, and being copied into place * using some sort of technique that is slow enough that it takes a while for * the entire war file to exist in place. * <p> * This is to test the quiet time logic to ensure that only a single MODIFIED event occurs on this new war file * * @throws Exception * on test failure */ @Test public void testDeployFiles_NewWar_LargeSlowCopy() throws Exception { Path dir = testdir.getEmptyPathDir(); // Files we are interested in Files.createFile(dir.resolve("foo.war")); Files.createDirectories(dir.resolve("bar/WEB-INF")); Files.createFile(dir.resolve("bar/WEB-INF/web.xml")); PathWatcher pathWatcher = new PathWatcher(); pathWatcher.setUpdateQuietTime(500,TimeUnit.MILLISECONDS); // Add listener PathWatchEventCapture capture = new PathWatchEventCapture(dir); pathWatcher.addListener(capture); // Add test dir configuration PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir); baseDirConfig.setRecurseDepth(2); baseDirConfig.addExcludeHidden(); baseDirConfig.addIncludeGlobRelative("*.war"); baseDirConfig.addIncludeGlobRelative("*/WEB-INF/web.xml"); pathWatcher.watch(baseDirConfig); try { pathWatcher.start(); // Pretend that startup occurred awaitQuietTime(pathWatcher); // New war added (slowly) Path warFile = dir.resolve("hello.war"); capture.setFinishTrigger(warFile,MODIFIED); updateFileOverTime(warFile,50 * MB,3,TimeUnit.SECONDS); // Let capture finish capture.awaitFinish(pathWatcher); Map<String, PathWatchEventType[]> expected = new HashMap<>(); expected.put("bar/WEB-INF/web.xml",new PathWatchEventType[] { ADDED }); expected.put("foo.war",new PathWatchEventType[] { ADDED }); expected.put("hello.war",new PathWatchEventType[] { ADDED, MODIFIED }); capture.assertEvents(expected); } finally { pathWatcher.stop(); } } }