465854 - Provide java.nio.file.WatchService alternative for Scanner

+ Fix "glob:" PathMatcher behavior to work consistently
  even on Windows.
This commit is contained in:
joakim 2015-04-29 18:08:30 -07:00
parent a6cc4ff2f5
commit cbc263da92
2 changed files with 206 additions and 45 deletions

View File

@ -20,6 +20,7 @@ package org.eclipse.jetty.util;
import static java.nio.file.StandardWatchEventKinds.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.FileSystem;
@ -41,6 +42,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
@ -60,8 +62,40 @@ import org.eclipse.jetty.util.log.Logger;
*/
public class PathWatcher extends AbstractLifeCycle implements Runnable
{
/**
* Set to true to enable super noisy debug logging
*/
private static final boolean NOISY = false;
private static final boolean IS_WINDOWS;
static
{
String os = System.getProperty("os.name");
if (os == null)
{
IS_WINDOWS = false;
}
else
{
IS_WINDOWS = os.toLowerCase(Locale.ENGLISH).contains("windows");
}
}
public static class Config
{
private static final String PATTERN_SEP;
static
{
String sep = File.separator;
if (File.separatorChar == '\\')
{
sep = "\\\\";
}
PATTERN_SEP = sep;
}
protected final Path dir;
protected int recurseDepth = 0; // 0 means no sub-directories are scanned
protected List<PathMatcher> includes;
@ -87,13 +121,15 @@ public class PathWatcher extends AbstractLifeCycle implements Runnable
}
/**
* Add an exclude PathMatcher
* Add an exclude PathMatcher.
* <p>
* Note: this pattern is FileSystem specific (so use "/" for Linux and OSX, and "\\" for Windows)
*
* @param syntaxAndPattern
* the PathMatcher syntax and pattern to use
* @see FileSystem#getPathMatcher(String) for detail on syntax and pattern
*/
public void addExclude(String syntaxAndPattern)
public void addExclude(final String syntaxAndPattern)
{
if (LOG.isDebugEnabled())
{
@ -102,6 +138,29 @@ public class PathWatcher extends AbstractLifeCycle implements Runnable
addExclude(dir.getFileSystem().getPathMatcher(syntaxAndPattern));
}
/**
* Add a <code>glob:</code> syntax pattern exclude reference in a directory relative, os neutral, pattern.
* <p>
*
* <pre>
* On Linux:
* Config config = new Config(Path("/home/user/example"));
* config.addExcludeGlobRelative("*.war") => "glob:/home/user/example/*.war"
*
* On Windows
* Config config = new Config(Path("D:/code/examples"));
* config.addExcludeGlobRelative("*.war") => "glob:D:\\code\\examples\\*.war"
*
* </pre>
*
* @param pattern
* the pattern, in unixy format, relative to config.dir
*/
public void addExcludeGlobRelative(String pattern)
{
addExclude(toGlobPattern(dir,pattern));
}
/**
* Exclude hidden files and hidden directories
*/
@ -114,8 +173,9 @@ public class PathWatcher extends AbstractLifeCycle implements Runnable
LOG.debug("Adding hidden files and directories to exclusions");
}
excludeHidden = true;
addExclude("regex:^.*/\\..*$"); // ignore hidden files
addExclude("regex:^.*/\\..*/.*$"); // ignore files in hidden directories
addExclude("regex:^.*" + PATTERN_SEP + "\\..*$"); // ignore hidden files
addExclude("regex:^.*" + PATTERN_SEP + "\\..*" + PATTERN_SEP + ".*$"); // ignore files in hidden directories
}
}
@ -161,6 +221,29 @@ public class PathWatcher extends AbstractLifeCycle implements Runnable
addInclude(dir.getFileSystem().getPathMatcher(syntaxAndPattern));
}
/**
* Add a <code>glob:</code> syntax pattern reference in a directory relative, os neutral, pattern.
* <p>
*
* <pre>
* On Linux:
* Config config = new Config(Path("/home/user/example"));
* config.addIncludeGlobRelative("*.war") => "glob:/home/user/example/*.war"
*
* On Windows
* Config config = new Config(Path("D:/code/examples"));
* config.addIncludeGlobRelative("*.war") => "glob:D:\\code\\examples\\*.war"
*
* </pre>
*
* @param pattern
* the pattern, in unixy format, relative to config.dir
*/
public void addIncludeGlobRelative(String pattern)
{
addInclude(toGlobPattern(dir,pattern));
}
/**
* Add multiple include PathMatchers
*
@ -205,9 +288,12 @@ public class PathWatcher extends AbstractLifeCycle implements Runnable
{
if (matcher.matches(path))
{
if(NOISY) LOG.debug("Matched TRUE on {}",path);
return true;
}
}
if(NOISY) LOG.debug("Matched FALSE on {}",path);
return false;
}
@ -267,8 +353,7 @@ public class PathWatcher extends AbstractLifeCycle implements Runnable
}
/**
* Determine if the provided child directory should be recursed into
* based on the configured {@link #setRecurseDepth(int)}
* Determine if the provided child directory should be recursed into based on the configured {@link #setRecurseDepth(int)}
*
* @param child
* the child directory to test against
@ -286,6 +371,47 @@ public class PathWatcher extends AbstractLifeCycle implements Runnable
return (childDepth <= recurseDepth);
}
private String toGlobPattern(Path path, String subPattern)
{
StringBuilder s = new StringBuilder();
s.append("glob:");
if (path.getRoot() != null)
{
for (char c : path.getRoot().toString().toCharArray())
{
if (c == '\\')
{
s.append(PATTERN_SEP);
}
else
{
s.append(c);
}
}
}
for (Path segment : path)
{
s.append(segment);
s.append(PATTERN_SEP);
}
for (char c : subPattern.toCharArray())
{
if (c == '/')
{
s.append(PATTERN_SEP);
}
else
{
s.append(c);
}
}
return s.toString();
}
@Override
public String toString()
{
@ -394,7 +520,7 @@ public class PathWatcher extends AbstractLifeCycle implements Runnable
{
long now = System.currentTimeMillis();
long pastdue = this.timestamp + expiredUnit.toMillis(expiredDuration);
if (now >= pastdue)
if (now > pastdue)
{
return true;
}
@ -427,8 +553,8 @@ public class PathWatcher extends AbstractLifeCycle implements Runnable
{
final int prime = 31;
int result = 1;
result = (prime * result) + ((path == null) ? 0 : path.hashCode());
result = (prime * result) + ((type == null) ? 0 : type.hashCode());
result = (prime * result) + ((path == null)?0:path.hashCode());
result = (prime * result) + ((type == null)?0:type.hashCode());
return result;
}
@ -464,7 +590,10 @@ public class PathWatcher extends AbstractLifeCycle implements Runnable
private Map<WatchKey, Config> keys = new HashMap<>();
private List<Listener> listeners = new ArrayList<>();
private List<PathWatchEvent> pendingAddEvents = new ArrayList<>();
private long updateQuietTimeDuration = 500;
/**
* Update Quiet Time - set to 1000 ms as default (a lower value in Windows is not supported)
*/
private long updateQuietTimeDuration = 1000;
private TimeUnit updateQuietTimeUnit = TimeUnit.MILLISECONDS;
private Thread thread;
@ -560,9 +689,9 @@ public class PathWatcher extends AbstractLifeCycle implements Runnable
}
Config config = new Config(abs.getParent());
// the include for the directory itself
config.addInclude("glob:" + abs.getParent().toString());
config.addIncludeGlobRelative("");
// the include for the file
config.addInclude("glob:" + abs.toString());
config.addIncludeGlobRelative(file.getFileName().toString());
addDirectoryWatch(config);
}
@ -684,19 +813,13 @@ public class PathWatcher extends AbstractLifeCycle implements Runnable
// Process new events
if (pendingUpdateEvents.isEmpty())
{
if (LOG.isDebugEnabled())
{
LOG.debug("Waiting for take()");
}
if(NOISY) LOG.debug("Waiting for take()");
// wait for any event
key = watcher.take();
}
else
{
if (LOG.isDebugEnabled())
{
LOG.debug("Waiting for poll({}, {})",updateQuietTimeDuration,updateQuietTimeUnit);
}
if(NOISY) LOG.debug("Waiting for poll({}, {})",updateQuietTimeDuration,updateQuietTimeUnit);
key = watcher.poll(updateQuietTimeDuration,updateQuietTimeUnit);
if (key == null)
{
@ -823,8 +946,20 @@ public class PathWatcher extends AbstractLifeCycle implements Runnable
public void setUpdateQuietTime(long duration, TimeUnit unit)
{
this.updateQuietTimeDuration = duration;
this.updateQuietTimeUnit = unit;
long desiredMillis = unit.toMillis(duration);
if (IS_WINDOWS && desiredMillis < 1000)
{
LOG.warn("Quiet Time is too low for Microsoft Windows: {} < 1000 ms (defaulting to 1000 ms)",desiredMillis);
this.updateQuietTimeDuration = 1000;
this.updateQuietTimeUnit = TimeUnit.MILLISECONDS;
}
else
{
// All other OS's can use desired setting
this.updateQuietTimeDuration = duration;
this.updateQuietTimeUnit = unit;
}
}
@Override

View File

@ -18,15 +18,20 @@
package org.eclipse.jetty.util;
import static java.nio.file.StandardOpenOption.*;
import static org.eclipse.jetty.util.PathWatcher.PathWatchEventType.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
import static org.eclipse.jetty.util.PathWatcher.PathWatchEventType.ADDED;
import static org.eclipse.jetty.util.PathWatcher.PathWatchEventType.DELETED;
import static org.eclipse.jetty.util.PathWatcher.PathWatchEventType.MODIFIED;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@ -38,14 +43,17 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.toolchain.test.FS;
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("Temporary ignore until all platforms are tested")
public class PathWatcherTest
{
public static class PathWatchEventCapture implements PathWatcher.Listener
@ -75,7 +83,6 @@ public class PathWatcherTest
if (types == null)
{
types = new ArrayList<>();
this.events.put(key,types);
}
types.add(event.getType());
this.events.put(key,types);
@ -169,7 +176,7 @@ public class PathWatcherTest
byte chunkBuf[] = new byte[chunkBufLen];
Arrays.fill(chunkBuf,(byte)'x');
try (OutputStream out = Files.newOutputStream(path,CREATE,TRUNCATE_EXISTING,WRITE))
try (FileOutputStream out = new FileOutputStream(path.toFile()))
{
int left = fileSize;
@ -178,11 +185,30 @@ public class PathWatcherTest
int len = Math.min(left,chunkBufLen);
out.write(chunkBuf,0,len);
left -= chunkBufLen;
TimeUnit.MILLISECONDS.sleep(sleepMs);
out.flush();
// 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 = 1.5;
if (OS.IS_WINDOWS)
{
// Microsoft Windows filesystem is too slow for a lower multiplier
multiplier = 2.5;
}
TimeUnit.MILLISECONDS.sleep((long)((double)pathWatcher.getUpdateQuietTimeMillis() * multiplier));
}
private static final int KB = 1024;
private static final int MB = KB * KB;
@ -275,8 +301,8 @@ public class PathWatcherTest
PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
baseDirConfig.setRecurseDepth(2);
baseDirConfig.addExcludeHidden();
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*.war");
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*/WEB-INF/web.xml");
baseDirConfig.addIncludeGlobRelative("*.war");
baseDirConfig.addIncludeGlobRelative("*/WEB-INF/web.xml");
pathWatcher.addDirectoryWatch(baseDirConfig);
try
@ -284,7 +310,7 @@ public class PathWatcherTest
pathWatcher.start();
// Let quiet time do its thing
TimeUnit.MILLISECONDS.sleep(500);
awaitQuietTime(pathWatcher);
Map<String, PathWatchEventType[]> expected = new HashMap<>();
@ -320,8 +346,8 @@ public class PathWatcherTest
PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
baseDirConfig.setRecurseDepth(2);
baseDirConfig.addExcludeHidden();
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*.war");
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*/WEB-INF/web.xml");
baseDirConfig.addIncludeGlobRelative("*.war");
baseDirConfig.addIncludeGlobRelative("*/WEB-INF/web.xml");
pathWatcher.addDirectoryWatch(baseDirConfig);
try
@ -329,7 +355,7 @@ public class PathWatcherTest
pathWatcher.start();
// Pretend that startup occurred
TimeUnit.MILLISECONDS.sleep(500);
awaitQuietTime(pathWatcher);
// Update web.xml
updateFile(dir.resolve("bar/WEB-INF/web.xml"),"Hello Update");
@ -339,7 +365,7 @@ public class PathWatcherTest
Files.delete(dir.resolve("foo.war"));
// Let quiet time elapse
TimeUnit.MILLISECONDS.sleep(500);
awaitQuietTime(pathWatcher);
Map<String, PathWatchEventType[]> expected = new HashMap<>();
@ -375,8 +401,8 @@ public class PathWatcherTest
PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
baseDirConfig.setRecurseDepth(2);
baseDirConfig.addExcludeHidden();
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*.war");
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*/WEB-INF/web.xml");
baseDirConfig.addIncludeGlobRelative("*.war");
baseDirConfig.addIncludeGlobRelative("*/WEB-INF/web.xml");
pathWatcher.addDirectoryWatch(baseDirConfig);
try
@ -384,13 +410,13 @@ public class PathWatcherTest
pathWatcher.start();
// Pretend that startup occurred
TimeUnit.MILLISECONDS.sleep(500);
awaitQuietTime(pathWatcher);
// New war added
updateFile(dir.resolve("hello.war"),"Hello Update");
// Let quiet time elapse
TimeUnit.MILLISECONDS.sleep(500);
awaitQuietTime(pathWatcher);
Map<String, PathWatchEventType[]> expected = new HashMap<>();
@ -437,8 +463,8 @@ public class PathWatcherTest
PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
baseDirConfig.setRecurseDepth(2);
baseDirConfig.addExcludeHidden();
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*.war");
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*/WEB-INF/web.xml");
baseDirConfig.addIncludeGlobRelative("*.war");
baseDirConfig.addIncludeGlobRelative("*/WEB-INF/web.xml");
pathWatcher.addDirectoryWatch(baseDirConfig);
try
@ -446,13 +472,13 @@ public class PathWatcherTest
pathWatcher.start();
// Pretend that startup occurred
TimeUnit.MILLISECONDS.sleep(500);
awaitQuietTime(pathWatcher);
// New war added (slowly)
updateFileOverTime(dir.resolve("hello.war"),50 * MB,3,TimeUnit.SECONDS);
// Let quiet time elapse
TimeUnit.MILLISECONDS.sleep(500);
awaitQuietTime(pathWatcher);
Map<String, PathWatchEventType[]> expected = new HashMap<>();