package gw.util.filewatcher; import gw.config.CommonServices; import gw.lang.reflect.TypeSystem; import gw.fs.IDirectory; import gw.lang.reflect.module.IModule; import gw.lang.UnstableAPI; import gw.util.GosuObjectUtil; import gw.util.Pair; import javax.swing.*; import java.io.File; import java.net.URI; import java.net.URISyntaxException; import java.util.*; @UnstableAPI public class FileWatcher { private static final FileWatcher INSTANCE = new FileWatcher(); private static boolean _SCAN_ENTIRE = true; private final Map<URI, Pair<Long, Long>> _currentState = new HashMap<URI, Pair<Long, Long>>(); private WeakHashMap<IFileChangeListener, Object> _listeners = new WeakHashMap<IFileChangeListener, Object>(); private boolean _watcherThreadStarted; private volatile boolean _checkNow; public static FileWatcher instance() { return INSTANCE; } private FileWatcher() { } public void setScanEntire(boolean scanEntire) { _SCAN_ENTIRE = scanEntire; } public void scanForChangesAndNotify() { maybeStartWatcherThread(); synchronized( FileWatcher.this ) { _checkNow = true; FileWatcher.this.notifyAll(); } } private void maybeStartWatcherThread() { synchronized( FileWatcher.this ) { if ( _watcherThreadStarted ) { return; } _watcherThreadStarted = true; } Thread watcherThread = new Thread("File Watcher Thread") { public void run() { boolean firstRun = true; try { MAINLOOP: //noinspection InfiniteLoopStatement while ( true ) { synchronized( FileWatcher.this ) { while ( ! _checkNow ) { FileWatcher.this.wait(); } _checkNow = false; } Map<URI,FileChangeInfo> changedFiles = new HashMap<URI,FileChangeInfo>(); //noinspection LoopStatementThatDoesntLoop while ( true ) { try { Thread.sleep( 1000 ); } catch ( InterruptedException e ) { break; } Set<URI> allFiles = new HashSet<URI>(); synchronized ( _currentState ) { scanForChanges( changedFiles, allFiles ); if ( firstRun ) { firstRun = false; continue MAINLOOP; } Iterator<URI> iter = _currentState.keySet().iterator(); while ( iter.hasNext() ) { URI uri = iter.next(); if ( !allFiles.contains( uri ) ) { // file was deleted iter.remove(); changedFiles.put( uri, new FileChangeInfo( FileChangeType.DELETED, null, null ) ); } } } if ( ! changedFiles.isEmpty() ) { // scanForChangesAndNotify(); // run algorithm again // for ( Map.Entry<URI, FileChangeInfo> changedFile : changedFiles.entrySet() ) { // System.out.println( "*** " + changedFile.getValue().getFileChangeType() + ": " + changedFile.getKey() ); // } CommonServices.getFileSystem().clearAllCaches(); TypeSystem.lock(); try { for ( IFileChangeListener listener : _listeners.keySet() ) { listener.filesChanged( changedFiles ); listener.fileScanComplete(); } } finally { TypeSystem.unlock(); } } break; } for ( IFileChangeListener listener : _listeners.keySet() ) { listener.fileScanComplete(); } } } catch ( Throwable throwable ) { throwable.printStackTrace(); } } }; watcherThread.setDaemon(true); watcherThread.start(); } private void scanForChanges( Map<URI, FileChangeInfo> changedFiles, Set<URI> allFiles ) { boolean debug = CommonServices.getEntityAccess().getLogger().isDebugEnabled(); long start = debug ? System.nanoTime() : 0; for ( IModule module : TypeSystem.getExecutionEnvironment().getModules() ) { scanForChanges(module, changedFiles, allFiles); } if(debug) { long end = System.nanoTime(); long delta = (end - start + 500000) / 1000000; CommonServices.getEntityAccess().getLogger().debug("FileWatcher.scanForChanges() scan took " + delta + "ms"); } } private void scanForChanges( IModule module, Map<URI, FileChangeInfo> changedFiles, Set<URI> allFiles ) { WatcherHandler handler = new WatcherHandler(changedFiles, allFiles); if(_SCAN_ENTIRE) { for ( IDirectory root : module.getResourceAccess().getRoots() ) { scanForChanges(root, handler); } } else { // Optimization for Studio. Should encompass all areas containing reloadable files that could be modified. for ( IDirectory root : module.getResourceAccess().getSourceEntries() ) { scanForChanges(root, handler); } for ( IDirectory root : module.getResourceAccess().getRoots() ) { if ( root.isJavaFile() ) { scanForChanges(root.dir("config"), handler); } } } } private void scanForChanges(IDirectory root, IFileHandler handler) { if(root == null) { return; } if(!root.isJavaFile()) { // TODO: store checksum and contents of containing jar file return; } // the root "directory" of a jar is actually considered a java file, whereas it's subdirectories are not File dir = root.toJavaFile(); if ( dir.isDirectory() ) { String path = dir.getAbsolutePath(); if (handler.filter(path)) { if(NativeFileSupport.isEnabled()) { try { NativeFileSupport.nativeFindFiles(path, handler); } catch(Throwable t) { NativeFileSupport.disable(); CommonServices.getEntityAccess().getLogger().info("Unexpected exception during native execution. " + "NativeFileSupport disabled for session.", t); // TODO Better to detect remaining work and finish it in pure java impl but this will work. scanForChangesImpl(path, handler); } } else { scanForChangesImpl(path, handler); } } } } private void scanForChangesImpl(String parentPath, IFileHandler handler) { for (File file : new File(parentPath).listFiles()) { String childPath = file.getAbsolutePath(); if (handler.filter(childPath)) { boolean isDir = file.isDirectory(); if (isDir) { scanForChangesImpl(childPath, handler); } handler.process(childPath, isDir, file.lastModified(), file.length()); } } } private class WatcherHandler implements IFileHandler { private Map<URI, FileChangeInfo> _changedFiles; private Set<URI> _allFiles; public WatcherHandler(Map<URI, FileChangeInfo> changedFiles, Set<URI> allFiles) { _changedFiles = changedFiles; _allFiles = allFiles; } @Override public boolean filter(String path) { return true; } @Override public void process(String path, boolean isDir, long timestamp, long length) { File file = new File(path); if ( isDir ) { // TODO does anything need to happen here? } else { // do not use File.toURI() here -- too slow. URI uri = toURI(file, isDir); _allFiles.add( uri ); Pair<Long, Long> currentStateForFile = _currentState.get( uri ); Pair<Long, Long> newState = new Pair<Long, Long>( length, timestamp ); if ( ! GosuObjectUtil.equals( currentStateForFile, newState ) ) { _currentState.put( uri, newState ); _changedFiles.put( uri, new FileChangeInfo( currentStateForFile == null ? FileChangeType.ADDED : FileChangeType.CHANGED, newState.getFirst(), newState.getSecond() ) ); } } } } private URI toURI(File file, boolean isDir) { try { File f = file.getAbsoluteFile(); String sp = slashify(f.getPath(), isDir); if (sp.startsWith("//")) { sp = "//" + sp; } return new URI("file", null, sp, null); } catch (URISyntaxException x) { throw new Error(x); // Can't happen } } private static String slashify(String path, boolean isDirectory) { String p = path; if (File.separatorChar != '/') { p = p.replace(File.separatorChar, '/'); } if (!p.startsWith("/")) { p = "/" + p; } if (!p.endsWith("/") && isDirectory) { p = p + "/"; } return p; } // Do not call this from inside WatcherHandler. private Pair<Long,Long> getFileState( URI uri ) { File file = new File( uri ); if ( file.exists() ) { long currentLength = file.length(); long modificationTime = file.lastModified(); return new Pair<Long, Long>( currentLength, modificationTime ); } else { return new Pair<Long, Long>( null, null ); } } public void addFileChangeListenerAsWeakRef( IFileChangeListener listener ) { TypeSystem.lock(); try { _listeners.put( listener, null ); } finally { TypeSystem.unlock(); } } public void removeFileChangeListener( IFileChangeListener listener ) { TypeSystem.lock(); try { _listeners.remove( listener ); } finally { TypeSystem.unlock(); } } public void markAsCurrent( File file ) { synchronized( _currentState ) { URI fileUri = file.getAbsoluteFile().toURI(); Pair<Long, Long> fileState = getFileState( fileUri ); if ( fileState.getFirst() == null ) { _currentState.remove( fileUri ); } else { _currentState.put( fileUri, fileState ); } } } public void markAsCurrent( File... files ) { markAsCurrent( Arrays.asList( files )); } public void markAsCurrent( final List<File> files ) { if(SwingUtilities.isEventDispatchThread()) { final Map<URI, Pair<Long, Long>> fileStates = collectFileStates(files); Thread bgmarker = new Thread(new Runnable() { public void run() { markCurrent(fileStates); } }); bgmarker.setDaemon(true); bgmarker.start(); } else { markCurrent(collectFileStates(files)); } } private void markCurrent( Map<URI, Pair<Long, Long>> fileStates ) { synchronized ( _currentState ) { for(Map.Entry<URI, Pair<Long, Long>> fileEntry : fileStates.entrySet() ) { URI fileUri = fileEntry.getKey(); Pair<Long, Long> fileState = fileEntry.getValue(); if ( fileState.getFirst() == null ) { _currentState.remove( fileUri ); } else { _currentState.put( fileUri, fileState ); } } } } private Map<URI, Pair<Long, Long>> collectFileStates( List<File> files ) { if(files != null && files.size() > 0) { Map<URI, Pair<Long, Long>> fileStates = new HashMap<URI, Pair<Long, Long>>(files.size() * 2); for(File file : files) { URI fileUri = file.getAbsoluteFile().toURI(); fileStates.put(fileUri, getFileState(fileUri)); } return fileStates; } return Collections.emptyMap(); } }