package org.red5.server.tomcat; /* * RED5 Open Source Flash Server - http://www.osflash.org/red5 * * Copyright (c) 2006-2009 by respective authors (see below). All rights reserved. * * This library is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software * Foundation; either version 2.1 of the License, or (at your option) any later * version. * * This library is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along * with this library; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ import java.io.File; import java.io.FilenameFilter; import java.net.BindException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.management.MBeanServer; import javax.management.MBeanServerInvocationHandler; import javax.management.ObjectName; import javax.servlet.ServletContext; import org.apache.catalina.Container; import org.apache.catalina.Context; import org.apache.catalina.Engine; import org.apache.catalina.Host; import org.apache.catalina.Loader; import org.apache.catalina.Realm; import org.apache.catalina.Valve; import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardContext; import org.apache.catalina.core.StandardHost; import org.apache.catalina.loader.WebappLoader; import org.apache.catalina.realm.MemoryRealm; import org.apache.catalina.startup.Embedded; import org.red5.logging.Red5LoggerFactory; import org.red5.server.ContextLoader; import org.red5.server.ContextLoaderMBean; import org.red5.server.LoaderBase; import org.red5.server.LoaderMBean; import org.red5.server.api.IApplicationContext; import org.red5.server.jmx.JMXAgent; import org.red5.server.jmx.JMXFactory; import org.red5.server.util.FileUtil; import org.slf4j.Logger; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.XmlWebApplicationContext; /** * Red5 loader for Tomcat. * * @author Paul Gregoire (mondain@gmail.com) */ public class TomcatLoader extends LoaderBase implements ApplicationContextAware, LoaderMBean { /* * http://blog.springsource.com/2007/06/11/using-a-shared-parent-application-context-in-a-multi-war-spring-application/ */ /** * Filters directory content */ protected class DirectoryFilter implements FilenameFilter { /** * Check whether file matches filter rules * * @param dir Directory * @param name File name * @return true If file does match filter rules, false otherwise */ public boolean accept(File dir, String name) { File f = new File(dir, name); log.trace("Filtering: {} name: {}", dir.getName(), name); log.trace("Constructed dir: {}", f.getAbsolutePath()); // filter out all non-directories that are hidden and/or not // readable boolean result = f.isDirectory() && f.canRead() && !f.isHidden(); // nullify f = null; return result; } } // Initialize Logging private static Logger log = Red5LoggerFactory.getLogger(TomcatLoader.class); public static final String defaultSpringConfigLocation = "/WEB-INF/red5-*.xml"; public static final String defaultParentContextKey = "default.context"; static { log.debug("Initializing tomcat"); } /** * Base container host. */ protected Host host; /** * Tomcat connector. */ protected Connector connector; /** * Embedded Tomcat service (like Catalina). */ protected static Embedded embedded; /** * Tomcat engine. */ protected static Engine engine; /** * Tomcat realm. */ protected Realm realm; /** * Hosts */ protected List<Host> hosts; /** * Valves */ protected List<Valve> valves = new ArrayList<Valve>(); /** * Additional connection properties to be set at init. */ protected Map<String, String> connectionProperties = new HashMap<String, String>(); /** * Add context for path and docbase to current host. * * @param path Path * @param docBase Document base * @return Catalina context (that is, web application) */ public Context addContext(String path, String docBase) { log.debug("Add context - path: {} docbase: {}", path, docBase); org.apache.catalina.Context c = embedded.createContext(path, docBase); log.trace("Context name: {} docbase: {} encoded: {}", new Object[] { c.getName(), c.getDocBase(), c.getEncodedPath() }); if (c != null) { //ClassLoader classLoader = new ChildFirstClassLoader(new URL[]{}, Thread.currentThread().getContextClassLoader()); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); //log.debug("Classloaders - Parent {}\nTCL {}\n\n", new Object[] {classLoader.getParent(), classLoader}); c.setParentClassLoader(classLoader); // Object ldr = c.getLoader(); log.trace("Context loader (null if the context has not been started): {}", ldr); if (ldr == null) { WebappLoader wldr = new WebappLoader(classLoader); //add the Loader to the context c.setLoader(wldr); } } log.debug("Context loader (check): {} Context classloader: {}", c .getLoader(), c.getLoader().getClassLoader()); host.addChild(c); LoaderBase.setRed5ApplicationContext(path, new TomcatApplicationContext(c)); return c; } /** * Remove context from the current host. * * @param path Path */ @Override public void removeContext(String path) { Container[] children = host.findChildren(); for (Container c : children) { if (c instanceof StandardContext && c.getName().equals(path)) { try { ((StandardContext) c).stop(); host.removeChild(c); break; } catch (Exception e) { log.error("Could not remove context: {}", c.getName(), e); } } } IApplicationContext ctx = LoaderBase.removeRed5ApplicationContext(path); if (ctx != null) { ctx.stop(); } else { log.warn("Context could not be stopped, it was null for path: {}", path); } } /** * Get base host. * * @return Base host */ public Host getBaseHost() { return host; } /** * Return connector. * * @return Connector */ public Connector getConnector() { return connector; } /** * Getter for embedded object. * * @return Embedded object */ public Embedded getEmbedded() { return embedded; } /** * Return Tomcat engine. * * @return Tomcat engine */ public Engine getEngine() { return engine; } /** * Getter for realm. * * @return Realm */ public Realm getRealm() { return realm; } /** * Initialization. */ public void init() { log.info("Loading tomcat context"); //get a reference to the current threads classloader final ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); // root location for servlet container String serverRoot = System.getProperty("red5.root"); log.info("Server root: {}", serverRoot); String confRoot = System.getProperty("red5.config_root"); log.info("Config root: {}", confRoot); // create one embedded (server) and use it everywhere embedded = new Embedded(); embedded.createLoader(originalClassLoader); embedded.setCatalinaBase(serverRoot); embedded.setCatalinaHome(serverRoot); log.trace("Classloader for embedded: {} TCL: {}", Embedded.class.getClassLoader(), originalClassLoader); engine = embedded.createEngine(); engine.setDefaultHost(host.getName()); engine.setName("red5Engine"); if (webappFolder == null) { // Use default webapps directory webappFolder = FileUtil.formatPath(System.getProperty("red5.root"), "/webapps"); } System.setProperty("red5.webapp.root", webappFolder); log.info("Application root: {}", webappFolder); // scan for additional webapp contexts // Root applications directory File appDirBase = new File(webappFolder); // Subdirs of root apps dir File[] dirs = appDirBase.listFiles(new DirectoryFilter()); // Search for additional context files for (File dir : dirs) { String dirName = '/' + dir.getName(); // check to see if the directory is already mapped if (null == host.findChild(dirName)) { String webappContextDir = FileUtil.formatPath(appDirBase .getAbsolutePath(), dirName); log.debug("Webapp context directory (full path): {}", webappContextDir); Context ctx = null; if ("/root".equals(dirName) || "/root".equalsIgnoreCase(dirName)) { log.trace("Adding ROOT context"); ctx = addContext("/", webappContextDir); } else { log.trace("Adding context from directory scan: {}", dirName); ctx = addContext(dirName, webappContextDir); } log.trace("Context: {}", ctx); /* // put watches on context and web configs ctx.addWatchedResource("WEB-INF/web.xml"); ctx.addWatchedResource("META-INF/context.xml"); File contextConfig = new File(webappContextDir, "META-INF/context.xml"); if (contextConfig.exists()) { log.trace("Setting default context.xml"); ((StandardContext) ctx).setDefaultContextXml(contextConfig .getAbsolutePath()); } */ webappContextDir = null; } } appDirBase = null; dirs = null; // Dump context list if (log.isDebugEnabled()) { for (Container cont : host.findChildren()) { log.debug("Context child name: {}", cont.getName()); } } // Set a realm if (realm == null) { realm = new MemoryRealm(); } embedded.setRealm(realm); // use Tomcat jndi or not if (System.getProperty("catalina.useNaming") != null) { embedded.setUseNaming(Boolean.valueOf(System .getProperty("catalina.useNaming"))); } // add the valves to the host for (Valve valve : valves) { log.debug("Adding host valve: {}", valve); ((StandardHost) host).addValve(valve); } // baseHost = embedded.createHost(hostName, appRoot); engine.addChild(host); // add any additional hosts if (hosts != null && !hosts.isEmpty()) { log.info("Adding {} additional hosts", hosts.size()); for (Host h : hosts) { log.debug("Host - name: {} appBase: {} info: {}", new Object[] { h.getName(), h.getAppBase(), h.getInfo() }); engine.addChild(h); } } // Add new Engine to set of Engine for embedded server embedded.addEngine(engine); // set connection properties for (String key : connectionProperties.keySet()) { log.debug("Setting connection property: {} = {}", key, connectionProperties.get(key)); connector.setProperty(key, connectionProperties.get(key)); } // Start server try { // Add new Connector to set of Connectors for embedded server, // associated with Engine embedded.addConnector(connector); log.info("Starting Tomcat servlet engine"); embedded.start(); LoaderBase.setApplicationLoader(new TomcatApplicationLoader( embedded, host, applicationContext)); for (Container cont : host.findChildren()) { if (cont instanceof StandardContext) { StandardContext ctx = (StandardContext) cont; final ServletContext servletContext = ctx .getServletContext(); log.debug("Context initialized: {}", servletContext .getContextPath()); String prefix = servletContext.getRealPath("/"); log.debug("Path: {}", prefix); try { if (ctx.resourcesStart()) { log.debug("Resources started"); } log.debug("Context - available: {} privileged: {}, start time: {}, reloadable: {}", new Object[] { ctx.getAvailable(), ctx.getPrivileged(), ctx.getStartTime(), ctx.getReloadable()}); Loader cldr = ctx.getLoader(); log.debug("Loader delegate: {} type: {}", cldr.getDelegate(), cldr.getClass().getName()); if (cldr instanceof WebappLoader) { log.debug("WebappLoader class path: {}", ((WebappLoader) cldr).getClasspath()); } final ClassLoader webClassLoader = cldr.getClassLoader(); log.debug("Webapp classloader: {}", webClassLoader); if (log.isTraceEnabled()) { ClassLoader currentThreadCL = Thread.currentThread().getContextClassLoader(); if (currentThreadCL == null) { log .trace( "Classloaders:\nWebappParentParent {}\nWebappParent {}\nWebapp {}\n\n", new Object[] { webClassLoader .getParent() .getParent(), webClassLoader .getParent(), webClassLoader }); } else if (webClassLoader == null) { log .trace( "Classloaders:\nParent {}\nThread {}\n\n", currentThreadCL.getParent(), currentThreadCL); } else { log .trace( "Classloaders:\nParent {}\nThread {}\nWebappParentParent {}\nWebappParent {}\nWebapp {}\n\n", new Object[] { currentThreadCL .getParent(), currentThreadCL, webClassLoader .getParent() .getParent(), webClassLoader .getParent(), webClassLoader }); } } // get the (spring) config file path final String contextConfigLocation = servletContext .getInitParameter("contextConfigLocation") == null ? defaultSpringConfigLocation : servletContext.getInitParameter("contextConfigLocation"); log.debug("Spring context config location: {}", contextConfigLocation); // get the (spring) parent context key final String parentContextKey = servletContext .getInitParameter("parentContextKey") == null ? defaultParentContextKey : servletContext.getInitParameter("parentContextKey"); log.debug("Spring parent context key: {}", parentContextKey); //set current threads classloader to the webapp parent classloader ClassLoader webappParentClassLoader = webClassLoader.getParent(); Thread.currentThread().setContextClassLoader(webappParentClassLoader); //create a thread to speed-up application loading Thread thread = new Thread("Launcher:" + servletContext .getContextPath()) { public void run() { // create a spring web application context XmlWebApplicationContext appctx = new XmlWebApplicationContext(); appctx.setClassLoader(webClassLoader); appctx.setConfigLocations(new String[] { contextConfigLocation }); appctx.setParent((ApplicationContext) applicationContext .getBean(parentContextKey)); appctx.setServletContext(servletContext); // set the root webapp ctx attr on the each // servlet context so spring can find it later servletContext.setAttribute( WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, appctx); appctx.refresh(); } }; thread.setDaemon(true); thread.start(); } catch (Throwable t) { log.error("Error setting up context: {} due to: {}", servletContext.getContextPath(), t.getMessage()); t.printStackTrace(); } finally { //reset the classloader Thread.currentThread().setContextClassLoader(originalClassLoader); } } } // if everything is ok at this point then call the rtmpt and rtmps // beans so they will init if (applicationContext.containsBean("red5.core")) { ApplicationContext core = (ApplicationContext) applicationContext .getBean("red5.core"); if (core.containsBean("rtmpt.server")) { log.debug("Initializing RTMPT"); core.getBean("rtmpt.server"); log.debug("Finished initializing RTMPT"); } else { log.info("RTMPT server bean was not found"); } if (core.containsBean("rtmps.server")) { log.debug("Initializing RTMPS"); core.getBean("rtmps.server"); log.debug("Finished initializing RTMPS"); } else { log.info("RTMPS server bean was not found"); } } else { log.info("Core context was not found"); } } catch (Exception e) { if (e instanceof BindException || e.getMessage().indexOf("BindException") != -1) { log .error( "Error loading tomcat, unable to bind connector. You may not have permission to use the selected port", e); } else { log.error("Error loading tomcat", e); } } finally { registerJMX(); } } /** * Starts a web application and its red5 (spring) component. This is * basically a stripped down version of init(). * * @return true on success */ public boolean startWebApplication(String applicationName) { log.info("Starting Tomcat - Web application"); boolean result = false; //get a reference to the current threads classloader final ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); log.debug("Webapp root: {}", webappFolder); // application directory String contextName = '/' + applicationName; Container ctx = null; if (webappFolder == null) { // Use default webapps directory webappFolder = System.getProperty("red5.root") + "/webapps"; } System.setProperty("red5.webapp.root", webappFolder); log.info("Application root: {}", webappFolder); // scan for additional webapp contexts // Root applications directory File appDirBase = new File(webappFolder); // check if the context already exists for the host if ((ctx = host.findChild(contextName)) == null) { log.debug("Context did not exist in host"); String webappContextDir = FileUtil.formatPath(appDirBase .getAbsolutePath(), applicationName); log.debug("Webapp context directory (full path): {}", webappContextDir); // set the newly created context as the current container ctx = addContext(contextName, webappContextDir); } else { log.debug("Context already exists in host"); } final ServletContext servletContext = ((Context) ctx).getServletContext(); log.debug("Context initialized: {}", servletContext.getContextPath()); String prefix = servletContext.getRealPath("/"); log.debug("Path: {}", prefix); try { Loader cldr = ctx.getLoader(); log.debug("Loader delegate: {} type: {}", cldr.getDelegate(), cldr.getClass().getName()); if (cldr instanceof WebappLoader) { log.debug("WebappLoader class path: {}", ((WebappLoader) cldr).getClasspath()); } final ClassLoader webClassLoader = cldr.getClassLoader(); log.debug("Webapp classloader: {}", webClassLoader); // get the (spring) config file path final String contextConfigLocation = servletContext .getInitParameter("contextConfigLocation") == null ? defaultSpringConfigLocation : servletContext.getInitParameter("contextConfigLocation"); log.debug("Spring context config location: {}", contextConfigLocation); // get the (spring) parent context key final String parentContextKey = servletContext .getInitParameter("parentContextKey") == null ? defaultParentContextKey : servletContext.getInitParameter("parentContextKey"); log.debug("Spring parent context key: {}", parentContextKey); //set current threads classloader to the webapp parent classloader ClassLoader webappParentClassLoader = webClassLoader.getParent(); Thread.currentThread().setContextClassLoader(webappParentClassLoader); //create a thread to speed-up application loading Thread thread = new Thread("Launcher:" + servletContext .getContextPath()) { public void run() { // create a spring web application context XmlWebApplicationContext appctx = new XmlWebApplicationContext(); appctx.setClassLoader(webClassLoader); appctx.setConfigLocations(new String[] { contextConfigLocation }); // check for red5 context bean ApplicationContext parentAppCtx = null; if (applicationContext.containsBean(defaultParentContextKey)) { parentAppCtx = (ApplicationContext) applicationContext.getBean(defaultParentContextKey); } else { log.warn("{} bean was not found in context: {}", defaultParentContextKey, applicationContext .getDisplayName()); // lookup context loader and attempt to get what we need from it if (applicationContext.containsBean("context.loader")) { ContextLoader contextLoader = (ContextLoader) applicationContext .getBean("context.loader"); parentAppCtx = contextLoader.getContext(defaultParentContextKey); } else { log.debug("Context loader was not found, trying JMX"); MBeanServer mbs = JMXFactory.getMBeanServer(); // get the ContextLoader from jmx ObjectName oName = JMXFactory.createObjectName("type", "ContextLoader"); ContextLoaderMBean proxy = null; if (mbs.isRegistered(oName)) { proxy = (ContextLoaderMBean) MBeanServerInvocationHandler .newProxyInstance(mbs, oName, ContextLoaderMBean.class, true); log.debug("Context loader was found"); parentAppCtx = proxy.getContext(defaultParentContextKey); } else { log.warn("Context loader was not found"); } } } if (log.isDebugEnabled()) { if (appctx.getParent() != null) { log.debug("Parent application context: {}", appctx .getParent().getDisplayName()); } } appctx.setParent(parentAppCtx); appctx.setServletContext(servletContext); // set the root webapp ctx attr on the each // servlet context so spring can find it later servletContext.setAttribute( WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, appctx); appctx.refresh(); } }; thread.setDaemon(true); thread.start(); result = true; } catch (Throwable t) { log.error("Error setting up context: {} due to: {}", servletContext.getContextPath(), t.getMessage()); t.printStackTrace(); } finally { //reset the classloader Thread.currentThread().setContextClassLoader(originalClassLoader); } return result; } /** * Set base host. * * @param baseHost Base host */ public void setBaseHost(Host baseHost) { log.debug("setBaseHost"); this.host = baseHost; } /** * Set connector. * * @param connector Connector */ public void setConnector(Connector connector) { log.info("Setting connector: " + connector.getClass().getName()); this.connector = connector; } /** * Set additional connectors. * * @param connectors Additional connectors */ public void setConnectors(List<Connector> connectors) { log.debug("setConnectors: {}", connectors.size()); for (Connector ctr : connectors) { embedded.addConnector(ctr); } } /** * Set additional contexts. * * @param contexts Map of contexts */ public void setContexts(Map<String, String> contexts) { log.debug("setContexts: {}", contexts.size()); for (String key : contexts.keySet()) { host.addChild(embedded.createContext(key, webappFolder + contexts.get(key))); } } /** * Setter for embedded object. * * @param embedded Embedded object */ public void setEmbedded(Embedded embedded) { log.info("Setting embedded: {}", embedded.getClass().getName()); TomcatLoader.embedded = embedded; } /** * Get the host. * * @return host */ public Host getHost() { return host; } /** * Set the host. * * @param host host */ public void setHost(Host host) { log.debug("setHost"); this.host = host; } /** * Set additional hosts. * * @param hosts List of hosts added to engine */ public void setHosts(List<Host> hosts) { log.debug("setHosts: {}", hosts.size()); this.hosts = hosts; } /** * Setter for realm. * * @param realm Realm */ public void setRealm(Realm realm) { log.info("Setting realm: {}", realm.getClass().getName()); this.realm = realm; } /** * Set additional valves. * * @param valves List of valves */ public void setValves(List<Valve> valves) { log.debug("setValves: {}", valves.size()); this.valves.addAll(valves); } /** * Set connection properties for the connector * * @param props additional properties to set */ public void setConnectionProperties(Map<String, String> props) { log.debug("Connection props: {}", props.size()); this.connectionProperties.putAll(props); } public void registerJMX() { JMXAgent.registerMBean(this, this.getClass().getName(), LoaderMBean.class); } /** * Shut server down. */ public void shutdown() { log.info("Shutting down Tomcat context"); JMXAgent.shutdown(); try { embedded.stop(); System.exit(0); } catch (Exception e) { log.warn("Tomcat could not be stopped", e); System.exit(1); } } }