/** * Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package org.python.pydev.shared_core.path_watch; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.nio.file.ClosedWatchServiceException; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchEvent.Kind; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.core.runtime.Assert; import org.python.pydev.shared_core.callbacks.ListenerList; import org.python.pydev.shared_core.io.FileUtils; import org.python.pydev.shared_core.log.Log; import org.python.pydev.shared_core.string.FastStringBuffer; /** * @author fabioz * * Service to watch filesystem changes at a given path. Works with the default watch service from JDK 1.7. * * Multiple events are stacked and reported as soon as it happens (from a non-main thread). * * Note that if a directory being watched is removed, it should notify that the given path was removed * (and will remove all the listeners for the path afterwards). */ public class PathWatch implements IPathWatch { /** * The service that'll give us notifications. */ private WatchService watchService; /** * If != null, logs will be added to this buffer. */ public FastStringBuffer log; /** * The path being watched and the stacker object that'll stack many requests into one. * * The stacker object contains the actual key in the watchService (although it may be none if the key * becomes invalid). */ private Map<Path, EventsStackerRunnable> pathToStacker = Collections .synchronizedMap(new HashMap<Path, EventsStackerRunnable>()); private Map<WatchKey, Path> keyToPath = Collections .synchronizedMap(new HashMap<WatchKey, Path>()); private final PollThread pollThread; private final Object lock = new Object(); private volatile boolean disposed = false; private FileFilter fileFilter = new FileFilter() { @Override public boolean accept(File pathname) { return true; //by default accepts everything. } }; private FileFilter dirsFilter = new FileFilter() { @Override public boolean accept(File pathname) { return true; //by default accepts everything. } }; private boolean registeredTracker; public PathWatch() { try { watchService = FileSystems.getDefault().newWatchService(); } catch (IOException e) { Log.log("Error starting watch service", e); } pollThread = new PollThread(); pollThread.setDaemon(true); pollThread.setPriority(Thread.MIN_PRIORITY + 1); //Just a bit above minimum. pollThread.start(); } private class PollThread extends Thread { @Override public void run() { for (;;) { try { if (disposed) { return; } if (watchService == null) { Log.log("Error: watchService is null. Unable to track file changes."); return; } if (log != null) { log.append("Wating (watchService.take)\n"); } // take() will block until a file has been created/deleted WatchKey signalledKey; try { signalledKey = watchService.take(); } catch (InterruptedException ix) { // we'll ignore being interrupted if (log != null) { log.append("Interrupted\n"); } continue; } catch (ClosedWatchServiceException cwse) { // other thread closed watch service // System.out.println("watch service closed, terminating."); break; } List<WatchEvent<?>> list; Path watchedPath; EventsStackerRunnable stacker; synchronized (lock) { watchedPath = keyToPath.get(signalledKey); if (watchedPath == null) { continue; } // get list of events from key list = signalledKey.pollEvents(); stacker = pathToStacker.get(watchedPath); if (stacker == null) { //if the stacker does not exist, go on without rescheduling the key! if (log != null) { log.append("Stacker for: ").appendObject(watchedPath).append("is null\n"); } continue; } // VERY IMPORTANT! call reset() AFTER pollEvents() to allow the // key to be reported again by the watch service. if (new File(watchedPath.toString()).exists()) { signalledKey.reset(); } for (WatchEvent<?> e : list) { Path context = (Path) e.context(); Path resolve = watchedPath.resolve(context); File file = new File(resolve.toString()); Kind<?> kind = e.kind(); if (log != null) { log.append("Event: ").appendObject(kind).append(" file: ").appendObject(file) .append('\n'); } if (kind == StandardWatchEventKinds.OVERFLOW) { if (!file.exists()) { //It may be that it became invalid... keyToPath.remove(signalledKey); stacker.key = null; stacker.removed(file); } else { } //On an overflow, wait a bit and signal that all files being watched were removed, //do a list and say that the current files were added again. stacker.overflow(file); } else { if (kind == StandardWatchEventKinds.ENTRY_CREATE || kind == StandardWatchEventKinds.ENTRY_MODIFY) { stacker.added(file); } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) { stacker.removed(file); //Only available on jpath watch //} else if (kind == ExtendedWatchEventKind.KEY_INVALID) { // //Invalidated means it was removed... (so, no need to reschedule to listen again) // keyToPath.remove(signalledKey); // stacker.key = null; // stacker.removed(file); // pathToStacker.remove(watchedPath); } } } try { stacker.run(); } catch (Exception e1) { Log.log(e1); } } } catch (Exception e) { Log.log(e); } } } } /* (non-Javadoc) * @see org.python.pydev.shared_core.path_watch.IPathWatch#stopTrack(java.io.File, org.python.pydev.shared_core.path_watch.IFilesystemChangesListener) */ @Override public void stopTrack(File path, IFilesystemChangesListener listener) { if (disposed) { return; } Assert.isNotNull(path); Assert.isNotNull(listener); Path watchedPath = Paths.get(FileUtils.getFileAbsolutePath(path)); if (log != null) { log.append("STOP Track: ").appendObject(path).append("Listener: ").appendObject(listener).append('\n'); } synchronized (lock) { EventsStackerRunnable stacker = pathToStacker.get(watchedPath); if (stacker != null && stacker.list != null) { ListenerList<IFilesystemChangesListener> list = stacker.list; list.remove(listener); if (list.getListeners().length == 0) { pathToStacker.remove(watchedPath); keyToPath.remove(stacker.key); } } } } /* (non-Javadoc) * @see org.python.pydev.shared_core.path_watch.IPathWatch#hasTracker(java.io.File, org.python.pydev.shared_core.path_watch.IFilesystemChangesListener) */ @Override public boolean hasTracker(File path, IFilesystemChangesListener listener) { if (disposed) { return false; } Assert.isNotNull(path); Assert.isNotNull(listener); Path watchedPath = Paths.get(FileUtils.getFileAbsolutePath(path)); if (log != null) { log.append("Has Tracker: ").appendObject(path).append("Listener: ").appendObject(listener).append('\n'); } synchronized (lock) { EventsStackerRunnable stacker = pathToStacker.get(watchedPath); if (stacker != null && stacker.list != null) { ListenerList<IFilesystemChangesListener> list = stacker.list; IFilesystemChangesListener[] listeners = list.getListeners(); for (IFilesystemChangesListener iFilesystemChangesListener : listeners) { if (list.equals(iFilesystemChangesListener)) { return true; } } } } return false; } /* (non-Javadoc) * @see org.python.pydev.shared_core.path_watch.IPathWatch#dispose() */ @Override public void dispose() { disposed = true; try { synchronized (lock) { pathToStacker.clear(); keyToPath.clear(); try { if (watchService != null) { watchService.close(); } } catch (IOException e) { Log.log(e); } pollThread.interrupt(); } } catch (Exception e) { Log.log(e); } } /* (non-Javadoc) * @see org.python.pydev.shared_core.path_watch.IPathWatch#track(java.io.File, org.python.pydev.shared_core.path_watch.IFilesystemChangesListener) */ @Override public void track(File path, IFilesystemChangesListener listener) { if (disposed) { return; } registeredTracker = true; Assert.isNotNull(path); Assert.isNotNull(listener); if (!path.exists()) { Log.logInfo("Unable to track file that does not exist: " + path); return; } Path watchedPath = Paths.get(FileUtils.getFileAbsolutePath(path)); synchronized (lock) { EventsStackerRunnable stacker = pathToStacker.get(watchedPath); if (stacker != null) { //already being tracked -- or already in invalid list ;) stacker.list.add(listener); return; } if (log != null) { log.append("Track: ").appendObject(path).append(" Listener: ").appendObject(listener).append('\n'); } boolean add = true; WatchKey key = null; try { if (watchService != null) { key = watchedPath.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.OVERFLOW //, ExtendedWatchEventKind.KEY_INVALID ); } else { Log.log("watchService is null. Unable to track: " + path); } } catch (UnsupportedOperationException uox) { if (log != null) { log.append("UnsupportedOperationException: ").appendObject(uox).append('\n'); } add = false; Log.log(uox); } catch (IOException iox) { //Ignore: it may not exist now, but may start existing later on... } catch (Throwable e) { if (log != null) { log.append("Throwable: ").appendObject(e).append('\n'); } add = false; Log.log(e); } if (add) { if (stacker == null) { stacker = new EventsStackerRunnable(key, watchedPath, new ListenerList<IFilesystemChangesListener>( IFilesystemChangesListener.class), path, fileFilter, dirsFilter); pathToStacker.put(watchedPath, stacker); } stacker.list.add(listener); if (key != null) { keyToPath.put(key, watchedPath); } } } } /* (non-Javadoc) * @see org.python.pydev.shared_core.path_watch.IPathWatch#setDirectoryFileFilter(java.io.FileFilter, java.io.FileFilter) */ @Override public void setDirectoryFileFilter(FileFilter fileFilter, FileFilter dirsFilter) { if (registeredTracker) { throw new AssertionError("After registering a tracker, the file filter can no longer be changed."); } this.fileFilter = fileFilter; this.dirsFilter = dirsFilter; } }