/* * Copyright 2010-2012 VMware and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springsource.loaded.agent; import java.io.File; import java.io.FileFilter; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.springsource.loaded.FileChangeListener; import org.springsource.loaded.GlobalConfiguration; import org.springsource.loaded.TypeRegistry; /** * A simple watcher for the file system. Uses a thread to keep an eye on a number of files and calls back registered * interested parties when a change is observed. The thread only starts when there is something to watch. The thread is * given a name indicating the classloader for which it is watching files. Once it starts to watch files the name will * be enhanced to indicate how many. * * @author Andy Clement * @since 0.5.0 */ public class FileSystemWatcher { // the thread being managed private Thread thread; // whether the thread is running private boolean threadRunning = false; // The Watcher running inside the thread private Watcher watchThread; public FileSystemWatcher(FileChangeListener listener, int typeRegistryId, String classloadername) { watchThread = new Watcher(listener, typeRegistryId, classloadername); } /** * Start the thread if it isn't already started. */ private void ensureWatchThreadRunning() { if (!threadRunning) { thread = new Thread(watchThread); thread.setDaemon(true); thread.start(); watchThread.setThread(thread); watchThread.updateName(); threadRunning = true; } } /** * Shutdown the thread. */ public void shutdown() { if (threadRunning) { watchThread.timeToStop(); } } /** * Add a new file to the list of those being monitored. If the file is something that can be watched, then this * method will cause the thread to start (if it hasn't already been started). * * @param fileToMonitor the file to start monitor */ public void register(File fileToMonitor) { if (watchThread.addFile(fileToMonitor)) { ensureWatchThreadRunning(); watchThread.updateName(); } } /** * Enables the filesystem watching to be paused/unpaused. * * @param shouldBePaused watching should be paused? */ public void setPaused(boolean shouldBePaused) { watchThread.paused = shouldBePaused; } } class Watcher implements Runnable { private static Logger log = Logger.getLogger(Watcher.class.getName()); long lastScanTime; // TODO configurable scan interval? private static long interval = 1100;// ms List<File> watchListFiles = new ArrayList<File>(); List<Long> watchListLMTs = new ArrayList<Long>(); FileChangeListener listener; private boolean timeToStop = false; public boolean paused = false; private Thread thread = null; private int typeRegistryId; private String classloadername; private int registryLivenessCount = 0; private static int registryLivenessCountInterval = 300; public Watcher(FileChangeListener listener, int typeRegistryId, String classloadername) { this.listener = listener; this.typeRegistryId = typeRegistryId; this.classloadername = classloadername; } public void setThread(Thread thread) { this.thread = thread; } /** * Add a new File that the thread should start watching. If the file does not exist nothing happens (this may be * because a class has been generated on the fly and really there is nothing to watch on disk). * * @param fileToWatch the new file to watch * @return true if the file is now being watched, false otherwise */ public boolean addFile(File fileToWatch) { if (!fileToWatch.exists()) { return false; } synchronized (this) { if (GlobalConfiguration.verboseMode && log.isLoggable(Level.INFO)) { log.info("Now watching " + fileToWatch); } int insertionPos = findPosition(fileToWatch); if (insertionPos == -1) { watchListFiles.add(fileToWatch); watchListLMTs.add(fileToWatch.lastModified()); } else { watchListFiles.add(insertionPos, fileToWatch); watchListLMTs.add(insertionPos, fileToWatch.lastModified()); } return true; } } public void updateName() { if (thread != null) { thread.setName("FileSystemWatcher: files=#" + watchListFiles.size() + " cl=" + classloadername); } } private int findPosition(File file) { String filename = file.getName(); int len = watchListFiles.size(); if (len == 0) { return 0; } for (int f = 0; f < len; f++) { File file2 = watchListFiles.get(f); int cmp = file2.getName().compareTo(filename); // as we are using 'names' we are only considering the last part, so foo/bar/Goo.class and foo/Goo.class look the same // and will return cmp==0. Not really sure it matters about using fq names if (cmp > 0) { return f; } else if (GlobalConfiguration.assertsMode && cmp == 0) { // Are we watching the same file twice, that is bad! if (file2.getAbsoluteFile().toString().equals(file.getAbsoluteFile().toString())) { log.severe("Watching the same file twice: " + file.getAbsoluteFile().toString()); } } } return -1; } public void run() { while (!timeToStop) { registryLivenessCount++; if ((registryLivenessCount % registryLivenessCountInterval) == 0) { // Time to check if the registry is still alive! if (!TypeRegistry.typeRegistryExistsForId(typeRegistryId)) { if (GlobalConfiguration.verboseMode && log.isLoggable(Level.INFO)) { log.info("TypeRegistry " + typeRegistryId + " gone, no point in thread continuing!"); } return; } registryLivenessCount = 0; } try { Thread.sleep(interval); } catch (Exception e) { } if (!paused) { List<File> changedFiles = new ArrayList<File>(); synchronized (this) { int len = watchListFiles.size(); for (int f = 0; f < len; f++) { File file = watchListFiles.get(f); long lastModTime = file.lastModified(); if (lastModTime > watchListLMTs.get(f)) { if (GlobalConfiguration.verboseMode && log.isLoggable(Level.INFO)) { log.info("Observed last modification time change for " + file + " (lastScanTime=" + lastScanTime + ")"); } watchListLMTs.set(f, lastModTime); changedFiles.add(file); } } lastScanTime = System.currentTimeMillis(); } for (File changedFile : changedFiles) { determineChangesSince(changedFile, lastScanTime); } } } } /* * problem is that we check some file X, it hasn't changed - we then take longer than interval to check all the * other files we are watching. */ private void determineChangesSince(File file, long lastScanTime) { try { if (GlobalConfiguration.verboseMode && log.isLoggable(Level.INFO)) { log.info("Firing file changed event " + file); } listener.fileChanged(file); if (file.isDirectory()) { File[] filesOfInterest = file.listFiles(new RecentChangeFilter(lastScanTime)); for (File f : filesOfInterest) { if (f.isDirectory()) { determineChangesSince(f, lastScanTime); } else { if (GlobalConfiguration.verboseMode && log.isLoggable(Level.INFO)) { log.info("Observed last modification time change for " + f + " (lastScanTime=" + lastScanTime + ")"); log.info("Firing file changed event " + file); } listener.fileChanged(f); } } } } catch (Throwable t) { if (log.isLoggable(Level.SEVERE)) { log.log(Level.SEVERE, "FileWatcher caught serious error, see cause", t); } } } static class RecentChangeFilter implements FileFilter { private long lastScanTime; public RecentChangeFilter(long lastScanTime) { this.lastScanTime = lastScanTime; } public boolean accept(File pathname) { return (pathname.lastModified() > lastScanTime); } } public void timeToStop() { timeToStop = true; } }