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: 706640 $ $Date: 2008-10-21 14:44:05 +0000 (Tue, 21 Oct 2008) $
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 (action.info.getConfigId() == null || 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    }