Skip to content

Commit 339595f

Browse files
committed
With the work done by @erichelgeson we now have native OS X File Watching services that do not require file polling for large projects
1 parent 39b8259 commit 339595f

File tree

3 files changed

+197
-1
lines changed

3 files changed

+197
-1
lines changed

grails-bootstrap/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ dependencies {
44
compile ( "org.codehaus.groovy:groovy-xml:$groovyVersion" )
55
compile ( "org.codehaus.groovy:groovy-templates:$groovyVersion" )
66
compile "org.yaml:snakeyaml:1.14"
7+
compile "io.methvin:directory-watcher:0.3.0" //TODO: Needs JNA to run
78

89
compileOnly("org.fusesource.jansi:jansi:$jansiVersion")
910
compileOnly("jline:jline:$jlineVersion")

grails-bootstrap/src/main/groovy/org/grails/io/watch/DirectoryWatcher.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,22 @@ public DirectoryWatcher() {
4444
setDaemon(true);
4545
AbstractDirectoryWatcher directoryWatcherDelegate;
4646
try {
47-
directoryWatcherDelegate = (AbstractDirectoryWatcher) Class.forName("org.grails.io.watch.WatchServiceDirectoryWatcher").newInstance();
47+
if(System.getProperty("os.name").equals("Mac OS X")) {
48+
Boolean jnaAvailable = false;
49+
try {
50+
Class.forName( "com.sun.jna.Pointer" );
51+
jnaAvailable = true;
52+
} catch( ClassNotFoundException e ) {
53+
LOG.error("Error Initializing Native OS X File Event Watcher. Add JNA to classpath for Faster File Watching performance.");
54+
}
55+
if(jnaAvailable) {
56+
directoryWatcherDelegate = (AbstractDirectoryWatcher) Class.forName("org.grails.io.watch.MacOsWatchServiceDirectoryWatcher").newInstance();
57+
} else {
58+
directoryWatcherDelegate = (AbstractDirectoryWatcher) Class.forName("org.grails.io.watch.WatchServiceDirectoryWatcher").newInstance();
59+
}
60+
} else {
61+
directoryWatcherDelegate = (AbstractDirectoryWatcher) Class.forName("org.grails.io.watch.WatchServiceDirectoryWatcher").newInstance();
62+
}
4863
} catch (Throwable e) {
4964
LOG.info("Exception while trying to load WatchServiceDirectoryWatcher (this is probably Java 6 and WatchService isn't available). Falling back to PollingDirectoryWatcher.", e);
5065
directoryWatcherDelegate = new PollingDirectoryWatcher();
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package org.grails.io.watch;
2+
3+
import java.nio.file.WatchEvent.Kind;
4+
import io.methvin.watchservice.MacOSXListeningWatchService;
5+
import io.methvin.watchservice.WatchablePath;
6+
import static io.methvin.watcher.DirectoryChangeEvent.EventType.*;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
import java.io.File;
10+
import java.io.IOException;
11+
import java.nio.file.*;
12+
import java.nio.file.attribute.BasicFileAttributes;
13+
import java.util.*;
14+
import java.util.concurrent.ConcurrentHashMap;
15+
16+
/**
17+
* Implementation of a {@link AbstractDirectoryWatcher} that uses {@link java.nio.WatchService}.
18+
* This implementation is used for Java 7 and later.
19+
* @author Eric Helgeson
20+
* @author David Estes
21+
* @since 3.2
22+
* @see DirectoryWatcher
23+
*/
24+
class MacOsWatchServiceDirectoryWatcher extends AbstractDirectoryWatcher {
25+
26+
private static final Logger LOG = LoggerFactory.getLogger(MacOsWatchServiceDirectoryWatcher.class);
27+
private Map<WatchKey, List<String>> watchKeyToExtensionsMap = new ConcurrentHashMap<WatchKey, List<String>>();
28+
private Set<Path> individualWatchedFiles = new HashSet<Path>();
29+
30+
private final WatchService watchService;
31+
32+
@SuppressWarnings("unchecked")
33+
private static <T> WatchEvent<T> cast(WatchEvent<?> event) {
34+
return (WatchEvent<T>)event;
35+
}
36+
37+
public MacOsWatchServiceDirectoryWatcher(){
38+
try {
39+
watchService = new MacOSXListeningWatchService();
40+
} catch (Exception e) {
41+
throw new RuntimeException(e);
42+
}
43+
}
44+
45+
@Override
46+
public void run() {
47+
while (active) {
48+
try {
49+
WatchKey watchKey;
50+
try {
51+
watchKey = watchService.take();
52+
} catch (InterruptedException x) {
53+
return;
54+
}
55+
for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
56+
WatchEvent.Kind<?> kind = watchEvent.kind();
57+
if (kind == StandardWatchEventKinds.OVERFLOW) {
58+
// TODO how is this supposed to be handled? I think the best thing to do is ignore it, but I'm not positive
59+
LOG.warn("WatchService Overflow occurred");
60+
continue;
61+
}
62+
WatchEvent<Path> pathWatchEvent = cast(watchEvent);
63+
Path child = pathWatchEvent.context();
64+
File childFile = child.toFile();
65+
if(individualWatchedFiles.contains(child) || individualWatchedFiles.contains(child.normalize())){
66+
if(kind == StandardWatchEventKinds.ENTRY_CREATE){
67+
fireOnNew(childFile);
68+
}else if(kind == StandardWatchEventKinds.ENTRY_MODIFY){
69+
fireOnChange(childFile);
70+
}else if(kind == StandardWatchEventKinds.ENTRY_DELETE){
71+
// do nothing... there's no way to communicate deletions
72+
}
73+
}else{
74+
List<String> fileExtensions = watchKeyToExtensionsMap.get(watchKey);
75+
if(fileExtensions==null){
76+
// this event didn't match a file in individualWatchedFiles so it's a not an individual file we're interested in
77+
// this event also didn't match a directory that we're interested in (if it did, fileExtentions wouldn't be null)
78+
// so it must be event for a file we're not interested in. An example of how this can happen is:
79+
// there's a directory with files in it like this:
80+
// /images/a.png
81+
// /images/b.png
82+
// by using the addWatchFile method, /images/a.png is watched.
83+
// Now, /images/b.png is changed. Because java.nio.file.WatchService watches directories, it gets a WatchEvent
84+
// for /images/b.png. But we aren't interested in that.
85+
LOG.debug("WatchService received an event for a file/directory that it's not interested in.");
86+
}else{
87+
if(kind==StandardWatchEventKinds.ENTRY_CREATE){
88+
// new directory created, so watch its contents
89+
addWatchDirectory(child,fileExtensions);
90+
if(childFile.isDirectory() && childFile.exists()) {
91+
final File[] files = childFile.listFiles();
92+
if(files != null) {
93+
for (File newFile : files) {
94+
if(isValidFileToMonitor(newFile, fileExtensions)) {
95+
fireOnNew(newFile);
96+
}
97+
}
98+
}
99+
}
100+
}
101+
if(isValidFileToMonitor(childFile,fileExtensions)){
102+
if(kind == StandardWatchEventKinds.ENTRY_CREATE){
103+
fireOnNew(childFile);
104+
}else if(kind == StandardWatchEventKinds.ENTRY_MODIFY){
105+
fireOnChange(childFile);
106+
}else if(kind == StandardWatchEventKinds.ENTRY_DELETE){
107+
// do nothing... there's no way to communicate deletions
108+
}
109+
}
110+
}
111+
}
112+
}
113+
watchKey.reset();
114+
} catch (Exception e) {
115+
LOG.error(e.toString());
116+
// ignore
117+
}
118+
}
119+
try {
120+
watchService.close();
121+
} catch (IOException e) {
122+
LOG.debug("Exception while closing watchService", e);
123+
}
124+
}
125+
126+
@Override
127+
public void addWatchFile(File fileToWatch) {
128+
if(!isValidFileToMonitor(fileToWatch, Arrays.asList("*"))) return;
129+
try {
130+
if(!fileToWatch.exists()) return;
131+
Path pathToWatch = fileToWatch.toPath().toAbsolutePath();
132+
individualWatchedFiles.add(pathToWatch);
133+
WatchablePath watchPath = new WatchablePath(pathToWatch);
134+
Kind[] events = new Kind[] { StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY };
135+
watchPath.register(watchService, events);
136+
} catch (IOException e) {
137+
throw new RuntimeException(e);
138+
}
139+
}
140+
141+
@Override
142+
public void addWatchDirectory(File dir, final List<String> fileExtensions) {
143+
Path dirPath = dir.toPath();
144+
addWatchDirectory(dirPath, fileExtensions);
145+
}
146+
147+
private void addWatchDirectory(Path dir, final List<String> fileExtensions) {
148+
if(!isValidDirectoryToMonitor(dir.toFile())){
149+
return;
150+
}
151+
try {
152+
//add the subdirectories too
153+
Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
154+
@Override
155+
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
156+
throws IOException
157+
{
158+
if(!isValidDirectoryToMonitor(dir.toFile())){
159+
return FileVisitResult.SKIP_SUBTREE;
160+
}
161+
WatchablePath watchPath = new WatchablePath(dir);
162+
Kind[] events = new Kind[] { StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY };
163+
WatchKey watchKey = watchPath.register(watchService, events);
164+
final List<String> originalFileExtensions = watchKeyToExtensionsMap.get(watchKey);
165+
if(originalFileExtensions==null){
166+
watchKeyToExtensionsMap.put(watchKey, fileExtensions);
167+
}else{
168+
final HashSet<String> newFileExtensions = new HashSet<String>(originalFileExtensions);
169+
newFileExtensions.addAll(fileExtensions);
170+
watchKeyToExtensionsMap.put(watchKey, Collections.unmodifiableList(new ArrayList(newFileExtensions)));
171+
}
172+
return FileVisitResult.CONTINUE;
173+
}
174+
});
175+
} catch (IOException e) {
176+
throw new RuntimeException(e);
177+
}
178+
}
179+
180+
}

0 commit comments

Comments
 (0)