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