blob: 5e05758366c5e31cb54d642b32e16e67b55edb96 [file] [log] [blame]
//
// ========================================================================
// 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();
}
}
}