1 /*
2 * Copyright 1999,2004-2005 The Apache Software Foundation.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.apache.catalina.cluster.deploy;
18
19 import java.io.File;
20 import java.io.IOException;
21 import java.net.URL;
22 import java.util.HashMap;
23
24 import javax.management.MBeanServer;
25 import javax.management.ObjectName;
26
27 import org.apache.catalina.Context;
28 import org.apache.catalina.Engine;
29 import org.apache.catalina.Host;
30 import org.apache.catalina.Lifecycle;
31 import org.apache.catalina.LifecycleException;
32 import org.apache.catalina.cluster.CatalinaCluster;
33 import org.apache.catalina.cluster.ClusterDeployer;
34 import org.apache.catalina.cluster.ClusterMessage;
35 import org.apache.catalina.cluster.Member;
36 import org.apache.commons.modeler.Registry;
37
38 /**
39 * <p>
40 * A farm war deployer is a class that is able to deploy/undeploy web
41 * applications in WAR form within the cluster.
42 * </p>
43 * Any host can act as the admin, and will have three directories
44 * <ul>
45 * <li>deployDir - the directory where we watch for changes</li>
46 * <li>applicationDir - the directory where we install applications</li>
47 * <li>tempDir - a temporaryDirectory to store binary data when downloading a
48 * war from the cluster</li>
49 * </ul>
50 * Currently we only support deployment of WAR files since they are easier to
51 * send across the wire.
52 *
53 * @author Filip Hanik
54 * @author Peter Rossbach
55 * @version 1.1
56 */
57 public class FarmWarDeployer implements ClusterDeployer, FileChangeListener {
58 /*--Static Variables----------------------------------------*/
59 public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory
60 .getLog(FarmWarDeployer.class);
61
62 /*--Instance Variables--------------------------------------*/
63 protected CatalinaCluster cluster = null;
64
65 protected boolean started = false; //default 5 seconds
66
67 protected HashMap fileFactories = new HashMap();
68
69 protected String deployDir;
70
71 protected String tempDir;
72
73 protected String watchDir;
74
75 protected boolean watchEnabled = false;
76
77 protected WarWatcher watcher = null;
78
79 /**
80 * Iteration count for background processing.
81 */
82 private int count = 0;
83
84 /**
85 * Frequency of the Farm watchDir check. Cluster wide deployment will be
86 * done once for the specified amount of backgrondProcess calls (ie, the
87 * lower the amount, the most often the checks will occur).
88 */
89 protected int processDeployFrequency = 2;
90
91 /**
92 * Path where context descriptors should be deployed.
93 */
94 protected File configBase = null;
95
96 /**
97 * The associated host.
98 */
99 protected Host host = null;
100
101 /**
102 * The host appBase.
103 */
104 protected File appBase = null;
105
106 /**
107 * MBean server.
108 */
109 protected MBeanServer mBeanServer = null;
110
111 /**
112 * The associated deployer ObjectName.
113 */
114 protected ObjectName oname = null;
115
116 /*--Constructor---------------------------------------------*/
117 public FarmWarDeployer() {
118 }
119
120 /*--Logic---------------------------------------------------*/
121 public void start() throws Exception {
122 if (started)
123 return;
124 getCluster().addClusterListener(this);
125 if (watchEnabled) {
126 watcher = new WarWatcher(this, new File(getWatchDir()));
127 if (log.isInfoEnabled())
128 log.info("Cluster deployment is watching " + getWatchDir()
129 + " for changes.");
130 } //end if
131
132 // Check to correct engine and host setup
133 host = (Host) getCluster().getContainer();
134 Engine engine = (Engine) host.getParent();
135 try {
136 oname = new ObjectName(engine.getName() + ":type=Deployer,host="
137 + host.getName());
138 } catch (Exception e) {
139 log.error("Can't construct MBean object name" + e);
140 }
141 configBase = new File(System.getProperty("catalina.base"), "conf");
142 if (engine != null) {
143 configBase = new File(configBase, engine.getName());
144 }
145 if (host != null) {
146 configBase = new File(configBase, host.getName());
147 }
148
149 // Retrieve the MBean server
150 mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
151
152 started = true;
153 count = 0;
154 if (log.isInfoEnabled())
155 log.info("Cluster FarmWarDeployer started.");
156 }
157
158 /*
159 * stop cluster wide deployments
160 *
161 * @see org.apache.catalina.cluster.ClusterDeployer#stop()
162 */
163 public void stop() throws LifecycleException {
164 started = false;
165 getCluster().removeClusterListener(this);
166 count = 0;
167 if (watcher != null) {
168 watcher.clear();
169 watcher = null;
170
171 }
172 if (log.isInfoEnabled())
173 log.info("Cluster FarmWarDeployer stopped.");
174 }
175
176 public void cleanDeployDir() {
177 throw new java.lang.UnsupportedOperationException(
178 "Method cleanDeployDir() not yet implemented.");
179 }
180
181 /**
182 * Callback from the cluster, when a message is received, The cluster will
183 * broadcast it invoking the messageReceived on the receiver.
184 *
185 * @param msg
186 * ClusterMessage - the message received from the cluster
187 */
188 public void messageReceived(ClusterMessage msg) {
189 try {
190 if (msg instanceof FileMessage && msg != null) {
191 FileMessage fmsg = (FileMessage) msg;
192 if (log.isDebugEnabled())
193 log.debug("receive cluster deployment [ path: "
194 + fmsg.getContextPath() + " war: "
195 + fmsg.getFileName() + " ]");
196 FileMessageFactory factory = getFactory(fmsg);
197 // TODO correct second try after app is in service!
198 if (factory.writeMessage(fmsg)) {
199 //last message received war file is completed
200 String name = factory.getFile().getName();
201 if (!name.endsWith(".war"))
202 name = name + ".war";
203 File deployable = new File(getDeployDir(), name);
204 try {
205 String path = fmsg.getContextPath();
206 if (!isServiced(path)) {
207 addServiced(path);
208 try {
209 remove(path);
210 factory.getFile().renameTo(deployable);
211 check(path);
212 } finally {
213 removeServiced(path);
214 }
215 if (log.isDebugEnabled())
216 log.debug("deployment from " + path
217 + " finished.");
218 } else
219 log.error("Application " + path
220 + " in used. touch war file " + name
221 + " again!");
222 } catch (Exception ex) {
223 log.error(ex);
224 } finally {
225 removeFactory(fmsg);
226 }
227 } //end if
228 } else if (msg instanceof UndeployMessage && msg != null) {
229 try {
230 UndeployMessage umsg = (UndeployMessage) msg;
231 String path = umsg.getContextPath();
232 if (log.isDebugEnabled())
233 log.debug("receive cluster undeployment from " + path);
234 if (!isServiced(path)) {
235 addServiced(path);
236 try {
237 remove(path);
238 } finally {
239 removeServiced(path);
240 }
241 if (log.isDebugEnabled())
242 log.debug("undeployment from " + path
243 + " finished.");
244 } else
245 log
246 .error("Application "
247 + path
248 + " in used. Sorry not remove from backup cluster nodes!");
249 } catch (Exception ex) {
250 log.error(ex);
251 }
252 } //end if
253 } catch (java.io.IOException x) {
254 log.error("Unable to read farm deploy file message.", x);
255 }
256 }
257
258 /**
259 * create factory for all transported war files
260 *
261 * @param msg
262 * @return Factory for all app message (war files)
263 * @throws java.io.FileNotFoundException
264 * @throws java.io.IOException
265 */
266 public synchronized FileMessageFactory getFactory(FileMessage msg)
267 throws java.io.FileNotFoundException, java.io.IOException {
268 File tmpFile = new File(msg.getFileName());
269 File writeToFile = new File(getTempDir(), tmpFile.getName());
270 FileMessageFactory factory = (FileMessageFactory) fileFactories.get(msg
271 .getFileName());
272 if (factory == null) {
273 factory = FileMessageFactory.getInstance(writeToFile, true);
274 fileFactories.put(msg.getFileName(), factory);
275 }
276 return factory;
277 }
278
279 /**
280 * Remove file (war) from messages)
281 *
282 * @param msg
283 */
284 public void removeFactory(FileMessage msg) {
285 fileFactories.remove(msg.getFileName());
286 }
287
288 /**
289 * Before the cluster invokes messageReceived the cluster will ask the
290 * receiver to accept or decline the message, In the future, when messages
291 * get big, the accept method will only take a message header
292 *
293 * @param msg
294 * ClusterMessage
295 * @return boolean - returns true to indicate that messageReceived should be
296 * invoked. If false is returned, the messageReceived method will
297 * not be invoked.
298 */
299 public boolean accept(ClusterMessage msg) {
300 return (msg instanceof FileMessage) || (msg instanceof UndeployMessage);
301 }
302
303 /**
304 * Install a new web application, whose web application archive is at the
305 * specified URL, into this container and all the other members of the
306 * cluster with the specified context path. A context path of "" (the empty
307 * string) should be used for the root application for this container.
308 * Otherwise, the context path must start with a slash.
309 * <p>
310 * If this application is successfully installed locally, a ContainerEvent
311 * of type <code>INSTALL_EVENT</code> will be sent to all registered
312 * listeners, with the newly created <code>Context</code> as an argument.
313 *
314 * @param contextPath
315 * The context path to which this application should be installed
316 * (must be unique)
317 * @param war
318 * A URL of type "jar:" that points to a WAR file, or type
319 * "file:" that points to an unpacked directory structure
320 * containing the web application to be installed
321 *
322 * @exception IllegalArgumentException
323 * if the specified context path is malformed (it must be ""
324 * or start with a slash)
325 * @exception IllegalStateException
326 * if the specified context path is already attached to an
327 * existing web application
328 * @exception IOException
329 * if an input/output error was encountered during
330 * installation
331 */
332 public void install(String contextPath, URL war) throws IOException {
333 Member[] members = getCluster().getMembers();
334 Member localMember = getCluster().getLocalMember();
335 FileMessageFactory factory = FileMessageFactory.getInstance(new File(
336 war.getFile()), false);
337 FileMessage msg = new FileMessage(localMember, war.getFile(),
338 contextPath);
339 if(log.isDebugEnabled())
340 log.debug("Send cluster war deployment [ path:"
341 + contextPath + " war: " + war + " ] started.");
342 msg = factory.readMessage(msg);
343 while (msg != null) {
344 for (int i = 0; i < members.length; i++) {
345 if (log.isDebugEnabled())
346 log.debug("Send cluster war fragment [ path: "
347 + contextPath + " war: " + war + " to: " + members[i] + " ]");
348 getCluster().send(msg, members[i]);
349 } //for
350 msg = factory.readMessage(msg);
351 } //while
352 if(log.isDebugEnabled())
353 log.debug("Send cluster war deployment [ path: "
354 + contextPath + " war: " + war + " ] finished.");
355 }
356
357 /**
358 * Remove an existing web application, attached to the specified context
359 * path. If this application is successfully removed, a ContainerEvent of
360 * type <code>REMOVE_EVENT</code> will be sent to all registered
361 * listeners, with the removed <code>Context</code> as an argument.
362 * Deletes the web application war file and/or directory if they exist in
363 * the Host's appBase.
364 *
365 * @param contextPath
366 * The context path of the application to be removed
367 * @param undeploy
368 * boolean flag to remove web application from server
369 *
370 * @exception IllegalArgumentException
371 * if the specified context path is malformed (it must be ""
372 * or start with a slash)
373 * @exception IllegalArgumentException
374 * if the specified context path does not identify a
375 * currently installed web application
376 * @exception IOException
377 * if an input/output error occurs during removal
378 */
379 public void remove(String contextPath, boolean undeploy) throws IOException {
380 if (log.isInfoEnabled())
381 log.info("Cluster wide remove of web app " + contextPath);
382 // send to other nodes
383 Member[] members = getCluster().getMembers();
384 Member localMember = getCluster().getLocalMember();
385 UndeployMessage msg = new UndeployMessage(localMember, System
386 .currentTimeMillis(), "Undeploy:" + contextPath + ":"
387 + System.currentTimeMillis(), contextPath, undeploy);
388 if (log.isDebugEnabled())
389 log.debug("Send cluster wide undeployment from "
390 + contextPath );
391 cluster.send(msg);
392 // remove locally
393 if (undeploy) {
394 try {
395 if (!isServiced(contextPath)) {
396 addServiced(contextPath);
397 try {
398 remove(contextPath);
399 } finally {
400 removeServiced(contextPath);
401 }
402 } else
403 log.error("Local remove from " + contextPath
404 + "failed, other manager has app in service!");
405
406 } catch (Exception ex) {
407 log.error("local remove from " + contextPath + " failed", ex);
408 }
409 }
410
411 }
412
413 /*
414 * Modifcation from watchDir war detected!
415 *
416 * @see org.apache.catalina.cluster.deploy.FileChangeListener#fileModified(java.io.File)
417 */
418 public void fileModified(File newWar) {
419 try {
420 File deployWar = new File(getDeployDir(), newWar.getName());
421 copy(newWar, deployWar);
422 String contextName = "/"
423 + deployWar.getName().substring(0,
424 deployWar.getName().lastIndexOf(".war"));
425 if (log.isInfoEnabled())
426 log.info("Installing webapp[" + contextName + "] from "
427 + deployWar.getAbsolutePath());
428 try {
429 remove(contextName, false);
430 } catch (Exception x) {
431 log.error("No removal", x);
432 }
433 install(contextName, deployWar.toURL());
434 } catch (Exception x) {
435 log.error("Unable to install WAR file", x);
436 }
437 }
438
439 /*
440 * War remvoe from watchDir
441 *
442 * @see org.apache.catalina.cluster.deploy.FileChangeListener#fileRemoved(java.io.File)
443 */
444 public void fileRemoved(File removeWar) {
445 try {
446 String contextName = "/"
447 + removeWar.getName().substring(0,
448 removeWar.getName().lastIndexOf(".war"));
449 if (log.isInfoEnabled())
450 log.info("Removing webapp[" + contextName + "]");
451 remove(contextName, true);
452 } catch (Exception x) {
453 log.error("Unable to remove WAR file", x);
454 }
455 }
456
457 /**
458 * Given a context path, get the config file name.
459 */
460 protected String getConfigFile(String path) {
461 String basename = null;
462 if (path.equals("")) {
463 basename = "ROOT";
464 } else {
465 basename = path.substring(1).replace('/', '#');
466 }
467 return (basename);
468 }
469
470 /**
471 * Given a context path, get the config file name.
472 */
473 protected String getDocBase(String path) {
474 String basename = null;
475 if (path.equals("")) {
476 basename = "ROOT";
477 } else {
478 basename = path.substring(1);
479 }
480 return (basename);
481 }
482
483 /**
484 * Return a File object representing the "application root" directory for
485 * our associated Host.
486 */
487 protected File getAppBase() {
488
489 if (appBase != null) {
490 return appBase;
491 }
492
493 File file = new File(host.getAppBase());
494 if (!file.isAbsolute())
495 file = new File(System.getProperty("catalina.base"), host
496 .getAppBase());
497 try {
498 appBase = file.getCanonicalFile();
499 } catch (IOException e) {
500 appBase = file;
501 }
502 return (appBase);
503
504 }
505
506 /**
507 * Invoke the remove method on the deployer.
508 */
509 protected void remove(String path) throws Exception {
510 // TODO Handle remove also work dir content !
511 // Stop the context first to be nicer
512 Context context = (Context) host.findChild(path);
513 if (context != null) {
514 if(log.isDebugEnabled())
515 log.debug("Undeploy local context " +path );
516 ((Lifecycle) context).stop();
517 File war = new File(getAppBase(), getDocBase(path) + ".war");
518 File dir = new File(getAppBase(), getDocBase(path));
519 File xml = new File(configBase, getConfigFile(path) + ".xml");
520 if (war.exists()) {
521 war.delete();
522 } else if (dir.exists()) {
523 undeployDir(dir);
524 } else {
525 xml.delete();
526 }
527 // Perform new deployment and remove internal HostConfig state
528 check(path);
529 }
530
531 }
532
533 /**
534 * Delete the specified directory, including all of its contents and
535 * subdirectories recursively.
536 *
537 * @param dir
538 * File object representing the directory to be deleted
539 */
540 protected void undeployDir(File dir) {
541
542 String files[] = dir.list();
543 if (files == null) {
544 files = new String[0];
545 }
546 for (int i = 0; i < files.length; i++) {
547 File file = new File(dir, files[i]);
548 if (file.isDirectory()) {
549 undeployDir(file);
550 } else {
551 file.delete();
552 }
553 }
554 dir.delete();
555
556 }
557
558 /*
559 * Call watcher to check for deploy changes
560 *
561 * @see org.apache.catalina.cluster.ClusterDeployer#backgroundProcess()
562 */
563 public void backgroundProcess() {
564 if (started) {
565 count = (count + 1) % processDeployFrequency;
566 if (count == 0 && watchEnabled) {
567 watcher.check();
568 }
569 }
570
571 }
572
573 /*--Deployer Operations ------------------------------------*/
574
575 /**
576 * Invoke the check method on the deployer.
577 */
578 protected void check(String name) throws Exception {
579 String[] params = { name };
580 String[] signature = { "java.lang.String" };
581 mBeanServer.invoke(oname, "check", params, signature);
582 }
583
584 /**
585 * Invoke the check method on the deployer.
586 */
587 protected boolean isServiced(String name) throws Exception {
588 String[] params = { name };
589 String[] signature = { "java.lang.String" };
590 Boolean result = (Boolean) mBeanServer.invoke(oname, "isServiced",
591 params, signature);
592 return result.booleanValue();
593 }
594
595 /**
596 * Invoke the check method on the deployer.
597 */
598 protected void addServiced(String name) throws Exception {
599 String[] params = { name };
600 String[] signature = { "java.lang.String" };
601 mBeanServer.invoke(oname, "addServiced", params, signature);
602 }
603
604 /**
605 * Invoke the check method on the deployer.
606 */
607 protected void removeServiced(String name) throws Exception {
608 String[] params = { name };
609 String[] signature = { "java.lang.String" };
610 mBeanServer.invoke(oname, "removeServiced", params, signature);
611 }
612
613 /*--Instance Getters/Setters--------------------------------*/
614 public CatalinaCluster getCluster() {
615 return cluster;
616 }
617
618 public void setCluster(CatalinaCluster cluster) {
619 this.cluster = cluster;
620 }
621
622 public boolean equals(Object listener) {
623 return super.equals(listener);
624 }
625
626 public int hashCode() {
627 return super.hashCode();
628 }
629
630 public String getDeployDir() {
631 return deployDir;
632 }
633
634 public void setDeployDir(String deployDir) {
635 this.deployDir = deployDir;
636 }
637
638 public String getTempDir() {
639 return tempDir;
640 }
641
642 public void setTempDir(String tempDir) {
643 this.tempDir = tempDir;
644 }
645
646 public String getWatchDir() {
647 return watchDir;
648 }
649
650 public void setWatchDir(String watchDir) {
651 this.watchDir = watchDir;
652 }
653
654 public boolean isWatchEnabled() {
655 return watchEnabled;
656 }
657
658 public boolean getWatchEnabled() {
659 return watchEnabled;
660 }
661
662 public void setWatchEnabled(boolean watchEnabled) {
663 this.watchEnabled = watchEnabled;
664 }
665
666 /**
667 * Return the frequency of watcher checks.
668 */
669 public int getProcessDeployFrequency() {
670
671 return (this.processDeployFrequency);
672
673 }
674
675 /**
676 * Set the watcher checks frequency.
677 *
678 * @param processExpiresFrequency
679 * the new manager checks frequency
680 */
681 public void setProcessDeployFrequency(int processExpiresFrequency) {
682
683 if (processExpiresFrequency <= 0) {
684 return;
685 }
686 this.processDeployFrequency = processExpiresFrequency;
687 }
688
689 /**
690 * Copy a file to the specified temp directory. This is required only
691 * because Jasper depends on it.
692 */
693 protected boolean copy(File from, File to) {
694 try {
695 if (!to.exists())
696 to.createNewFile();
697 java.io.FileInputStream is = new java.io.FileInputStream(from);
698 java.io.FileOutputStream os = new java.io.FileOutputStream(to,
699 false);
700 byte[] buf = new byte[4096];
701 while (true) {
702 int len = is.read(buf);
703 if (len < 0)
704 break;
705 os.write(buf, 0, len);
706 }
707 is.close();
708 os.close();
709 } catch (IOException e) {
710 log.error("Unable to copy file from:" + from + " to:" + to, e);
711 return false;
712 }
713 return true;
714 }
715
716 }