1
2
3
4
5
6
7 package org.mule.module.launcher;
8
9 import static org.mule.util.SplashScreen.miniSplash;
10 import org.mule.config.StartupContext;
11 import org.mule.config.i18n.MessageFactory;
12 import org.mule.module.launcher.application.Application;
13 import org.mule.module.launcher.application.ApplicationFactory;
14 import org.mule.module.launcher.util.DebuggableReentrantLock;
15 import org.mule.module.launcher.util.ElementAddedEvent;
16 import org.mule.module.launcher.util.ElementRemovedEvent;
17 import org.mule.module.launcher.util.ObservableList;
18 import org.mule.module.reboot.MuleContainerBootstrapUtils;
19 import org.mule.util.ArrayUtils;
20 import org.mule.util.CollectionUtils;
21 import org.mule.util.FileUtils;
22 import org.mule.util.StringUtils;
23
24 import java.beans.PropertyChangeEvent;
25 import java.beans.PropertyChangeListener;
26 import java.io.File;
27 import java.io.IOException;
28 import java.net.MalformedURLException;
29 import java.net.URL;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.Collection;
33 import java.util.Collections;
34 import java.util.HashMap;
35 import java.util.LinkedList;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.concurrent.CopyOnWriteArrayList;
39 import java.util.concurrent.Executors;
40 import java.util.concurrent.ScheduledExecutorService;
41 import java.util.concurrent.TimeUnit;
42 import java.util.concurrent.locks.ReentrantLock;
43
44 import org.apache.commons.beanutils.BeanPropertyValueEqualsPredicate;
45 import org.apache.commons.beanutils.BeanToPropertyValueTransformer;
46 import org.apache.commons.io.filefilter.AndFileFilter;
47 import org.apache.commons.io.filefilter.DirectoryFileFilter;
48 import org.apache.commons.io.filefilter.FileFileFilter;
49 import org.apache.commons.io.filefilter.IOFileFilter;
50 import org.apache.commons.io.filefilter.SuffixFileFilter;
51 import org.apache.commons.logging.Log;
52 import org.apache.commons.logging.LogFactory;
53
54 public class DeploymentService
55 {
56 public static final String APP_ANCHOR_SUFFIX = "-anchor.txt";
57 public static final String ZIP_FILE_SUFFIX = ".zip";
58 public static final IOFileFilter ZIP_APPS_FILTER = new AndFileFilter(new SuffixFileFilter(ZIP_FILE_SUFFIX), FileFileFilter.FILE);
59
60 protected static final int DEFAULT_CHANGES_CHECK_INTERVAL_MS = 5000;
61
62 protected ScheduledExecutorService appDirMonitorTimer;
63
64 protected transient final Log logger = LogFactory.getLog(getClass());
65 protected MuleDeployer deployer;
66 protected ApplicationFactory appFactory;
67
68 private ReentrantLock lock = new DebuggableReentrantLock(true);
69
70 private ObservableList<Application> applications = new ObservableList<Application>();
71 private Map<URL, Long> zombieMap = new HashMap<URL, Long>();
72
73 private List<StartupListener> startupListeners = new ArrayList<StartupListener>();
74
75 private List<DeploymentListener> deploymentListeners = new CopyOnWriteArrayList<DeploymentListener>();
76
77 public DeploymentService()
78 {
79 deployer = new DefaultMuleDeployer(this);
80 appFactory = new ApplicationFactory(this);
81 }
82
83 public void start()
84 {
85
86 final Map<String, Object> options = StartupContext.get().getStartupOptions();
87 String appString = (String) options.get("app");
88
89 final File appsDir = MuleContainerBootstrapUtils.getMuleAppsDir();
90
91
92 String[] appAnchors = appsDir.list(new SuffixFileFilter(APP_ANCHOR_SUFFIX));
93 for (String anchor : appAnchors)
94 {
95
96 new File(appsDir, anchor).delete();
97 }
98
99 String[] apps = ArrayUtils.EMPTY_STRING_ARRAY;
100
101
102 final boolean explicitAppSet = appString != null;
103
104 DeploymentStatusTracker deploymentStatusTracker = new DeploymentStatusTracker();
105 addDeploymentListener(deploymentStatusTracker);
106
107 StartupSummaryDeploymentListener summaryDeploymentListener = new StartupSummaryDeploymentListener(deploymentStatusTracker);
108 addStartupListener(summaryDeploymentListener);
109
110 if (!explicitAppSet)
111 {
112 String[] dirApps = appsDir.list(DirectoryFileFilter.DIRECTORY);
113 apps = (String[]) ArrayUtils.addAll(apps, dirApps);
114
115 String[] zipApps = appsDir.list(ZIP_APPS_FILTER);
116 for (int i = 0; i < zipApps.length; i++)
117 {
118 zipApps[i] = StringUtils.removeEndIgnoreCase(zipApps[i], ZIP_FILE_SUFFIX);
119 }
120
121
122
123 apps = (String[]) ArrayUtils.addAll(dirApps, zipApps);
124 Arrays.sort(apps);
125 }
126 else
127 {
128 apps = appString.split(":");
129 }
130
131 apps = removeDuplicateAppNames(apps);
132
133 for (String app : apps)
134 {
135 final Application a;
136 String appMarker = app;
137 File applicationFile = null;
138 try
139 {
140
141 applicationFile = new File(appsDir, app + ".zip");
142 if (applicationFile.exists() && applicationFile.isFile())
143 {
144 appMarker = app + ZIP_FILE_SUFFIX;
145 a = deployer.installFromAppDir(applicationFile.getName());
146 }
147 else
148 {
149
150 applicationFile = new File(appsDir, appMarker);
151 a = appFactory.createApp(app);
152 }
153 applications.add(a);
154 }
155 catch (Throwable t)
156 {
157 fireOnDeploymentFailure(appMarker, t);
158
159 try
160 {
161 URL url = applicationFile.toURI().toURL();
162 addZombie(url);
163 }
164 catch (MalformedURLException e)
165 {
166 if (logger.isDebugEnabled())
167 {
168 logger.debug("Error adding zombie app", e);
169 }
170 }
171 logger.error(String.format("Failed to create application [%s]", appMarker), t);
172 }
173 }
174
175
176 for (Application application : applications)
177 {
178 try
179 {
180 fireOnDeploymentStart(application.getAppName());
181 deployer.deploy(application);
182 fireOnDeploymentSuccess(application.getAppName());
183 }
184 catch (Throwable t)
185 {
186 fireOnDeploymentFailure(application.getAppName(), t);
187
188
189 final String msg = miniSplash(String.format("Failed to deploy app '%s', see below", application.getAppName()));
190 logger.error(msg);
191 logger.error(t);
192 }
193 }
194
195 for (StartupListener listener : startupListeners)
196 {
197 try
198 {
199 listener.onAfterStartup();
200 }
201 catch (Throwable t)
202 {
203 logger.error(t);
204 }
205 }
206
207
208
209 if (!explicitAppSet)
210 {
211 scheduleChangeMonitor(appsDir);
212 }
213 else
214 {
215 if (logger.isInfoEnabled())
216 {
217 logger.info(miniSplash("Mule is up and running in a fixed app set mode"));
218 }
219 }
220 }
221
222 private String[] removeDuplicateAppNames(String[] apps)
223 {
224 List<String> appNames = new LinkedList<String>();
225
226 for (String appName : apps)
227 {
228 if (!appNames.contains(appName))
229 {
230 appNames.add(appName);
231 }
232 }
233
234 return appNames.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
235 }
236
237 protected void scheduleChangeMonitor(File appsDir)
238 {
239 final int reloadIntervalMs = DEFAULT_CHANGES_CHECK_INTERVAL_MS;
240 appDirMonitorTimer = Executors.newSingleThreadScheduledExecutor(new AppDeployerMonitorThreadFactory());
241
242 appDirMonitorTimer.scheduleWithFixedDelay(new AppDirWatcher(appsDir),
243 0,
244 reloadIntervalMs,
245 TimeUnit.MILLISECONDS);
246
247 if (logger.isInfoEnabled())
248 {
249 logger.info(miniSplash(String.format("Mule is up and kicking (every %dms)", reloadIntervalMs)));
250 }
251 }
252
253 public void stop()
254 {
255 if (appDirMonitorTimer != null)
256 {
257 appDirMonitorTimer.shutdownNow();
258 }
259
260
261 Collections.reverse(applications);
262 for (Application application : applications)
263 {
264 try
265 {
266 application.stop();
267 application.dispose();
268 }
269 catch (Throwable t)
270 {
271 logger.error(t);
272 }
273 }
274
275 }
276
277
278
279
280
281 public Application findApplication(String appName)
282 {
283 return (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName));
284 }
285
286
287
288
289 public List<Application> getApplications()
290 {
291 return Collections.unmodifiableList(applications);
292 }
293
294
295
296
297 public Map<URL, Long> getZombieMap()
298 {
299 return zombieMap;
300 }
301
302 protected MuleDeployer getDeployer()
303 {
304 return deployer;
305 }
306
307 public void setDeployer(MuleDeployer deployer)
308 {
309 this.deployer = deployer;
310 }
311
312 public void setAppFactory(ApplicationFactory appFactory)
313 {
314 this.appFactory = appFactory;
315 }
316
317 public ApplicationFactory getAppFactory()
318 {
319 return appFactory;
320 }
321
322 public ReentrantLock getLock() {
323 return lock;
324 }
325
326 public void onApplicationInstalled(Application a)
327 {
328 applications.add(a);
329 }
330
331 protected void undeploy(Application app)
332 {
333 if (logger.isInfoEnabled())
334 {
335 logger.info("================== Request to Undeploy Application: " + app.getAppName());
336 }
337
338 applications.remove(app);
339 deployer.undeploy(app);
340 }
341
342 public void undeploy(String appName)
343 {
344 Application app = (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName));
345 undeploy(app);
346 }
347
348 public void deploy(URL appArchiveUrl) throws IOException
349 {
350 final Application application;
351 try
352 {
353 application = deployer.installFrom(appArchiveUrl);
354 applications.add(application);
355 deployer.deploy(application);
356 }
357 catch (Throwable t)
358 {
359 addZombie(appArchiveUrl);
360 if (t instanceof DeploymentException)
361 {
362
363 throw ((DeploymentException) t);
364 }
365
366 final String msg = "Failed to deploy from URL: " + appArchiveUrl;
367 throw new DeploymentException(MessageFactory.createStaticMessage(msg), t);
368 }
369 }
370
371 protected void addZombie(URL appArchiveUrl)
372 {
373
374 if (appArchiveUrl == null)
375 {
376 return;
377 }
378
379 long lastModified = FileUtils.getFileTimeStamp(appArchiveUrl);
380
381 zombieMap.put(appArchiveUrl, lastModified);
382 }
383
384 public void addStartupListener(StartupListener listener)
385 {
386 this.startupListeners.add(listener);
387 }
388
389 public void removeStartupListener(StartupListener listener)
390 {
391 this.startupListeners.remove(listener);
392 }
393
394 public void addDeploymentListener(DeploymentListener listener)
395 {
396 this.deploymentListeners.add(listener);
397 }
398
399 public void removeDeploymentListener(DeploymentListener listener)
400 {
401 this.deploymentListeners.remove(listener);
402 }
403
404
405
406
407
408
409
410 protected void fireOnDeploymentStart(String appName)
411 {
412 for (DeploymentListener listener : deploymentListeners)
413 {
414 try
415 {
416 listener.onDeploymentStart(appName);
417 }
418 catch (Throwable t)
419 {
420 logger.error("Listener failed to process onDeploymentStart notification", t);
421 }
422 }
423 }
424
425
426
427
428
429
430
431 protected void fireOnDeploymentSuccess(String appName)
432 {
433 for (DeploymentListener listener : deploymentListeners)
434 {
435 try
436 {
437 listener.onDeploymentSuccess(appName);
438 }
439 catch (Throwable t)
440 {
441 logger.error("Listener failed to process onDeploymentSuccess notification", t);
442 }
443 }
444 }
445
446
447
448
449
450
451
452
453 protected void fireOnDeploymentFailure(String appName, Throwable cause)
454 {
455 for (DeploymentListener listener : deploymentListeners)
456 {
457 try
458 {
459 listener.onDeploymentFailure(appName, cause);
460 }
461 catch (Throwable t)
462 {
463 logger.error("Listener failed to process onDeploymentFailure notification", t);
464 }
465 }
466 }
467
468 public interface StartupListener
469 {
470
471
472
473
474
475 void onAfterStartup();
476 }
477
478
479
480
481 protected class AppDirWatcher implements Runnable
482 {
483 protected File appsDir;
484
485 protected volatile boolean dirty;
486
487 public AppDirWatcher(final File appsDir)
488 {
489 this.appsDir = appsDir;
490 applications.addPropertyChangeListener(new PropertyChangeListener()
491 {
492 public void propertyChange(PropertyChangeEvent e)
493 {
494 if (e instanceof ElementAddedEvent || e instanceof ElementRemovedEvent)
495 {
496 if (logger.isDebugEnabled())
497 {
498 logger.debug("Deployed applications set has been modified, flushing state.");
499 }
500 dirty = true;
501 }
502 }
503 });
504 }
505
506
507
508
509
510 public void run()
511 {
512 try
513 {
514 if (logger.isDebugEnabled())
515 {
516 logger.debug("Checking for changes...");
517 }
518
519
520 if (!lock.tryLock(0, TimeUnit.SECONDS))
521 {
522 if (logger.isDebugEnabled())
523 {
524 logger.debug("Another deployment operation in progress, will skip this cycle. Owner thread: " +
525 ((DebuggableReentrantLock) lock).getOwner());
526 }
527 return;
528 }
529
530
531 final String[] zips = appsDir.list(ZIP_APPS_FILTER);
532 String[] apps = appsDir.list(DirectoryFileFilter.DIRECTORY);
533
534
535 String[] currentAnchors = appsDir.list(new SuffixFileFilter(APP_ANCHOR_SUFFIX));
536 if (logger.isDebugEnabled())
537 {
538 StringBuilder sb = new StringBuilder();
539 sb.append(String.format("Current anchors:%n"));
540 for (String currentAnchor : currentAnchors)
541 {
542 sb.append(String.format(" %s%n", currentAnchor));
543 }
544 logger.debug(sb.toString());
545 }
546
547 String[] appAnchors = findExpectedAnchorFiles();
548
549 @SuppressWarnings("unchecked")
550 final Collection<String> deletedAnchors = CollectionUtils.subtract(Arrays.asList(appAnchors), Arrays.asList(currentAnchors));
551 if (logger.isDebugEnabled())
552 {
553 StringBuilder sb = new StringBuilder();
554 sb.append(String.format("Deleted anchors:%n"));
555 for (String deletedAnchor : deletedAnchors)
556 {
557 sb.append(String.format(" %s%n", deletedAnchor));
558 }
559 logger.debug(sb.toString());
560 }
561
562 for (String deletedAnchor : deletedAnchors)
563 {
564 String appName = StringUtils.removeEnd(deletedAnchor, APP_ANCHOR_SUFFIX);
565 try
566 {
567 if (findApplication(appName) != null)
568 {
569 undeploy(appName);
570 }
571 else if (logger.isDebugEnabled())
572 {
573 logger.debug(String.format("Application [%s] has already been undeployed via API", appName));
574 }
575 }
576 catch (Throwable t)
577 {
578 logger.error("Failed to undeploy application: " + appName, t);
579 }
580 }
581
582
583 for (String zip : zips)
584 {
585 URL url = null;
586 try
587 {
588
589 final String appName = StringUtils.removeEnd(zip, ".zip");
590 Application app = (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName));
591 if (app != null)
592 {
593 undeploy(appName);
594 }
595 url = new File(appsDir, zip).toURI().toURL();
596
597 if (isZombieApplicationFile(url))
598 {
599
600 continue;
601 }
602
603 deploy(url);
604 }
605 catch (Throwable t)
606 {
607 logger.error("Failed to deploy application archive: " + zip, t);
608 addZombie(url);
609 }
610 }
611
612
613 if (zips.length > 0 || dirty)
614 {
615 apps = appsDir.list(DirectoryFileFilter.DIRECTORY);
616 }
617
618 Collection deployedAppNames = CollectionUtils.collect(applications, new BeanToPropertyValueTransformer("appName"));
619
620
621 @SuppressWarnings("unchecked")
622 final Collection<String> addedApps = CollectionUtils.subtract(Arrays.asList(apps), deployedAppNames);
623 for (String addedApp : addedApps)
624 {
625 try
626 {
627 onNewExplodedApplication(addedApp);
628 }
629 catch (Throwable t)
630 {
631 logger.error("Failed to deploy exploded application: " + addedApp, t);
632 try
633 {
634 addZombie(new File(appsDir, addedApp).toURI().toURL());
635 }
636 catch (MalformedURLException e)
637 {
638 if (logger.isDebugEnabled())
639 {
640 logger.debug(e);
641 }
642 }
643 }
644 }
645
646 }
647 catch (InterruptedException e)
648 {
649
650 Thread.currentThread().interrupt();
651 }
652 finally
653 {
654 if (lock.isHeldByCurrentThread())
655 {
656 lock.unlock();
657 }
658 dirty = false;
659 }
660 }
661
662
663
664
665
666
667 private String[] findExpectedAnchorFiles()
668 {
669 String[] appAnchors = new String[applications.size()];
670 int i =0;
671 for (Application application : applications)
672 {
673 appAnchors[i++] = application.getAppName() + APP_ANCHOR_SUFFIX;
674 }
675 return appAnchors;
676 }
677
678
679
680
681
682
683
684
685
686 protected boolean isZombieApplicationFile(URL url)
687 {
688 boolean result = false;
689
690 if (FileUtils.isFile(url) && zombieMap.containsKey(url))
691 {
692 long originalTimeStamp = zombieMap.get(url);
693 long newTimeStamp = FileUtils.getFileTimeStamp(url);
694
695 if (originalTimeStamp == newTimeStamp)
696 {
697 result = true;
698 }
699 }
700
701 return result;
702 }
703
704
705
706
707 protected void onNewExplodedApplication(String appName) throws Exception
708 {
709 if (logger.isInfoEnabled())
710 {
711 logger.info("================== New Exploded Application: " + appName);
712 }
713
714 Application a = appFactory.createApp(appName);
715
716 onApplicationInstalled(a);
717 deployer.deploy(a);
718 }
719
720 }
721 }