Skip to content

Improved macOS support: java.nio.file Watch Service #41

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

Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
8b71de6
Merge branch 'improved-macos-support/jna' into improved-macos-support…
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
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: 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();
}
45 changes: 42 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,8 @@

import com.sun.nio.file.ExtendedWatchEventModifier;

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

/**
Expand All @@ -73,7 +77,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 +125,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 +161,38 @@ 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 new MacWatchable(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 = // Assumption: the platform doesn't change
com.sun.jna.Platform.isMac() ? MAC : DEFAULT;

static Platform get() {
return CURRENT;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* BSD 2-Clause License
*
* Copyright (c) 2023, Swat.engineering
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package engineering.swat.watch.impl.mac;

import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.WatchKey;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

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

/**
* Warning: This class is experimental, untested, and unused. We don't need
* blocking operations currently, but it would be nice (?) to have them
* eventually.
*/
public class MacBlockingWatchService extends MacWatchService {
private final Set<Thread> blockedThreads = ConcurrentHashMap.newKeySet();

Check warning on line 44 in src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java#L43-L44

Added lines #L43 - L44 were not covered by tests

private void throwIfClosed() {
if (isClosed()) {
throw new ClosedWatchServiceException();

Check warning on line 48 in src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java#L48

Added line #L48 was not covered by tests
}
}

Check warning on line 50 in src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java#L50

Added line #L50 was not covered by tests

private <K> K throwIfClosedDuring(BlockingSupplier<K> supplier) throws InterruptedException {
var t = Thread.currentThread();
blockedThreads.add(t);

Check warning on line 54 in src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java#L53-L54

Added lines #L53 - L54 were not covered by tests
try {
throwIfClosed();
return supplier.get();
} catch (InterruptedException e) {
throwIfClosed();

Check warning on line 59 in src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java#L56-L59

Added lines #L56 - L59 were not covered by tests
// If this service isn't closed yet, then definitely the interrupt
// can't have originated from this service. Thus, re-throw it.
throw e;

Check warning on line 62 in src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java#L62

Added line #L62 was not covered by tests
} finally {
blockedThreads.remove(t);

Check warning on line 64 in src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java#L64

Added line #L64 was not covered by tests
}
}

@FunctionalInterface
private static interface BlockingSupplier<T> {
T get() throws InterruptedException;
}

// -- MacWatchService --

@Override
public void close() throws IOException {
super.close();

Check warning on line 77 in src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java#L77

Added line #L77 was not covered by tests
for (var t : blockedThreads) {
t.interrupt();
}
}

Check warning on line 81 in src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java#L79-L81

Added lines #L79 - L81 were not covered by tests

@Override
public @Nullable WatchKey poll(long timeout, TimeUnit unit) throws InterruptedException {
return this.<@Nullable WatchKey> throwIfClosedDuring(
() -> pendingKeys.poll(timeout, unit));

Check warning on line 86 in src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java#L85-L86

Added lines #L85 - L86 were not covered by tests
}

@Override
public WatchKey take() throws InterruptedException {
return throwIfClosedDuring(pendingKeys::take);

Check warning on line 91 in src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/impl/mac/MacBlockingWatchService.java#L91

Added line #L91 was not covered by tests
}
}
Loading