package com.yoursway.fsmonitor; import static com.yoursway.utils.YsCollections.addIfNotNull; import static com.yoursway.utils.YsPathUtils.isChildOrParent; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import com.yoursway.fsmonitor.spi.ChangesDetector; import com.yoursway.fsmonitor.spi.ChangesListener; import com.yoursway.fsmonitor.spi.MonitoringRequest; import com.yoursway.utils.YsPathUtils; import com.yoursway.utils.annotations.SynchronizedWithMonitorOfField; import com.yoursway.utils.annotations.UseFromCarbonRunLoopThread; import com.yoursway.utils.annotations.UsedFromJNI; public class ChangesDetectorImpl implements ChangesDetector { static { System.loadLibrary("ys-fs-monitor-macosx_leopard"); } private native void initializeNatives(); private native boolean queueSafeReschedulingRequest(); private native long FSEventStreamCreate(String[] paths, long sinceWhen, double latency); private native void FSEventStreamScheduleWithRunLoop(long streamId); private native boolean FSEventStreamStart(long streamId); private native void FSEventStreamStop(long streamId); private native void FSEventStreamInvalidate(long streamId); private native void FSEventStreamRelease(long streamId); private native static void CFRunLoopRun(); class Request implements MonitoringRequest { private final String monitoredPath; private final ChangesListener listener; public Request(File directory, ChangesListener listener) { if (directory == null) throw new NullPointerException("directory is null"); if (listener == null) throw new NullPointerException("listener is null"); try { directory = directory.getCanonicalFile(); } catch (IOException e) { directory = directory.getAbsoluteFile(); } String directoryPath = directory.getPath(); directoryPath = YsPathUtils.removeTrailingSeparator(directoryPath); this.monitoredPath = directoryPath; this.listener = listener; } public String path() { return monitoredPath; } public void addNotifiersTo(Collection<Runnable> notifiers, String[] paths) { String monitoredPath = this.monitoredPath; for (final String path : paths) if (isChildOrParent(monitoredPath, path)) notifiers.add(new Runnable() { public void run() { listener.pathChanged(path); } }); } public void dispose() { Collection<String> paths; long changeId; synchronized (activeRequests) { activeRequests.remove(this); paths = calculateActivePaths(); changeId = activeRequestsChangeCount++; } schedule(paths, changeId); } } @UseFromCarbonRunLoopThread private long lastSeenEventId = -1; @SuppressWarnings("unused") @UsedFromJNI private long runLoopHandle = 0; /** * A collection of currently active (non-disposed) monitoring requests. */ @SynchronizedWithMonitorOfField("activeRequests") private Collection<Request> activeRequests = new HashSet<Request>(); @SynchronizedWithMonitorOfField("activeRequests") private long activeRequestsChangeCount = 0; /** * In <code>ACTIVE</code> state, the paths that are currently being * monitored by FSEventStream. * * In <code>RESCHEDULING</code> state, the paths that will be monitored by * FSEventStream once the rescheduling is complete. */ @SynchronizedWithMonitorOfField("stateLock") private Collection<String> currentlyMonitoredPaths = new HashSet<String>(); /** * Used to prevent later changes from being lost due to a race condition * that might occur because we are using two separate locks. */ @SynchronizedWithMonitorOfField("stateLock") private long lastScheduledChangedId = -1; @SynchronizedWithMonitorOfField("stateLock") private long streamId; @SynchronizedWithMonitorOfField("stateLock") private State state = State.INACTIVE; @SynchronizedWithMonitorOfField("stateLock") private boolean isScheduled = false; private Object stateLock = new Object(); public ChangesDetectorImpl() { initializeNatives(); } public MonitoringRequest monitor(File directory, ChangesListener listener) { Request newRequest = new Request(directory, listener); Collection<String> paths; long changeId; synchronized (activeRequests) { activeRequests.add(newRequest); paths = calculateActivePaths(); changeId = activeRequestsChangeCount++; } schedule(paths, changeId); return newRequest; } private Collection<String> calculateActivePaths() { Collection<String> paths; paths = new HashSet<String>(activeRequests.size()); for (Request request : activeRequests) addIfNotNull(paths, request.path()); return paths; } void schedule(Collection<String> newPaths, long changeId) { synchronized (stateLock) { if (!state.canChangeToAnotherState()) return; if (changeId < lastScheduledChangedId) return; // prevent later changes from being lost lastScheduledChangedId = changeId; if (newPaths.equals(currentlyMonitoredPaths)) return; currentlyMonitoredPaths = newPaths; if (!state.shouldInitiateRescheduling()) return; state = State.RESCHEDULING; } scheduleRestart(); } private void scheduleRestart() { if (!queueSafeReschedulingRequest()) handleSafeToReschedule(); } @UsedFromJNI void handleChange(String[] paths, long[] eventIds) { // System.out.println("CHANGE!"); // for (int i = 0; i < eventIds.length; i++) { // String path = paths[i]; // long id = eventIds[i]; // if (id > lastSeenEventId) // lastSeenEventId = id; // System.out.println(" #" + id + " - " + path); // } // System.out.flush(); for (int i = 0; i < paths.length; i++) paths[i] = removeTrailingSeparator(paths[i]); // minimize the time we spend inside the synchronized area Collection<Runnable> notifiers = new ArrayList<Runnable>(); synchronized (activeRequests) { for (Request request : activeRequests) request.addNotifiersTo(notifiers, paths); } for (Runnable runnable : notifiers) runnable.run(); } @UsedFromJNI void handleSafeToReschedule() { synchronized (state) { if (isScheduled) { FSEventStreamStop(streamId); // System.out.println("FSEventStream stopped."); FSEventStreamInvalidate(streamId); // System.out.println("FSEventStream invalidated."); FSEventStreamRelease(streamId); System.out.println("FSEventStream disposed."); isScheduled = false; } if (currentlyMonitoredPaths.isEmpty() || !state.canActivate()) { if (state.canChangeToAnotherState()) state = State.INACTIVE; } else { String[] paths = currentlyMonitoredPaths.toArray(new String[currentlyMonitoredPaths.size()]); streamId = FSEventStreamCreate(paths, lastSeenEventId, 1.0); if (streamId == 0) throw new RuntimeException("Cannot create FSEventStream"); // System.out.println("FSEventStream created."); FSEventStreamScheduleWithRunLoop(streamId); // System.out.println("FSEventStream scheduled."); if (!FSEventStreamStart(streamId)) throw new RuntimeException("Cannot start FSEventStream"); System.out.println("FSEventStream running..."); isScheduled = true; if (state.canChangeToAnotherState()) state = State.ACTIVE; } } } public static void main(String[] args) { ChangesDetectorImpl detector = new ChangesDetectorImpl(); detector.monitor(new File("/Users/andreyvit"), new ChangesListener() { public void pathChanged(String path) { System.out.println("Changed: " + path); } }); CFRunLoopRun(); detector.dispose(); } public void dispose() { synchronized(state) { state = State.DISPOSED; } scheduleRestart(); } /** * @deprecated Use {@link YsPathUtils#removeTrailingSeparator(String)} instead */ public static String removeTrailingSeparator(String directoryPath) { return YsPathUtils.removeTrailingSeparator(directoryPath); } }