1.9.30 sync.
[gae.git] / java / src / main / com / google / appengine / tools / development / JettyContainerService.java
blobaf00b0baa16bdbb880be9438bb027a80d59e1b07
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;
29 import java.io.File;
30 import java.io.FilenameFilter;
31 import java.io.IOException;
32 import java.net.URL;
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;
49 /**
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;
71 /**
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;
93 static {
94 System.setProperty("org.mortbay.log.class", JettyLogger.class.getName());
97 private static final int SCAN_INTERVAL_SECONDS = 5;
99 /**
100 * Jetty webapp context.
102 private WebAppContext context;
105 * Our webapp context.
107 private AppContext appContext;
110 * The Jetty server.
112 private Server server;
115 * Hot deployment support.
117 private Scanner scanner;
119 private class JettyAppContext implements AppContext {
120 @Override
121 public IsolatedAppClassLoader getClassLoader() {
122 return (IsolatedAppClassLoader) context.getClassLoader();
125 @Override
126 public Permissions getUserPermissions() {
127 return JettyContainerService.this.getUserPermissions();
130 @Override
131 public Permissions getApplicationPermissions() {
132 return getClassLoader().getAppPermissions();
135 @Override
136 public Object getContainerContext() {
137 return context;
141 public JettyContainerService() {
144 @Override
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;
174 return appRoot;
177 static class ServerShutdownServlet extends HttpServlet {
178 @Override
179 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
180 resp.getWriter().println("Shutting down local server.");
181 resp.flushBuffer();
182 DevAppServer server = (DevAppServer) getServletContext().getAttribute(
183 "com.google.appengine.devappserver.Server");
184 server.gracefulShutdown();
188 @Override
189 protected void connectContainer() throws Exception {
190 moduleConfigurationHandle.checkEnvironmentVariables();
192 Thread currentThread = Thread.currentThread();
193 ClassLoader previousCcl = currentThread.getContextClassLoader();
194 currentThread.setContextClassLoader(null);
196 try {
197 SelectChannelConnector connector = new SelectChannelConnector();
198 connector.setHost(address);
199 connector.setPort(port);
200 connector.setHeaderBufferSize(HEADER_BUFFER_SIZE);
201 connector.setSoLingerTime(0);
202 connector.open();
204 server = new Server();
205 server.addConnector(connector);
207 port = connector.getLocalPort();
208 } finally {
209 currentThread.setContextClassLoader(previousCcl);
213 @Override
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);
222 try {
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());
230 } else {
231 handler.setSessionManager(new StubSessionManager());
233 server.start();
234 } finally {
235 currentThread.setContextClassLoader(previousCcl);
239 @Override
240 protected void stopContainer() throws Exception {
241 server.stop();
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.
256 @Override
257 protected void startHotDeployScanner() throws Exception {
258 String fullScanInterval = System.getProperty("appengine.fullscan.seconds");
259 if (fullScanInterval != null) {
260 try {
261 int interval = Integer.parseInt(fullScanInterval);
262 if (interval < 1) {
263 log.info("Full scan of the web app for changes is disabled.");
264 return;
266 log.info("Full scan of the web app in place every " + interval + "s.");
267 fullWebAppScanner(interval);
268 return;
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() {
278 @Override
279 public boolean accept(File dir, String name) {
280 try {
281 if (name.equals(getScanTarget().getName())) {
282 return true;
284 return false;
286 catch (Exception e) {
287 return false;
291 scanner.scan();
292 scanner.addListener(new ScannerListener());
293 scanner.start();
296 @Override
297 protected void stopHotDeployScanner() throws Exception {
298 if (scanner != null) {
299 scanner.stop();
301 scanner = null;
304 private class ScannerListener implements Scanner.DiscreteListener {
305 @Override
306 public void fileAdded(String filename) throws Exception {
307 fileChanged(filename);
310 @Override
311 public void fileChanged(String filename) throws Exception {
312 log.info(filename + " updated, reloading the webapp!");
313 reloadWebApp();
316 @Override
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) {
326 return appDir;
327 } else {
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<>();
336 Collections.addAll(
337 scanList,
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);
348 scanner.scan();
350 scanner.addListener(
351 new Scanner.BulkListener() {
352 @Override
353 @SuppressWarnings("rawtypes")
354 public void filesChanged(List changedFiles) throws Exception {
355 log.info("A file has changed, reloading the web application.");
356 reloadWebApp();
360 scanner.start();
364 * Assuming Jetty handles race condition nicely, as this is how Jetty handles a hot deploy too.
366 @Override
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);
378 try {
379 initContext();
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);
392 apiHandler.start();
393 } finally {
394 currentThread.setContextClassLoader(previousCcl);
398 @Override
399 public AppContext getAppContext() {
400 return appContext;
403 @Override
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.");
419 return appDir;
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")
439 @Override
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);
458 try {
459 super.handle(target, request, wrappedResponse, dispatch);
460 if (request.getRequestURI().startsWith(AH_URL_RELOAD)) {
461 try {
462 reloadWebApp();
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);
468 } finally {
469 try {
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;
478 try {
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();
499 } finally {
500 ApiProxy.clearEnvironmentForCurrentThread();
504 } else {
505 super.handle(target, request, response, dispatch);
510 private class RecordingResponseWrapper extends HttpServletResponseWrapper {
511 private int status = SC_OK;
513 RecordingResponseWrapper(HttpServletResponse response) {
514 super(response);
517 @Override
518 public void setStatus(int sc) {
519 status = sc;
520 super.setStatus(sc);
523 public int getStatus() {
524 return status;
527 @Override
528 public void sendError(int sc) throws IOException {
529 status = sc;
530 super.sendError(sc);
533 @Override
534 public void sendError(int sc, String msg) throws IOException {
535 status = sc;
536 super.sendError(sc, msg);
539 @Override
540 public void sendRedirect(String location) throws IOException {
541 status = SC_MOVED_TEMPORARILY;
542 super.sendRedirect(location);
545 @Override
546 public void setStatus(int status, String string) {
547 super.setStatus(status, string);
548 this.status = status;
551 @Override
552 public void reset() {
553 super.reset();
554 this.status = SC_OK;