package com.pastdev.jsch.nio.file; import java.io.IOException; import java.nio.file.ClosedWatchServiceException; import java.nio.file.Path; 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.attribute.PosixFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class UnixSshPathWatchKey implements WatchKey, Runnable { private static Logger logger = LoggerFactory.getLogger( UnixSshPathWatchKey.class ); private Map<UnixSshPath, UnixSshPathWatchEvent<Path>> addMap; private boolean cancelled; private Map<UnixSshPath, UnixSshPathWatchEvent<Path>> deleteMap; UnixSshPath dir; Map<UnixSshPath, PosixFileAttributes> entries; private boolean initialized; private Set<Kind<?>> kindsToWatch; private Map<UnixSshPath, UnixSshPathWatchEvent<Path>> modifyMap; private long pollingInterval; private TimeUnit pollingIntervalTimeUnit; private State state; private UnixSshFileSystemWatchService watchService; private ReentrantLock mapLock = new ReentrantLock(); private ReentrantLock stateLock = new ReentrantLock(); ReentrantLock pollerLock = new ReentrantLock(); private Condition initializationComplete = pollerLock.newCondition(); private Condition runImmediately = pollerLock.newCondition(); public UnixSshPathWatchKey( UnixSshFileSystemWatchService watchService, UnixSshPath dir, Kind<?>[] kinds, long pollingInterval, TimeUnit pollingIntervalTimeUnit ) { this.watchService = watchService; this.dir = dir; this.kindsToWatch = new HashSet<>(); this.kindsToWatch.addAll( Arrays.asList( kinds ) ); this.pollingInterval = pollingInterval; this.pollingIntervalTimeUnit = pollingIntervalTimeUnit; this.cancelled = false; this.initialized = false; this.addMap = new HashMap<>(); this.deleteMap = new HashMap<>(); this.modifyMap = new HashMap<>(); this.state = State.READY; } void addCreateEvent( UnixSshPath path ) { if ( kindsToWatch.contains( StandardWatchEventKinds.ENTRY_CREATE ) ) { try { mapLock.lock(); logger.trace( "added: {}", path ); if ( !addMap.containsKey( path ) ) { addMap.put( path, new UnixSshPathWatchEvent<Path>( StandardWatchEventKinds.ENTRY_CREATE, path ) ); } } finally { mapLock.unlock(); } signal(); } } void addDeleteEvent( UnixSshPath path ) { if ( kindsToWatch.contains( StandardWatchEventKinds.ENTRY_DELETE ) ) { try { mapLock.lock(); logger.trace( "deleted: {}", path ); if ( !deleteMap.containsKey( path ) ) { deleteMap.put( path, new UnixSshPathWatchEvent<Path>( StandardWatchEventKinds.ENTRY_DELETE, path ) ); } } finally { mapLock.unlock(); } signal(); } } void addModifyEvent( UnixSshPath path ) { if ( kindsToWatch.contains( StandardWatchEventKinds.ENTRY_MODIFY ) ) { try { mapLock.lock(); logger.trace( "modified: {}", path ); if ( modifyMap.containsKey( path ) ) { modifyMap.get( path ).increment(); } else { UnixSshPathWatchEvent<Path> event = new UnixSshPathWatchEvent<Path>( StandardWatchEventKinds.ENTRY_MODIFY, path ); modifyMap.put( path, event ); } } finally { mapLock.unlock(); } signal(); } } @Override public void cancel() { watchService.unregister( this ); cancelled = true; } @Override public boolean isValid() { return (!cancelled) && (!watchService.closed()); } private static boolean modified( PosixFileAttributes attributes, PosixFileAttributes otherAttributes ) { if ( attributes.size() != otherAttributes.size() ) { return true; } if ( attributes.lastModifiedTime() != otherAttributes.lastModifiedTime() ) { return true; } return false; } @Override public List<WatchEvent<?>> pollEvents() { try { mapLock.lock(); List<WatchEvent<?>> currentEvents = new ArrayList<>(); currentEvents.addAll( addMap.values() ); currentEvents.addAll( deleteMap.values() ); currentEvents.addAll( modifyMap.values() ); addMap.clear(); deleteMap.clear(); modifyMap.clear(); return Collections.unmodifiableList( currentEvents ); } finally { mapLock.unlock(); } } @Override public boolean reset() { if ( !isValid() ) { return false; } try { mapLock.lock(); if ( addMap.size() > 0 || deleteMap.size() > 0 || modifyMap.size() > 0 ) { signal(); return true; } } finally { mapLock.unlock(); } try { stateLock.lock(); state = State.READY; } finally { stateLock.unlock(); } return true; } @Override public void run() { boolean first = true; try { while ( true ) { if ( !isValid() ) { break; } try { logger.trace( "polling {}", dir ); Map<UnixSshPath, PosixFileAttributes> entries = dir.getFileSystem().provider().statDirectory( dir ); logger.trace( "got response {}", dir ); if ( first ) { first = false; this.entries = entries; try { pollerLock.lock(); logger.trace( "initialization complete got lock" ); initialized = true; initializationComplete.signalAll(); logger.debug( "poller is initialized" ); } finally { pollerLock.unlock(); } } else { for ( UnixSshPath entryPath : entries.keySet() ) { if ( this.entries.containsKey( entryPath ) ) { if ( modified( entries.get( entryPath ), this.entries.remove( entryPath ) ) ) { addModifyEvent( entryPath ); } } else { addCreateEvent( entryPath ); } } for ( UnixSshPath entryPath : this.entries.keySet() ) { addDeleteEvent( entryPath ); } this.entries = entries; } } catch ( IOException e ) { logger.error( "checking {} failed: {}", dir, e ); logger.debug( "checking directory failed: ", e ); } try { pollerLock.lock(); logger.trace( "poller entering await {} {}", pollingInterval, pollingIntervalTimeUnit ); runImmediately.await( pollingInterval, pollingIntervalTimeUnit ); } finally { pollerLock.unlock(); } } } catch ( ClosedWatchServiceException e ) { logger.debug( "watch service was closed, so exit" ); } catch ( InterruptedException e ) { // time to close out logger.debug( "interrupt caught, closing down poller" ); } logger.info( "poller stopped for {}", dir ); } void runImmediately() { try { pollerLock.lock(); runImmediately.signal(); } finally { pollerLock.unlock(); } } private void signal() { try { stateLock.lock(); logger.trace( "signaling" ); if ( state != State.SIGNALLED ) { state = State.SIGNALLED; logger.trace( "enqueueing {}", this ); watchService.enqueue( this ); } } finally { stateLock.unlock(); } } boolean waitForInitialization( long time, TimeUnit timeUnit ) { logger.debug( "waiting {} {} for initialization", time, timeUnit ); try { pollerLock.lock(); logger.debug( "wait for initialization obtained lock" ); if ( initialized ) { return true; } initializationComplete.await( time, timeUnit ); logger.debug( "initialization complete" ); return true; } catch ( InterruptedException e ) { return false; } finally { pollerLock.unlock(); } } @Override public UnixSshPath watchable() { return dir; } private enum State { READY, SIGNALLED } }