1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.tools
.development
;
5 import static com
.google
.appengine
.tools
.development
.LocalEnvironment
.DEFAULT_VERSION_HOSTNAME
;
7 import com
.google
.appengine
.api
.log
.dev
.DevLogHandler
;
8 import com
.google
.appengine
.api
.log
.dev
.LocalLogService
;
9 import com
.google
.apphosting
.api
.ApiProxy
;
10 import com
.google
.apphosting
.utils
.config
.AppEngineConfigException
;
11 import com
.google
.apphosting
.utils
.config
.AppEngineWebXml
;
12 import com
.google
.apphosting
.utils
.config
.WebModule
;
13 import com
.google
.apphosting
.utils
.jetty
.JettyLogger
;
14 import com
.google
.apphosting
.utils
.jetty
.StubSessionManager
;
16 import org
.mortbay
.jetty
.Server
;
17 import org
.mortbay
.jetty
.handler
.HandlerWrapper
;
18 import org
.mortbay
.jetty
.nio
.SelectChannelConnector
;
19 import org
.mortbay
.jetty
.servlet
.SessionHandler
;
20 import org
.mortbay
.jetty
.webapp
.Configuration
;
21 import org
.mortbay
.jetty
.webapp
.JettyWebXmlConfiguration
;
22 import org
.mortbay
.jetty
.webapp
.WebAppContext
;
23 import org
.mortbay
.resource
.Resource
;
24 import org
.mortbay
.util
.Scanner
;
27 import java
.io
.FilenameFilter
;
28 import java
.io
.IOException
;
30 import java
.security
.Permissions
;
31 import java
.util
.Date
;
32 import java
.util
.concurrent
.Semaphore
;
33 import java
.util
.logging
.Level
;
34 import java
.util
.logging
.Logger
;
36 import javax
.servlet
.ServletException
;
37 import javax
.servlet
.http
.HttpServletRequest
;
38 import javax
.servlet
.http
.HttpServletResponse
;
39 import javax
.servlet
.http
.HttpServletResponseWrapper
;
42 * Implements a Jetty backed {@link ContainerService}.
45 @ServiceProvider(ContainerService
.class)
46 public class JettyContainerService
extends AbstractContainerService
{
48 private static final Logger log
= Logger
.getLogger(JettyContainerService
.class.getName());
50 public final static String WEB_DEFAULTS_XML
=
51 "com/google/appengine/tools/development/webdefault.xml";
53 private static final int MAX_SIMULTANEOUS_API_CALLS
= 100;
55 private static final Long SOFT_DEADLINE_DELAY_MS
= 60000L;
58 * Specify which {@link Configuration} objects should be invoked when
59 * configuring a web application.
61 * <p>This is a subset of:
62 * org.mortbay.jetty.webapp.WebAppContext.__dftConfigurationClasses
64 * <p>Specifically, we've removed {@link JettyWebXmlConfiguration} which
65 * allows users to use {@code jetty-web.xml} files.
67 private static final String CONFIG_CLASSES
[] = new String
[] {
68 "org.mortbay.jetty.webapp.WebXmlConfiguration",
69 "org.mortbay.jetty.webapp.TagLibConfiguration"
72 private static final String WEB_XML_ATTR
=
73 "com.google.appengine.tools.development.webXml";
74 private static final String APPENGINE_WEB_XML_ATTR
=
75 "com.google.appengine.tools.development.appEngineWebXml";
78 System
.setProperty("org.mortbay.log.class", JettyLogger
.class.getName());
81 private final static int SCAN_INTERVAL_SECONDS
= 5;
84 * Jetty webapp context.
86 private WebAppContext context
;
91 private AppContext appContext
;
96 private Server server
;
99 * Hot deployment support.
101 private Scanner scanner
;
103 private class JettyAppContext
implements AppContext
{
105 public IsolatedAppClassLoader
getClassLoader() {
106 return (IsolatedAppClassLoader
) context
.getClassLoader();
110 public Permissions
getUserPermissions() {
111 return JettyContainerService
.this.getUserPermissions();
115 public Permissions
getApplicationPermissions() {
116 return getClassLoader().getAppPermissions();
120 public Object
getContainerContext() {
125 public JettyContainerService() {
129 protected File
initContext() throws IOException
{
130 this.context
= new DevAppEngineWebAppContext(appDir
, externalResourceDir
, devAppServerVersion
,
132 this.appContext
= new JettyAppContext();
134 context
.setDescriptor(webXmlLocation
== null ?
null : webXmlLocation
.getAbsolutePath());
136 context
.setDefaultsDescriptor(WEB_DEFAULTS_XML
);
138 context
.setConfigurationClasses(CONFIG_CLASSES
);
140 File appRoot
= determineAppRoot();
141 installLocalInitializationEnvironment();
143 URL
[] classPath
= getClassPathForApp(appRoot
);
144 context
.setClassLoader(new IsolatedAppClassLoader(appRoot
, externalResourceDir
, classPath
,
145 JettyContainerService
.class.getClassLoader()));
151 protected void connectContainer() throws Exception
{
152 serverConfigurationHandle
.checkEnvironmentVariables();
154 Thread currentThread
= Thread
.currentThread();
155 ClassLoader previousCcl
= currentThread
.getContextClassLoader();
156 currentThread
.setContextClassLoader(null);
159 SelectChannelConnector connector
= new SelectChannelConnector();
160 connector
.setHost(address
);
161 connector
.setPort(port
);
162 connector
.setSoLingerTime(0);
165 server
= new Server();
166 server
.addConnector(connector
);
168 port
= connector
.getLocalPort();
170 currentThread
.setContextClassLoader(previousCcl
);
175 protected void startContainer() throws Exception
{
176 context
.setAttribute(WEB_XML_ATTR
, webXml
);
177 context
.setAttribute(APPENGINE_WEB_XML_ATTR
, appEngineWebXml
);
179 Thread currentThread
= Thread
.currentThread();
180 ClassLoader previousCcl
= currentThread
.getContextClassLoader();
181 currentThread
.setContextClassLoader(null);
184 ApiProxyHandler apiHandler
= new ApiProxyHandler(appEngineWebXml
);
185 apiHandler
.setHandler(context
);
187 server
.setHandler(apiHandler
);
188 SessionHandler handler
= context
.getSessionHandler();
189 if (isSessionsEnabled()) {
190 handler
.setSessionManager(new SerializableObjectsOnlyHashSessionManager());
192 handler
.setSessionManager(new StubSessionManager());
196 currentThread
.setContextClassLoader(previousCcl
);
201 protected void stopContainer() throws Exception
{
206 * Unlike the actual Jetty hot deployment support, we monitor the webapp war file or the
207 * appengine-web.xml in case of a pre-exploded webapp directory, and reload the webapp whenever an
208 * update is detected, i.e. a newer timestamp for the monitored file. As a single-context
209 * deployment, add/delete is not applicable here.
211 * appengine-web.xml will be reloaded too. However, changes that require a server restart, e.g.
212 * address/port, will not be part of the reload.
215 protected void startHotDeployScanner() throws Exception
{
216 scanner
= new Scanner();
217 scanner
.setScanInterval(SCAN_INTERVAL_SECONDS
);
218 scanner
.setScanDir(getScanTarget());
219 scanner
.setFilenameFilter(new FilenameFilter() {
221 public boolean accept(File dir
, String name
) {
223 if (name
.equals(getScanTarget().getName())) {
228 catch (Exception e
) {
234 scanner
.addListener(new ScannerListener());
239 protected void stopHotDeployScanner() throws Exception
{
240 if (scanner
!= null) {
246 private class ScannerListener
implements Scanner
.DiscreteListener
{
248 public void fileAdded(String filename
) throws Exception
{
249 fileChanged(filename
);
253 public void fileChanged(String filename
) throws Exception
{
254 log
.info(filename
+ " updated, reloading the webapp!");
259 public void fileRemoved(String filename
) throws Exception
{
264 * To minimize the overhead, we point the scanner right to the single file in question.
266 private File
getScanTarget() throws Exception
{
267 if (appDir
.isFile() || context
.getWebInf() == null) {
270 return new File(context
.getWebInf().getFile().getPath()
271 + File
.separator
+ "appengine-web.xml");
276 * Assuming Jetty handles race condition nicely, as this is how Jetty handles a hot deploy too.
279 protected void reloadWebApp() throws Exception
{
280 server
.getHandler().stop();
281 serverConfigurationHandle
.restoreSystemProperties();
282 serverConfigurationHandle
.readConfiguration();
283 serverConfigurationHandle
.checkEnvironmentVariables();
284 extractFieldsFromWebModule(serverConfigurationHandle
.getModule());
286 /** same as what's in startContainer, we need suppress the ContextClassLoader here. */
287 Thread currentThread
= Thread
.currentThread();
288 ClassLoader previousCcl
= currentThread
.getContextClassLoader();
289 currentThread
.setContextClassLoader(null);
291 File webAppDir
= initContext();
292 installLoggingServiceHandler();
293 installLocalInitializationEnvironment();
295 if (!isSessionsEnabled()) {
296 context
.getSessionHandler().setSessionManager(new StubSessionManager());
298 context
.setAttribute(WEB_XML_ATTR
, webXml
);
299 context
.setAttribute(APPENGINE_WEB_XML_ATTR
, appEngineWebXml
);
301 ApiProxyHandler apiHandler
= new ApiProxyHandler(appEngineWebXml
);
302 apiHandler
.setHandler(context
);
303 server
.setHandler(apiHandler
);
307 currentThread
.setContextClassLoader(previousCcl
);
312 public AppContext
getAppContext() {
316 private File
determineAppRoot() throws IOException
{
317 Resource webInf
= context
.getWebInf();
318 if (webInf
== null) {
319 if (userCodeClasspathManager
.requiresWebInf()) {
320 throw new AppEngineConfigException(
321 "Supplied application has to contain WEB-INF directory.");
325 return webInf
.getFile().getParentFile();
329 * {@code ApiProxyHandler} wraps around an existing {@link org.mortbay.jetty.Handler}
330 * and surrounds each top-level request (i.e. not includes or
331 * forwards) with a try finally block that maintains the {@link
332 * com.google.apphosting.api.ApiProxy.Environment} {@link ThreadLocal}.
334 private class ApiProxyHandler
extends HandlerWrapper
{
335 private final AppEngineWebXml appEngineWebXml
;
337 public ApiProxyHandler(AppEngineWebXml appEngineWebXml
) {
338 this.appEngineWebXml
= appEngineWebXml
;
341 @SuppressWarnings("unchecked")
343 public void handle(String target
,
344 HttpServletRequest request
,
345 HttpServletResponse response
,
346 int dispatch
) throws IOException
, ServletException
{
347 if (dispatch
== REQUEST
) {
348 long startTimeUsec
= System
.currentTimeMillis() * 1000;
349 Semaphore semaphore
= new Semaphore(MAX_SIMULTANEOUS_API_CALLS
);
351 LocalEnvironment env
= new LocalHttpRequestEnvironment(appEngineWebXml
.getAppId(),
352 WebModule
.getServerName(appEngineWebXml
), appEngineWebXml
.getMajorVersionId(),
353 instance
, request
, SOFT_DEADLINE_DELAY_MS
);
354 env
.getAttributes().put(LocalEnvironment
.API_CALL_SEMAPHORE
, semaphore
);
355 env
.getAttributes().put(DEFAULT_VERSION_HOSTNAME
, "localhost:" + devAppServer
.getPort());
357 ApiProxy
.setEnvironmentForCurrentThread(env
);
359 RecordingResponseWrapper wrappedResponse
= new RecordingResponseWrapper(response
);
361 super.handle(target
, request
, wrappedResponse
, dispatch
);
362 if (request
.getRequestURI().startsWith(_AH_URL_RELOAD
)) {
365 log
.info("Reloaded the webapp context: " + request
.getParameter("info"));
366 } catch (Exception ex
) {
367 log
.log(Level
.WARNING
, "Failed to reload the current webapp context.", ex
);
372 semaphore
.acquire(MAX_SIMULTANEOUS_API_CALLS
);
373 } catch (InterruptedException ex
) {
374 log
.log(Level
.WARNING
, "Interrupted while waiting for API calls to complete:", ex
);
376 env
.callRequestEndListeners();
379 String appId
= env
.getAppId();
380 String versionId
= env
.getVersionId();
381 String requestId
= DevLogHandler
.getRequestId();
382 long endTimeUsec
= new Date().getTime() * 1000;
384 LocalLogService logService
= (LocalLogService
)
385 apiProxyLocal
.getService(LocalLogService
.PACKAGE
);
387 logService
.addRequestInfo(appId
, versionId
, requestId
,
388 request
.getRemoteAddr(), request
.getRemoteUser(),
389 startTimeUsec
, endTimeUsec
, request
.getMethod(),
390 request
.getRequestURI(), request
.getProtocol(),
391 request
.getHeader("User-Agent"), true,
392 wrappedResponse
.getStatus(), request
.getHeader("Referrer"));
393 logService
.clearResponseSize();
395 ApiProxy
.clearEnvironmentForCurrentThread();
399 super.handle(target
, request
, response
, dispatch
);
404 private class RecordingResponseWrapper
extends HttpServletResponseWrapper
{
405 private int status
= SC_OK
;
407 RecordingResponseWrapper(HttpServletResponse response
) {
412 public void setStatus(int sc
) {
417 public int getStatus() {
422 public void sendError(int sc
) throws IOException
{
428 public void sendError(int sc
, String msg
) throws IOException
{
430 super.sendError(sc
, msg
);
434 public void sendRedirect(String location
) throws IOException
{
435 status
= SC_MOVED_TEMPORARILY
;
436 super.sendRedirect(location
);
440 public void setStatus(int status
, String string
) {
441 super.setStatus(status
, string
);
442 this.status
= status
;
446 public void reset() {