Skip to content

Commit 671d5eb

Browse files
committed
Add JDKFileTreeWatch
1 parent d51d88f commit 671d5eb

File tree

2 files changed

+167
-3
lines changed

2 files changed

+167
-3
lines changed

src/main/java/engineering/swat/watch/Watcher.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040

4141
import engineering.swat.watch.impl.EventHandlingWatch;
4242
import engineering.swat.watch.impl.jdk.JDKDirectoryWatch;
43+
import engineering.swat.watch.impl.jdk.JDKFileTreeWatch;
4344
import engineering.swat.watch.impl.jdk.JDKFileWatch;
44-
import engineering.swat.watch.impl.jdk.JDKRecursiveDirectoryWatch;
4545
import engineering.swat.watch.impl.overflows.MemorylessRescanner;
4646

4747
/**
@@ -185,14 +185,14 @@ public ActiveWatch start() throws IOException {
185185
}
186186
case PATH_AND_ALL_DESCENDANTS: {
187187
try {
188-
var result = new JDKDirectoryWatch(path, executor, eventHandler, true);
188+
var result = new JDKDirectoryWatch(path, executor, h, true);
189189
result.open();
190190
return result;
191191
} catch (Throwable ex) {
192192
// no native support, use the simulation
193193
logger.debug("Not possible to register the native watcher, using fallback for {}", path);
194194
logger.trace(ex);
195-
var result = new JDKRecursiveDirectoryWatch(path, executor, eventHandler);
195+
var result = new JDKFileTreeWatch(path, executor, h);
196196
result.open();
197197
return result;
198198
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* BSD 2-Clause License
3+
*
4+
* Copyright (c) 2023, Swat.engineering
5+
*
6+
* Redistribution and use in source and binary forms, with or without
7+
* modification, are permitted provided that the following conditions are met:
8+
*
9+
* 1. Redistributions of source code must retain the above copyright notice, this
10+
* list of conditions and the following disclaimer.
11+
*
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
package engineering.swat.watch.impl.jdk;
28+
29+
import java.io.IOException;
30+
import java.nio.file.Files;
31+
import java.nio.file.Path;
32+
import java.util.Map;
33+
import java.util.concurrent.ConcurrentHashMap;
34+
import java.util.concurrent.Executor;
35+
import java.util.function.BiConsumer;
36+
import java.util.function.Consumer;
37+
38+
import org.apache.logging.log4j.LogManager;
39+
import org.apache.logging.log4j.Logger;
40+
41+
import engineering.swat.watch.WatchEvent;
42+
import engineering.swat.watch.WatchScope;
43+
import engineering.swat.watch.impl.EventHandlingWatch;
44+
45+
public class JDKFileTreeWatch extends JDKBaseWatch {
46+
private final Logger logger = LogManager.getLogger();
47+
private final Map<Path, JDKFileTreeWatch> childWatches = new ConcurrentHashMap<>();
48+
private final JDKBaseWatch internal;
49+
50+
public JDKFileTreeWatch(Path root, Executor exec,
51+
BiConsumer<EventHandlingWatch, WatchEvent> eventHandler) {
52+
53+
super(root, exec, eventHandler);
54+
var internalEventHandler = updateChildWatches().andThen(eventHandler);
55+
this.internal = new JDKDirectoryWatch(root, exec, internalEventHandler);
56+
}
57+
58+
/**
59+
* @return An event handler that updates the child watches according to the
60+
* following rules: (a) when an overflow happens, it's propagated to each
61+
* existing child watch; (b) when a subdirectory creation happens, a new
62+
* child watch is opened for that subdirectory; (c) when a subdirectory
63+
* deletion happens, an existing child watch is closed for that
64+
* subdirectory.
65+
*/
66+
private BiConsumer<EventHandlingWatch, WatchEvent> updateChildWatches() {
67+
return (watch, event) -> {
68+
var kind = event.getKind();
69+
70+
if (kind == WatchEvent.Kind.OVERFLOW) {
71+
forEachChild(this::reportOverflowToChildWatch);
72+
return;
73+
}
74+
75+
var child = event.calculateFullPath();
76+
var directory = child.toFile().isDirectory();
77+
78+
if (kind == WatchEvent.Kind.CREATED && directory) {
79+
openChildWatch(child);
80+
// Events in the newly created directory (`child`) might have
81+
// been missed between its creation (`event`) and setting up its
82+
// watch. Erring on the side of caution, generate an overflow
83+
// event for the watch.
84+
reportOverflowToChildWatch(child);
85+
}
86+
87+
if (kind == WatchEvent.Kind.DELETED && directory) {
88+
closeChildWatch(child);
89+
}
90+
};
91+
}
92+
93+
private void openChildWatch(Path child) {
94+
var childWatch = new JDKFileTreeWatch(child, exec, (w, e) ->
95+
// Same as `eventHandler`, except each event is pre-processed such
96+
// that the last segment of the root path becomes the first segment
97+
// of the relative path. For instance, `foo/bar` (root path) and
98+
// `baz.txt` (relative path) are pre-processed to `foo` (root path)
99+
// and `bar/baz.txt` (relative path). This is to ensure the parent
100+
// directory of a child directory is reported as the root directory
101+
// of the event.
102+
eventHandler.accept(w, relativize(e))
103+
);
104+
105+
if (childWatches.putIfAbsent(child, childWatch) == null) {
106+
try {
107+
childWatch.open();
108+
} catch (IOException e) {
109+
logger.error("Could not open (nested) file tree watch for: {} ({})", child, e);
110+
}
111+
}
112+
}
113+
114+
private void closeChildWatch(Path child) {
115+
var childWatch = childWatches.remove(child);
116+
if (childWatch != null) {
117+
try {
118+
childWatch.close();
119+
} catch (IOException e) {
120+
logger.error("Could not close (nested) file tree watch for: {} ({})", child, e);
121+
}
122+
}
123+
}
124+
125+
private void reportOverflowToChildWatch(Path child) {
126+
var childWatch = childWatches.get(child);
127+
if (childWatch != null) {
128+
var overflow = new WatchEvent(WatchEvent.Kind.OVERFLOW, child);
129+
childWatch.handleEvent(overflow);
130+
}
131+
}
132+
133+
private void forEachChild(Consumer<Path> action) {
134+
try (var children = Files.find(path, 1, (p, attrs) -> p != path && attrs.isDirectory())) {
135+
children.forEach(action);
136+
} catch (IOException e) {
137+
logger.error("File tree watch (for: {}) could not iterate over its children ({})", path, e);
138+
}
139+
}
140+
141+
// -- JDKBaseWatch --
142+
143+
@Override
144+
public WatchScope getScope() {
145+
return WatchScope.PATH_AND_ALL_DESCENDANTS;
146+
}
147+
148+
@Override
149+
public void handleEvent(WatchEvent event) {
150+
internal.handleEvent(event);
151+
}
152+
153+
@Override
154+
public synchronized void close() throws IOException {
155+
forEachChild(this::closeChildWatch);
156+
internal.close();
157+
}
158+
159+
@Override
160+
protected synchronized void start() throws IOException {
161+
internal.open();
162+
forEachChild(this::openChildWatch);
163+
}
164+
}

0 commit comments

Comments
 (0)