Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / development / JettyContainerService.java
blob4043a4397679a407a7abe037b53841e9cd51e662
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;
17 import org.mortbay.jetty.Server;
18 import org.mortbay.jetty.handler.HandlerWrapper;
19 import org.mortbay.jetty.nio.SelectChannelConnector;
20 import org.mortbay.jetty.servlet.ServletHolder;
21 import org.mortbay.jetty.servlet.SessionHandler;
22 import org.mortbay.jetty.webapp.Configuration;
23 import org.mortbay.jetty.webapp.JettyWebXmlConfiguration;
24 import org.mortbay.jetty.webapp.WebAppContext;
25 import org.mortbay.resource.Resource;
26 import org.mortbay.util.Scanner;
28 import java.io.File;
29 import java.io.FilenameFilter;
30 import java.io.IOException;
31 import java.net.URL;
32 import java.security.Permissions;
33 import java.util.ArrayList;
34 import java.util.Date;
35 import java.util.List;
36 import java.util.concurrent.Semaphore;
37 import java.util.logging.Level;
38 import java.util.logging.Logger;
40 import javax.servlet.RequestDispatcher;
41 import javax.servlet.ServletException;
42 import javax.servlet.http.HttpServlet;
43 import javax.servlet.http.HttpServletRequest;
44 import javax.servlet.http.HttpServletResponse;
45 import javax.servlet.http.HttpServletResponseWrapper;
47 /**
48 * Implements a Jetty backed {@link ContainerService}.
51 @ServiceProvider(ContainerService.class)
52 public class JettyContainerService extends AbstractContainerService {
54 private static final Logger log = Logger.getLogger(JettyContainerService.class.getName());
56 public final static String WEB_DEFAULTS_XML =
57 "com/google/appengine/tools/development/webdefault.xml";
59 private static final int MAX_SIMULTANEOUS_API_CALLS = 100;
61 private static final Long SOFT_DEADLINE_DELAY_MS = 60000L;
63 /**
64 * Specify which {@link Configuration} objects should be invoked when
65 * configuring a web application.
67 * <p>This is a subset of:
68 * org.mortbay.jetty.webapp.WebAppContext.__dftConfigurationClasses
70 * <p>Specifically, we've removed {@link JettyWebXmlConfiguration} which
71 * allows users to use {@code jetty-web.xml} files.
73 private static final String CONFIG_CLASSES[] = new String[] {
74 "org.mortbay.jetty.webapp.WebXmlConfiguration",
75 "org.mortbay.jetty.webapp.TagLibConfiguration"
78 private static final String WEB_XML_ATTR =
79 "com.google.appengine.tools.development.webXml";
80 private static final String APPENGINE_WEB_XML_ATTR =
81 "com.google.appengine.tools.development.appEngineWebXml";
83 static {
84 System.setProperty("org.mortbay.log.class", JettyLogger.class.getName());
87 private final static int SCAN_INTERVAL_SECONDS = 5;
89 /**
90 * Jetty webapp context.
92 private WebAppContext context;
94 /**
95 * Our webapp context.
97 private AppContext appContext;
99 /**
100 * The Jetty server.
102 private Server server;
105 * Hot deployment support.
107 private Scanner scanner;
109 private class JettyAppContext implements AppContext {
110 @Override
111 public IsolatedAppClassLoader getClassLoader() {
112 return (IsolatedAppClassLoader) context.getClassLoader();
115 @Override
116 public Permissions getUserPermissions() {
117 return JettyContainerService.this.getUserPermissions();
120 @Override
121 public Permissions getApplicationPermissions() {
122 return getClassLoader().getAppPermissions();
125 @Override
126 public Object getContainerContext() {
127 return context;
131 public JettyContainerService() {
134 @Override
135 protected File initContext() throws IOException {
136 this.context = new DevAppEngineWebAppContext(appDir, externalResourceDir, devAppServerVersion,
137 apiProxyLocal, devAppServer);
138 this.appContext = new JettyAppContext();
140 context.setDescriptor(webXmlLocation == null ? null : webXmlLocation.getAbsolutePath());
142 context.setDefaultsDescriptor(WEB_DEFAULTS_XML);
144 context.setConfigurationClasses(CONFIG_CLASSES);
146 File appRoot = determineAppRoot();
147 installLocalInitializationEnvironment();
149 URL[] classPath = getClassPathForApp(appRoot);
150 context.setClassLoader(new IsolatedAppClassLoader(appRoot, externalResourceDir, classPath,
151 JettyContainerService.class.getClassLoader()));
152 if (Boolean.parseBoolean(System.getProperty("appengine.allowRemoteShutdown"))) {
153 context.addServlet(new ServletHolder(new ServerShutdownServlet()), "/_ah/admin/quit");
156 return appRoot;
159 static class ServerShutdownServlet extends HttpServlet {
160 @Override
161 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
162 resp.getWriter().println("Shutting down local server.");
163 resp.flushBuffer();
164 DevAppServer server = (DevAppServer) getServletContext().getAttribute(
165 "com.google.appengine.devappserver.Server");
166 server.gracefulShutdown();
170 @Override
171 protected void connectContainer() throws Exception {
172 serverConfigurationHandle.checkEnvironmentVariables();
174 Thread currentThread = Thread.currentThread();
175 ClassLoader previousCcl = currentThread.getContextClassLoader();
176 currentThread.setContextClassLoader(null);
178 try {
179 SelectChannelConnector connector = new SelectChannelConnector();
180 connector.setHost(address);
181 connector.setPort(port);
182 connector.setSoLingerTime(0);
183 connector.open();
185 server = new Server();
186 server.addConnector(connector);
188 port = connector.getLocalPort();
189 } finally {
190 currentThread.setContextClassLoader(previousCcl);
194 @Override
195 protected void startContainer() throws Exception {
196 context.setAttribute(WEB_XML_ATTR, webXml);
197 context.setAttribute(APPENGINE_WEB_XML_ATTR, appEngineWebXml);
199 Thread currentThread = Thread.currentThread();
200 ClassLoader previousCcl = currentThread.getContextClassLoader();
201 currentThread.setContextClassLoader(null);
203 try {
204 ApiProxyHandler apiHandler = new ApiProxyHandler(appEngineWebXml);
205 apiHandler.setHandler(context);
207 server.setHandler(apiHandler);
208 SessionHandler handler = context.getSessionHandler();
209 if (isSessionsEnabled()) {
210 handler.setSessionManager(new SerializableObjectsOnlyHashSessionManager());
211 } else {
212 handler.setSessionManager(new StubSessionManager());
214 server.start();
215 } finally {
216 currentThread.setContextClassLoader(previousCcl);
220 @Override
221 protected void stopContainer() throws Exception {
222 server.stop();
226 * If the property "appengine.fullscan.seconds" is set to a positive integer, the entire web app
227 * content is scanned for changes that will trigger the reloading of the application.
228 * If the property is not set (default), we monitor the webapp war file or the
229 * appengine-web.xml in case of a pre-exploded webapp directory, and reload the webapp whenever an
230 * update is detected, i.e. a newer timestamp for the monitored file. As a single-context
231 * deployment, add/delete is not applicable here.
233 * appengine-web.xml will be reloaded too. However, changes that require a server restart, e.g.
234 * address/port, will not be part of the reload.
236 @Override
237 protected void startHotDeployScanner() throws Exception {
238 String fullScanInterval = System.getProperty("appengine.fullscan.seconds");
239 if (fullScanInterval != null) {
240 try {
241 int interval = Integer.parseInt(fullScanInterval);
242 if (interval < 1) {
243 log.info("Full scan of the web app for changes is disabled.");
244 return;
246 log.info("Full scan of the web app in place every " + interval + "s.");
247 fullWebAppScanner(interval);
248 return;
249 } catch (NumberFormatException ex) {
250 log.log(Level.WARNING, "appengine.fullscan.seconds property is not an integer:", ex);
251 log.log(Level.WARNING, "Using the default scanning method.");
254 scanner = new Scanner();
255 scanner.setScanInterval(SCAN_INTERVAL_SECONDS);
256 scanner.setScanDirs(ImmutableList.of(getScanTarget()));
257 scanner.setFilenameFilter(new FilenameFilter() {
258 @Override
259 public boolean accept(File dir, String name) {
260 try {
261 if (name.equals(getScanTarget().getName())) {
262 return true;
264 return false;
266 catch (Exception e) {
267 return false;
271 scanner.scan();
272 scanner.addListener(new ScannerListener());
273 scanner.start();
276 @Override
277 protected void stopHotDeployScanner() throws Exception {
278 if (scanner != null) {
279 scanner.stop();
281 scanner = null;
284 private class ScannerListener implements Scanner.DiscreteListener {
285 @Override
286 public void fileAdded(String filename) throws Exception {
287 fileChanged(filename);
290 @Override
291 public void fileChanged(String filename) throws Exception {
292 log.info(filename + " updated, reloading the webapp!");
293 reloadWebApp();
296 @Override
297 public void fileRemoved(String filename) throws Exception {
302 * To minimize the overhead, we point the scanner right to the single file in question.
304 private File getScanTarget() throws Exception {
305 if (appDir.isFile() || context.getWebInf() == null) {
306 return appDir;
307 } else {
308 return new File(context.getWebInf().getFile().getPath()
309 + File.separator + "appengine-web.xml");
313 private void fullWebAppScanner(int interval) throws IOException {
314 String webInf = context.getWebInf().getFile().getPath();
315 List<File> scanList = new ArrayList<File>();
316 scanList.add(new File(webInf));
318 scanner = new Scanner();
319 scanner.setScanInterval(interval);
320 scanner.setScanDirs(scanList);
321 scanner.setReportExistingFilesOnStartup(false);
322 scanner.setRecursive(true);
323 scanner.scan();
325 scanner.addListener(new Scanner.BulkListener() {
326 public void filesChanged(List changedFiles) throws Exception {
327 log.info("A file has changed, reloading the web application.");
328 reloadWebApp();
332 scanner.start();
336 * Assuming Jetty handles race condition nicely, as this is how Jetty handles a hot deploy too.
338 @Override
339 protected void reloadWebApp() throws Exception {
340 server.getHandler().stop();
341 serverConfigurationHandle.restoreSystemProperties();
342 serverConfigurationHandle.readConfiguration();
343 serverConfigurationHandle.checkEnvironmentVariables();
344 extractFieldsFromWebModule(serverConfigurationHandle.getModule());
346 /** same as what's in startContainer, we need suppress the ContextClassLoader here. */
347 Thread currentThread = Thread.currentThread();
348 ClassLoader previousCcl = currentThread.getContextClassLoader();
349 currentThread.setContextClassLoader(null);
350 try {
351 File webAppDir = initContext();
352 installLoggingServiceHandler();
353 installLocalInitializationEnvironment();
355 if (!isSessionsEnabled()) {
356 context.getSessionHandler().setSessionManager(new StubSessionManager());
358 context.setAttribute(WEB_XML_ATTR, webXml);
359 context.setAttribute(APPENGINE_WEB_XML_ATTR, appEngineWebXml);
361 ApiProxyHandler apiHandler = new ApiProxyHandler(appEngineWebXml);
362 apiHandler.setHandler(context);
363 server.setHandler(apiHandler);
365 apiHandler.start();
366 } finally {
367 currentThread.setContextClassLoader(previousCcl);
371 @Override
372 public AppContext getAppContext() {
373 return appContext;
376 @Override
377 public void forwardToServer(HttpServletRequest hrequest,
378 HttpServletResponse hresponse) throws IOException, ServletException {
379 log.finest("forwarding request to server: " + appEngineWebXml.getServer() + "." + instance);
380 RequestDispatcher requestDispatcher =
381 context.getServletContext().getRequestDispatcher(hrequest.getRequestURI());
382 requestDispatcher.forward(hrequest, hresponse);
385 private File determineAppRoot() throws IOException {
386 Resource webInf = context.getWebInf();
387 if (webInf == null) {
388 if (userCodeClasspathManager.requiresWebInf()) {
389 throw new AppEngineConfigException(
390 "Supplied application has to contain WEB-INF directory.");
392 return appDir;
394 return webInf.getFile().getParentFile();
398 * {@code ApiProxyHandler} wraps around an existing {@link org.mortbay.jetty.Handler}
399 * and surrounds each top-level request (i.e. not includes or
400 * forwards) with a try finally block that maintains the {@link
401 * com.google.apphosting.api.ApiProxy.Environment} {@link ThreadLocal}.
403 private class ApiProxyHandler extends HandlerWrapper {
404 @SuppressWarnings("hiding")
405 private final AppEngineWebXml appEngineWebXml;
407 public ApiProxyHandler(AppEngineWebXml appEngineWebXml) {
408 this.appEngineWebXml = appEngineWebXml;
411 @SuppressWarnings("unchecked")
412 @Override
413 public void handle(String target,
414 HttpServletRequest request,
415 HttpServletResponse response,
416 int dispatch) throws IOException, ServletException {
417 if (dispatch == REQUEST) {
418 long startTimeUsec = System.currentTimeMillis() * 1000;
419 Semaphore semaphore = new Semaphore(MAX_SIMULTANEOUS_API_CALLS);
421 LocalEnvironment env = new LocalHttpRequestEnvironment(appEngineWebXml.getAppId(),
422 WebModule.getServerName(appEngineWebXml), appEngineWebXml.getMajorVersionId(),
423 instance, request, SOFT_DEADLINE_DELAY_MS, serversFilterHelper);
424 env.getAttributes().put(LocalEnvironment.API_CALL_SEMAPHORE, semaphore);
425 env.getAttributes().put(DEFAULT_VERSION_HOSTNAME, "localhost:" + devAppServer.getPort());
427 ApiProxy.setEnvironmentForCurrentThread(env);
429 RecordingResponseWrapper wrappedResponse = new RecordingResponseWrapper(response);
430 try {
431 super.handle(target, request, wrappedResponse, dispatch);
432 if (request.getRequestURI().startsWith(_AH_URL_RELOAD)) {
433 try {
434 reloadWebApp();
435 log.info("Reloaded the webapp context: " + request.getParameter("info"));
436 } catch (Exception ex) {
437 log.log(Level.WARNING, "Failed to reload the current webapp context.", ex);
440 } finally {
441 try {
442 semaphore.acquire(MAX_SIMULTANEOUS_API_CALLS);
443 } catch (InterruptedException ex) {
444 log.log(Level.WARNING, "Interrupted while waiting for API calls to complete:", ex);
446 env.callRequestEndListeners();
448 try {
449 String appId = env.getAppId();
450 String versionId = env.getVersionId();
451 String requestId = DevLogHandler.getRequestId();
452 long endTimeUsec = new Date().getTime() * 1000;
454 LocalLogService logService = (LocalLogService)
455 apiProxyLocal.getService(LocalLogService.PACKAGE);
457 logService.addRequestInfo(appId, versionId, requestId,
458 request.getRemoteAddr(), request.getRemoteUser(),
459 startTimeUsec, endTimeUsec, request.getMethod(),
460 request.getRequestURI(), request.getProtocol(),
461 request.getHeader("User-Agent"), true,
462 wrappedResponse.getStatus(), request.getHeader("Referrer"));
463 logService.clearResponseSize();
464 } finally {
465 ApiProxy.clearEnvironmentForCurrentThread();
468 } else {
469 super.handle(target, request, response, dispatch);
474 private class RecordingResponseWrapper extends HttpServletResponseWrapper {
475 private int status = SC_OK;
477 RecordingResponseWrapper(HttpServletResponse response) {
478 super(response);
481 @Override
482 public void setStatus(int sc) {
483 status = sc;
484 super.setStatus(sc);
487 public int getStatus() {
488 return status;
491 @Override
492 public void sendError(int sc) throws IOException {
493 status = sc;
494 super.sendError(sc);
497 @Override
498 public void sendError(int sc, String msg) throws IOException {
499 status = sc;
500 super.sendError(sc, msg);
503 @Override
504 public void sendRedirect(String location) throws IOException {
505 status = SC_MOVED_TEMPORARILY;
506 super.sendRedirect(location);
509 @Override
510 public void setStatus(int status, String string) {
511 super.setStatus(status, string);
512 this.status = status;
515 @Override
516 public void reset() {
517 super.reset();
518 this.status = SC_OK;