Skip to content

Commit e21382d

Browse files
authored
Merge pull request #41 from SWAT-engineering/improved-macos-support/nio-file-watch-service
Improved macOS support: `java.nio.file` Watch Service
2 parents 73b0238 + 5b04d16 commit e21382d

File tree

7 files changed

+536
-5
lines changed

7 files changed

+536
-5
lines changed

README.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![javadoc](https://javadoc.io/badge2/engineering.swat/java-watch/docs.svg?style=flat-square)](https://javadoc.io/doc/engineering.swat/java-watch)
44
[![Codecov](https://img.shields.io/codecov/c/github/SWAT-engineering/java-watch?style=flat-square)](https://codecov.io/gh/SWAT-engineering/java-watch)
55

6-
a java file watcher that works across platforms and supports recursion, single file watches, and tries to make sure no events are missed. Where possible it uses Java's NIO WatchService.
6+
A Java file watcher that works across platforms and supports recursion, single file watches, and tries to make sure no events are missed.
77

88
## Features
99

@@ -21,7 +21,6 @@ Features:
2121

2222
Planned features:
2323

24-
- Avoid poll based watcher in macOS/OSX that only detects changes every 2 seconds (see [#4](https://github.com/SWAT-engineering/java-watch/issues/4))
2524
- Support single file watches natively in linux (see [#11](https://github.com/SWAT-engineering/java-watch/issues/11))
2625
- Monitor only specific events (such as only CREATE events)
2726

@@ -58,6 +57,14 @@ try(var active = watcherSetup.start()) {
5857
// no new events will be scheduled on the threadpool
5958
```
6059

60+
## Internals
61+
62+
On all platforms except macOS, the library internally uses the JDK default implementation of the Java NIO [`WatchService`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/WatchService.html) API.
63+
64+
On macOS, the library internally uses our custom `WatchService` implementation based on macOS's native [file system event streams](https://developer.apple.com/documentation/coreservices/file_system_events?language=objc) (using JNA).
65+
Generally, it offers better performance than the JDK default implementation (because the latter uses a polling loop to detect changes only once every two seconds).
66+
To force the library to use the JDK default implementation on macOS, set system property `engineering.swat.watch.impl` to `default`.
67+
6168
## Related work
6269

6370
Before starting this library, we wanted to use existing libraries, but they all lacked proper support for recursive file watches, single file watches or lacked configurability. This library now has a growing collection of tests and a small API that should allow for future improvements without breaking compatibility.
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package java.nio.file;
2+
3+
import org.checkerframework.checker.nullness.qual.Nullable;
4+
5+
public interface WatchService {
6+
@Nullable WatchKey poll();
7+
8+
@Nullable WatchKey poll(long timeout, TimeUnit unit)
9+
throws InterruptedException;
10+
}
11+
12+
public interface WatchEvent<T> {
13+
@Nullable T context();
14+
}

src/main/java/engineering/swat/watch/impl/jdk/JDKPoller.java

+57-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@
3434
import java.io.Closeable;
3535
import java.io.IOException;
3636
import java.nio.file.FileSystems;
37+
import java.nio.file.Path;
3738
import java.nio.file.WatchEvent;
3839
import java.nio.file.WatchKey;
3940
import java.nio.file.WatchService;
41+
import java.nio.file.Watchable;
4042
import java.util.List;
4143
import java.util.Map;
4244
import java.util.concurrent.CompletableFuture;
@@ -53,6 +55,7 @@
5355

5456
import com.sun.nio.file.ExtendedWatchEventModifier;
5557

58+
import engineering.swat.watch.impl.mac.MacWatchService;
5659
import engineering.swat.watch.impl.util.SubscriptionKey;
5760

5861
/**
@@ -73,7 +76,7 @@ private JDKPoller() {}
7376

7477
static {
7578
try {
76-
service = FileSystems.getDefault().newWatchService();
79+
service = Platform.get().newWatchService();
7780
} catch (IOException e) {
7881
throw new RuntimeException("Could not start watcher", e);
7982
}
@@ -121,12 +124,13 @@ public static Closeable register(SubscriptionKey path, Consumer<List<WatchEvent<
121124
try {
122125
return CompletableFuture.supplyAsync(() -> {
123126
try {
127+
Watchable watchable = Platform.get().newWatchable(path.getPath());
124128
WatchEvent.Kind<?>[] kinds = new WatchEvent.Kind[]{ ENTRY_CREATE, ENTRY_MODIFY, OVERFLOW, ENTRY_DELETE };
125129
if (path.isRecursive()) {
126-
return path.getPath().register(service, kinds, ExtendedWatchEventModifier.FILE_TREE);
130+
return watchable.register(service, kinds, ExtendedWatchEventModifier.FILE_TREE);
127131
}
128132
else {
129-
return path.getPath().register(service, kinds);
133+
return watchable.register(service, kinds);
130134
}
131135
} catch (IOException e) {
132136
throw new RuntimeException(e);
@@ -156,4 +160,54 @@ public void close() throws IOException {
156160
throw new IOException("The registration was canceled");
157161
}
158162
}
163+
164+
private static interface Platform {
165+
WatchService newWatchService() throws IOException;
166+
Watchable newWatchable(Path path);
167+
168+
static final Platform MAC = new Platform() {
169+
@Override
170+
public WatchService newWatchService() throws IOException {
171+
return new MacWatchService();
172+
}
173+
@Override
174+
public Watchable newWatchable(Path path) {
175+
return MacWatchService.newWatchable(path);
176+
}
177+
};
178+
179+
static final Platform DEFAULT = new Platform() {
180+
@Override
181+
public WatchService newWatchService() throws IOException {
182+
return FileSystems.getDefault().newWatchService();
183+
}
184+
@Override
185+
public Watchable newWatchable(Path path) {
186+
return path;
187+
}
188+
};
189+
190+
static final Platform CURRENT = current(); // Assumption: the platform doesn't change
191+
192+
private static Platform current() {
193+
var key = "engineering.swat.watch.impl";
194+
var val = System.getProperty(key);
195+
if (val != null) {
196+
if (val.equals("mac")) {
197+
return MAC;
198+
} else if (val.equals("default")) {
199+
return DEFAULT;
200+
} else {
201+
logger.warn("Unexpected value \"{}\" for system property \"{}\". Using value \"default\" instead.", val, key);
202+
return DEFAULT;
203+
}
204+
}
205+
206+
return com.sun.jna.Platform.isMac() ? MAC : DEFAULT;
207+
}
208+
209+
static Platform get() {
210+
return CURRENT;
211+
}
212+
}
159213
}

0 commit comments

Comments
 (0)