View Javadoc

1   /**
2    *
3    * Copyright 2003-2004 The Apache Software Foundation
4    *
5    *  Licensed under the Apache License, Version 2.0 (the "License");
6    *  you may not use this file except in compliance with the License.
7    *  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  package org.apache.geronimo.deployment.hot;
18  
19  import org.apache.commons.logging.LogFactory;
20  import org.apache.commons.logging.Log;
21  import org.apache.geronimo.deployment.cli.DeployUtils;
22  import org.apache.geronimo.kernel.repository.Artifact;
23  import org.apache.geronimo.kernel.config.IOUtil;
24  
25  import java.io.File;
26  import java.io.Serializable;
27  import java.io.IOException;
28  import java.util.Map;
29  import java.util.HashMap;
30  import java.util.HashSet;
31  import java.util.Iterator;
32  import java.util.List;
33  import java.util.LinkedList;
34  import java.util.Set;
35  
36  /**
37   * Meant to be run as a Thread that tracks the contents of a directory.
38   * It sends notifications for changes to its immediate children (it
39   * will look into subdirs for changes, but will not send notifications
40   * for files within subdirectories).  If a file continues to change on
41   * every pass, this will wait until it stabilizes before sending an
42   * add or update notification (to handle slow uploads, etc.).
43   *
44   * @version $Rev: 409771 $ $Date: 2006-05-26 15:39:58 -0700 (Fri, 26 May 2006) $
45   */
46  public class DirectoryMonitor implements Runnable {
47      private static final Log log = LogFactory.getLog(DirectoryMonitor.class);
48  
49      public static interface Listener {
50          /**
51           * The directory monitor doesn't take any action unless this method
52           * returns true (to avoid deploying before the deploy GBeans are
53           * running, etc.).
54           */
55          boolean isServerRunning();
56  
57          /**
58           * Called during initialization on all files in the hot deploy
59           * directory.
60           *
61           * @return true if the file in question is already available in the
62           *         server, false if it should be deployed on the next pass.
63           */
64          boolean isFileDeployed(File file, String configId);
65  
66          /**
67           * Called during initialization on previously deployed files.
68           *
69           * @return The time that the file was deployed.  If the current
70           *         version in the directory is newer, the file will be
71           *         updated on the first pass.
72           */
73          long getDeploymentTime(File file, String configId);
74  
75          /**
76           * Called to indicate that the monitor has fully initialized
77           * and will be doing normal deployment operations from now on.
78           */
79          void started();
80  
81          /**
82           * Called to check whether a file passes the smell test before
83           * attempting to deploy it.
84           *
85           * @return true if there's nothing obviously wrong with this file.
86           *         false if there is (for example, it's clearly not
87           *         deployable).
88           */
89          boolean validateFile(File file, String configId);
90  
91          /**
92           * @return A configId for the deployment if the addition was processed
93           *         successfully (or an empty String if the addition was OK but
94           *         the configId could not be determined).  null if the addition
95           *         failed, in which case the file will be added again next time
96           *         it changes.
97           */
98          String fileAdded(File file);
99  
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         void fileUpdated(File file, String configId);
107     }
108 
109     private int pollIntervalMillis;
110     private File directory;
111     private boolean done = false;
112     private Listener listener; // a little cheesy, but do we really need multiple listeners?
113     private final Map files = new HashMap();
114     private volatile String workingOnConfigId;
115 
116     public DirectoryMonitor(File directory, Listener listener, int pollIntervalMillis) {
117         this.directory = directory;
118         this.listener = listener;
119         this.pollIntervalMillis = pollIntervalMillis;
120     }
121 
122     public int getPollIntervalMillis() {
123         return pollIntervalMillis;
124     }
125 
126     public void setPollIntervalMillis(int pollIntervalMillis) {
127         this.pollIntervalMillis = pollIntervalMillis;
128     }
129 
130     public Listener getListener() {
131         return listener;
132     }
133 
134     public void setListener(Listener listener) {
135         this.listener = listener;
136     }
137 
138     public File getDirectory() {
139         return directory;
140     }
141 
142     /**
143      * Warning: changing the directory at runtime will cause all files in the
144      * old directory to be removed and all files in the new directory to be
145      * added, next time the thread awakens.
146      */
147     public void setDirectory(File directory) {
148         if (!directory.isDirectory() || !directory.canRead()) {
149             throw new IllegalArgumentException("Cannot monitor directory " + directory.getAbsolutePath());
150         }
151         this.directory = directory;
152     }
153 
154     public synchronized boolean isDone() {
155         return done;
156     }
157 
158     public synchronized void close() {
159         this.done = true;
160     }
161 
162     public void removeModuleId(Artifact id) {
163         log.info("Hot deployer notified that an artifact was removed: "+id);
164         if(id.toString().equals(workingOnConfigId)) {
165             // since the redeploy process inserts a new thread to handle progress,
166             // this is called by a different thread than the hot deploy thread during
167             // a redeploy, and this check must be executed outside the synchronized
168             // block or else it will cause a deadlock!
169             return; // don't react to events we generated ourselves
170         }
171         synchronized(files) {
172             for (Iterator it = files.keySet().iterator(); it.hasNext();) {
173                 String path = (String) it.next();
174                 FileInfo info = (FileInfo) files.get(path);
175                 Artifact target = Artifact.create(info.getConfigId());
176                 if(id.matches(target)) { // need to remove record & delete file
177                     File file = new File(path);
178                     if(file.exists()) { // if not, probably it's deletion kicked off this whole process
179                         log.info("Hot deployer deleting "+id);
180                         if(!IOUtil.recursiveDelete(file)) {
181                             log.error("Hot deployer unable to delete "+path);
182                         }
183                         it.remove();
184                     }
185                 }
186             }
187         }
188     }
189 
190     public void run() {
191         boolean serverStarted = false, initialized = false;
192         while (!done) {
193             try {
194                 Thread.sleep(pollIntervalMillis);
195             } catch (InterruptedException e) {
196                 continue;
197             }
198             try {
199                 if (listener != null) {
200                     if (!serverStarted && listener.isServerRunning()) {
201                         serverStarted = true;
202                     }
203                     if (serverStarted) {
204                         if (!initialized) {
205                             initialized = true;
206                             initialize();
207                             listener.started();
208                         } else {
209                             scanDirectory();
210                         }
211                     }
212                 }
213             } catch (Exception e) {
214                 log.error("Error during hot deployment", e);
215             }
216         }
217     }
218 
219     public void initialize() {
220         File parent = directory;
221         File[] children = parent.listFiles();
222         for (int i = 0; i < children.length; i++) {
223             File child = children[i];
224             if (!child.canRead()) {
225                 continue;
226             }
227             FileInfo now = child.isDirectory() ? getDirectoryInfo(child) : getFileInfo(child);
228             now.setChanging(false);
229             try {
230                 now.setConfigId(calculateModuleId(child));
231                 if (listener == null || listener.isFileDeployed(child, now.getConfigId())) {
232                     if (listener != null) {
233                         now.setModified(listener.getDeploymentTime(child, now.getConfigId()));
234                     }
235 log.info("At startup, found "+now.getPath()+" with deploy time "+now.getModified()+" and file time "+new File(now.getPath()).lastModified());
236                     files.put(now.getPath(), now);
237                 }
238             } catch (Exception e) {
239                 log.error("Unable to scan file " + child.getAbsolutePath() + " during initialization", e);
240             }
241         }
242     }
243 
244     /**
245      * Looks for changes to the immediate contents of the directory we're watching.
246      */
247     private void scanDirectory() {
248         File parent = directory;
249         File[] children = parent.listFiles();
250         if (!directory.exists() || children == null) {
251             log.error("Hot deploy directory has disappeared!  Shutting down directory monitor.");
252             done = true;
253             return;
254         }
255         synchronized (files) {
256             Set oldList = new HashSet(files.keySet());
257             List actions = new LinkedList();
258             for (int i = 0; i < children.length; i++) {
259                 File child = children[i];
260                 if (!child.canRead()) {
261                     continue;
262                 }
263                 FileInfo now = child.isDirectory() ? getDirectoryInfo(child) : getFileInfo(child);
264                 FileInfo then = (FileInfo) files.get(now.getPath());
265                 if (then == null) { // Brand new, wait a bit to make sure it's not still changing
266                     now.setNewFile(true);
267                     files.put(now.getPath(), now);
268                     log.debug("New File: " + now.getPath());
269                 } else {
270                     oldList.remove(then.getPath());
271                     if (now.isSame(then)) { // File is the same as the last time we scanned it
272                         if (then.isChanging()) {
273                             log.debug("File finished changing: " + now.getPath());
274                             // Used to be changing, now in (hopefully) its final state
275                             if (then.isNewFile()) {
276                                 actions.add(new FileAction(FileAction.NEW_FILE, child, then));
277                             } else {
278                                 actions.add(new FileAction(FileAction.UPDATED_FILE, child, then));
279                             }
280                             then.setChanging(false);
281                         } // else it's just totally unchanged and we ignore it this pass
282                     } else if(then.isNewFile() || now.getModified() > then.getModified()) {
283                         // The two records are different -- record the latest as a file that's changing
284                         // and later when it stops changing we'll do the add or update as appropriate.
285                         now.setConfigId(then.getConfigId());
286                         now.setNewFile(then.isNewFile());
287                         files.put(now.getPath(), now);
288                         log.debug("File Changed: " + now.getPath());
289                     }
290                 }
291             }
292             // Look for any files we used to know about but didn't find in this pass
293             for (Iterator it = oldList.iterator(); it.hasNext();) {
294                 String name = (String) it.next();
295                 FileInfo info = (FileInfo) files.get(name);
296                 log.debug("File removed: " + name);
297                 if (info.isNewFile()) { // Was never added, just whack it
298                     files.remove(name);
299                 } else {
300                     actions.add(new FileAction(FileAction.REMOVED_FILE, new File(name), info));
301                 }
302             }
303             if (listener != null) {
304                 // First pass: validate all changed files, so any obvious errors come out first
305                 for (Iterator it = actions.iterator(); it.hasNext();) {
306                     FileAction action = (FileAction) it.next();
307                     if (!listener.validateFile(action.child, action.info.getConfigId())) {
308                         resolveFile(action);
309                         it.remove();
310                     }
311                 }
312                 // Second pass: do what we're meant to do
313                 for (Iterator it = actions.iterator(); it.hasNext();) {
314                     FileAction action = (FileAction) it.next();
315                     try {
316                         if (action.action == FileAction.REMOVED_FILE) {
317                             workingOnConfigId = action.info.getConfigId();
318                             if (listener.fileRemoved(action.child, action.info.getConfigId())) {
319                                 files.remove(action.child.getPath());
320                             }
321                             workingOnConfigId = null;
322                         } else if (action.action == FileAction.NEW_FILE) {
323                             String result = listener.fileAdded(action.child);
324                             if (result != null) {
325                                 if (!result.equals("")) {
326                                     action.info.setConfigId(result);
327                                 } else {
328                                     action.info.setConfigId(calculateModuleId(action.child));
329                                 }
330                                 action.info.setNewFile(false);
331                             }
332                         } else if (action.action == FileAction.UPDATED_FILE) {
333                             workingOnConfigId = action.info.getConfigId();
334                             listener.fileUpdated(action.child, action.info.getConfigId());
335                             workingOnConfigId = null;
336                         }
337                     } catch (Exception e) {
338                         log.error("Unable to " + action.getActionName() + " file " + action.child.getAbsolutePath(), e);
339                     } finally {
340                         resolveFile(action);
341                     }
342                 }
343             }
344         }
345     }
346 
347     private void resolveFile(FileAction action) {
348         if (action.action == FileAction.REMOVED_FILE) {
349             files.remove(action.child.getPath());
350         } else {
351             action.info.setChanging(false);
352         }
353     }
354 
355     private static String calculateModuleId(File module) {
356         String moduleId = null;
357         try {
358             moduleId = DeployUtils.extractModuleIdFromArchive(module);
359         } catch (Exception e) {
360             try {
361                 moduleId = DeployUtils.extractModuleIdFromPlan(module);
362             } catch (IOException e2) {
363                 log.warn("Unable to calculate module ID for file " + module.getAbsolutePath() + " [" + e2.getMessage() + "]");
364             }
365         }
366         if (moduleId == null) {
367             int pos = module.getName().lastIndexOf('.');
368             moduleId = pos > -1 ? module.getName().substring(0, pos) : module.getName();
369         }
370         return moduleId;
371     }
372 
373     /**
374      * We don't pay attention to the size of the directory or files in the
375      * directory, only the highest last modified time of anything in the
376      * directory.  Hopefully this is good enough.
377      */
378     private FileInfo getDirectoryInfo(File dir) {
379         FileInfo info = new FileInfo(dir.getAbsolutePath());
380         info.setSize(0);
381         info.setModified(getLastModifiedInDir(dir));
382         return info;
383     }
384 
385     private long getLastModifiedInDir(File dir) {
386         long value = dir.lastModified();
387         File[] children = dir.listFiles();
388         long test;
389         for (int i = 0; i < children.length; i++) {
390             File child = children[i];
391             if (!child.canRead()) {
392                 continue;
393             }
394             if (child.isDirectory()) {
395                 test = getLastModifiedInDir(child);
396             } else {
397                 test = child.lastModified();
398             }
399             if (test > value) {
400                 value = test;
401             }
402         }
403         return value;
404     }
405 
406     private FileInfo getFileInfo(File child) {
407         FileInfo info = new FileInfo(child.getAbsolutePath());
408         info.setSize(child.length());
409         info.setModified(child.lastModified());
410         return info;
411     }
412 
413     private static class FileAction {
414         private static int NEW_FILE = 1;
415         private static int UPDATED_FILE = 2;
416         private static int REMOVED_FILE = 3;
417         private int action;
418         private File child;
419         private FileInfo info;
420 
421         public FileAction(int action, File child, FileInfo info) {
422             this.action = action;
423             this.child = child;
424             this.info = info;
425         }
426 
427         public String getActionName() {
428             return action == NEW_FILE ? "deploy" : action == UPDATED_FILE ? "redeploy" : "undeploy";
429         }
430     }
431 
432     private static class FileInfo implements Serializable {
433         private String path;
434         private long size;
435         private long modified;
436         private boolean newFile;
437         private boolean changing;
438         private String configId;
439 
440         public FileInfo(String path) {
441             this.path = path;
442             newFile = false;
443             changing = true;
444         }
445 
446         public String getPath() {
447             return path;
448         }
449 
450         public long getSize() {
451             return size;
452         }
453 
454         public void setSize(long size) {
455             this.size = size;
456         }
457 
458         public long getModified() {
459             return modified;
460         }
461 
462         public void setModified(long modified) {
463             this.modified = modified;
464         }
465 
466         public boolean isNewFile() {
467             return newFile;
468         }
469 
470         public void setNewFile(boolean newFile) {
471             this.newFile = newFile;
472         }
473 
474         public boolean isChanging() {
475             return changing;
476         }
477 
478         public void setChanging(boolean changing) {
479             this.changing = changing;
480         }
481 
482         public String getConfigId() {
483             return configId;
484         }
485 
486         public void setConfigId(String configId) {
487             this.configId = configId;
488         }
489 
490         public boolean isSame(FileInfo info) {
491             if (!path.equals(info.path)) {
492                 throw new IllegalArgumentException("Should only be used to compare two files representing the same path!");
493             }
494             return size == info.size && modified == info.modified;
495         }
496     }
497 }