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
;
15 import com
.google
.common
.collect
.ImmutableList
;
16 import com
.google
.common
.net
.HttpHeaders
;
18 import org
.mortbay
.jetty
.Server
;
19 import org
.mortbay
.jetty
.handler
.HandlerWrapper
;
20 import org
.mortbay
.jetty
.nio
.SelectChannelConnector
;
21 import org
.mortbay
.jetty
.servlet
.ServletHolder
;
22 import org
.mortbay
.jetty
.servlet
.SessionHandler
;
23 import org
.mortbay
.jetty
.webapp
.Configuration
;
24 import org
.mortbay
.jetty
.webapp
.JettyWebXmlConfiguration
;
25 import org
.mortbay
.jetty
.webapp
.WebAppContext
;
26 import org
.mortbay
.resource
.Resource
;
27 import org
.mortbay
.util
.Scanner
;
30 import java
.io
.FilenameFilter
;
31 import java
.io
.IOException
;
33 import java
.security
.Permissions
;
34 import java
.util
.ArrayList
;
35 import java
.util
.Collections
;
36 import java
.util
.Date
;
37 import java
.util
.List
;
38 import java
.util
.concurrent
.Semaphore
;
39 import java
.util
.logging
.Level
;
40 import java
.util
.logging
.Logger
;
42 import javax
.servlet
.RequestDispatcher
;
43 import javax
.servlet
.ServletException
;
44 import javax
.servlet
.http
.HttpServlet
;
45 import javax
.servlet
.http
.HttpServletRequest
;
46 import javax
.servlet
.http
.HttpServletResponse
;
47 import javax
.servlet
.http
.HttpServletResponseWrapper
;
50 * Implements a Jetty backed {@link ContainerService}.
53 @ServiceProvider(ContainerService
.class)
54 public class JettyContainerService
extends AbstractContainerService
{
56 private static final Logger log
= Logger
.getLogger(JettyContainerService
.class.getName());
58 private static final String FILES_API_DEPRECATION_WARNING
=
59 "The Google Cloud Storage Java API is deprecated and will soon be removed. Please use the"
60 + " Google Cloud Storage Client library instead. Migration documentation is available here:"
61 + " https://cloud.google.com/appengine/docs/java/googlecloudstorageclient/migrate";
63 public final static String WEB_DEFAULTS_XML
=
64 "com/google/appengine/tools/development/webdefault.xml";
66 private static final int MAX_SIMULTANEOUS_API_CALLS
= 100;
68 private static final Long SOFT_DEADLINE_DELAY_MS
= 60000L;
71 * Specify which {@link Configuration} objects should be invoked when
72 * configuring a web application.
74 * <p>This is a subset of:
75 * org.mortbay.jetty.webapp.WebAppContext.__dftConfigurationClasses
77 * <p>Specifically, we've removed {@link JettyWebXmlConfiguration} which
78 * allows users to use {@code jetty-web.xml} files.
80 private static final String CONFIG_CLASSES
[] = new String
[] {
81 "org.mortbay.jetty.webapp.WebXmlConfiguration",
82 "org.mortbay.jetty.webapp.TagLibConfiguration"
85 private static final String WEB_XML_ATTR
=
86 "com.google.appengine.tools.development.webXml";
87 private static final String APPENGINE_WEB_XML_ATTR
=
88 "com.google.appengine.tools.development.appEngineWebXml";
90 private boolean disableFilesApiWarning
= false;
93 System
.setProperty("org.mortbay.log.class", JettyLogger
.class.getName());
96 private final static int SCAN_INTERVAL_SECONDS
= 5;
99 * Jetty webapp context.
101 private WebAppContext context
;
104 * Our webapp context.
106 private AppContext appContext
;
111 private Server server
;
114 * Hot deployment support.
116 private Scanner scanner
;
118 private class JettyAppContext
implements AppContext
{
120 public IsolatedAppClassLoader
getClassLoader() {
121 return (IsolatedAppClassLoader
) context
.getClassLoader();
125 public Permissions
getUserPermissions() {
126 return JettyContainerService
.this.getUserPermissions();
130 public Permissions
getApplicationPermissions() {
131 return getClassLoader().getAppPermissions();
135 public Object
getContainerContext() {
140 public JettyContainerService() {
144 protected File
initContext() throws IOException
{
145 this.context
= new DevAppEngineWebAppContext(appDir
, externalResourceDir
, devAppServerVersion
,
146 apiProxyDelegate
, devAppServer
);
147 this.appContext
= new JettyAppContext();
149 context
.setDescriptor(webXmlLocation
== null ?
null : webXmlLocation
.getAbsolutePath());
151 String webDefaultXml
= devAppServer
.getServiceProperties().get("appengine.webdefault.xml");
152 if (webDefaultXml
== null) {
153 webDefaultXml
= WEB_DEFAULTS_XML
;
155 context
.setDefaultsDescriptor(webDefaultXml
);
157 context
.setConfigurationClasses(CONFIG_CLASSES
);
159 File appRoot
= determineAppRoot();
160 installLocalInitializationEnvironment();
162 URL
[] classPath
= getClassPathForApp(appRoot
);
163 context
.setClassLoader(new IsolatedAppClassLoader(appRoot
, externalResourceDir
, classPath
,
164 JettyContainerService
.class.getClassLoader()));
165 if (Boolean
.parseBoolean(System
.getProperty("appengine.allowRemoteShutdown"))) {
166 context
.addServlet(new ServletHolder(new ServerShutdownServlet()), "/_ah/admin/quit");
169 if (Boolean
.parseBoolean(System
.getProperty("appengine.disableFilesApiWarning"))) {
170 disableFilesApiWarning
= true;
176 static class ServerShutdownServlet
extends HttpServlet
{
178 protected void doPost(HttpServletRequest req
, HttpServletResponse resp
) throws IOException
{
179 resp
.getWriter().println("Shutting down local server.");
181 DevAppServer server
= (DevAppServer
) getServletContext().getAttribute(
182 "com.google.appengine.devappserver.Server");
183 server
.gracefulShutdown();
188 protected void connectContainer() throws Exception
{
189 moduleConfigurationHandle
.checkEnvironmentVariables();
191 Thread currentThread
= Thread
.currentThread();
192 ClassLoader previousCcl
= currentThread
.getContextClassLoader();
193 currentThread
.setContextClassLoader(null);
196 SelectChannelConnector connector
= new SelectChannelConnector();
197 connector
.setHost(address
);
198 connector
.setPort(port
);
199 connector
.setSoLingerTime(0);
202 server
= new Server();
203 server
.addConnector(connector
);
205 port
= connector
.getLocalPort();
207 currentThread
.setContextClassLoader(previousCcl
);
212 protected void startContainer() throws Exception
{
213 context
.setAttribute(WEB_XML_ATTR
, webXml
);
214 context
.setAttribute(APPENGINE_WEB_XML_ATTR
, appEngineWebXml
);
216 Thread currentThread
= Thread
.currentThread();
217 ClassLoader previousCcl
= currentThread
.getContextClassLoader();
218 currentThread
.setContextClassLoader(null);
221 ApiProxyHandler apiHandler
= new ApiProxyHandler(appEngineWebXml
);
222 apiHandler
.setHandler(context
);
224 server
.setHandler(apiHandler
);
225 SessionHandler handler
= context
.getSessionHandler();
226 if (isSessionsEnabled()) {
227 handler
.setSessionManager(new SerializableObjectsOnlyHashSessionManager());
229 handler
.setSessionManager(new StubSessionManager());
233 currentThread
.setContextClassLoader(previousCcl
);
238 protected void stopContainer() throws Exception
{
243 * If the property "appengine.fullscan.seconds" is set to a positive integer, the web app
244 * content (deployment descriptors, classes/ and lib/) is scanned for changes that will trigger
245 * the reloading of the application.
246 * If the property is not set (default), we monitor the webapp war file or the
247 * appengine-web.xml in case of a pre-exploded webapp directory, and reload the webapp whenever an
248 * update is detected, i.e. a newer timestamp for the monitored file. As a single-context
249 * deployment, add/delete is not applicable here.
251 * appengine-web.xml will be reloaded too. However, changes that require a module instance
252 * restart, e.g. address/port, will not be part of the reload.
255 protected void startHotDeployScanner() throws Exception
{
256 String fullScanInterval
= System
.getProperty("appengine.fullscan.seconds");
257 if (fullScanInterval
!= null) {
259 int interval
= Integer
.parseInt(fullScanInterval
);
261 log
.info("Full scan of the web app for changes is disabled.");
264 log
.info("Full scan of the web app in place every " + interval
+ "s.");
265 fullWebAppScanner(interval
);
267 } catch (NumberFormatException ex
) {
268 log
.log(Level
.WARNING
, "appengine.fullscan.seconds property is not an integer:", ex
);
269 log
.log(Level
.WARNING
, "Using the default scanning method.");
272 scanner
= new Scanner();
273 scanner
.setScanInterval(SCAN_INTERVAL_SECONDS
);
274 scanner
.setScanDirs(ImmutableList
.of(getScanTarget()));
275 scanner
.setFilenameFilter(new FilenameFilter() {
277 public boolean accept(File dir
, String name
) {
279 if (name
.equals(getScanTarget().getName())) {
284 catch (Exception e
) {
290 scanner
.addListener(new ScannerListener());
295 protected void stopHotDeployScanner() throws Exception
{
296 if (scanner
!= null) {
302 private class ScannerListener
implements Scanner
.DiscreteListener
{
304 public void fileAdded(String filename
) throws Exception
{
305 fileChanged(filename
);
309 public void fileChanged(String filename
) throws Exception
{
310 log
.info(filename
+ " updated, reloading the webapp!");
315 public void fileRemoved(String filename
) throws Exception
{
320 * To minimize the overhead, we point the scanner right to the single file in question.
322 private File
getScanTarget() throws Exception
{
323 if (appDir
.isFile() || context
.getWebInf() == null) {
326 return new File(context
.getWebInf().getFile().getPath()
327 + File
.separator
+ "appengine-web.xml");
331 private void fullWebAppScanner(int interval
) throws IOException
{
332 String webInf
= context
.getWebInf().getFile().getPath();
333 List
<File
> scanList
= new ArrayList
<File
>();
334 Collections
.addAll(scanList
,
335 new File(webInf
, "classes"),
336 new File(webInf
, "lib"),
337 new File(webInf
, "web.xml"),
338 new File(webInf
, "appengine-web.xml"));
340 scanner
= new Scanner();
341 scanner
.setScanInterval(interval
);
342 scanner
.setScanDirs(scanList
);
343 scanner
.setReportExistingFilesOnStartup(false);
344 scanner
.setRecursive(true);
347 scanner
.addListener(new Scanner
.BulkListener() {
349 public void filesChanged(List changedFiles
) throws Exception
{
350 log
.info("A file has changed, reloading the web application.");
359 * Assuming Jetty handles race condition nicely, as this is how Jetty handles a hot deploy too.
362 protected void reloadWebApp() throws Exception
{
363 server
.getHandler().stop();
364 moduleConfigurationHandle
.restoreSystemProperties();
365 moduleConfigurationHandle
.readConfiguration();
366 moduleConfigurationHandle
.checkEnvironmentVariables();
367 extractFieldsFromWebModule(moduleConfigurationHandle
.getModule());
369 /** same as what's in startContainer, we need suppress the ContextClassLoader here. */
370 Thread currentThread
= Thread
.currentThread();
371 ClassLoader previousCcl
= currentThread
.getContextClassLoader();
372 currentThread
.setContextClassLoader(null);
375 installLocalInitializationEnvironment();
377 if (!isSessionsEnabled()) {
378 context
.getSessionHandler().setSessionManager(new StubSessionManager());
380 context
.setAttribute(WEB_XML_ATTR
, webXml
);
381 context
.setAttribute(APPENGINE_WEB_XML_ATTR
, appEngineWebXml
);
383 ApiProxyHandler apiHandler
= new ApiProxyHandler(appEngineWebXml
);
384 apiHandler
.setHandler(context
);
385 server
.setHandler(apiHandler
);
389 currentThread
.setContextClassLoader(previousCcl
);
394 public AppContext
getAppContext() {
399 public void forwardToServer(HttpServletRequest hrequest
,
400 HttpServletResponse hresponse
) throws IOException
, ServletException
{
401 log
.finest("forwarding request to module: " + appEngineWebXml
.getModule() + "." + instance
);
402 RequestDispatcher requestDispatcher
=
403 context
.getServletContext().getRequestDispatcher(hrequest
.getRequestURI());
404 requestDispatcher
.forward(hrequest
, hresponse
);
407 private File
determineAppRoot() throws IOException
{
408 Resource webInf
= context
.getWebInf();
409 if (webInf
== null) {
410 if (userCodeClasspathManager
.requiresWebInf()) {
411 throw new AppEngineConfigException(
412 "Supplied application has to contain WEB-INF directory.");
416 return webInf
.getFile().getParentFile();
420 * {@code ApiProxyHandler} wraps around an existing {@link org.mortbay.jetty.Handler}
421 * and surrounds each top-level request (i.e. not includes or
422 * forwards) with a try finally block that maintains the {@link
423 * com.google.apphosting.api.ApiProxy.Environment} {@link ThreadLocal}.
425 private class ApiProxyHandler
extends HandlerWrapper
{
426 @SuppressWarnings("hiding")
427 private final AppEngineWebXml appEngineWebXml
;
429 ApiProxyHandler(AppEngineWebXml appEngineWebXml
) {
430 this.appEngineWebXml
= appEngineWebXml
;
433 @SuppressWarnings("unchecked")
435 public void handle(String target
,
436 HttpServletRequest request
,
437 HttpServletResponse response
,
438 int dispatch
) throws IOException
, ServletException
{
439 if (dispatch
== REQUEST
) {
440 long startTimeUsec
= System
.currentTimeMillis() * 1000;
441 Semaphore semaphore
= new Semaphore(MAX_SIMULTANEOUS_API_CALLS
);
443 LocalEnvironment env
= new LocalHttpRequestEnvironment(appEngineWebXml
.getAppId(),
444 WebModule
.getModuleName(appEngineWebXml
), appEngineWebXml
.getMajorVersionId(),
445 instance
, getPort(), request
, SOFT_DEADLINE_DELAY_MS
, modulesFilterHelper
);
446 env
.getAttributes().put(LocalEnvironment
.API_CALL_SEMAPHORE
, semaphore
);
447 env
.getAttributes().put(DEFAULT_VERSION_HOSTNAME
, "localhost:" + devAppServer
.getPort());
448 env
.getAttributes().put(LocalEnvironment
.FILESAPI_WAS_USED
, false);
450 ApiProxy
.setEnvironmentForCurrentThread(env
);
452 RecordingResponseWrapper wrappedResponse
= new RecordingResponseWrapper(response
);
454 super.handle(target
, request
, wrappedResponse
, dispatch
);
455 if (request
.getRequestURI().startsWith(_AH_URL_RELOAD
)) {
458 log
.info("Reloaded the webapp context: " + request
.getParameter("info"));
459 } catch (Exception ex
) {
460 log
.log(Level
.WARNING
, "Failed to reload the current webapp context.", ex
);
465 semaphore
.acquire(MAX_SIMULTANEOUS_API_CALLS
);
466 } catch (InterruptedException ex
) {
467 log
.log(Level
.WARNING
, "Interrupted while waiting for API calls to complete:", ex
);
469 env
.callRequestEndListeners();
471 if (apiProxyDelegate
instanceof ApiProxyLocal
) {
472 ApiProxyLocal apiProxyLocal
= (ApiProxyLocal
) apiProxyDelegate
;
474 String appId
= env
.getAppId();
475 String versionId
= env
.getVersionId();
476 String requestId
= DevLogHandler
.getRequestId();
477 long endTimeUsec
= new Date().getTime() * 1000;
479 LocalLogService logService
= (LocalLogService
)
480 apiProxyLocal
.getService(LocalLogService
.PACKAGE
);
482 if (!disableFilesApiWarning
483 && (Boolean
) env
.getAttributes().get(LocalEnvironment
.FILESAPI_WAS_USED
)) {
484 log
.warning(FILES_API_DEPRECATION_WARNING
);
487 logService
.addRequestInfo(appId
, versionId
, requestId
,
488 request
.getRemoteAddr(), request
.getRemoteUser(),
489 startTimeUsec
, endTimeUsec
, request
.getMethod(),
490 request
.getRequestURI(), request
.getProtocol(),
491 request
.getHeader("User-Agent"), true,
492 wrappedResponse
.getStatus(), request
.getHeader(HttpHeaders
.REFERER
));
493 logService
.clearResponseSize();
495 ApiProxy
.clearEnvironmentForCurrentThread();
500 super.handle(target
, request
, response
, dispatch
);
505 private class RecordingResponseWrapper
extends HttpServletResponseWrapper
{
506 private int status
= SC_OK
;
508 RecordingResponseWrapper(HttpServletResponse response
) {
513 public void setStatus(int sc
) {
518 public int getStatus() {
523 public void sendError(int sc
) throws IOException
{
529 public void sendError(int sc
, String msg
) throws IOException
{
531 super.sendError(sc
, msg
);
535 public void sendRedirect(String location
) throws IOException
{
536 status
= SC_MOVED_TEMPORARILY
;
537 super.sendRedirect(location
);
541 public void setStatus(int status
, String string
) {
542 super.setStatus(status
, string
);
543 this.status
= status
;
547 public void reset() {