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