/** * * Copyright 2003-2004 The Apache Software Foundation * * 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.apache.geronimo.deployment.hot; import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.Log; import org.apache.geronimo.deployment.cli.DeployUtils; import java.io.File; import java.io.Serializable; import java.util.Map; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.LinkedList; /** * Meant to be run as a Thread that tracks the contents of a directory. * It sends notifications for changes to its immediate children (it * will look into subdirs for changes, but will not send notifications * for files within subdirectories). If a file continues to change on * every pass, this will wait until it stabilizes before sending an * add or update notification (to handle slow uploads, etc.). * * @version $Rev$ $Date$ */ public class DirectoryMonitor implements Runnable { private static final Log log = LogFactory.getLog(DirectoryMonitor.class); public static interface Listener { /** * The directory monitor doesn't take any action unless this method * returns true (to avoid deploying before the deploy GBeans are * running, etc.). */ boolean isServerRunning(); /** * Called during initialization on all files in the hot deploy * directory. * * @return true if the file in question is already available in the * server, false if it should be deployed on the next pass. */ boolean isFileDeployed(File file, String configId); /** * Called during initialization on previously deployed files. * * @return The time that the file was deployed. If the current * version in the directory is newer, the file will be * updated on the first pass. */ long getDeploymentTime(File file, String configId); /** * Called to indicate that the monitor has fully initialized * and will be doing normal deployment operations from now on. */ void started(); /** * Called to check whether a file passes the smell test before * attempting to deploy it. * * @return true if there's nothing obviously wrong with this file. * false if there is (for example, it's clearly not * deployable). */ boolean validateFile(File file, String configId); /** * @return A configId for the deployment if the addition was processed * successfully (or an empty String if the addition was OK but * the configId could not be determined). null if the addition * failed, in which case the file will be added again next time * it changes. */ String fileAdded(File file); /** * @return true if the removal was processed successfully. If not * the file will be removed again on the next pass. */ boolean fileRemoved(File file, String configId); void fileUpdated(File file, String configId); } private int pollIntervalMillis; private File directory; private boolean done = false; private Listener listener; // a little cheesy, but do we really need multiple listeners? private Map files = new HashMap(); public DirectoryMonitor(File directory, Listener listener, int pollIntervalMillis) { this.directory = directory; this.listener = listener; this.pollIntervalMillis = pollIntervalMillis; } public int getPollIntervalMillis() { return pollIntervalMillis; } public void setPollIntervalMillis(int pollIntervalMillis) { this.pollIntervalMillis = pollIntervalMillis; } public Listener getListener() { return listener; } public void setListener(Listener listener) { this.listener = listener; } public File getDirectory() { return directory; } /** * Warning: changing the directory at runtime will cause all files in the * old directory to be removed and all files in the new directory to be * added, next time the thread awakens. */ public void setDirectory(File directory) { if(!directory.isDirectory() || !directory.canRead()) { throw new IllegalArgumentException("Cannot monitor directory "+directory.getAbsolutePath()); } this.directory = directory; } public synchronized boolean isDone() { return done; } public synchronized void close() { this.done = true; } public void run() { boolean serverStarted = false, initialized = false; while(!done) { try { Thread.sleep(pollIntervalMillis); } catch (InterruptedException e) { continue; } if(listener != null) { if(!serverStarted && listener.isServerRunning()) { serverStarted = true; } if(serverStarted) { if(!initialized) { initialized = true; initialize(); listener.started(); } else { scanDirectory(); } } } } } public void initialize() { File parent = directory; File[] children = parent.listFiles(); for (int i = 0; i < children.length; i++) { File child = children[i]; if(!child.canRead()) { continue; } FileInfo now = child.isDirectory() ? getDirectoryInfo(child) : getFileInfo(child); now.setChanging(false); try { now.setConfigId(calculateModuleId(child)); if(listener == null || listener.isFileDeployed(child, now.getConfigId())) { if(listener != null) { now.setModified(listener.getDeploymentTime(child, now.getConfigId())); } files.put(now.getPath(), now); } } catch (Exception e) { log.error("Unable to scan file "+child.getAbsolutePath()+" during initialization", e); } } } /** * Looks for changes to the immediate contents of the directory we're watching. */ private void scanDirectory() { File parent = directory; File[] children = parent.listFiles(); if(!directory.exists() || children == null) { log.error("Hot deploy directory has disappeared! Shutting down directory monitor."); done = true; return; } HashSet oldList = new HashSet(files.keySet()); List actions = new LinkedList(); for (int i = 0; i < children.length; i++) { File child = children[i]; if(!child.canRead()) { continue; } FileInfo now = child.isDirectory() ? getDirectoryInfo(child) : getFileInfo(child); FileInfo then = (FileInfo) files.get(now.getPath()); if(then == null) { // Brand new, wait a bit to make sure it's not still changing now.setNewFile(true); files.put(now.getPath(), now); log.debug("New File: "+now.getPath()); } else { oldList.remove(then.getPath()); if(now.isSame(then)) { // File is the same as the last time we scanned it if(then.isChanging()) { log.debug("File finished changing: "+now.getPath()); // Used to be changing, now in (hopefully) its final state if(then.isNewFile()) { actions.add(new FileAction(FileAction.NEW_FILE, child, then)); } else { actions.add(new FileAction(FileAction.UPDATED_FILE, child, then)); } then.setChanging(false); } // else it's just totally unchanged and we ignore it this pass } else { // The two records are different -- record the latest as a file that's changing // and later when it stops changing we'll do the add or update as appropriate. now.setConfigId(then.getConfigId()); now.setNewFile(then.isNewFile()); files.put(now.getPath(), now); log.debug("File Changed: "+now.getPath()); } } } // Look for any files we used to know about but didn't find in this pass for (Iterator it = oldList.iterator(); it.hasNext();) { String name = (String) it.next(); FileInfo info = (FileInfo) files.get(name); log.debug("File removed: "+name); if(info.isNewFile()) { // Was never added, just whack it files.remove(name); } else { actions.add(new FileAction(FileAction.REMOVED_FILE, new File(name), info)); } } if(listener != null) { // First pass: validate all changed files, so any obvious errors come out first for (Iterator it = actions.iterator(); it.hasNext();) { FileAction action = (FileAction) it.next(); if(!listener.validateFile(action.child, action.info.getConfigId())) { resolveFile(action); it.remove(); } } // Second pass: do what we're meant to do for (Iterator it = actions.iterator(); it.hasNext();) { FileAction action = (FileAction) it.next(); try { if(action.action == FileAction.REMOVED_FILE) { if(listener.fileRemoved(action.child, action.info.getConfigId())) { files.remove(action.child.getPath()); } } else if(action.action == FileAction.NEW_FILE) { String result = listener.fileAdded(action.child); if(result != null) { if(!result.equals("")) { action.info.setConfigId(result); } else { action.info.setConfigId(calculateModuleId(action.child)); } action.info.setNewFile(false); } } else if(action.action == FileAction.UPDATED_FILE) { listener.fileUpdated(action.child, action.info.getConfigId()); } } catch (Exception e) { log.error("Unable to "+action.getActionName()+" file "+action.child.getAbsolutePath(), e); } finally { resolveFile(action); } } } } private void resolveFile(FileAction action) { if(action.action == FileAction.REMOVED_FILE) { files.remove(action.child.getPath()); } else { action.info.setChanging(false); } } private static String calculateModuleId(File module) { String moduleId = null; try { moduleId = DeployUtils.extractModuleIdFromArchive(module); } catch (Exception e) { log.warn("Unable to calculate module ID for module "+module.getAbsolutePath()+" ["+e.getMessage()+"]"); } if(moduleId == null) { int pos = module.getName().lastIndexOf('.'); moduleId = pos > -1 ? module.getName().substring(0, pos) : module.getName(); } return moduleId; } /** * We don't pay attention to the size of the directory or files in the * directory, only the highest last modified time of anything in the * directory. Hopefully this is good enough. */ private FileInfo getDirectoryInfo(File dir) { FileInfo info = new FileInfo(dir.getAbsolutePath()); info.setSize(0); info.setModified(getLastModifiedInDir(dir)); return info; } private long getLastModifiedInDir(File dir) { long value = dir.lastModified(); File[] children = dir.listFiles(); long test; for (int i = 0; i < children.length; i++) { File child = children[i]; if(!child.canRead()) { continue; } if(child.isDirectory()) { test = getLastModifiedInDir(child); } else { test = child.lastModified(); } if(test > value) { value = test; } } return value; } private FileInfo getFileInfo(File child) { FileInfo info = new FileInfo(child.getAbsolutePath()); info.setSize(child.length()); info.setModified(child.lastModified()); return info; } private static class FileAction { private static int NEW_FILE = 1; private static int UPDATED_FILE = 2; private static int REMOVED_FILE = 3; private int action; private File child; private FileInfo info; public FileAction(int action, File child, FileInfo info) { this.action = action; this.child = child; this.info = info; } public String getActionName() { return action == NEW_FILE ? "deploy" : action == UPDATED_FILE ? "redeploy" : "undeploy"; } } private static class FileInfo implements Serializable { private String path; private long size; private long modified; private boolean newFile; private boolean changing; private String configId; public FileInfo(String path) { this.path = path; newFile = false; changing = true; } public String getPath() { return path; } public long getSize() { return size; } public void setSize(long size) { this.size = size; } public long getModified() { return modified; } public void setModified(long modified) { this.modified = modified; } public boolean isNewFile() { return newFile; } public void setNewFile(boolean newFile) { this.newFile = newFile; } public boolean isChanging() { return changing; } public void setChanging(boolean changing) { this.changing = changing; } public String getConfigId() { return configId; } public void setConfigId(String configId) { this.configId = configId; } public boolean isSame(FileInfo info) { if(!path.equals(info.path)) { throw new IllegalArgumentException("Should only be used to compare two files representing the same path!"); } return size == info.size && modified == info.modified; } } }