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 Files API is deprecated and will soon be removed. Further information is available "
60 + " here: https://cloud.google.com/appengine/docs/deprecations/files_api";
62 public static final String WEB_DEFAULTS_XML
=
63 "com/google/appengine/tools/development/webdefault.xml";
65 private static final int MAX_SIMULTANEOUS_API_CALLS
= 100;
67 private static final Long SOFT_DEADLINE_DELAY_MS
= 60000L;
69 private static final int HEADER_BUFFER_SIZE
= 64 << 10;
72 * Specify which {@link Configuration} objects should be invoked when
73 * configuring a web application.
75 * <p>This is a subset of:
76 * org.mortbay.jetty.webapp.WebAppContext.__dftConfigurationClasses
78 * <p>Specifically, we've removed {@link JettyWebXmlConfiguration} which
79 * allows users to use {@code jetty-web.xml} files.
81 private static final String
[] CONFIG_CLASSES
= {
82 "org.mortbay.jetty.webapp.WebXmlConfiguration",
83 "org.mortbay.jetty.webapp.TagLibConfiguration"
86 private static final String WEB_XML_ATTR
=
87 "com.google.appengine.tools.development.webXml";
88 private static final String APPENGINE_WEB_XML_ATTR
=
89 "com.google.appengine.tools.development.appEngineWebXml";
91 private boolean disableFilesApiWarning
= false;
94 System
.setProperty("org.mortbay.log.class", JettyLogger
.class.getName());
97 private static final int SCAN_INTERVAL_SECONDS
= 5;
100 * Jetty webapp context.
102 private WebAppContext context
;
105 * Our webapp context.
107 private AppContext appContext
;
112 private Server server
;
115 * Hot deployment support.
117 private Scanner scanner
;
119 private class JettyAppContext
implements AppContext
{
121 public IsolatedAppClassLoader
getClassLoader() {
122 return (IsolatedAppClassLoader
) context
.getClassLoader();
126 public Permissions
getUserPermissions() {
127 return JettyContainerService
.this.getUserPermissions();
131 public Permissions
getApplicationPermissions() {
132 return getClassLoader().getAppPermissions();
136 public Object
getContainerContext() {
141 public JettyContainerService() {
145 protected File
initContext() throws IOException
{
146 this.context
= new DevAppEngineWebAppContext(appDir
, externalResourceDir
, devAppServerVersion
,
147 apiProxyDelegate
, devAppServer
);
148 this.appContext
= new JettyAppContext();
150 context
.setDescriptor(webXmlLocation
== null ?
null : webXmlLocation
.getAbsolutePath());
152 String webDefaultXml
= devAppServer
.getServiceProperties().get("appengine.webdefault.xml");
153 if (webDefaultXml
== null) {
154 webDefaultXml
= WEB_DEFAULTS_XML
;
156 context
.setDefaultsDescriptor(webDefaultXml
);
158 context
.setConfigurationClasses(CONFIG_CLASSES
);
160 File appRoot
= determineAppRoot();
161 installLocalInitializationEnvironment();
163 URL
[] classPath
= getClassPathForApp(appRoot
);
164 context
.setClassLoader(new IsolatedAppClassLoader(appRoot
, externalResourceDir
, classPath
,
165 JettyContainerService
.class.getClassLoader()));
166 if (Boolean
.parseBoolean(System
.getProperty("appengine.allowRemoteShutdown"))) {
167 context
.addServlet(new ServletHolder(new ServerShutdownServlet()), "/_ah/admin/quit");
170 if (Boolean
.parseBoolean(System
.getProperty("appengine.disableFilesApiWarning"))) {
171 disableFilesApiWarning
= true;
177 static class ServerShutdownServlet
extends HttpServlet
{
179 protected void doPost(HttpServletRequest req
, HttpServletResponse resp
) throws IOException
{
180 resp
.getWriter().println("Shutting down local server.");
182 DevAppServer server
= (DevAppServer
) getServletContext().getAttribute(
183 "com.google.appengine.devappserver.Server");
184 server
.gracefulShutdown();
189 protected void connectContainer() throws Exception
{
190 moduleConfigurationHandle
.checkEnvironmentVariables();
192 Thread currentThread
= Thread
.currentThread();
193 ClassLoader previousCcl
= currentThread
.getContextClassLoader();
194 currentThread
.setContextClassLoader(null);
197 SelectChannelConnector connector
= new SelectChannelConnector();
198 connector
.setHost(address
);
199 connector
.setPort(port
);
200 connector
.setHeaderBufferSize(HEADER_BUFFER_SIZE
);
201 connector
.setSoLingerTime(0);
204 server
= new Server();
205 server
.addConnector(connector
);
207 port
= connector
.getLocalPort();
209 currentThread
.setContextClassLoader(previousCcl
);
214 protected void startContainer() throws Exception
{
215 context
.setAttribute(WEB_XML_ATTR
, webXml
);
216 context
.setAttribute(APPENGINE_WEB_XML_ATTR
, appEngineWebXml
);
218 Thread currentThread
= Thread
.currentThread();
219 ClassLoader previousCcl
= currentThread
.getContextClassLoader();
220 currentThread
.setContextClassLoader(null);
223 ApiProxyHandler apiHandler
= new ApiProxyHandler(appEngineWebXml
);
224 apiHandler
.setHandler(context
);
226 server
.setHandler(apiHandler
);
227 SessionHandler handler
= context
.getSessionHandler();
228 if (isSessionsEnabled()) {
229 handler
.setSessionManager(new SerializableObjectsOnlyHashSessionManager());
231 handler
.setSessionManager(new StubSessionManager());
235 currentThread
.setContextClassLoader(previousCcl
);
240 protected void stopContainer() throws Exception
{
245 * If the property "appengine.fullscan.seconds" is set to a positive integer, the web app
246 * content (deployment descriptors, classes/ and lib/) is scanned for changes that will trigger
247 * the reloading of the application.
248 * If the property is not set (default), we monitor the webapp war file or the
249 * appengine-web.xml in case of a pre-exploded webapp directory, and reload the webapp whenever an
250 * update is detected, i.e. a newer timestamp for the monitored file. As a single-context
251 * deployment, add/delete is not applicable here.
253 * <p>appengine-web.xml will be reloaded too. However, changes that require a module instance
254 * restart, e.g. address/port, will not be part of the reload.
257 protected void startHotDeployScanner() throws Exception
{
258 String fullScanInterval
= System
.getProperty("appengine.fullscan.seconds");
259 if (fullScanInterval
!= null) {
261 int interval
= Integer
.parseInt(fullScanInterval
);
263 log
.info("Full scan of the web app for changes is disabled.");
266 log
.info("Full scan of the web app in place every " + interval
+ "s.");
267 fullWebAppScanner(interval
);
269 } catch (NumberFormatException ex
) {
270 log
.log(Level
.WARNING
, "appengine.fullscan.seconds property is not an integer:", ex
);
271 log
.log(Level
.WARNING
, "Using the default scanning method.");
274 scanner
= new Scanner();
275 scanner
.setScanInterval(SCAN_INTERVAL_SECONDS
);
276 scanner
.setScanDirs(ImmutableList
.of(getScanTarget()));
277 scanner
.setFilenameFilter(new FilenameFilter() {
279 public boolean accept(File dir
, String name
) {
281 if (name
.equals(getScanTarget().getName())) {
286 catch (Exception e
) {
292 scanner
.addListener(new ScannerListener());
297 protected void stopHotDeployScanner() throws Exception
{
298 if (scanner
!= null) {
304 private class ScannerListener
implements Scanner
.DiscreteListener
{
306 public void fileAdded(String filename
) throws Exception
{
307 fileChanged(filename
);
311 public void fileChanged(String filename
) throws Exception
{
312 log
.info(filename
+ " updated, reloading the webapp!");
317 public void fileRemoved(String filename
) throws Exception
{
322 * To minimize the overhead, we point the scanner right to the single file in question.
324 private File
getScanTarget() throws Exception
{
325 if (appDir
.isFile() || context
.getWebInf() == null) {
328 return new File(context
.getWebInf().getFile().getPath()
329 + File
.separator
+ "appengine-web.xml");
333 private void fullWebAppScanner(int interval
) throws IOException
{
334 String webInf
= context
.getWebInf().getFile().getPath();
335 List
<File
> scanList
= new ArrayList
<>();
338 new File(webInf
, "classes"),
339 new File(webInf
, "lib"),
340 new File(webInf
, "web.xml"),
341 new File(webInf
, "appengine-web.xml"));
343 scanner
= new Scanner();
344 scanner
.setScanInterval(interval
);
345 scanner
.setScanDirs(scanList
);
346 scanner
.setReportExistingFilesOnStartup(false);
347 scanner
.setRecursive(true);
351 new Scanner
.BulkListener() {
353 @SuppressWarnings("rawtypes")
354 public void filesChanged(List changedFiles
) throws Exception
{
355 log
.info("A file has changed, reloading the web application.");
364 * Assuming Jetty handles race condition nicely, as this is how Jetty handles a hot deploy too.
367 protected void reloadWebApp() throws Exception
{
368 server
.getHandler().stop();
369 moduleConfigurationHandle
.restoreSystemProperties();
370 moduleConfigurationHandle
.readConfiguration();
371 moduleConfigurationHandle
.checkEnvironmentVariables();
372 extractFieldsFromWebModule(moduleConfigurationHandle
.getModule());
374 /** same as what's in startContainer, we need suppress the ContextClassLoader here. */
375 Thread currentThread
= Thread
.currentThread();
376 ClassLoader previousCcl
= currentThread
.getContextClassLoader();
377 currentThread
.setContextClassLoader(null);
380 installLocalInitializationEnvironment();
382 if (!isSessionsEnabled()) {
383 context
.getSessionHandler().setSessionManager(new StubSessionManager());
385 context
.setAttribute(WEB_XML_ATTR
, webXml
);
386 context
.setAttribute(APPENGINE_WEB_XML_ATTR
, appEngineWebXml
);
388 ApiProxyHandler apiHandler
= new ApiProxyHandler(appEngineWebXml
);
389 apiHandler
.setHandler(context
);
390 server
.setHandler(apiHandler
);
394 currentThread
.setContextClassLoader(previousCcl
);
399 public AppContext
getAppContext() {
404 public void forwardToServer(HttpServletRequest hrequest
,
405 HttpServletResponse hresponse
) throws IOException
, ServletException
{
406 log
.finest("forwarding request to module: " + appEngineWebXml
.getModule() + "." + instance
);
407 RequestDispatcher requestDispatcher
=
408 context
.getServletContext().getRequestDispatcher(hrequest
.getRequestURI());
409 requestDispatcher
.forward(hrequest
, hresponse
);
412 private File
determineAppRoot() throws IOException
{
413 Resource webInf
= context
.getWebInf();
414 if (webInf
== null) {
415 if (userCodeClasspathManager
.requiresWebInf()) {
416 throw new AppEngineConfigException(
417 "Supplied application has to contain WEB-INF directory.");
421 return webInf
.getFile().getParentFile();
425 * {@code ApiProxyHandler} wraps around an existing {@link org.mortbay.jetty.Handler}
426 * and surrounds each top-level request (i.e. not includes or
427 * forwards) with a try finally block that maintains the {@link
428 * com.google.apphosting.api.ApiProxy.Environment} {@link ThreadLocal}.
430 private class ApiProxyHandler
extends HandlerWrapper
{
431 @SuppressWarnings("hiding")
432 private final AppEngineWebXml appEngineWebXml
;
434 ApiProxyHandler(AppEngineWebXml appEngineWebXml
) {
435 this.appEngineWebXml
= appEngineWebXml
;
438 @SuppressWarnings("unchecked")
440 public void handle(String target
,
441 HttpServletRequest request
,
442 HttpServletResponse response
,
443 int dispatch
) throws IOException
, ServletException
{
444 if (dispatch
== REQUEST
) {
445 long startTimeUsec
= System
.currentTimeMillis() * 1000;
446 Semaphore semaphore
= new Semaphore(MAX_SIMULTANEOUS_API_CALLS
);
448 LocalEnvironment env
= new LocalHttpRequestEnvironment(appEngineWebXml
.getAppId(),
449 WebModule
.getModuleName(appEngineWebXml
), appEngineWebXml
.getMajorVersionId(),
450 instance
, getPort(), request
, SOFT_DEADLINE_DELAY_MS
, modulesFilterHelper
);
451 env
.getAttributes().put(LocalEnvironment
.API_CALL_SEMAPHORE
, semaphore
);
452 env
.getAttributes().put(DEFAULT_VERSION_HOSTNAME
, "localhost:" + devAppServer
.getPort());
453 env
.getAttributes().put(LocalEnvironment
.FILESAPI_WAS_USED
, false);
455 ApiProxy
.setEnvironmentForCurrentThread(env
);
457 RecordingResponseWrapper wrappedResponse
= new RecordingResponseWrapper(response
);
459 super.handle(target
, request
, wrappedResponse
, dispatch
);
460 if (request
.getRequestURI().startsWith(AH_URL_RELOAD
)) {
463 log
.info("Reloaded the webapp context: " + request
.getParameter("info"));
464 } catch (Exception ex
) {
465 log
.log(Level
.WARNING
, "Failed to reload the current webapp context.", ex
);
470 semaphore
.acquire(MAX_SIMULTANEOUS_API_CALLS
);
471 } catch (InterruptedException ex
) {
472 log
.log(Level
.WARNING
, "Interrupted while waiting for API calls to complete:", ex
);
474 env
.callRequestEndListeners();
476 if (apiProxyDelegate
instanceof ApiProxyLocal
) {
477 ApiProxyLocal apiProxyLocal
= (ApiProxyLocal
) apiProxyDelegate
;
479 String appId
= env
.getAppId();
480 String versionId
= env
.getVersionId();
481 String requestId
= DevLogHandler
.getRequestId();
482 long endTimeUsec
= new Date().getTime() * 1000;
484 LocalLogService logService
= (LocalLogService
)
485 apiProxyLocal
.getService(LocalLogService
.PACKAGE
);
487 if (!disableFilesApiWarning
488 && (Boolean
) env
.getAttributes().get(LocalEnvironment
.FILESAPI_WAS_USED
)) {
489 log
.warning(FILES_API_DEPRECATION_WARNING
);
492 logService
.addRequestInfo(appId
, versionId
, requestId
,
493 request
.getRemoteAddr(), request
.getRemoteUser(),
494 startTimeUsec
, endTimeUsec
, request
.getMethod(),
495 request
.getRequestURI(), request
.getProtocol(),
496 request
.getHeader("User-Agent"), true,
497 wrappedResponse
.getStatus(), request
.getHeader(HttpHeaders
.REFERER
));
498 logService
.clearResponseSize();
500 ApiProxy
.clearEnvironmentForCurrentThread();
505 super.handle(target
, request
, response
, dispatch
);
510 private class RecordingResponseWrapper
extends HttpServletResponseWrapper
{
511 private int status
= SC_OK
;
513 RecordingResponseWrapper(HttpServletResponse response
) {
518 public void setStatus(int sc
) {
523 public int getStatus() {
528 public void sendError(int sc
) throws IOException
{
534 public void sendError(int sc
, String msg
) throws IOException
{
536 super.sendError(sc
, msg
);
540 public void sendRedirect(String location
) throws IOException
{
541 status
= SC_MOVED_TEMPORARILY
;
542 super.sendRedirect(location
);
546 public void setStatus(int status
, String string
) {
547 super.setStatus(status
, string
);
548 this.status
= status
;
552 public void reset() {