001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package org.apache.geronimo.deployment.hot; 018 019 import org.apache.commons.logging.LogFactory; 020 import org.apache.commons.logging.Log; 021 import org.apache.geronimo.deployment.cli.DeployUtils; 022 import org.apache.geronimo.deployment.util.DeploymentUtil; 023 import org.apache.geronimo.kernel.repository.Artifact; 024 import org.apache.geronimo.kernel.config.IOUtil; 025 026 import java.io.File; 027 import java.io.Serializable; 028 import java.io.IOException; 029 import java.util.Map; 030 import java.util.HashMap; 031 import java.util.HashSet; 032 import java.util.Iterator; 033 import java.util.List; 034 import java.util.LinkedList; 035 import java.util.Set; 036 037 /** 038 * Meant to be run as a Thread that tracks the contents of a directory. 039 * It sends notifications for changes to its immediate children (it 040 * will look into subdirs for changes, but will not send notifications 041 * for files within subdirectories). If a file continues to change on 042 * every pass, this will wait until it stabilizes before sending an 043 * add or update notification (to handle slow uploads, etc.). 044 * 045 * @version $Rev: 539130 $ $Date: 2007-05-17 17:45:24 -0400 (Thu, 17 May 2007) $ 046 */ 047 public class DirectoryMonitor implements Runnable { 048 private static final Log log = LogFactory.getLog(DirectoryMonitor.class); 049 050 public static interface Listener { 051 /** 052 * The directory monitor doesn't take any action unless this method 053 * returns true (to avoid deploying before the deploy GBeans are 054 * running, etc.). 055 */ 056 boolean isServerRunning(); 057 058 /** 059 * Checks if the file with same configID is already deployed 060 * 061 * @return true if the file in question is already available in the 062 * server, false if it should be deployed on the next pass. 063 */ 064 boolean isFileDeployed(File file, String configId); 065 066 /** 067 * Called during initialization on previously deployed files. 068 * 069 * @return The time that the file was deployed. If the current 070 * version in the directory is newer, the file will be 071 * updated on the first pass. 072 */ 073 long getDeploymentTime(File file, String configId); 074 075 /** 076 * Called to indicate that the monitor has fully initialized 077 * and will be doing normal deployment operations from now on. 078 */ 079 void started(); 080 081 /** 082 * Called to check whether a file passes the smell test before 083 * attempting to deploy it. 084 * 085 * @return true if there's nothing obviously wrong with this file. 086 * false if there is (for example, it's clearly not 087 * deployable). 088 */ 089 boolean validateFile(File file, String configId); 090 091 /** 092 * @return A configId for the deployment if the addition was processed 093 * successfully (or an empty String if the addition was OK but 094 * the configId could not be determined). null if the addition 095 * failed, in which case the file will be added again next time 096 * it changes. 097 */ 098 String fileAdded(File file); 099 100 /** 101 * @return true if the removal was processed successfully. If not 102 * the file will be removed again on the next pass. 103 */ 104 boolean fileRemoved(File file, String configId); 105 106 String fileUpdated(File file, String configId); 107 108 /** 109 * This method returns the module id of an application deployed in the default group. 110 * @return String respresenting the ModuleId if the application is already deployed 111 */ 112 String getModuleId(String config); 113 114 } 115 116 private int pollIntervalMillis; 117 private File directory; 118 private boolean done = false; 119 private Listener listener; // a little cheesy, but do we really need multiple listeners? 120 private final Map files = new HashMap(); 121 private volatile String workingOnConfigId; 122 123 public DirectoryMonitor(File directory, Listener listener, int pollIntervalMillis) { 124 this.directory = directory; 125 this.listener = listener; 126 this.pollIntervalMillis = pollIntervalMillis; 127 } 128 129 public int getPollIntervalMillis() { 130 return pollIntervalMillis; 131 } 132 133 public void setPollIntervalMillis(int pollIntervalMillis) { 134 this.pollIntervalMillis = pollIntervalMillis; 135 } 136 137 public Listener getListener() { 138 return listener; 139 } 140 141 public void setListener(Listener listener) { 142 this.listener = listener; 143 } 144 145 public File getDirectory() { 146 return directory; 147 } 148 149 /** 150 * Warning: changing the directory at runtime will cause all files in the 151 * old directory to be removed and all files in the new directory to be 152 * added, next time the thread awakens. 153 */ 154 public void setDirectory(File directory) { 155 if (!directory.isDirectory() || !directory.canRead()) { 156 throw new IllegalArgumentException("Cannot monitor directory " + directory.getAbsolutePath()); 157 } 158 this.directory = directory; 159 } 160 161 public synchronized boolean isDone() { 162 return done; 163 } 164 165 public synchronized void close() { 166 this.done = true; 167 } 168 169 public void removeModuleId(Artifact id) { 170 log.info("Hot deployer notified that an artifact was removed: "+id); 171 if(id.toString().equals(workingOnConfigId)) { 172 // since the redeploy process inserts a new thread to handle progress, 173 // this is called by a different thread than the hot deploy thread during 174 // a redeploy, and this check must be executed outside the synchronized 175 // block or else it will cause a deadlock! 176 return; // don't react to events we generated ourselves 177 } 178 synchronized(files) { 179 for (Iterator it = files.keySet().iterator(); it.hasNext();) { 180 String path = (String) it.next(); 181 FileInfo info = (FileInfo) files.get(path); 182 Artifact target = Artifact.create(info.getConfigId()); 183 if(id.matches(target)) { // need to remove record & delete file 184 File file = new File(path); 185 if(file.exists()) { // if not, probably it's deletion kicked off this whole process 186 log.info("Hot deployer deleting "+id); 187 if(!IOUtil.recursiveDelete(file)) { 188 log.error("Hot deployer unable to delete "+path); 189 } 190 it.remove(); 191 } 192 } 193 } 194 } 195 } 196 197 public void run() { 198 boolean serverStarted = false, initialized = false; 199 while (!done) { 200 try { 201 Thread.sleep(pollIntervalMillis); 202 } catch (InterruptedException e) { 203 continue; 204 } 205 try { 206 if (listener != null) { 207 if (!serverStarted && listener.isServerRunning()) { 208 serverStarted = true; 209 } 210 if (serverStarted) { 211 if (!initialized) { 212 initialized = true; 213 initialize(); 214 listener.started(); 215 } else { 216 scanDirectory(); 217 } 218 } 219 } 220 } catch (Exception e) { 221 log.error("Error during hot deployment", e); 222 } 223 } 224 } 225 226 public void initialize() { 227 File parent = directory; 228 File[] children = parent.listFiles(); 229 for (int i = 0; i < children.length; i++) { 230 File child = children[i]; 231 if (!child.canRead()) { 232 continue; 233 } 234 FileInfo now = child.isDirectory() ? getDirectoryInfo(child) : getFileInfo(child); 235 now.setChanging(false); 236 try { 237 now.setConfigId(calculateModuleId(child)); 238 if (listener == null || listener.isFileDeployed(child, now.getConfigId())) { 239 if (listener != null) { 240 now.setModified(listener.getDeploymentTime(child, now.getConfigId())); 241 } 242 log.info("At startup, found "+now.getPath()+" with deploy time "+now.getModified()+" and file time "+new File(now.getPath()).lastModified()); 243 files.put(now.getPath(), now); 244 } 245 } catch (Exception e) { 246 log.error("Unable to scan file " + child.getAbsolutePath() + " during initialization", e); 247 } 248 } 249 } 250 251 /** 252 * Looks for changes to the immediate contents of the directory we're watching. 253 */ 254 private void scanDirectory() { 255 File parent = directory; 256 File[] children = parent.listFiles(); 257 if (!directory.exists() || children == null) { 258 log.error("Hot deploy directory has disappeared! Shutting down directory monitor."); 259 done = true; 260 return; 261 } 262 synchronized (files) { 263 Set oldList = new HashSet(files.keySet()); 264 List actions = new LinkedList(); 265 for (int i = 0; i < children.length; i++) { 266 File child = children[i]; 267 if (!child.canRead()) { 268 continue; 269 } 270 FileInfo now = child.isDirectory() ? getDirectoryInfo(child) : getFileInfo(child); 271 FileInfo then = (FileInfo) files.get(now.getPath()); 272 if (then == null) { // Brand new, wait a bit to make sure it's not still changing 273 now.setNewFile(true); 274 files.put(now.getPath(), now); 275 log.debug("New File: " + now.getPath()); 276 } else { 277 oldList.remove(then.getPath()); 278 if (now.isSame(then)) { // File is the same as the last time we scanned it 279 if (then.isChanging()) { 280 log.debug("File finished changing: " + now.getPath()); 281 // Used to be changing, now in (hopefully) its final state 282 if (then.isNewFile()) { 283 actions.add(new FileAction(FileAction.NEW_FILE, child, then)); 284 } else { 285 actions.add(new FileAction(FileAction.UPDATED_FILE, child, then)); 286 } 287 then.setChanging(false); 288 } // else it's just totally unchanged and we ignore it this pass 289 } else if(then.isNewFile() || now.getModified() > then.getModified()) { 290 // The two records are different -- record the latest as a file that's changing 291 // and later when it stops changing we'll do the add or update as appropriate. 292 now.setConfigId(then.getConfigId()); 293 now.setNewFile(then.isNewFile()); 294 files.put(now.getPath(), now); 295 log.debug("File Changed: " + now.getPath()); 296 } 297 } 298 } 299 // Look for any files we used to know about but didn't find in this pass 300 for (Iterator it = oldList.iterator(); it.hasNext();) { 301 String name = (String) it.next(); 302 FileInfo info = (FileInfo) files.get(name); 303 log.debug("File removed: " + name); 304 if (info.isNewFile()) { // Was never added, just whack it 305 files.remove(name); 306 } else { 307 actions.add(new FileAction(FileAction.REMOVED_FILE, new File(name), info)); 308 } 309 } 310 if (listener != null) { 311 // First pass: validate all changed files, so any obvious errors come out first 312 for (Iterator it = actions.iterator(); it.hasNext();) { 313 FileAction action = (FileAction) it.next(); 314 if (!listener.validateFile(action.child, action.info.getConfigId())) { 315 resolveFile(action); 316 it.remove(); 317 } 318 } 319 // Second pass: do what we're meant to do 320 for (Iterator it = actions.iterator(); it.hasNext();) { 321 FileAction action = (FileAction) it.next(); 322 try { 323 if (action.action == FileAction.REMOVED_FILE) { 324 workingOnConfigId = action.info.getConfigId(); 325 if (listener.fileRemoved(action.child, action.info.getConfigId())) { 326 files.remove(action.child.getPath()); 327 } 328 workingOnConfigId = null; 329 } else if (action.action == FileAction.NEW_FILE) { 330 if (listener.isFileDeployed(action.child, calculateModuleId(action.child))) { 331 workingOnConfigId = calculateModuleId(action.child); 332 String result = listener.fileUpdated(action.child, workingOnConfigId); 333 if (result != null) { 334 if (!result.equals("")) { 335 action.info.setConfigId(result); 336 } 337 else { 338 action.info.setConfigId(calculateModuleId(action.child)); 339 } 340 } 341 // remove the previous jar or directory if duplicate 342 File[] childs = directory.listFiles(); 343 for (int i = 0; i < childs.length; i++) { 344 String path = childs[i].getAbsolutePath(); 345 String configId = ((FileInfo)files.get(path)).configId; 346 if (configId != null && configId.equals(workingOnConfigId) && !action.child.getAbsolutePath().equals(path)) { 347 File fd = new File(path); 348 if (fd.isDirectory()) { 349 log.info("Deleting the Directory: "+path); 350 if (DeploymentUtil.recursiveDelete(fd)) 351 log.debug("Successfully deleted the Directory: "+path); 352 else 353 log.error("Couldn't delete the hot deployed directory="+path); 354 } 355 else if (fd.isFile()) { 356 log.info("Deleting the File: "+path); 357 if (fd.delete()) { 358 log.debug("Successfully deleted the File: "+path); 359 } 360 else 361 log.error("Couldn't delete the hot deployed file="+path); 362 } 363 files.remove(path); 364 } 365 } 366 workingOnConfigId = null; 367 } 368 else { 369 String result = listener.fileAdded(action.child); 370 if (result != null) { 371 if (!result.equals("")) { 372 action.info.setConfigId(result); 373 } 374 else { 375 action.info.setConfigId(calculateModuleId(action.child)); 376 } 377 } 378 } 379 action.info.setNewFile(false); 380 } else if (action.action == FileAction.UPDATED_FILE) { 381 workingOnConfigId = action.info.getConfigId(); 382 String result = listener.fileUpdated(action.child, action.info.getConfigId()); 383 FileInfo update = action.info; 384 if (result != null) { 385 if (!result.equals("")) { 386 update.setConfigId(result); 387 } else { 388 update.setConfigId(calculateModuleId(action.child)); 389 } 390 } 391 workingOnConfigId = null; 392 } 393 } catch (Exception e) { 394 log.error("Unable to " + action.getActionName() + " file " + action.child.getAbsolutePath(), e); 395 } finally { 396 resolveFile(action); 397 } 398 } 399 } 400 } 401 } 402 403 private void resolveFile(FileAction action) { 404 if (action.action == FileAction.REMOVED_FILE) { 405 files.remove(action.child.getPath()); 406 } else { 407 action.info.setChanging(false); 408 } 409 } 410 411 private String calculateModuleId(File module) { 412 String moduleId = null; 413 try { 414 moduleId = DeployUtils.extractModuleIdFromArchive(module); 415 } catch (Exception e) { 416 try { 417 moduleId = DeployUtils.extractModuleIdFromPlan(module); 418 } catch (IOException e2) { 419 log.warn("Unable to calculate module ID for file " + module.getAbsolutePath() + " [" + e2.getMessage() + "]"); 420 } 421 } 422 if (moduleId == null) { 423 int pos = module.getName().lastIndexOf('.'); 424 moduleId = pos > -1 ? module.getName().substring(0, pos) : module.getName(); 425 moduleId = listener.getModuleId(moduleId); 426 } 427 return moduleId; 428 } 429 430 /** 431 * We don't pay attention to the size of the directory or files in the 432 * directory, only the highest last modified time of anything in the 433 * directory. Hopefully this is good enough. 434 */ 435 private FileInfo getDirectoryInfo(File dir) { 436 FileInfo info = new FileInfo(dir.getAbsolutePath()); 437 info.setSize(0); 438 info.setModified(getLastModifiedInDir(dir)); 439 return info; 440 } 441 442 private long getLastModifiedInDir(File dir) { 443 long value = dir.lastModified(); 444 File[] children = dir.listFiles(); 445 long test; 446 for (int i = 0; i < children.length; i++) { 447 File child = children[i]; 448 if (!child.canRead()) { 449 continue; 450 } 451 if (child.isDirectory()) { 452 test = getLastModifiedInDir(child); 453 } else { 454 test = child.lastModified(); 455 } 456 if (test > value) { 457 value = test; 458 } 459 } 460 return value; 461 } 462 463 private FileInfo getFileInfo(File child) { 464 FileInfo info = new FileInfo(child.getAbsolutePath()); 465 info.setSize(child.length()); 466 info.setModified(child.lastModified()); 467 return info; 468 } 469 470 private static class FileAction { 471 private static int NEW_FILE = 1; 472 private static int UPDATED_FILE = 2; 473 private static int REMOVED_FILE = 3; 474 private int action; 475 private File child; 476 private FileInfo info; 477 478 public FileAction(int action, File child, FileInfo info) { 479 this.action = action; 480 this.child = child; 481 this.info = info; 482 } 483 484 public String getActionName() { 485 return action == NEW_FILE ? "deploy" : action == UPDATED_FILE ? "redeploy" : "undeploy"; 486 } 487 } 488 489 private static class FileInfo implements Serializable { 490 private String path; 491 private long size; 492 private long modified; 493 private boolean newFile; 494 private boolean changing; 495 private String configId; 496 497 public FileInfo(String path) { 498 this.path = path; 499 newFile = false; 500 changing = true; 501 } 502 503 public String getPath() { 504 return path; 505 } 506 507 public long getSize() { 508 return size; 509 } 510 511 public void setSize(long size) { 512 this.size = size; 513 } 514 515 public long getModified() { 516 return modified; 517 } 518 519 public void setModified(long modified) { 520 this.modified = modified; 521 } 522 523 public boolean isNewFile() { 524 return newFile; 525 } 526 527 public void setNewFile(boolean newFile) { 528 this.newFile = newFile; 529 } 530 531 public boolean isChanging() { 532 return changing; 533 } 534 535 public void setChanging(boolean changing) { 536 this.changing = changing; 537 } 538 539 public String getConfigId() { 540 return configId; 541 } 542 543 public void setConfigId(String configId) { 544 this.configId = configId; 545 } 546 547 public boolean isSame(FileInfo info) { 548 if (!path.equals(info.path)) { 549 throw new IllegalArgumentException("Should only be used to compare two files representing the same path!"); 550 } 551 return size == info.size && modified == info.modified; 552 } 553 } 554 }