Skip to content

Improved macOS support #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 51 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
47df093
Add temporary PR target branch to trigger build
sungshik Apr 14, 2025
cdc0b70
Merge branch 'main' into improved-macos-support-main
sungshik Apr 15, 2025
0c16dae
Add interfaces to access native macOS APIs (Dispatch Objects; Dispatc…
sungshik Apr 21, 2025
c063710
Add smoke test (and a corresponding auxiliary `main`) to start/stop a…
sungshik Apr 21, 2025
9e0a0c4
Add a facade-like class to open/close an FS event stream without expo…
sungshik Apr 21, 2025
7490e1b
Add license
sungshik Apr 21, 2025
3fe8d47
Add implementation of the `java.nio.file`'s Watch Service API
sungshik Apr 21, 2025
2495559
Add an experimental/untested/unused extension of `MacWatchService` th…
sungshik Apr 21, 2025
587c01a
Add `MacWatchService` and `MacWatchable` to the poller
sungshik Apr 21, 2025
d33858f
Improve comments
sungshik Apr 21, 2025
ca8ff64
Remove unnecessary volatile modifiers
sungshik Apr 21, 2025
19d1c9b
Merge branch 'file-move-test' into improved-macos-support/file-moves
sungshik Apr 21, 2025
9da18b5
Update minimal working example with support for `USE_EXTENDED_DATA` t…
sungshik Apr 21, 2025
ccce387
Make a few static methods non-static to directly use fields (instead …
sungshik Apr 22, 2025
c6f901f
Fix exception message
sungshik Apr 22, 2025
7b5ad2b
Merge branch 'main' into improved-macos-support-main
sungshik Apr 22, 2025
9aed20f
Enable macOS-specific test only on macOS
sungshik Apr 22, 2025
0798afd
Reduce visibility of classes/interfaces where possible
sungshik Apr 22, 2025
3589680
Simplify code in a few places (nits)
sungshik Apr 22, 2025
c22ca44
Fix confusing comments about deallocation
sungshik Apr 22, 2025
d17f342
Fix Checker Framework idioms
sungshik Apr 22, 2025
8b71de6
Merge branch 'improved-macos-support/jna' into improved-macos-support…
sungshik Apr 22, 2025
73b0238
Merge pull request #40 from SWAT-engineering/improved-macos-support/jna
sungshik Apr 22, 2025
8161950
Merge branch 'improved-macos-support-main' into improved-macos-suppor…
sungshik Apr 22, 2025
57ba87e
Disable move/rename test (temporarily)
sungshik Apr 22, 2025
8d85f1f
Make `stream` in `MacWatchKey` final
sungshik Apr 22, 2025
3c14f6a
Fix NPE
sungshik Apr 22, 2025
feb2f9b
Simplify code in a few places (nits)
sungshik Apr 23, 2025
76eb623
Use `computeIfAbsent` instead of `putIfAbsent`
sungshik Apr 23, 2025
442b98c
Add system property to configure which WatchService implementation sh…
sungshik Apr 30, 2025
4087179
Remove `MacBlockingWatchService`
sungshik Apr 30, 2025
1cccd6e
Reduce visibility of `MacWatchKey` to package-private
sungshik Apr 30, 2025
1c5742e
Reduce visibility of `MacWatchable` to package-private
sungshik Apr 30, 2025
10692a7
Reduce visibility of internal methods to package-private
sungshik Apr 30, 2025
cb07075
Clarify logic for when to ignore events
sungshik Apr 30, 2025
ee77302
Add comment to clarify why we don't need to interrupt threads when a …
sungshik Apr 30, 2025
5b04d16
Add helper inner class to group thread-safety-critical code together …
sungshik Apr 30, 2025
e21382d
Merge pull request #41 from SWAT-engineering/improved-macos-support/n…
sungshik Apr 30, 2025
51ae70b
Merge branch 'improved-macos-support-main' into improved-macos-suppor…
sungshik May 1, 2025
e361b18
Refactor variable name in test to use JDK terminology "regular file"
sungshik May 2, 2025
3899227
Add extra test for moving directories
sungshik May 2, 2025
b63c4e6
Add support for renames/moves
sungshik May 2, 2025
5652cc2
Add property for JNA version to pom.xml
sungshik May 2, 2025
edd905b
Rename header in README
sungshik May 2, 2025
cc62112
Fix system property name
sungshik May 2, 2025
ec40ead
Extend build workflow to run tests on macOS with/without JNA
sungshik May 2, 2025
06da23f
Merge branch 'improved-macos-support/small-fixes' into improved-macos…
sungshik May 2, 2025
4aba4b8
Improve test output
sungshik May 2, 2025
da4156c
Use JNA version property consistently in pom.xml
sungshik May 2, 2025
4e3f180
Merge pull request #47 from SWAT-engineering/improved-macos-support/s…
sungshik May 2, 2025
3d244ad
Merge pull request #44 from SWAT-engineering/improved-macos-support/f…
sungshik May 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,23 @@ on:
pull_request:
branches:
- main
- improved-macos-support-main

jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
os:
- image: ubuntu-latest
- image: macos-latest
mac-backend: jdk
- image: macos-latest
mac-backend: jna
- image: windows-latest
jdk: [11, 17, 21]

fail-fast: false
runs-on: ${{ matrix.os }}
runs-on: ${{ matrix.os.image }}
steps:
- uses: actions/checkout@v4
- run: echo " " >> pom.xml # make sure the cache is slightly different for these runners
Expand All @@ -26,7 +34,7 @@ jobs:
cache: 'maven'

- name: test
run: mvn -B clean test
run: mvn -B clean test -DargLine="-Dengineering.swat.java-watch.mac=${{ matrix.os.mac-backend }}"
env:
DELAY_FACTOR: 3

Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![javadoc](https://javadoc.io/badge2/engineering.swat/java-watch/docs.svg?style=flat-square)](https://javadoc.io/doc/engineering.swat/java-watch)
[![Codecov](https://img.shields.io/codecov/c/github/SWAT-engineering/java-watch?style=flat-square)](https://codecov.io/gh/SWAT-engineering/java-watch)

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.
A Java file watcher that works across platforms and supports recursion, single file watches, and tries to make sure no events are missed.

## Features

Expand All @@ -21,7 +21,6 @@ Features:

Planned features:

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

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

## Backends

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.

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).
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).
To force the library to use the JDK default implementation on macOS, set system property `engineering.swat.java-watch.mac` to `jdk`.

## Related work

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.
Expand Down
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
<checkerframework.version>3.49.2</checkerframework.version>
<junit.version>5.12.2</junit.version>
<log4j.version>2.24.3</log4j.version>
<jna.version>5.16.0</jna.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
Expand Down Expand Up @@ -223,6 +224,16 @@
<version>${log4j.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>${jna.version}</version>
</dependency>
</dependencies>

<profiles>
Expand Down
14 changes: 14 additions & 0 deletions src/main/checkerframework/nio-file.astub
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package java.nio.file;

import org.checkerframework.checker.nullness.qual.Nullable;

public interface WatchService {
@Nullable WatchKey poll();

@Nullable WatchKey poll(long timeout, TimeUnit unit)
throws InterruptedException;
}

public interface WatchEvent<T> {
@Nullable T context();
}
62 changes: 59 additions & 3 deletions src/main/java/engineering/swat/watch/impl/jdk/JDKPoller.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.Watchable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
Expand All @@ -53,6 +55,7 @@

import com.sun.nio.file.ExtendedWatchEventModifier;

import engineering.swat.watch.impl.mac.MacWatchService;
import engineering.swat.watch.impl.util.SubscriptionKey;

/**
Expand All @@ -73,7 +76,7 @@ private JDKPoller() {}

static {
try {
service = FileSystems.getDefault().newWatchService();
service = Platform.get().newWatchService();
} catch (IOException e) {
throw new RuntimeException("Could not start watcher", e);
}
Expand Down Expand Up @@ -121,12 +124,13 @@ public static Closeable register(SubscriptionKey path, Consumer<List<WatchEvent<
try {
return CompletableFuture.supplyAsync(() -> {
try {
Watchable watchable = Platform.get().newWatchable(path.getPath());
WatchEvent.Kind<?>[] kinds = new WatchEvent.Kind[]{ ENTRY_CREATE, ENTRY_MODIFY, OVERFLOW, ENTRY_DELETE };
if (path.isRecursive()) {
return path.getPath().register(service, kinds, ExtendedWatchEventModifier.FILE_TREE);
return watchable.register(service, kinds, ExtendedWatchEventModifier.FILE_TREE);
}
else {
return path.getPath().register(service, kinds);
return watchable.register(service, kinds);
}
} catch (IOException e) {
throw new RuntimeException(e);
Expand Down Expand Up @@ -156,4 +160,56 @@ public void close() throws IOException {
throw new IOException("The registration was canceled");
}
}

private static interface Platform {
WatchService newWatchService() throws IOException;
Watchable newWatchable(Path path);

static final Platform MAC = new Platform() {
@Override
public WatchService newWatchService() throws IOException {
return new MacWatchService();
}
@Override
public Watchable newWatchable(Path path) {
return MacWatchService.newWatchable(path);
}
};

static final Platform DEFAULT = new Platform() {
@Override
public WatchService newWatchService() throws IOException {
return FileSystems.getDefault().newWatchService();
}
@Override
public Watchable newWatchable(Path path) {
return path;
}
};

static final Platform CURRENT = current(); // Assumption: the platform doesn't change

private static Platform current() {
if (com.sun.jna.Platform.isMac()) {
var key = "engineering.swat.java-watch.mac";
var val = System.getProperty(key);
if (val != null) {
if (val.equals("jna")) {
return MAC;
} else if (val.equals("jdk")) {
return DEFAULT;
} else {
logger.warn("Unexpected value \"{}\" for system property \"{}\". Using value \"jdk\" instead.", val, key);
return DEFAULT;
}
}
}

return DEFAULT;
}

static Platform get() {
return CURRENT;
}
}
}
Loading