| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.catalina.ha.deploy; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.util.HashMap; |
| |
| import javax.management.MBeanServer; |
| import javax.management.ObjectName; |
| |
| import org.apache.catalina.Container; |
| import org.apache.catalina.Context; |
| import org.apache.catalina.Engine; |
| import org.apache.catalina.Host; |
| import org.apache.catalina.LifecycleException; |
| import org.apache.catalina.ha.ClusterDeployer; |
| import org.apache.catalina.ha.ClusterListener; |
| import org.apache.catalina.ha.ClusterMessage; |
| import org.apache.catalina.tribes.Member; |
| import org.apache.catalina.util.ContextName; |
| import org.apache.juli.logging.Log; |
| import org.apache.juli.logging.LogFactory; |
| import org.apache.tomcat.util.modeler.Registry; |
| import org.apache.tomcat.util.res.StringManager; |
| |
| |
| /** |
| * <p> |
| * A farm war deployer is a class that is able to deploy/undeploy web applications in WAR from within the cluster. |
| * </p> |
| * Any host can act as the admin, and will have three directories |
| * <ul> |
| * <li>watchDir - the directory where we watch for changes</li> |
| * <li>deployDir - the directory where we install applications</li> |
| * <li>tempDir - a temporaryDirectory to store binary data when downloading a war from the cluster</li> |
| * </ul> |
| * Currently we only support deployment of WAR files since they are easier to send across the wire. |
| */ |
| public class FarmWarDeployer extends ClusterListener implements ClusterDeployer, FileChangeListener { |
| /*--Static Variables----------------------------------------*/ |
| private static final Log log = LogFactory.getLog(FarmWarDeployer.class); |
| private static final StringManager sm = StringManager.getManager(FarmWarDeployer.class); |
| |
| /*--Instance Variables--------------------------------------*/ |
| protected boolean started = false; |
| |
| protected final HashMap<String,FileMessageFactory> fileFactories = new HashMap<>(); |
| |
| /** |
| * Deployment directory. |
| */ |
| protected String deployDir; |
| private File deployDirFile = null; |
| |
| /** |
| * Temporary directory. |
| */ |
| protected String tempDir; |
| private File tempDirFile = null; |
| |
| /** |
| * Watch directory. |
| */ |
| protected String watchDir; |
| private File watchDirFile = null; |
| |
| protected boolean watchEnabled = false; |
| |
| protected WarWatcher watcher = null; |
| |
| /** |
| * Iteration count for background processing. |
| */ |
| private int count = 0; |
| |
| /** |
| * Frequency of the Farm watchDir check. Cluster wide deployment will be done once for the specified amount of |
| * backgroundProcess calls (ie, the lower the amount, the most often the checks will occur). |
| */ |
| protected int processDeployFrequency = 2; |
| |
| /** |
| * Path where context descriptors should be deployed. |
| */ |
| protected File configBase = null; |
| |
| /** |
| * The associated host. |
| */ |
| protected Host host = null; |
| |
| /** |
| * MBean server. |
| */ |
| protected MBeanServer mBeanServer = null; |
| |
| /** |
| * The associated deployer ObjectName. |
| */ |
| protected ObjectName oname = null; |
| |
| /** |
| * The maximum valid time(in seconds) for FileMessageFactory. |
| */ |
| protected int maxValidTime = 5 * 60; |
| |
| /*--Constructor---------------------------------------------*/ |
| public FarmWarDeployer() { |
| } |
| |
| /*--Logic---------------------------------------------------*/ |
| @Override |
| public void start() throws Exception { |
| if (started) { |
| return; |
| } |
| Container hcontainer = getCluster().getContainer(); |
| if (!(hcontainer instanceof Host)) { |
| log.error(sm.getString("farmWarDeployer.hostOnly")); |
| return; |
| } |
| host = (Host) hcontainer; |
| |
| // Check to correct engine and host setup |
| Container econtainer = host.getParent(); |
| if (!(econtainer instanceof Engine)) { |
| log.error(sm.getString("farmWarDeployer.hostParentEngine", host.getName())); |
| return; |
| } |
| Engine engine = (Engine) econtainer; |
| String hostname = host.getName(); |
| try { |
| oname = new ObjectName(engine.getName() + ":type=Deployer,host=" + hostname); |
| } catch (Exception e) { |
| log.error(sm.getString("farmWarDeployer.mbeanNameFail", engine.getName(), hostname), e); |
| return; |
| } |
| if (watchEnabled) { |
| watcher = new WarWatcher(this, getWatchDirFile()); |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("farmWarDeployer.watchDir", getWatchDir())); |
| } |
| } |
| |
| configBase = host.getConfigBaseFile(); |
| |
| // Retrieve the MBean server |
| mBeanServer = Registry.getRegistry(null).getMBeanServer(); |
| |
| started = true; |
| count = 0; |
| |
| getCluster().addClusterListener(this); |
| |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("farmWarDeployer.started")); |
| } |
| } |
| |
| /* |
| * stop cluster wide deployments |
| * |
| * @see org.apache.catalina.ha.ClusterDeployer#stop() |
| */ |
| @Override |
| public void stop() throws LifecycleException { |
| started = false; |
| getCluster().removeClusterListener(this); |
| count = 0; |
| if (watcher != null) { |
| watcher.clear(); |
| watcher = null; |
| |
| } |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("farmWarDeployer.stopped")); |
| } |
| } |
| |
| /** |
| * Callback from the cluster, when a message is received, The cluster will broadcast it invoking the messageReceived |
| * on the receiver. |
| * |
| * @param msg ClusterMessage - the message received from the cluster |
| */ |
| @Override |
| public void messageReceived(ClusterMessage msg) { |
| try { |
| if (msg instanceof FileMessage) { |
| FileMessage fmsg = (FileMessage) msg; |
| if (log.isTraceEnabled()) { |
| log.trace(sm.getString("farmWarDeployer.msgRxDeploy", fmsg.getContextName(), fmsg.getFileName())); |
| } |
| FileMessageFactory factory = getFactory(fmsg); |
| // TODO correct second try after app is in service! |
| if (factory.writeMessage(fmsg)) { |
| // last message received war file is completed |
| String name = factory.getFile().getName(); |
| if (!name.endsWith(".war")) { |
| name = name + ".war"; |
| } |
| File deployable = new File(getDeployDirFile(), name); |
| try { |
| String contextName = fmsg.getContextName(); |
| if (tryAddServiced(contextName)) { |
| try { |
| remove(contextName); |
| if (!factory.getFile().renameTo(deployable)) { |
| log.error( |
| sm.getString("farmWarDeployer.renameFail", factory.getFile(), deployable)); |
| } |
| } finally { |
| removeServiced(contextName); |
| } |
| check(contextName); |
| if (log.isTraceEnabled()) { |
| log.trace(sm.getString("farmWarDeployer.deployEnd", contextName)); |
| } |
| } else { |
| log.error(sm.getString("farmWarDeployer.servicingDeploy", contextName, name)); |
| } |
| } catch (Exception e) { |
| log.error(sm.getString("farmWarDeployer.fileMessageError"), e); |
| } finally { |
| removeFactory(fmsg); |
| } |
| } |
| } else if (msg instanceof UndeployMessage) { |
| try { |
| UndeployMessage umsg = (UndeployMessage) msg; |
| String contextName = umsg.getContextName(); |
| if (log.isTraceEnabled()) { |
| log.trace(sm.getString("farmWarDeployer.msgRxUndeploy", contextName)); |
| } |
| if (tryAddServiced(contextName)) { |
| try { |
| remove(contextName); |
| } finally { |
| removeServiced(contextName); |
| } |
| if (log.isTraceEnabled()) { |
| log.trace(sm.getString("farmWarDeployer.undeployEnd", contextName)); |
| } |
| } else { |
| log.error(sm.getString("farmWarDeployer.servicingUndeploy", contextName)); |
| } |
| } catch (Exception e) { |
| log.error(sm.getString("farmWarDeployer.undeployMessageError"), e); |
| } |
| } |
| } catch (IOException ioe) { |
| log.error(sm.getString("farmWarDeployer.msgIoe"), ioe); |
| } |
| } |
| |
| /** |
| * Create factory for all transported war files |
| * |
| * @param msg The file |
| * |
| * @return Factory for all app message (war files) |
| * |
| * @throws FileNotFoundException Missing file error |
| * @throws IOException Other IO error |
| */ |
| public synchronized FileMessageFactory getFactory(FileMessage msg) throws FileNotFoundException, IOException { |
| File writeToFile = new File(getTempDirFile(), msg.getFileName()); |
| FileMessageFactory factory = fileFactories.get(msg.getFileName()); |
| if (factory == null) { |
| factory = FileMessageFactory.getInstance(writeToFile, true); |
| factory.setMaxValidTime(maxValidTime); |
| fileFactories.put(msg.getFileName(), factory); |
| } |
| return factory; |
| } |
| |
| /** |
| * Remove file (war) from messages |
| * |
| * @param msg The file |
| */ |
| public void removeFactory(FileMessage msg) { |
| fileFactories.remove(msg.getFileName()); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * <p> |
| * This listener accepts only FileMessage or UndeployMessage. |
| */ |
| @Override |
| public boolean accept(ClusterMessage msg) { |
| return msg instanceof FileMessage || msg instanceof UndeployMessage; |
| } |
| |
| /** |
| * Install a new web application, whose web application archive is at the specified URL, into this container and all |
| * the other members of the cluster with the specified context name. |
| * <p> |
| * If this application is successfully installed locally, a ContainerEvent of type <code>INSTALL_EVENT</code> will |
| * be sent to all registered listeners, with the newly created <code>Context</code> as an argument. |
| * |
| * @param contextName The context name to which this application should be installed (must be unique) |
| * @param webapp A WAR file or unpacked directory structure containing the web application to be installed |
| * |
| * @exception IllegalArgumentException if the specified context name is malformed |
| * @exception IllegalStateException if the specified context name is already deployed |
| * @exception IOException if an input/output error was encountered during installation |
| */ |
| @Override |
| public void install(String contextName, File webapp) throws IOException { |
| Member[] members = getCluster().getMembers(); |
| if (members.length == 0) { |
| return; |
| } |
| |
| Member localMember = getCluster().getLocalMember(); |
| FileMessageFactory factory = FileMessageFactory.getInstance(webapp, false); |
| FileMessage msg = new FileMessage(localMember, webapp.getName(), contextName); |
| if (log.isTraceEnabled()) { |
| log.trace(sm.getString("farmWarDeployer.sendStart", contextName, webapp)); |
| } |
| msg = factory.readMessage(msg); |
| while (msg != null) { |
| for (Member member : members) { |
| if (log.isTraceEnabled()) { |
| log.trace(sm.getString("farmWarDeployer.sendFragment", contextName, webapp, member)); |
| } |
| getCluster().send(msg, member); |
| } |
| msg = factory.readMessage(msg); |
| } |
| if (log.isTraceEnabled()) { |
| log.trace(sm.getString("farmWarDeployer.sendEnd", contextName, webapp)); |
| } |
| } |
| |
| /** |
| * Remove an existing web application, attached to the specified context name. If this application is successfully |
| * removed, a ContainerEvent of type <code>REMOVE_EVENT</code> will be sent to all registered listeners, with the |
| * removed <code>Context</code> as an argument. Deletes the web application war file and/or directory if they exist |
| * in the Host's appBase. |
| * |
| * @param contextName The context name of the application to be removed |
| * @param undeploy boolean flag to remove web application from server |
| * |
| * @exception IllegalArgumentException if the specified context name is malformed |
| * @exception IllegalArgumentException if the specified context name does not identify a currently installed web |
| * application |
| * @exception IOException if an input/output error occurs during removal |
| */ |
| @Override |
| public void remove(String contextName, boolean undeploy) throws IOException { |
| if (getCluster().getMembers().length > 0) { |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("farmWarDeployer.removeStart", contextName)); |
| } |
| Member localMember = getCluster().getLocalMember(); |
| UndeployMessage msg = new UndeployMessage(localMember, System.currentTimeMillis(), |
| "Undeploy:" + contextName + ":" + System.currentTimeMillis(), contextName); |
| if (log.isTraceEnabled()) { |
| log.trace(sm.getString("farmWarDeployer.removeTxMsg", contextName)); |
| } |
| cluster.send(msg); |
| } |
| // remove locally |
| if (undeploy) { |
| try { |
| if (tryAddServiced(contextName)) { |
| try { |
| remove(contextName); |
| } finally { |
| removeServiced(contextName); |
| } |
| check(contextName); |
| } else { |
| log.error(sm.getString("farmWarDeployer.removeFailRemote", contextName)); |
| } |
| |
| } catch (Exception e) { |
| log.error(sm.getString("farmWarDeployer.removeFailLocal", contextName), e); |
| } |
| } |
| |
| } |
| |
| @Override |
| public void fileModified(File newWar) { |
| try { |
| File deployWar = new File(getDeployDirFile(), newWar.getName()); |
| ContextName cn = new ContextName(deployWar.getName(), true); |
| if (deployWar.exists() && deployWar.lastModified() > newWar.lastModified()) { |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("farmWarDeployer.alreadyDeployed", cn.getName())); |
| } |
| return; |
| } |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("farmWarDeployer.modInstall", cn.getName(), deployWar.getAbsolutePath())); |
| } |
| // install local |
| if (tryAddServiced(cn.getName())) { |
| try { |
| copy(newWar, deployWar); |
| } finally { |
| removeServiced(cn.getName()); |
| } |
| check(cn.getName()); |
| } else { |
| log.error(sm.getString("farmWarDeployer.servicingDeploy", cn.getName(), deployWar.getName())); |
| } |
| install(cn.getName(), deployWar); |
| } catch (Exception e) { |
| log.error(sm.getString("farmWarDeployer.modInstallFail"), e); |
| } |
| } |
| |
| @Override |
| public void fileRemoved(File removeWar) { |
| try { |
| ContextName cn = new ContextName(removeWar.getName(), true); |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("farmWarDeployer.removeLocal", cn.getName())); |
| } |
| remove(cn.getName(), true); |
| } catch (Exception e) { |
| log.error(sm.getString("farmWarDeployer.removeLocalFail"), e); |
| } |
| } |
| |
| /** |
| * Invoke the remove method on the deployer. |
| * |
| * @param contextName The context to remove |
| * |
| * @throws Exception If an error occurs removing the context |
| */ |
| protected void remove(String contextName) throws Exception { |
| // TODO Handle remove also work dir content ! |
| // Stop the context first to be nicer |
| Context context = (Context) host.findChild(contextName); |
| if (context != null) { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("farmWarDeployer.undeployLocal", contextName)); |
| } |
| context.stop(); |
| String baseName = context.getBaseName(); |
| File war = new File(host.getAppBaseFile(), baseName + ".war"); |
| File dir = new File(host.getAppBaseFile(), baseName); |
| File xml = new File(configBase, baseName + ".xml"); |
| if (war.exists()) { |
| if (!war.delete()) { |
| log.error(sm.getString("farmWarDeployer.deleteFail", war)); |
| } |
| } else if (dir.exists()) { |
| undeployDir(dir); |
| } else { |
| if (!xml.delete()) { |
| log.error(sm.getString("farmWarDeployer.deleteFail", xml)); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Delete the specified directory, including all of its contents and subdirectories recursively. |
| * |
| * @param dir File object representing the directory to be deleted |
| */ |
| protected void undeployDir(File dir) { |
| |
| String[] files = dir.list(); |
| if (files == null) { |
| files = new String[0]; |
| } |
| for (String s : files) { |
| File file = new File(dir, s); |
| if (file.isDirectory()) { |
| undeployDir(file); |
| } else { |
| if (!file.delete()) { |
| log.error(sm.getString("farmWarDeployer.deleteFail", file)); |
| } |
| } |
| } |
| if (!dir.delete()) { |
| log.error(sm.getString("farmWarDeployer.deleteFail", dir)); |
| } |
| } |
| |
| /** |
| * Call watcher to check for deploy changes |
| * |
| * @see org.apache.catalina.ha.ClusterDeployer#backgroundProcess() |
| */ |
| @Override |
| public void backgroundProcess() { |
| if (started) { |
| if (watchEnabled) { |
| count = (count + 1) % processDeployFrequency; |
| if (count == 0) { |
| watcher.check(); |
| } |
| } |
| removeInvalidFileFactories(); |
| } |
| |
| } |
| |
| /*--Deployer Operations ------------------------------------*/ |
| |
| /** |
| * Check a context for deployment operations. |
| * |
| * @param name The context name |
| * |
| * @throws Exception Error invoking the deployer |
| */ |
| protected void check(String name) throws Exception { |
| String[] params = { name }; |
| String[] signature = { "java.lang.String" }; |
| mBeanServer.invoke(oname, "check", params, signature); |
| } |
| |
| /** |
| * Attempt to mark a context as being serviced |
| * |
| * @param name The context name |
| * |
| * @return {@code true} if the application was marked as being serviced and {@code false} if the application was |
| * already marked as being serviced |
| * |
| * @throws Exception Error invoking the deployer |
| */ |
| protected boolean tryAddServiced(String name) throws Exception { |
| String[] params = { name }; |
| String[] signature = { "java.lang.String" }; |
| Boolean result = (Boolean) mBeanServer.invoke(oname, "tryAddServiced", params, signature); |
| return result.booleanValue(); |
| } |
| |
| /** |
| * Mark a context as no longer being serviced. |
| * |
| * @param name The context name |
| * |
| * @throws Exception Error invoking the deployer |
| */ |
| protected void removeServiced(String name) throws Exception { |
| String[] params = { name }; |
| String[] signature = { "java.lang.String" }; |
| mBeanServer.invoke(oname, "removeServiced", params, signature); |
| } |
| |
| /*--Instance Getters/Setters--------------------------------*/ |
| public String getDeployDir() { |
| return deployDir; |
| } |
| |
| public File getDeployDirFile() { |
| if (deployDirFile != null) { |
| return deployDirFile; |
| } |
| |
| File dir = getAbsolutePath(getDeployDir()); |
| this.deployDirFile = dir; |
| return dir; |
| } |
| |
| public void setDeployDir(String deployDir) { |
| this.deployDir = deployDir; |
| } |
| |
| public String getTempDir() { |
| return tempDir; |
| } |
| |
| public File getTempDirFile() { |
| if (tempDirFile != null) { |
| return tempDirFile; |
| } |
| |
| File dir = getAbsolutePath(getTempDir()); |
| this.tempDirFile = dir; |
| return dir; |
| } |
| |
| public void setTempDir(String tempDir) { |
| this.tempDir = tempDir; |
| } |
| |
| public String getWatchDir() { |
| return watchDir; |
| } |
| |
| public File getWatchDirFile() { |
| if (watchDirFile != null) { |
| return watchDirFile; |
| } |
| |
| File dir = getAbsolutePath(getWatchDir()); |
| this.watchDirFile = dir; |
| return dir; |
| } |
| |
| public void setWatchDir(String watchDir) { |
| this.watchDir = watchDir; |
| } |
| |
| public boolean isWatchEnabled() { |
| return watchEnabled; |
| } |
| |
| public boolean getWatchEnabled() { |
| return watchEnabled; |
| } |
| |
| public void setWatchEnabled(boolean watchEnabled) { |
| this.watchEnabled = watchEnabled; |
| } |
| |
| /** |
| * @return the frequency of watcher checks. |
| */ |
| public int getProcessDeployFrequency() { |
| return this.processDeployFrequency; |
| } |
| |
| /** |
| * Set the watcher checks frequency. |
| * |
| * @param processExpiresFrequency the new manager checks frequency |
| */ |
| public void setProcessDeployFrequency(int processExpiresFrequency) { |
| |
| if (processExpiresFrequency <= 0) { |
| return; |
| } |
| this.processDeployFrequency = processExpiresFrequency; |
| } |
| |
| public int getMaxValidTime() { |
| return maxValidTime; |
| } |
| |
| public void setMaxValidTime(int maxValidTime) { |
| this.maxValidTime = maxValidTime; |
| } |
| |
| /** |
| * Copy a file to the specified temp directory. |
| * |
| * @param from copy from temp |
| * @param to to host appBase directory |
| * |
| * @return true, copy successful |
| */ |
| protected boolean copy(File from, File to) { |
| try { |
| if (!to.exists()) { |
| if (!to.createNewFile()) { |
| log.error(sm.getString("fileNewFail", to)); |
| return false; |
| } |
| } |
| } catch (IOException ioe) { |
| log.error(sm.getString("farmWarDeployer.fileCopyFail", from, to), ioe); |
| return false; |
| } |
| |
| try (java.io.FileInputStream is = new java.io.FileInputStream(from); |
| java.io.FileOutputStream os = new java.io.FileOutputStream(to, false)) { |
| byte[] buf = new byte[4096]; |
| while (true) { |
| int len = is.read(buf); |
| if (len < 0) { |
| break; |
| } |
| os.write(buf, 0, len); |
| } |
| } catch (IOException ioe) { |
| log.error(sm.getString("farmWarDeployer.fileCopyFail", from, to), ioe); |
| return false; |
| } |
| return true; |
| } |
| |
| protected void removeInvalidFileFactories() { |
| String[] fileNames = fileFactories.keySet().toArray(new String[0]); |
| for (String fileName : fileNames) { |
| FileMessageFactory factory = fileFactories.get(fileName); |
| if (!factory.isValid()) { |
| fileFactories.remove(fileName); |
| } |
| } |
| } |
| |
| private File getAbsolutePath(String path) { |
| File dir = new File(path); |
| if (!dir.isAbsolute()) { |
| dir = new File(getCluster().getContainer().getCatalinaBase(), dir.getPath()); |
| } |
| try { |
| dir = dir.getCanonicalFile(); |
| } catch (IOException ignore) { |
| // Ignore |
| } |
| return dir; |
| } |
| } |