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;
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
166
167
168
169 return;
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)) {
177 File file = new File(path);
178 if(file.exists()) {
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) {
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)) {
272 if (then.isChanging()) {
273 log.debug("File finished changing: " + now.getPath());
274
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 }
282 } else if(then.isNewFile() || now.getModified() > then.getModified()) {
283
284
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
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()) {
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
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
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 }