Skip to content

Commit 042298e

Browse files
authored
Merge pull request #39 from SWAT-engineering/improved-macos-support-main
Improved macOS support
2 parents ec14259 + 97dc29d commit 042298e

File tree

16 files changed

+1529
-22
lines changed

16 files changed

+1529
-22
lines changed

.github/workflows/build.yaml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,23 @@ on:
66
pull_request:
77
branches:
88
- main
9+
- improved-macos-support-main
910

1011
jobs:
1112
test:
1213
strategy:
1314
matrix:
14-
os: [ubuntu-latest, macos-latest, windows-latest]
15+
os:
16+
- image: ubuntu-latest
17+
- image: macos-latest
18+
mac-backend: jdk
19+
- image: macos-latest
20+
mac-backend: fsevents
21+
- image: windows-latest
1522
jdk: [11, 17, 21]
23+
1624
fail-fast: false
17-
runs-on: ${{ matrix.os }}
25+
runs-on: ${{ matrix.os.image }}
1826
steps:
1927
- uses: actions/checkout@v4
2028
- run: echo " " >> pom.xml # make sure the cache is slightly different for these runners
@@ -26,7 +34,7 @@ jobs:
2634
cache: 'maven'
2735

2836
- name: test
29-
run: mvn -B clean test
37+
run: mvn -B clean test "-Dwatch.mac.backend=${{ matrix.os.mac-backend }}"
3038
env:
3139
DELAY_FACTOR: 3
3240

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
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

1010
Features:
1111

1212
- monitor a single file (or directory) for changes
13-
- monitor a directory for changes to its direct descendants
13+
- monitor a directory for changes to its direct children
1414
- monitor a directory for changes for all its descendants (aka recursive directory watch)
15+
- backends supported:
16+
- the JDK [`WatchService`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/WatchService.html) API on any platform
17+
- the native [FSEvents](https://developer.apple.com/documentation/coreservices/file_system_events) API on macOS
1518
- edge cases dealt with:
1619
- recursive watches will also continue in new directories
1720
- multiple watches for the same directory are merged to avoid overloading the kernel
@@ -21,7 +24,6 @@ Features:
2124

2225
Planned features:
2326

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))
2527
- Support single file watches natively in linux (see [#11](https://github.com/SWAT-engineering/java-watch/issues/11))
2628
- Monitor only specific events (such as only CREATE events)
2729

@@ -58,6 +60,14 @@ try(var active = watcherSetup.start()) {
5860
// no new events will be scheduled on the threadpool
5961
```
6062

63+
## Backends
64+
65+
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.
66+
67+
On macOS, the library internally uses our custom `WatchService` implementation based on macOS's native [FSEvents](https://developer.apple.com/documentation/coreservices/file_system_events) API.
68+
Generally, it offers better performance than the JDK default implementation (because the latter uses a polling loop to detect changes at fixed time intervals).
69+
To force the library to use the JDK default implementation on macOS, set system property `engineering.swat.java-watch.mac` to `jdk`.
70+
6171
## Related work
6272

6373
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.

pom.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@
7474
<checkerframework.version>3.49.2</checkerframework.version>
7575
<junit.version>5.12.2</junit.version>
7676
<log4j.version>2.24.3</log4j.version>
77+
<jna.version>5.16.0</jna.version>
7778
<maven.compiler.source>11</maven.compiler.source>
7879
<maven.compiler.target>11</maven.compiler.target>
80+
<watch.mac.backend>fsevents</watch.mac.backend>
7981
</properties>
8082

8183
<build>
@@ -103,6 +105,9 @@
103105
<groupId>org.apache.maven.plugins</groupId>
104106
<artifactId>maven-surefire-plugin</artifactId>
105107
<version>3.5.3</version>
108+
<configuration>
109+
<argLine>@{argLine} -Dengineering.swat.java-watch.mac=${watch.mac.backend}</argLine>
110+
</configuration>
106111
</plugin>
107112
<plugin> <!-- code coverage -->
108113
<groupId>org.jacoco</groupId>
@@ -223,6 +228,16 @@
223228
<version>${log4j.version}</version>
224229
<scope>test</scope>
225230
</dependency>
231+
<dependency>
232+
<groupId>net.java.dev.jna</groupId>
233+
<artifactId>jna</artifactId>
234+
<version>${jna.version}</version>
235+
</dependency>
236+
<dependency>
237+
<groupId>net.java.dev.jna</groupId>
238+
<artifactId>jna-platform</artifactId>
239+
<version>${jna.version}</version>
240+
</dependency>
226241
</dependencies>
227242

228243
<profiles>
Lines changed: 14 additions & 0 deletions
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

Lines changed: 59 additions & 3 deletions
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,56 @@ 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+
if (com.sun.jna.Platform.isMac()) {
194+
var key = "engineering.swat.java-watch.mac";
195+
var val = System.getProperty(key);
196+
if (val != null) {
197+
if (val.equals("fsevents")) {
198+
return MAC;
199+
} else if (val.equals("jdk")) {
200+
return DEFAULT;
201+
} else {
202+
logger.warn("Unexpected value \"{}\" for system property \"{}\". Using value \"jdk\" instead.", val, key);
203+
return DEFAULT;
204+
}
205+
}
206+
}
207+
208+
return DEFAULT;
209+
}
210+
211+
static Platform get() {
212+
return CURRENT;
213+
}
214+
}
159215
}

0 commit comments

Comments
 (0)