001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.catalina.ha.deploy;
019:
020: import java.io.File;
021: import java.io.IOException;
022: import java.net.URL;
023: import java.util.HashMap;
024: import javax.management.MBeanServer;
025: import javax.management.ObjectName;
026:
027: import org.apache.catalina.Context;
028: import org.apache.catalina.Engine;
029: import org.apache.catalina.Host;
030: import org.apache.catalina.Lifecycle;
031: import org.apache.catalina.LifecycleException;
032: import org.apache.catalina.ha.CatalinaCluster;
033: import org.apache.catalina.ha.ClusterDeployer;
034: import org.apache.catalina.ha.ClusterListener;
035: import org.apache.catalina.ha.ClusterMessage;
036: import org.apache.catalina.tribes.Member;
037: import org.apache.tomcat.util.modeler.Registry;
038:
039: /**
040: * <p>
041: * A farm war deployer is a class that is able to deploy/undeploy web
042: * applications in WAR form within the cluster.
043: * </p>
044: * Any host can act as the admin, and will have three directories
045: * <ul>
046: * <li>deployDir - the directory where we watch for changes</li>
047: * <li>applicationDir - the directory where we install applications</li>
048: * <li>tempDir - a temporaryDirectory to store binary data when downloading a
049: * war from the cluster</li>
050: * </ul>
051: * Currently we only support deployment of WAR files since they are easier to
052: * send across the wire.
053: *
054: * @author Filip Hanik
055: * @author Peter Rossbach
056: * @version $Revision: 516438 $
057: */
058: public class FarmWarDeployer extends ClusterListener implements
059: ClusterDeployer, FileChangeListener {
060: /*--Static Variables----------------------------------------*/
061: public static org.apache.juli.logging.Log log = org.apache.juli.logging.LogFactory
062: .getLog(FarmWarDeployer.class);
063: /**
064: * The descriptive information about this implementation.
065: */
066: private static final String info = "FarmWarDeployer/1.2";
067:
068: /*--Instance Variables--------------------------------------*/
069: protected CatalinaCluster cluster = null;
070:
071: protected boolean started = false; //default 5 seconds
072:
073: protected HashMap fileFactories = new HashMap();
074:
075: protected String deployDir;
076:
077: protected String tempDir;
078:
079: protected String watchDir;
080:
081: protected boolean watchEnabled = false;
082:
083: protected WarWatcher watcher = null;
084:
085: /**
086: * Iteration count for background processing.
087: */
088: private int count = 0;
089:
090: /**
091: * Frequency of the Farm watchDir check. Cluster wide deployment will be
092: * done once for the specified amount of backgrondProcess calls (ie, the
093: * lower the amount, the most often the checks will occur).
094: */
095: protected int processDeployFrequency = 2;
096:
097: /**
098: * Path where context descriptors should be deployed.
099: */
100: protected File configBase = null;
101:
102: /**
103: * The associated host.
104: */
105: protected Host host = null;
106:
107: /**
108: * The host appBase.
109: */
110: protected File appBase = null;
111:
112: /**
113: * MBean server.
114: */
115: protected MBeanServer mBeanServer = null;
116:
117: /**
118: * The associated deployer ObjectName.
119: */
120: protected ObjectName oname = null;
121:
122: /*--Constructor---------------------------------------------*/
123: public FarmWarDeployer() {
124: }
125:
126: /**
127: * Return descriptive information about this deployer implementation and the
128: * corresponding version number, in the format
129: * <code><description>/<version></code>.
130: */
131: public String getInfo() {
132:
133: return (info);
134:
135: }
136:
137: /*--Logic---------------------------------------------------*/
138: public void start() throws Exception {
139: if (started)
140: return;
141: getCluster().addClusterListener(this );
142: if (watchEnabled) {
143: watcher = new WarWatcher(this , new File(getWatchDir()));
144: if (log.isInfoEnabled())
145: log.info("Cluster deployment is watching "
146: + getWatchDir() + " for changes.");
147: }
148:
149: // Check to correct engine and host setup
150: Object parent = getCluster().getContainer();
151: Engine engine = null;
152: String hostname = null;
153: if (parent instanceof Host) {
154: host = (Host) parent;
155: engine = (Engine) host.getParent();
156: hostname = host.getName();
157: } else {
158: engine = (Engine) parent;
159: hostname = engine.getDefaultHost();
160: }
161: try {
162: oname = new ObjectName(engine.getName()
163: + ":type=Deployer,host=" + hostname);
164: } catch (Exception e) {
165: log.error("Can't construct MBean object name" + e);
166: }
167: configBase = new File(System.getProperty("catalina.base"),
168: "conf");
169: if (engine != null) {
170: configBase = new File(configBase, engine.getName());
171: } else if (host != null) {
172: configBase = new File(configBase, host.getName());
173: }
174:
175: // Retrieve the MBean server
176: mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
177:
178: started = true;
179: count = 0;
180: if (log.isInfoEnabled())
181: log.info("Cluster FarmWarDeployer started.");
182: }
183:
184: /*
185: * stop cluster wide deployments
186: *
187: * @see org.apache.catalina.ha.ClusterDeployer#stop()
188: */
189: public void stop() throws LifecycleException {
190: started = false;
191: getCluster().removeClusterListener(this );
192: count = 0;
193: if (watcher != null) {
194: watcher.clear();
195: watcher = null;
196:
197: }
198: if (log.isInfoEnabled())
199: log.info("Cluster FarmWarDeployer stopped.");
200: }
201:
202: public void cleanDeployDir() {
203: throw new java.lang.UnsupportedOperationException(
204: "Method cleanDeployDir() not yet implemented.");
205: }
206:
207: /**
208: * Callback from the cluster, when a message is received, The cluster will
209: * broadcast it invoking the messageReceived on the receiver.
210: *
211: * @param msg
212: * ClusterMessage - the message received from the cluster
213: */
214: public void messageReceived(ClusterMessage msg) {
215: try {
216: if (msg instanceof FileMessage && msg != null) {
217: FileMessage fmsg = (FileMessage) msg;
218: if (log.isDebugEnabled())
219: log.debug("receive cluster deployment [ path: "
220: + fmsg.getContextPath() + " war: "
221: + fmsg.getFileName() + " ]");
222: FileMessageFactory factory = getFactory(fmsg);
223: // TODO correct second try after app is in service!
224: if (factory.writeMessage(fmsg)) {
225: //last message received war file is completed
226: String name = factory.getFile().getName();
227: if (!name.endsWith(".war"))
228: name = name + ".war";
229: File deployable = new File(getDeployDir(), name);
230: try {
231: String path = fmsg.getContextPath();
232: if (!isServiced(path)) {
233: addServiced(path);
234: try {
235: remove(path);
236: factory.getFile().renameTo(deployable);
237: check(path);
238: } finally {
239: removeServiced(path);
240: }
241: if (log.isDebugEnabled())
242: log.debug("deployment from " + path
243: + " finished.");
244: } else
245: log.error("Application " + path
246: + " in used. touch war file "
247: + name + " again!");
248: } catch (Exception ex) {
249: log.error(ex);
250: } finally {
251: removeFactory(fmsg);
252: }
253: }
254: } else if (msg instanceof UndeployMessage && msg != null) {
255: try {
256: UndeployMessage umsg = (UndeployMessage) msg;
257: String path = umsg.getContextPath();
258: if (log.isDebugEnabled())
259: log.debug("receive cluster undeployment from "
260: + path);
261: if (!isServiced(path)) {
262: addServiced(path);
263: try {
264: remove(path);
265: } finally {
266: removeServiced(path);
267: }
268: if (log.isDebugEnabled())
269: log.debug("undeployment from " + path
270: + " finished.");
271: } else
272: log
273: .error("Application "
274: + path
275: + " in used. Sorry not remove from backup cluster nodes!");
276: } catch (Exception ex) {
277: log.error(ex);
278: }
279: }
280: } catch (java.io.IOException x) {
281: log.error("Unable to read farm deploy file message.", x);
282: }
283: }
284:
285: /**
286: * create factory for all transported war files
287: *
288: * @param msg
289: * @return Factory for all app message (war files)
290: * @throws java.io.FileNotFoundException
291: * @throws java.io.IOException
292: */
293: public synchronized FileMessageFactory getFactory(FileMessage msg)
294: throws java.io.FileNotFoundException, java.io.IOException {
295: File tmpFile = new File(msg.getFileName());
296: File writeToFile = new File(getTempDir(), tmpFile.getName());
297: FileMessageFactory factory = (FileMessageFactory) fileFactories
298: .get(msg.getFileName());
299: if (factory == null) {
300: factory = FileMessageFactory.getInstance(writeToFile, true);
301: fileFactories.put(msg.getFileName(), factory);
302: }
303: return factory;
304: }
305:
306: /**
307: * Remove file (war) from messages)
308: *
309: * @param msg
310: */
311: public void removeFactory(FileMessage msg) {
312: fileFactories.remove(msg.getFileName());
313: }
314:
315: /**
316: * Before the cluster invokes messageReceived the cluster will ask the
317: * receiver to accept or decline the message, In the future, when messages
318: * get big, the accept method will only take a message header
319: *
320: * @param msg
321: * ClusterMessage
322: * @return boolean - returns true to indicate that messageReceived should be
323: * invoked. If false is returned, the messageReceived method will
324: * not be invoked.
325: */
326: public boolean accept(ClusterMessage msg) {
327: return (msg instanceof FileMessage)
328: || (msg instanceof UndeployMessage);
329: }
330:
331: /**
332: * Install a new web application, whose web application archive is at the
333: * specified URL, into this container and all the other members of the
334: * cluster with the specified context path. A context path of "" (the empty
335: * string) should be used for the root application for this container.
336: * Otherwise, the context path must start with a slash.
337: * <p>
338: * If this application is successfully installed locally, a ContainerEvent
339: * of type <code>INSTALL_EVENT</code> will be sent to all registered
340: * listeners, with the newly created <code>Context</code> as an argument.
341: *
342: * @param contextPath
343: * The context path to which this application should be installed
344: * (must be unique)
345: * @param war
346: * A URL of type "jar:" that points to a WAR file, or type
347: * "file:" that points to an unpacked directory structure
348: * containing the web application to be installed
349: *
350: * @exception IllegalArgumentException
351: * if the specified context path is malformed (it must be ""
352: * or start with a slash)
353: * @exception IllegalStateException
354: * if the specified context path is already attached to an
355: * existing web application
356: * @exception IOException
357: * if an input/output error was encountered during
358: * installation
359: */
360: public void install(String contextPath, URL war) throws IOException {
361: Member[] members = getCluster().getMembers();
362: Member localMember = getCluster().getLocalMember();
363: FileMessageFactory factory = FileMessageFactory.getInstance(
364: new File(war.getFile()), false);
365: FileMessage msg = new FileMessage(localMember, war.getFile(),
366: contextPath);
367: if (log.isDebugEnabled())
368: log.debug("Send cluster war deployment [ path:"
369: + contextPath + " war: " + war + " ] started.");
370: msg = factory.readMessage(msg);
371: while (msg != null) {
372: for (int i = 0; i < members.length; i++) {
373: if (log.isDebugEnabled())
374: log.debug("Send cluster war fragment [ path: "
375: + contextPath + " war: " + war + " to: "
376: + members[i] + " ]");
377: getCluster().send(msg, members[i]);
378: }
379: msg = factory.readMessage(msg);
380: }
381: if (log.isDebugEnabled())
382: log.debug("Send cluster war deployment [ path: "
383: + contextPath + " war: " + war + " ] finished.");
384: }
385:
386: /**
387: * Remove an existing web application, attached to the specified context
388: * path. If this application is successfully removed, a ContainerEvent of
389: * type <code>REMOVE_EVENT</code> will be sent to all registered
390: * listeners, with the removed <code>Context</code> as an argument.
391: * Deletes the web application war file and/or directory if they exist in
392: * the Host's appBase.
393: *
394: * @param contextPath
395: * The context path of the application to be removed
396: * @param undeploy
397: * boolean flag to remove web application from server
398: *
399: * @exception IllegalArgumentException
400: * if the specified context path is malformed (it must be ""
401: * or start with a slash)
402: * @exception IllegalArgumentException
403: * if the specified context path does not identify a
404: * currently installed web application
405: * @exception IOException
406: * if an input/output error occurs during removal
407: */
408: public void remove(String contextPath, boolean undeploy)
409: throws IOException {
410: if (log.isInfoEnabled())
411: log.info("Cluster wide remove of web app " + contextPath);
412: Member localMember = getCluster().getLocalMember();
413: UndeployMessage msg = new UndeployMessage(localMember, System
414: .currentTimeMillis(), "Undeploy:" + contextPath + ":"
415: + System.currentTimeMillis(), contextPath, undeploy);
416: if (log.isDebugEnabled())
417: log.debug("Send cluster wide undeployment from "
418: + contextPath);
419: cluster.send(msg);
420: // remove locally
421: if (undeploy) {
422: try {
423: if (!isServiced(contextPath)) {
424: addServiced(contextPath);
425: try {
426: remove(contextPath);
427: } finally {
428: removeServiced(contextPath);
429: }
430: } else
431: log
432: .error("Local remove from "
433: + contextPath
434: + "failed, other manager has app in service!");
435:
436: } catch (Exception ex) {
437: log.error("local remove from " + contextPath
438: + " failed", ex);
439: }
440: }
441:
442: }
443:
444: /*
445: * Modifcation from watchDir war detected!
446: *
447: * @see org.apache.catalina.ha.deploy.FileChangeListener#fileModified(java.io.File)
448: */
449: public void fileModified(File newWar) {
450: try {
451: File deployWar = new File(getDeployDir(), newWar.getName());
452: copy(newWar, deployWar);
453: String contextName = getContextName(deployWar);
454: if (log.isInfoEnabled())
455: log.info("Installing webapp[" + contextName + "] from "
456: + deployWar.getAbsolutePath());
457: try {
458: remove(contextName, false);
459: } catch (Exception x) {
460: log.error("No removal", x);
461: }
462: install(contextName, deployWar.toURL());
463: } catch (Exception x) {
464: log.error("Unable to install WAR file", x);
465: }
466: }
467:
468: /*
469: * War remvoe from watchDir
470: *
471: * @see org.apache.catalina.ha.deploy.FileChangeListener#fileRemoved(java.io.File)
472: */
473: public void fileRemoved(File removeWar) {
474: try {
475: String contextName = getContextName(removeWar);
476: if (log.isInfoEnabled())
477: log.info("Removing webapp[" + contextName + "]");
478: remove(contextName, true);
479: } catch (Exception x) {
480: log.error("Unable to remove WAR file", x);
481: }
482: }
483:
484: /**
485: * Create a context path from war
486: * @param war War filename
487: * @return '/filename' or if war name is ROOT.war context name is empty string ''
488: */
489: protected String getContextName(File war) {
490: String contextName = "/"
491: + war.getName().substring(0,
492: war.getName().lastIndexOf(".war"));
493: if ("/ROOT".equals(contextName))
494: contextName = "";
495: return contextName;
496: }
497:
498: /**
499: * Given a context path, get the config file name.
500: */
501: protected String getConfigFile(String path) {
502: String basename = null;
503: if (path.equals("")) {
504: basename = "ROOT";
505: } else {
506: basename = path.substring(1).replace('/', '#');
507: }
508: return (basename);
509: }
510:
511: /**
512: * Given a context path, get the config file name.
513: */
514: protected String getDocBase(String path) {
515: String basename = null;
516: if (path.equals("")) {
517: basename = "ROOT";
518: } else {
519: basename = path.substring(1);
520: }
521: return (basename);
522: }
523:
524: /**
525: * Return a File object representing the "application root" directory for
526: * our associated Host.
527: */
528: protected File getAppBase() {
529:
530: if (appBase != null) {
531: return appBase;
532: }
533:
534: File file = new File(host.getAppBase());
535: if (!file.isAbsolute())
536: file = new File(System.getProperty("catalina.base"), host
537: .getAppBase());
538: try {
539: appBase = file.getCanonicalFile();
540: } catch (IOException e) {
541: appBase = file;
542: }
543: return (appBase);
544:
545: }
546:
547: /**
548: * Invoke the remove method on the deployer.
549: */
550: protected void remove(String path) throws Exception {
551: // TODO Handle remove also work dir content !
552: // Stop the context first to be nicer
553: Context context = (Context) host.findChild(path);
554: if (context != null) {
555: if (log.isDebugEnabled())
556: log.debug("Undeploy local context " + path);
557: ((Lifecycle) context).stop();
558: File war = new File(getAppBase(), getDocBase(path) + ".war");
559: File dir = new File(getAppBase(), getDocBase(path));
560: File xml = new File(configBase, getConfigFile(path)
561: + ".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,
633: "isServiced", 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(
742: from);
743: java.io.FileOutputStream os = new java.io.FileOutputStream(
744: to, false);
745: byte[] buf = new byte[4096];
746: while (true) {
747: int len = is.read(buf);
748: if (len < 0)
749: break;
750: os.write(buf, 0, len);
751: }
752: is.close();
753: os.close();
754: } catch (IOException e) {
755: log.error("Unable to copy file from:" + from + " to:" + to,
756: e);
757: return false;
758: }
759: return true;
760: }
761:
762: }
|