Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / development / DevAppServerImpl.java
blob0e85e61bdfb726f0b0ec658ad524169f43287ee8
1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.development;
5 import com.google.appengine.api.modules.dev.LocalModulesService;
6 import com.google.appengine.tools.development.EnvironmentVariableChecker.MismatchReportingPolicy;
7 import com.google.appengine.tools.info.SdkInfo;
8 import com.google.apphosting.api.ApiProxy;
9 import com.google.apphosting.api.ApiProxy.Environment;
10 import com.google.apphosting.utils.config.AppEngineConfigException;
11 import com.google.apphosting.utils.config.EarHelper;
12 import com.google.common.base.Joiner;
13 import com.google.common.collect.ImmutableMap;
14 import com.google.common.collect.ImmutableSet;
16 import java.io.File;
17 import java.lang.reflect.Field;
18 import java.lang.reflect.Method;
19 import java.net.BindException;
20 import java.security.AccessController;
21 import java.security.PrivilegedAction;
22 import java.security.PrivilegedActionException;
23 import java.security.PrivilegedExceptionAction;
24 import java.util.HashMap;
25 import java.util.Map;
26 import java.util.TimeZone;
27 import java.util.concurrent.Callable;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.CountDownLatch;
30 import java.util.concurrent.Executors;
31 import java.util.concurrent.Future;
32 import java.util.concurrent.ScheduledExecutorService;
33 import java.util.concurrent.TimeUnit;
34 import java.util.logging.ConsoleHandler;
35 import java.util.logging.Handler;
36 import java.util.logging.Level;
37 import java.util.logging.Logger;
39 /**
40 * {@code DevAppServer} launches a local Jetty server (by default) with a single
41 * hosted web application. It can be invoked from the command-line by
42 * providing the path to the directory in which the application resides as the
43 * only argument.
46 class DevAppServerImpl implements DevAppServer {
47 public static final String MODULES_FILTER_HELPER_PROPERTY =
48 "com.google.appengine.tools.development.modules_filter_helper";
49 private static final Logger logger = Logger.getLogger(DevAppServerImpl.class.getName());
51 private final ApplicationConfigurationManager applicationConfigurationManager;
52 private final Modules modules;
53 private Map<String, String> serviceProperties = new HashMap<String, String>();
54 private final Map<String, Object> containerConfigProperties;
55 private final int requestedPort;
57 enum ServerState { INITIALIZING, RUNNING, STOPPING, SHUTDOWN }
59 /**
60 * The current state of the server.
62 private ServerState serverState = ServerState.INITIALIZING;
64 /**
65 * Contains the backend servers configured as part of the "Servers" feature.
66 * Each backend server is started on a separate port and keep their own
67 * internal state. Memcache, datastore, and other API services are shared by
68 * all servers, including the "main" server.
70 private final BackendServers backendContainer;
72 /**
73 * The api proxy we created when we started the web containers. Not initialized until after
74 * {@link #start()} is called.
76 private ApiProxyLocal apiProxyLocal;
78 /**
79 * We defer reporting construction time configuration exceptions until
80 * {@link #start()} for compatibility.
82 private final AppEngineConfigException configurationException;
84 /**
85 * Used to schedule the graceful shutdown of the server.
87 private final ScheduledExecutorService shutdownScheduler = Executors.newScheduledThreadPool(1);
89 /**
90 * Latch that we decrement when the server is shutdown or restarted.
91 * Will be {@code null} until the server is started.
93 private CountDownLatch shutdownLatch = null;
95 /**
96 * Constructs a development application server that runs the application located in the given
97 * WAR or EAR directory.
99 * @param appDir The location of the application to run.
100 * @param externalResourceDir If not {@code null}, a resource directory external to the appDir.
101 * This will be searched before appDir when looking for resources.
102 * @param webXmlLocation The location of a file whose format complies with
103 * http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd. If {@code null},
104 * defaults to <appDir>/WEB-INF/web.xml
105 * @param appEngineWebXmlLocation The name of the app engine config file. If
106 * {@code null}, defaults to <appDir>/WEB-INF/appengine-web.xml
107 * @param address The address on which to run
108 * @param port The port on which to run
109 * @param useCustomStreamHandler If {@code true} (typical), install {@link StreamHandlerFactory}.
110 * @param requestedContainerConfigProperties Additional properties used in the
111 * configuration of the specific container implementation.
113 public DevAppServerImpl(File appDir, File externalResourceDir, File webXmlLocation,
114 File appEngineWebXmlLocation, String address, int port, boolean useCustomStreamHandler,
115 Map<String, Object> requestedContainerConfigProperties) {
116 String serverInfo = ContainerUtils.getServerInfo();
117 if (useCustomStreamHandler) {
118 StreamHandlerFactory.install();
120 DevSocketImplFactory.install();
122 backendContainer = BackendServers.getInstance();
123 requestedPort = port;
124 ApplicationConfigurationManager tempManager = null;
125 File schemaFile = new File(SdkInfo.getSdkRoot(), "docs/appengine-application.xsd");
126 try {
127 if (EarHelper.isEar(appDir.getAbsolutePath())) {
128 tempManager = ApplicationConfigurationManager.newEarConfigurationManager(appDir,
129 SdkInfo.getLocalVersion().getRelease(), schemaFile);
130 String contextRootWarning =
131 "Ignoring application.xml context-root element, for details see "
132 + "https://developers.google.com/appengine/docs/java/modules/#config";
133 logger.info(contextRootWarning);
134 } else {
135 tempManager = ApplicationConfigurationManager.newWarConfigurationManager(
136 appDir, appEngineWebXmlLocation, webXmlLocation, externalResourceDir,
137 SdkInfo.getLocalVersion().getRelease());
139 } catch (AppEngineConfigException configurationException) {
140 modules = null;
141 applicationConfigurationManager = null;
142 this.containerConfigProperties = null;
143 this.configurationException = configurationException;
144 return;
146 this.applicationConfigurationManager = tempManager;
147 this.modules = Modules.createModules(applicationConfigurationManager, serverInfo,
148 externalResourceDir, address, this);
149 DelegatingModulesFilterHelper modulesFilterHelper =
150 new DelegatingModulesFilterHelper(backendContainer, modules);
151 this.containerConfigProperties = ImmutableMap.<String, Object>builder()
152 .putAll(requestedContainerConfigProperties)
153 .put(MODULES_FILTER_HELPER_PROPERTY, modulesFilterHelper)
154 .put(AbstractContainerService.PORT_MAPPING_PROVIDER_PROP, backendContainer)
155 .build();
156 backendContainer.init(address,
157 applicationConfigurationManager.getPrimaryModuleConfigurationHandle(),
158 externalResourceDir, this.containerConfigProperties, this);
159 configurationException = null;
163 * Sets the properties that will be used by the local services to
164 * configure themselves. This method must be called before the server
165 * has been started.
167 * @param properties a, maybe {@code null}, set of properties.
169 * @throws IllegalStateException if the server has already been started.
171 @Override
172 public void setServiceProperties(Map<String, String> properties) {
173 if (serverState != ServerState.INITIALIZING) {
174 String msg = "Cannot set service properties after the server has been started.";
175 throw new IllegalStateException(msg);
178 if (configurationException == null) {
179 serviceProperties = new ConcurrentHashMap<String, String>(properties);
180 if (requestedPort != 0) {
181 DevAppServerPortPropertyHelper.setPort(modules.getMainModule().getModuleName(),
182 requestedPort, serviceProperties);
184 backendContainer.setServiceProperties(properties);
185 DevAppServerDatastorePropertyHelper.setDefaultProperties(serviceProperties);
189 @Override
190 public Map<String, String> getServiceProperties() {
191 return serviceProperties;
195 * Starts the server.
197 * @throws IllegalStateException If the server has already been started or
198 * shutdown.
199 * @throws AppEngineConfigException If no WEB-INF directory can be found or
200 * WEB-INF/appengine-web.xml does not exist.
201 * @return a latch that will be decremented to zero when the server is shutdown.
203 @Override
204 public CountDownLatch start() throws Exception {
205 try {
206 return AccessController.doPrivileged(new PrivilegedExceptionAction<CountDownLatch>() {
207 @Override public CountDownLatch run() throws Exception {
208 return doStart();
211 } catch (PrivilegedActionException e) {
212 throw e.getException();
216 private CountDownLatch doStart() throws Exception {
217 if (serverState != ServerState.INITIALIZING) {
218 throw new IllegalStateException("Cannot start a server that has already been started.");
221 reportDeferredConfigurationException();
223 initializeLogging();
225 modules.configure(containerConfigProperties);
226 try {
227 modules.createConnections();
228 } catch (BindException ex) {
229 System.err.println();
230 System.err.println("************************************************");
231 System.err.println("Could not open the requested socket: " + ex.getMessage());
232 System.err.println("Try overriding --address and/or --port.");
233 System.exit(2);
236 ApiProxyLocalFactory factory = new ApiProxyLocalFactory();
237 apiProxyLocal = factory.create(modules.getLocalServerEnvironment());
238 setInboundServicesProperty();
239 apiProxyLocal.setProperties(serviceProperties);
240 ApiProxy.setDelegate(apiProxyLocal);
241 LocalModulesService localModulesService =
242 (LocalModulesService) apiProxyLocal.getService(LocalModulesService.PACKAGE);
243 localModulesService.setModulesController(modules);
244 installLoggingServiceHandler((DevServices) apiProxyLocal);
245 TimeZone currentTimeZone = null;
246 try {
247 currentTimeZone = setServerTimeZone();
248 backendContainer.configureAll(apiProxyLocal);
249 modules.setApiProxyDelegate(apiProxyLocal);
250 modules.startup();
251 Module mainServer = modules.getMainModule();
252 Map<String, String> portMapping = backendContainer.getPortMapping();
253 AbstractContainerService.installLocalInitializationEnvironment(
254 mainServer.getMainContainer().getAppEngineWebXmlConfig(), LocalEnvironment.MAIN_INSTANCE,
255 getPort(), getPort(), null, -1, portMapping);
256 backendContainer.startupAll();
257 } finally {
258 ApiProxy.clearEnvironmentForCurrentThread();
259 restoreLocalTimeZone(currentTimeZone);
261 shutdownLatch = new CountDownLatch(1);
262 serverState = ServerState.RUNNING;
263 logger.info("Dev App Server is now running");
264 return shutdownLatch;
267 private void installLoggingServiceHandler(DevServices proxy) {
268 Logger root = Logger.getLogger("");
269 DevLogService logService = proxy.getLogService();
270 root.addHandler(logService.getLogHandler());
272 Handler[] handlers = root.getHandlers();
273 if (handlers != null) {
274 for (Handler handler : handlers) {
275 handler.setLevel(Level.FINEST);
280 public void setInboundServicesProperty() {
281 ImmutableSet.Builder<String> setBuilder = ImmutableSet.builder();
282 for (ApplicationConfigurationManager.ModuleConfigurationHandle moduleConfigurationHandle :
283 applicationConfigurationManager.getModuleConfigurationHandles()) {
284 setBuilder.addAll(
285 moduleConfigurationHandle.getModule().getAppEngineWebXml().getInboundServices());
288 serviceProperties.put("appengine.dev.inbound-services",
289 Joiner.on(",").join(setBuilder.build()));
293 * Change the TimeZone for the current thread. By calling this method before
294 * {@link ContainerService#startup()} start}, we set the default TimeZone for the
295 * DevAppServer and all of its related services.
297 * @return the previously installed ThreadLocal TimeZone
299 private TimeZone setServerTimeZone() {
300 String sysTimeZone = serviceProperties.get("appengine.user.timezone.impl");
301 if (sysTimeZone != null && sysTimeZone.trim().length() > 0) {
302 return null;
305 TimeZone utc = TimeZone.getTimeZone("UTC");
306 assert utc.getID().equals("UTC") : "Unable to retrieve the UTC TimeZone";
308 try {
309 Field f = TimeZone.class.getDeclaredField("defaultZoneTL");
310 f.setAccessible(true);
311 ThreadLocal<?> tl = (ThreadLocal<?>) f.get(null);
312 Method getZone = ThreadLocal.class.getMethod("get");
313 TimeZone previousZone = (TimeZone) getZone.invoke(tl);
314 Method setZone = ThreadLocal.class.getMethod("set", Object.class);
315 setZone.invoke(tl, utc);
316 return previousZone;
317 } catch (Exception e) {
318 try {
319 Method getZone = TimeZone.class.getDeclaredMethod("getDefaultInAppContext");
320 getZone.setAccessible(true);
321 TimeZone previousZone = (TimeZone) getZone.invoke(null);
322 Method setZone = TimeZone.class.getDeclaredMethod("setDefaultInAppContext", TimeZone.class);
323 setZone.setAccessible(true);
324 setZone.invoke(null, utc);
325 return previousZone;
326 } catch (Exception ex) {
327 logger.log(Level.WARNING,
328 "Unable to set the TimeZone to UTC (this is expected if running on JDK 8)");
329 return null;
335 * Restores the ThreadLocal TimeZone to {@code timeZone}.
337 private void restoreLocalTimeZone(TimeZone timeZone) {
338 if (timeZone == null) {
339 return;
342 String sysTimeZone = serviceProperties.get("appengine.user.timezone.impl");
343 if (sysTimeZone != null && sysTimeZone.trim().length() > 0) {
344 return;
347 try {
348 Field f = TimeZone.class.getDeclaredField("defaultZoneTL");
349 f.setAccessible(true);
350 ThreadLocal<?> tl = (ThreadLocal<?>) f.get(null);
351 Method setZone = ThreadLocal.class.getMethod("set", Object.class);
352 setZone.invoke(tl, timeZone);
353 } catch (Exception e) {
354 try {
355 Method setZone = TimeZone.class.getDeclaredMethod("setDefaultInAppContext", TimeZone.class);
356 setZone.setAccessible(true);
357 setZone.invoke(null, timeZone);
358 } catch (Exception ex) {
359 throw new RuntimeException("Unable to restore the previous TimeZone", ex);
364 @Override
365 public CountDownLatch restart() throws Exception {
366 if (serverState != ServerState.RUNNING) {
367 throw new IllegalStateException("Cannot restart a server that is not currently running.");
369 try {
370 return AccessController.doPrivileged(new PrivilegedExceptionAction<CountDownLatch>() {
371 @Override public CountDownLatch run() throws Exception {
372 modules.shutdown();
373 backendContainer.shutdownAll();
374 shutdownLatch.countDown();
375 modules.createConnections();
376 backendContainer.configureAll(apiProxyLocal);
377 modules.setApiProxyDelegate(apiProxyLocal);
378 modules.startup();
379 backendContainer.startupAll();
380 shutdownLatch = new CountDownLatch(1);
381 return shutdownLatch;
384 } catch (PrivilegedActionException e) {
385 throw e.getException();
389 @Override
390 public void shutdown() throws Exception {
391 if (serverState != ServerState.RUNNING) {
392 throw new IllegalStateException("Cannot shutdown a server that is not currently running.");
394 try {
395 AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
396 @Override public Void run() throws Exception {
397 modules.shutdown();
398 backendContainer.shutdownAll();
399 ApiProxy.setDelegate(null);
400 apiProxyLocal = null;
401 serverState = ServerState.SHUTDOWN;
402 shutdownLatch.countDown();
403 return null;
406 } catch (PrivilegedActionException e) {
407 throw e.getException();
411 @Override
412 public void gracefulShutdown() throws IllegalStateException {
414 AccessController.doPrivileged(new PrivilegedAction<Future<Void>>() {
415 @Override
416 public Future<Void> run() {
417 return shutdownScheduler.schedule(new Callable<Void>() {
418 @Override
419 public Void call() throws Exception {
420 shutdown();
421 return null;
423 }, 1000, TimeUnit.MILLISECONDS);
428 @Override
429 public int getPort() {
430 reportDeferredConfigurationException();
431 return modules.getMainModule().getMainContainer().getPort();
434 protected void reportDeferredConfigurationException() {
435 if (configurationException != null) {
436 throw new AppEngineConfigException("Invalid configuration", configurationException);
440 @Override
441 public AppContext getAppContext() {
442 reportDeferredConfigurationException();
443 return modules.getMainModule().getMainContainer().getAppContext();
446 @Override
447 public AppContext getCurrentAppContext() {
448 AppContext result = null;
449 Environment env = ApiProxy.getCurrentEnvironment();
450 if (env != null && env.getVersionId() != null) {
451 String moduleName = env.getModuleId();
452 result = modules.getModule(moduleName).getMainContainer().getAppContext();
454 return result;
457 @Override
458 public void setThrowOnEnvironmentVariableMismatch(boolean throwOnMismatch) {
459 if (configurationException == null) {
460 applicationConfigurationManager.setEnvironmentVariableMismatchReportingPolicy(
461 throwOnMismatch ? MismatchReportingPolicy.EXCEPTION : MismatchReportingPolicy.LOG);
466 * We're happy with the default logging behavior, which is to
467 * install a {@link ConsoleHandler} at the root level. The only
468 * issue is that we want its level to be FINEST to be consistent
469 * with our runtime environment.
471 * <p>Note that this does not mean that any fine messages will be
472 * logged by default -- each Logger still defaults to INFO.
473 * However, it is sufficient to call {@link Logger#setLevel(Level)}
474 * to adjust the level.
476 private void initializeLogging() {
477 for (Handler handler : Logger.getLogger("").getHandlers()) {
478 if (handler instanceof ConsoleHandler) {
479 handler.setLevel(Level.FINEST);
484 ServerState getServerState() {
485 return serverState;