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: */package org.apache.geronimo.deployment.hot;
017:
018: import org.apache.commons.logging.LogFactory;
019: import org.apache.commons.logging.Log;
020: import org.apache.geronimo.deployment.cli.DeployUtils;
021: import org.apache.geronimo.deployment.util.DeploymentUtil;
022: import org.apache.geronimo.kernel.repository.Artifact;
023: import org.apache.geronimo.kernel.config.IOUtil;
024:
025: import java.io.File;
026: import java.io.Serializable;
027: import java.io.IOException;
028: import java.util.Map;
029: import java.util.HashMap;
030: import java.util.HashSet;
031: import java.util.Iterator;
032: import java.util.List;
033: import java.util.LinkedList;
034: import java.util.Set;
035:
036: /**
037: * Meant to be run as a Thread that tracks the contents of a directory.
038: * It sends notifications for changes to its immediate children (it
039: * will look into subdirs for changes, but will not send notifications
040: * for files within subdirectories). If a file continues to change on
041: * every pass, this will wait until it stabilizes before sending an
042: * add or update notification (to handle slow uploads, etc.).
043: *
044: * @version $Rev: 539130 $ $Date: 2007-05-17 14:45:24 -0700 (Thu, 17 May 2007) $
045: */
046: public class DirectoryMonitor implements Runnable {
047: private static final Log log = LogFactory
048: .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,
124: int pollIntervalMillis) {
125: this .directory = directory;
126: this .listener = listener;
127: this .pollIntervalMillis = pollIntervalMillis;
128: }
129:
130: public int getPollIntervalMillis() {
131: return pollIntervalMillis;
132: }
133:
134: public void setPollIntervalMillis(int pollIntervalMillis) {
135: this .pollIntervalMillis = pollIntervalMillis;
136: }
137:
138: public Listener getListener() {
139: return listener;
140: }
141:
142: public void setListener(Listener listener) {
143: this .listener = listener;
144: }
145:
146: public File getDirectory() {
147: return directory;
148: }
149:
150: /**
151: * Warning: changing the directory at runtime will cause all files in the
152: * old directory to be removed and all files in the new directory to be
153: * added, next time the thread awakens.
154: */
155: public void setDirectory(File directory) {
156: if (!directory.isDirectory() || !directory.canRead()) {
157: throw new IllegalArgumentException(
158: "Cannot monitor directory "
159: + directory.getAbsolutePath());
160: }
161: this .directory = directory;
162: }
163:
164: public synchronized boolean isDone() {
165: return done;
166: }
167:
168: public synchronized void close() {
169: this .done = true;
170: }
171:
172: public void removeModuleId(Artifact id) {
173: log.info("Hot deployer notified that an artifact was removed: "
174: + id);
175: if (id.toString().equals(workingOnConfigId)) {
176: // since the redeploy process inserts a new thread to handle progress,
177: // this is called by a different thread than the hot deploy thread during
178: // a redeploy, and this check must be executed outside the synchronized
179: // block or else it will cause a deadlock!
180: return; // don't react to events we generated ourselves
181: }
182: synchronized (files) {
183: for (Iterator it = files.keySet().iterator(); it.hasNext();) {
184: String path = (String) it.next();
185: FileInfo info = (FileInfo) files.get(path);
186: Artifact target = Artifact.create(info.getConfigId());
187: if (id.matches(target)) { // need to remove record & delete file
188: File file = new File(path);
189: if (file.exists()) { // if not, probably it's deletion kicked off this whole process
190: log.info("Hot deployer deleting " + id);
191: if (!IOUtil.recursiveDelete(file)) {
192: log.error("Hot deployer unable to delete "
193: + path);
194: }
195: it.remove();
196: }
197: }
198: }
199: }
200: }
201:
202: public void run() {
203: boolean serverStarted = false, initialized = false;
204: while (!done) {
205: try {
206: Thread.sleep(pollIntervalMillis);
207: } catch (InterruptedException e) {
208: continue;
209: }
210: try {
211: if (listener != null) {
212: if (!serverStarted && listener.isServerRunning()) {
213: serverStarted = true;
214: }
215: if (serverStarted) {
216: if (!initialized) {
217: initialized = true;
218: initialize();
219: listener.started();
220: } else {
221: scanDirectory();
222: }
223: }
224: }
225: } catch (Exception e) {
226: log.error("Error during hot deployment", e);
227: }
228: }
229: }
230:
231: public void initialize() {
232: File parent = directory;
233: File[] children = parent.listFiles();
234: for (int i = 0; i < children.length; i++) {
235: File child = children[i];
236: if (!child.canRead()) {
237: continue;
238: }
239: FileInfo now = child.isDirectory() ? getDirectoryInfo(child)
240: : getFileInfo(child);
241: now.setChanging(false);
242: try {
243: now.setConfigId(calculateModuleId(child));
244: if (listener == null
245: || listener.isFileDeployed(child, now
246: .getConfigId())) {
247: if (listener != null) {
248: now.setModified(listener.getDeploymentTime(
249: child, now.getConfigId()));
250: }
251: log.info("At startup, found " + now.getPath()
252: + " with deploy time " + now.getModified()
253: + " and file time "
254: + new File(now.getPath()).lastModified());
255: files.put(now.getPath(), now);
256: }
257: } catch (Exception e) {
258: log.error("Unable to scan file "
259: + child.getAbsolutePath()
260: + " during initialization", e);
261: }
262: }
263: }
264:
265: /**
266: * Looks for changes to the immediate contents of the directory we're watching.
267: */
268: private void scanDirectory() {
269: File parent = directory;
270: File[] children = parent.listFiles();
271: if (!directory.exists() || children == null) {
272: log
273: .error("Hot deploy directory has disappeared! Shutting down directory monitor.");
274: done = true;
275: return;
276: }
277: synchronized (files) {
278: Set oldList = new HashSet(files.keySet());
279: List actions = new LinkedList();
280: for (int i = 0; i < children.length; i++) {
281: File child = children[i];
282: if (!child.canRead()) {
283: continue;
284: }
285: FileInfo now = child.isDirectory() ? getDirectoryInfo(child)
286: : getFileInfo(child);
287: FileInfo then = (FileInfo) files.get(now.getPath());
288: if (then == null) { // Brand new, wait a bit to make sure it's not still changing
289: now.setNewFile(true);
290: files.put(now.getPath(), now);
291: log.debug("New File: " + now.getPath());
292: } else {
293: oldList.remove(then.getPath());
294: if (now.isSame(then)) { // File is the same as the last time we scanned it
295: if (then.isChanging()) {
296: log.debug("File finished changing: "
297: + now.getPath());
298: // Used to be changing, now in (hopefully) its final state
299: if (then.isNewFile()) {
300: actions.add(new FileAction(
301: FileAction.NEW_FILE, child,
302: then));
303: } else {
304: actions.add(new FileAction(
305: FileAction.UPDATED_FILE, child,
306: then));
307: }
308: then.setChanging(false);
309: } // else it's just totally unchanged and we ignore it this pass
310: } else if (then.isNewFile()
311: || now.getModified() > then.getModified()) {
312: // The two records are different -- record the latest as a file that's changing
313: // and later when it stops changing we'll do the add or update as appropriate.
314: now.setConfigId(then.getConfigId());
315: now.setNewFile(then.isNewFile());
316: files.put(now.getPath(), now);
317: log.debug("File Changed: " + now.getPath());
318: }
319: }
320: }
321: // Look for any files we used to know about but didn't find in this pass
322: for (Iterator it = oldList.iterator(); it.hasNext();) {
323: String name = (String) it.next();
324: FileInfo info = (FileInfo) files.get(name);
325: log.debug("File removed: " + name);
326: if (info.isNewFile()) { // Was never added, just whack it
327: files.remove(name);
328: } else {
329: actions.add(new FileAction(FileAction.REMOVED_FILE,
330: new File(name), info));
331: }
332: }
333: if (listener != null) {
334: // First pass: validate all changed files, so any obvious errors come out first
335: for (Iterator it = actions.iterator(); it.hasNext();) {
336: FileAction action = (FileAction) it.next();
337: if (!listener.validateFile(action.child,
338: action.info.getConfigId())) {
339: resolveFile(action);
340: it.remove();
341: }
342: }
343: // Second pass: do what we're meant to do
344: for (Iterator it = actions.iterator(); it.hasNext();) {
345: FileAction action = (FileAction) it.next();
346: try {
347: if (action.action == FileAction.REMOVED_FILE) {
348: workingOnConfigId = action.info
349: .getConfigId();
350: if (listener.fileRemoved(action.child,
351: action.info.getConfigId())) {
352: files.remove(action.child.getPath());
353: }
354: workingOnConfigId = null;
355: } else if (action.action == FileAction.NEW_FILE) {
356: if (listener.isFileDeployed(action.child,
357: calculateModuleId(action.child))) {
358: workingOnConfigId = calculateModuleId(action.child);
359: String result = listener
360: .fileUpdated(action.child,
361: workingOnConfigId);
362: if (result != null) {
363: if (!result.equals("")) {
364: action.info.setConfigId(result);
365: } else {
366: action.info
367: .setConfigId(calculateModuleId(action.child));
368: }
369: }
370: // remove the previous jar or directory if duplicate
371: File[] childs = directory.listFiles();
372: for (int i = 0; i < childs.length; i++) {
373: String path = childs[i]
374: .getAbsolutePath();
375: String configId = ((FileInfo) files
376: .get(path)).configId;
377: if (configId != null
378: && configId
379: .equals(workingOnConfigId)
380: && !action.child
381: .getAbsolutePath()
382: .equals(path)) {
383: File fd = new File(path);
384: if (fd.isDirectory()) {
385: log
386: .info("Deleting the Directory: "
387: + path);
388: if (DeploymentUtil
389: .recursiveDelete(fd))
390: log
391: .debug("Successfully deleted the Directory: "
392: + path);
393: else
394: log
395: .error("Couldn't delete the hot deployed directory="
396: + path);
397: } else if (fd.isFile()) {
398: log
399: .info("Deleting the File: "
400: + path);
401: if (fd.delete()) {
402: log
403: .debug("Successfully deleted the File: "
404: + path);
405: } else
406: log
407: .error("Couldn't delete the hot deployed file="
408: + path);
409: }
410: files.remove(path);
411: }
412: }
413: workingOnConfigId = null;
414: } else {
415: String result = listener
416: .fileAdded(action.child);
417: if (result != null) {
418: if (!result.equals("")) {
419: action.info.setConfigId(result);
420: } else {
421: action.info
422: .setConfigId(calculateModuleId(action.child));
423: }
424: }
425: }
426: action.info.setNewFile(false);
427: } else if (action.action == FileAction.UPDATED_FILE) {
428: workingOnConfigId = action.info
429: .getConfigId();
430: String result = listener.fileUpdated(
431: action.child, action.info
432: .getConfigId());
433: FileInfo update = action.info;
434: if (result != null) {
435: if (!result.equals("")) {
436: update.setConfigId(result);
437: } else {
438: update
439: .setConfigId(calculateModuleId(action.child));
440: }
441: }
442: workingOnConfigId = null;
443: }
444: } catch (Exception e) {
445: log.error("Unable to " + action.getActionName()
446: + " file "
447: + action.child.getAbsolutePath(), e);
448: } finally {
449: resolveFile(action);
450: }
451: }
452: }
453: }
454: }
455:
456: private void resolveFile(FileAction action) {
457: if (action.action == FileAction.REMOVED_FILE) {
458: files.remove(action.child.getPath());
459: } else {
460: action.info.setChanging(false);
461: }
462: }
463:
464: private String calculateModuleId(File module) {
465: String moduleId = null;
466: try {
467: moduleId = DeployUtils.extractModuleIdFromArchive(module);
468: } catch (Exception e) {
469: try {
470: moduleId = DeployUtils.extractModuleIdFromPlan(module);
471: } catch (IOException e2) {
472: log.warn("Unable to calculate module ID for file "
473: + module.getAbsolutePath() + " ["
474: + e2.getMessage() + "]");
475: }
476: }
477: if (moduleId == null) {
478: int pos = module.getName().lastIndexOf('.');
479: moduleId = pos > -1 ? module.getName().substring(0, pos)
480: : module.getName();
481: moduleId = listener.getModuleId(moduleId);
482: }
483: return moduleId;
484: }
485:
486: /**
487: * We don't pay attention to the size of the directory or files in the
488: * directory, only the highest last modified time of anything in the
489: * directory. Hopefully this is good enough.
490: */
491: private FileInfo getDirectoryInfo(File dir) {
492: FileInfo info = new FileInfo(dir.getAbsolutePath());
493: info.setSize(0);
494: info.setModified(getLastModifiedInDir(dir));
495: return info;
496: }
497:
498: private long getLastModifiedInDir(File dir) {
499: long value = dir.lastModified();
500: File[] children = dir.listFiles();
501: long test;
502: for (int i = 0; i < children.length; i++) {
503: File child = children[i];
504: if (!child.canRead()) {
505: continue;
506: }
507: if (child.isDirectory()) {
508: test = getLastModifiedInDir(child);
509: } else {
510: test = child.lastModified();
511: }
512: if (test > value) {
513: value = test;
514: }
515: }
516: return value;
517: }
518:
519: private FileInfo getFileInfo(File child) {
520: FileInfo info = new FileInfo(child.getAbsolutePath());
521: info.setSize(child.length());
522: info.setModified(child.lastModified());
523: return info;
524: }
525:
526: private static class FileAction {
527: private static int NEW_FILE = 1;
528: private static int UPDATED_FILE = 2;
529: private static int REMOVED_FILE = 3;
530: private int action;
531: private File child;
532: private FileInfo info;
533:
534: public FileAction(int action, File child, FileInfo info) {
535: this .action = action;
536: this .child = child;
537: this .info = info;
538: }
539:
540: public String getActionName() {
541: return action == NEW_FILE ? "deploy"
542: : action == UPDATED_FILE ? "redeploy" : "undeploy";
543: }
544: }
545:
546: private static class FileInfo implements Serializable {
547: private String path;
548: private long size;
549: private long modified;
550: private boolean newFile;
551: private boolean changing;
552: private String configId;
553:
554: public FileInfo(String path) {
555: this .path = path;
556: newFile = false;
557: changing = true;
558: }
559:
560: public String getPath() {
561: return path;
562: }
563:
564: public long getSize() {
565: return size;
566: }
567:
568: public void setSize(long size) {
569: this .size = size;
570: }
571:
572: public long getModified() {
573: return modified;
574: }
575:
576: public void setModified(long modified) {
577: this .modified = modified;
578: }
579:
580: public boolean isNewFile() {
581: return newFile;
582: }
583:
584: public void setNewFile(boolean newFile) {
585: this .newFile = newFile;
586: }
587:
588: public boolean isChanging() {
589: return changing;
590: }
591:
592: public void setChanging(boolean changing) {
593: this .changing = changing;
594: }
595:
596: public String getConfigId() {
597: return configId;
598: }
599:
600: public void setConfigId(String configId) {
601: this .configId = configId;
602: }
603:
604: public boolean isSame(FileInfo info) {
605: if (!path.equals(info.path)) {
606: throw new IllegalArgumentException(
607: "Should only be used to compare two files representing the same path!");
608: }
609: return size == info.size && modified == info.modified;
610: }
611: }
612: }
|