/
FileWatcher.java
151 lines (125 loc) · 5.3 KB
/
FileWatcher.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package shadow.util;
import clojure.lang.*;
import com.sun.nio.file.SensitivityWatchEventModifier;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.*;
public class FileWatcher implements AutoCloseable {
private final Path root;
private final WatchService ws;
private final Map<WatchKey, Path> keys;
private final PathMatcher matcher;
FileWatcher(Path dir, PathMatcher matcher) throws IOException {
this.root = dir;
this.ws = dir.getFileSystem().newWatchService();
this.keys = new HashMap<WatchKey, Path>();
this.matcher = matcher;
registerAll(dir);
}
@Override
public void close() throws Exception {
this.keys.clear();
this.ws.close();
}
private void registerAll(final Path start) throws IOException {
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
WatchKey key = dir.register(ws,
new WatchEvent.Kind[]{
ENTRY_CREATE,
ENTRY_DELETE,
ENTRY_MODIFY
}, SensitivityWatchEventModifier.HIGH); // OSX is way too slow without this
keys.put(key, dir);
return FileVisitResult.CONTINUE;
}
});
}
private final static Keyword KW_NEW = RT.keyword(null, "new");
private final static Keyword KW_MOD = RT.keyword(null, "mod");
private final static Keyword KW_DEL = RT.keyword(null, "del");
/**
* blocking operation to gather all changes, blocks until at least one change happened
* @return {"path-to-file" :new|:mod|:del}
* @throws IOException
* @throws InterruptedException
*/
public IPersistentMap waitForChanges() throws IOException, InterruptedException {
return pollForChanges(true);
}
public IPersistentMap pollForChanges() throws IOException, InterruptedException {
return pollForChanges(false);
}
IPersistentMap pollForChanges(boolean block) throws IOException, InterruptedException {
ITransientMap changes = PersistentHashMap.EMPTY.asTransient();
WatchKey key;
if (block) {
key = ws.take();
} else {
key = ws.poll();
}
while (key != null) {
Path dir = keys.get(key);
if (dir == null) {
throw new IllegalStateException("got a key for a path we don't know: " + key);
}
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind kind = event.kind();
if (kind == OVERFLOW) {
continue;
}
WatchEvent<Path> ev = (WatchEvent<Path>) event;
Path name = ev.context();
Path child = root.relativize(dir.resolve(name));
String childName = child.toString();
if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
// monitor new directories
// deleted directories will cause the key to become invalid and removed later
// not interested in modify
if (kind == ENTRY_CREATE) {
registerAll(child);
}
} else if (!Files.isHidden(child) && matcher.matches(name)) {
// skip hidden files (eg. emacs creates a #.something.scss file when editing)
// System.out.format("CSS: %s: %s\n", kind, child);
if (kind == ENTRY_CREATE) {
changes = changes.assoc(childName, KW_NEW);
} else if (kind == ENTRY_DELETE) {
changes = changes.assoc(childName, KW_DEL);
} else if (kind == ENTRY_MODIFY) {
changes = changes.assoc(childName, KW_MOD);
}
}
}
boolean valid = key.reset();
if (!valid) { // deleted dirs are no longer valid
keys.remove(key);
}
if (block && changes.count() == 0) {
// if no interesting changes happened, continue to block
// eg. empty directory created, "unwanted" file created/deleted
key = ws.take();
} else {
// peek at potential other changes, will terminate loop if nothing is waiting
key = ws.poll();
}
}
return changes.persistent();
}
public static FileWatcher create(Path dir, List<String> extensions) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append("glob:*.{");
sb.append(String.join(",", extensions));
sb.append("}");
return new FileWatcher(dir, dir.getFileSystem().getPathMatcher(sb.toString()));
}
public static FileWatcher create(File dir, List<String> extensions) throws IOException {
return create(dir.toPath(), extensions);
}
}