1 | |
|
2 | |
|
3 | |
|
4 | |
|
5 | |
|
6 | |
|
7 | |
|
8 | |
|
9 | |
|
10 | |
|
11 | |
package org.mule.module.launcher; |
12 | |
|
13 | |
import org.mule.config.StartupContext; |
14 | |
import org.mule.config.i18n.MessageFactory; |
15 | |
import org.mule.module.launcher.application.Application; |
16 | |
import org.mule.module.launcher.application.ApplicationFactory; |
17 | |
import org.mule.module.launcher.util.DebuggableReentrantLock; |
18 | |
import org.mule.module.launcher.util.ElementAddedEvent; |
19 | |
import org.mule.module.launcher.util.ElementRemovedEvent; |
20 | |
import org.mule.module.launcher.util.ObservableList; |
21 | |
import org.mule.module.reboot.MuleContainerBootstrapUtils; |
22 | |
import org.mule.util.CollectionUtils; |
23 | |
import org.mule.util.StringUtils; |
24 | |
|
25 | |
import java.beans.PropertyChangeEvent; |
26 | |
import java.beans.PropertyChangeListener; |
27 | |
import java.io.File; |
28 | |
import java.io.IOException; |
29 | |
import java.net.MalformedURLException; |
30 | |
import java.net.URL; |
31 | |
import java.util.Arrays; |
32 | |
import java.util.Collection; |
33 | |
import java.util.Collections; |
34 | |
import java.util.HashMap; |
35 | |
import java.util.List; |
36 | |
import java.util.Map; |
37 | |
import java.util.concurrent.Executors; |
38 | |
import java.util.concurrent.ScheduledExecutorService; |
39 | |
import java.util.concurrent.TimeUnit; |
40 | |
import java.util.concurrent.locks.ReentrantLock; |
41 | |
|
42 | |
import org.apache.commons.beanutils.BeanPropertyValueEqualsPredicate; |
43 | |
import org.apache.commons.beanutils.BeanToPropertyValueTransformer; |
44 | |
import org.apache.commons.io.filefilter.DirectoryFileFilter; |
45 | |
import org.apache.commons.io.filefilter.SuffixFileFilter; |
46 | |
import org.apache.commons.logging.Log; |
47 | |
import org.apache.commons.logging.LogFactory; |
48 | |
|
49 | 0 | public class DeploymentService |
50 | |
{ |
51 | |
public static final String APP_ANCHOR_SUFFIX = "-anchor.txt"; |
52 | |
protected static final int DEFAULT_CHANGES_CHECK_INTERVAL_MS = 5000; |
53 | |
|
54 | |
protected ScheduledExecutorService appDirMonitorTimer; |
55 | |
|
56 | 0 | protected transient final Log logger = LogFactory.getLog(getClass()); |
57 | |
protected MuleDeployer deployer; |
58 | |
protected ApplicationFactory appFactory; |
59 | |
|
60 | 0 | private ReentrantLock lock = new DebuggableReentrantLock(true); |
61 | |
|
62 | 0 | private ObservableList<Application> applications = new ObservableList<Application>(); |
63 | 0 | private Map<URL, Long> zombieMap = new HashMap<URL, Long>(); |
64 | 0 | private ObservableList<URL> zombieLand = new ObservableList<URL>(); |
65 | |
|
66 | |
public DeploymentService() |
67 | 0 | { |
68 | 0 | deployer = new DefaultMuleDeployer(this); |
69 | 0 | appFactory = new ApplicationFactory(this); |
70 | 0 | } |
71 | |
|
72 | |
public void start() |
73 | |
{ |
74 | |
|
75 | 0 | final Map<String, Object> options = StartupContext.get().getStartupOptions(); |
76 | 0 | String appString = (String) options.get("app"); |
77 | |
|
78 | 0 | final File appsDir = MuleContainerBootstrapUtils.getMuleAppsDir(); |
79 | |
|
80 | |
|
81 | 0 | String[] appAnchors = appsDir.list(new SuffixFileFilter(APP_ANCHOR_SUFFIX)); |
82 | 0 | for (String anchor : appAnchors) |
83 | |
{ |
84 | |
|
85 | 0 | new File(appsDir, anchor).delete(); |
86 | |
} |
87 | |
|
88 | |
String[] apps; |
89 | |
|
90 | |
|
91 | 0 | final boolean explicitAppSet = appString != null; |
92 | |
|
93 | 0 | if (!explicitAppSet) |
94 | |
{ |
95 | |
|
96 | 0 | final String[] zips = appsDir.list(new SuffixFileFilter(".zip")); |
97 | 0 | Arrays.sort(zips); |
98 | 0 | for (String zip : zips) |
99 | |
{ |
100 | |
try |
101 | |
{ |
102 | |
|
103 | 0 | deployer.installFromAppDir(zip); |
104 | |
} |
105 | 0 | catch (Throwable t) |
106 | |
{ |
107 | 0 | logger.error(String.format("Failed to install app from archive '%s'", zip), t); |
108 | 0 | File appFile = new File(appsDir, zip); |
109 | |
try |
110 | |
{ |
111 | 0 | addZombie(appFile.toURL()); |
112 | |
} |
113 | 0 | catch (MalformedURLException muex) |
114 | |
{ |
115 | 0 | if (logger.isDebugEnabled()) |
116 | |
{ |
117 | 0 | logger.debug(muex); |
118 | |
} |
119 | 0 | } |
120 | 0 | } |
121 | |
} |
122 | |
|
123 | |
|
124 | |
|
125 | 0 | apps = appsDir.list(DirectoryFileFilter.DIRECTORY); |
126 | 0 | Arrays.sort(apps); |
127 | 0 | } |
128 | |
else |
129 | |
{ |
130 | 0 | apps = appString.split(":"); |
131 | |
} |
132 | |
|
133 | 0 | for (String app : apps) |
134 | |
{ |
135 | |
final Application a; |
136 | |
try |
137 | |
{ |
138 | 0 | a = appFactory.createApp(app); |
139 | 0 | applications.add(a); |
140 | |
} |
141 | 0 | catch (IOException e) |
142 | |
{ |
143 | |
|
144 | 0 | e.printStackTrace(); |
145 | 0 | } |
146 | |
} |
147 | |
|
148 | |
|
149 | 0 | for (Application application : applications) |
150 | |
{ |
151 | |
try |
152 | |
{ |
153 | 0 | deployer.deploy(application); |
154 | |
} |
155 | 0 | catch (Throwable t) |
156 | |
{ |
157 | |
|
158 | 0 | t.printStackTrace(); |
159 | 0 | } |
160 | |
} |
161 | |
|
162 | |
|
163 | |
|
164 | 0 | if (!explicitAppSet) |
165 | |
{ |
166 | 0 | scheduleChangeMonitor(appsDir); |
167 | |
} |
168 | 0 | } |
169 | |
|
170 | |
protected void scheduleChangeMonitor(File appsDir) |
171 | |
{ |
172 | 0 | final int reloadIntervalMs = DEFAULT_CHANGES_CHECK_INTERVAL_MS; |
173 | 0 | appDirMonitorTimer = Executors.newSingleThreadScheduledExecutor(new AppDeployerMonitorThreadFactory()); |
174 | |
|
175 | 0 | appDirMonitorTimer.scheduleWithFixedDelay(new AppDirWatcher(appsDir), |
176 | |
0, |
177 | |
reloadIntervalMs, |
178 | |
TimeUnit.MILLISECONDS); |
179 | |
|
180 | 0 | if (logger.isInfoEnabled()) |
181 | |
{ |
182 | 0 | logger.info("Application directory check interval: " + reloadIntervalMs); |
183 | |
} |
184 | 0 | } |
185 | |
|
186 | |
public void stop() |
187 | |
{ |
188 | 0 | if (appDirMonitorTimer != null) |
189 | |
{ |
190 | 0 | appDirMonitorTimer.shutdownNow(); |
191 | |
} |
192 | |
|
193 | |
|
194 | 0 | Collections.reverse(applications); |
195 | 0 | for (Application application : applications) |
196 | |
{ |
197 | |
try |
198 | |
{ |
199 | 0 | application.stop(); |
200 | 0 | application.dispose(); |
201 | |
} |
202 | 0 | catch (Throwable t) |
203 | |
{ |
204 | |
|
205 | 0 | t.printStackTrace(); |
206 | 0 | } |
207 | |
} |
208 | |
|
209 | 0 | } |
210 | |
|
211 | |
|
212 | |
|
213 | |
|
214 | |
|
215 | |
public Application findApplication(String appName) |
216 | |
{ |
217 | 0 | return (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName)); |
218 | |
} |
219 | |
|
220 | |
|
221 | |
|
222 | |
|
223 | |
public List<Application> getApplications() |
224 | |
{ |
225 | 0 | return Collections.unmodifiableList(applications); |
226 | |
} |
227 | |
|
228 | |
|
229 | |
|
230 | |
|
231 | |
public Map<URL, Long> getZombieMap() |
232 | |
{ |
233 | 0 | return zombieMap; |
234 | |
} |
235 | |
|
236 | |
|
237 | |
|
238 | |
protected MuleDeployer getDeployer() |
239 | |
{ |
240 | 0 | return deployer; |
241 | |
} |
242 | |
|
243 | |
public void setDeployer(MuleDeployer deployer) |
244 | |
{ |
245 | 0 | this.deployer = deployer; |
246 | 0 | } |
247 | |
|
248 | |
public ApplicationFactory getAppFactory() |
249 | |
{ |
250 | 0 | return appFactory; |
251 | |
} |
252 | |
|
253 | |
public ReentrantLock getLock() { |
254 | 0 | return lock; |
255 | |
} |
256 | |
|
257 | |
public void onApplicationInstalled(Application a) |
258 | |
{ |
259 | 0 | applications.add(a); |
260 | 0 | } |
261 | |
|
262 | |
protected void undeploy(Application app) |
263 | |
{ |
264 | 0 | if (logger.isInfoEnabled()) |
265 | |
{ |
266 | 0 | logger.info("================== Request to Undeploy Application: " + app.getAppName()); |
267 | |
} |
268 | |
|
269 | 0 | deployer.undeploy(app); |
270 | 0 | } |
271 | |
|
272 | |
public void undeploy(String appName) |
273 | |
{ |
274 | 0 | Application app = (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName)); |
275 | 0 | applications.remove(app); |
276 | 0 | deployer.undeploy(app); |
277 | 0 | } |
278 | |
|
279 | |
public void deploy(URL appArchiveUrl) throws IOException |
280 | |
{ |
281 | |
final Application application; |
282 | |
try |
283 | |
{ |
284 | 0 | application = deployer.installFrom(appArchiveUrl); |
285 | 0 | applications.add(application); |
286 | 0 | deployer.deploy(application); |
287 | |
} |
288 | 0 | catch (Throwable t) |
289 | |
{ |
290 | 0 | addZombie(appArchiveUrl); |
291 | 0 | if (t instanceof DeploymentException) |
292 | |
{ |
293 | |
|
294 | 0 | throw ((DeploymentException) t); |
295 | |
} |
296 | |
|
297 | 0 | final String msg = "Failed to deploy from URL: " + appArchiveUrl; |
298 | 0 | throw new DeploymentException(MessageFactory.createStaticMessage(msg), t); |
299 | 0 | } |
300 | 0 | } |
301 | |
|
302 | |
protected void addZombie(URL appArchiveUrl) |
303 | |
{ |
304 | |
|
305 | 0 | if (appArchiveUrl == null) |
306 | |
{ |
307 | 0 | return; |
308 | |
} |
309 | |
|
310 | 0 | long lastModified = -1; |
311 | |
|
312 | 0 | if ("file".equals(appArchiveUrl.getProtocol())) |
313 | |
{ |
314 | 0 | lastModified = new File(appArchiveUrl.getFile()).lastModified(); |
315 | |
} |
316 | |
|
317 | 0 | zombieMap.put(appArchiveUrl, lastModified); |
318 | 0 | } |
319 | |
|
320 | |
|
321 | |
|
322 | |
|
323 | |
protected class AppDirWatcher implements Runnable |
324 | |
{ |
325 | |
protected File appsDir; |
326 | |
|
327 | |
|
328 | 0 | protected String[] appAnchors = new String[0]; |
329 | |
protected volatile boolean dirty; |
330 | |
|
331 | |
public AppDirWatcher(final File appsDir) |
332 | 0 | { |
333 | 0 | this.appsDir = appsDir; |
334 | 0 | applications.addPropertyChangeListener(new PropertyChangeListener() |
335 | 0 | { |
336 | |
public void propertyChange(PropertyChangeEvent e) |
337 | |
{ |
338 | 0 | if (e instanceof ElementAddedEvent || e instanceof ElementRemovedEvent) |
339 | |
{ |
340 | 0 | if (logger.isDebugEnabled()) |
341 | |
{ |
342 | 0 | logger.debug("Deployed applications set has been modified, flushing state."); |
343 | |
} |
344 | 0 | dirty = true; |
345 | |
} |
346 | 0 | } |
347 | |
}); |
348 | 0 | } |
349 | |
|
350 | |
|
351 | |
|
352 | |
|
353 | |
|
354 | |
public void run() |
355 | |
{ |
356 | |
try |
357 | |
{ |
358 | 0 | if (logger.isDebugEnabled()) |
359 | |
{ |
360 | 0 | logger.debug("Checking for changes..."); |
361 | |
} |
362 | |
|
363 | |
|
364 | 0 | if (!lock.tryLock(0, TimeUnit.SECONDS)) |
365 | |
{ |
366 | 0 | if (logger.isDebugEnabled()) |
367 | |
{ |
368 | 0 | logger.debug("Another deployment operation in progress, will skip this cycle. Owner thread: " + |
369 | |
((DebuggableReentrantLock) lock).getOwner()); |
370 | |
} |
371 | |
return; |
372 | |
} |
373 | |
|
374 | |
|
375 | |
|
376 | 0 | final String[] zips = appsDir.list(new SuffixFileFilter(".zip")); |
377 | 0 | String[] apps = appsDir.list(DirectoryFileFilter.DIRECTORY); |
378 | |
|
379 | |
|
380 | |
|
381 | 0 | String[] currentAnchors = appsDir.list(new SuffixFileFilter(APP_ANCHOR_SUFFIX)); |
382 | 0 | if (logger.isDebugEnabled()) |
383 | |
{ |
384 | 0 | StringBuilder sb = new StringBuilder(); |
385 | 0 | sb.append(String.format("Current anchors:%n")); |
386 | 0 | for (String currentAnchor : currentAnchors) |
387 | |
{ |
388 | 0 | sb.append(String.format(" %s%n", currentAnchor)); |
389 | |
} |
390 | 0 | logger.debug(sb.toString()); |
391 | |
} |
392 | |
@SuppressWarnings("unchecked") |
393 | 0 | final Collection<String> deletedAnchors = CollectionUtils.subtract(Arrays.asList(appAnchors), Arrays.asList(currentAnchors)); |
394 | 0 | if (logger.isDebugEnabled()) |
395 | |
{ |
396 | 0 | StringBuilder sb = new StringBuilder(); |
397 | 0 | sb.append(String.format("Deleted anchors:%n")); |
398 | 0 | for (String deletedAnchor : deletedAnchors) |
399 | |
{ |
400 | 0 | sb.append(String.format(" %s%n", deletedAnchor)); |
401 | |
} |
402 | 0 | logger.debug(sb.toString()); |
403 | |
} |
404 | |
|
405 | 0 | for (String deletedAnchor : deletedAnchors) |
406 | |
{ |
407 | 0 | String appName = StringUtils.removeEnd(deletedAnchor, APP_ANCHOR_SUFFIX); |
408 | |
try |
409 | |
{ |
410 | 0 | if (findApplication(appName) != null) |
411 | |
{ |
412 | 0 | undeploy(appName); |
413 | |
} |
414 | 0 | else if (logger.isDebugEnabled()) |
415 | |
{ |
416 | 0 | logger.debug(String.format("Application [%s] has already been undeployed via API", appName)); |
417 | |
} |
418 | |
} |
419 | 0 | catch (Throwable t) |
420 | |
{ |
421 | 0 | logger.error("Failed to undeploy application: " + appName, t); |
422 | 0 | } |
423 | 0 | } |
424 | 0 | appAnchors = currentAnchors; |
425 | |
|
426 | |
|
427 | |
|
428 | 0 | for (String zip : zips) |
429 | |
{ |
430 | 0 | URL url = null; |
431 | |
try |
432 | |
{ |
433 | |
|
434 | 0 | final String appName = StringUtils.removeEnd(zip, ".zip"); |
435 | 0 | Application app = (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName)); |
436 | 0 | if (app != null) |
437 | |
{ |
438 | 0 | undeploy(appName); |
439 | |
} |
440 | 0 | url = new File(appsDir, zip).toURI().toURL(); |
441 | 0 | deploy(url); |
442 | |
} |
443 | 0 | catch (Throwable t) |
444 | |
{ |
445 | 0 | logger.error("Failed to deploy application archive: " + zip, t); |
446 | 0 | addZombie(url); |
447 | 0 | } |
448 | |
} |
449 | |
|
450 | |
|
451 | 0 | if (zips.length > 0 || dirty) |
452 | |
{ |
453 | 0 | apps = appsDir.list(DirectoryFileFilter.DIRECTORY); |
454 | |
} |
455 | |
|
456 | 0 | Collection deployedAppNames = CollectionUtils.collect(applications, new BeanToPropertyValueTransformer("appName")); |
457 | |
|
458 | |
|
459 | |
@SuppressWarnings("unchecked") |
460 | 0 | final Collection<String> addedApps = CollectionUtils.subtract(Arrays.asList(apps), deployedAppNames); |
461 | 0 | for (String addedApp : addedApps) |
462 | |
{ |
463 | |
try |
464 | |
{ |
465 | 0 | onNewExplodedApplication(addedApp); |
466 | |
} |
467 | 0 | catch (Throwable t) |
468 | |
{ |
469 | 0 | logger.error("Failed to deploy exploded application: " + addedApp, t); |
470 | |
try |
471 | |
{ |
472 | 0 | addZombie(new File(appsDir, addedApp).toURI().toURL()); |
473 | |
} |
474 | 0 | catch (MalformedURLException e) |
475 | |
{ |
476 | 0 | if (logger.isDebugEnabled()) |
477 | |
{ |
478 | 0 | logger.debug(e); |
479 | |
} |
480 | 0 | } |
481 | 0 | } |
482 | |
} |
483 | |
|
484 | |
} |
485 | 0 | catch (InterruptedException e) |
486 | |
{ |
487 | |
|
488 | 0 | Thread.currentThread().interrupt(); |
489 | |
} |
490 | |
finally |
491 | |
{ |
492 | 0 | if (lock.isHeldByCurrentThread()) |
493 | |
{ |
494 | 0 | lock.unlock(); |
495 | |
} |
496 | 0 | dirty = false; |
497 | 0 | } |
498 | 0 | } |
499 | |
|
500 | |
|
501 | |
|
502 | |
|
503 | |
protected void onNewExplodedApplication(String appName) throws Exception |
504 | |
{ |
505 | 0 | if (logger.isInfoEnabled()) |
506 | |
{ |
507 | 0 | logger.info("================== New Exploded Application: " + appName); |
508 | |
} |
509 | |
|
510 | 0 | Application a = appFactory.createApp(appName); |
511 | |
|
512 | 0 | onApplicationInstalled(a); |
513 | 0 | deployer.deploy(a); |
514 | 0 | } |
515 | |
|
516 | |
} |
517 | |
|
518 | |
} |