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 }