App Engine SDK 1.8.4 release.
[gae.git] / java / src / main / com / google / appengine / tools / development / DevAppServerImpl.java
blob814164f2430e9926a3fcc24ebb184c2aed364e87
1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.development;
5 import com.google.appengine.api.labs.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.util.HashMap;
23 import java.util.Map;
24 import java.util.TimeZone;
25 import java.util.concurrent.Callable;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.CountDownLatch;
28 import java.util.concurrent.Executors;
29 import java.util.concurrent.Future;
30 import java.util.concurrent.ScheduledExecutorService;
31 import java.util.concurrent.TimeUnit;
32 import java.util.logging.ConsoleHandler;
33 import java.util.logging.Handler;
34 import java.util.logging.Level;
35 import java.util.logging.Logger;
37 /**
38 * {@code DevAppServer} launches a local Jetty server (by default) with a single
39 * hosted web application. It can be invoked from the command-line by
40 * providing the path to the directory in which the application resides as the
41 * only argument.
44 class DevAppServerImpl implements DevAppServer {
45 public static final String MODULES_FILTER_HELPER_PROPERTY =
46 "com.google.appengine.tools.development.modules_filter_helper";
47 private static final Logger logger = Logger.getLogger(DevAppServerImpl.class.getName());
49 private final ApplicationConfigurationManager applicationConfigurationManager;
50 private final Modules modules;
51 private Map<String, String> serviceProperties = new HashMap<String, String>();
52 private final Map<String, Object> containerConfigProperties;
53 private final int requestedPort;
55 enum ServerState { INITIALIZING, RUNNING, STOPPING, SHUTDOWN }
57 /**
58 * The current state of the server.
60 private ServerState serverState = ServerState.INITIALIZING;
62 /**
63 * Contains the backend servers configured as part of the "Servers" feature.
64 * Each backend server is started on a separate port and keep their own
65 * internal state. Memcache, datastore, and other API services are shared by
66 * all servers, including the "main" server.
68 private final BackendServers backendContainer;
70 /**
71 * The api proxy we created when we started the web containers. Not initialized until after
72 * {@link #start()} is called.
74 private ApiProxyLocal apiProxyLocal;
76 /**
77 * We defer reporting construction time configuration exceptions until
78 * {@link #start()} for compatibility.
80 private final AppEngineConfigException configurationException;
82 /**
83 * Used to schedule the graceful shutdown of the server.
85 private final ScheduledExecutorService shutdownScheduler = Executors.newScheduledThreadPool(1);
87 /**
88 * Latch that we decrement when the server is shutdown or restarted.
89 * Will be {@code null} until the server is started.
91 private CountDownLatch shutdownLatch = null;
93 /**
94 * Constructs a development application server that runs the application located in the given
95 * WAR or EAR directory.
97 * @param appDir The location of the application to run.
98 * @param externalResourceDir If not {@code null}, a resource directory external to the appDir.
99 * This will be searched before appDir when looking for resources.
100 * @param webXmlLocation The location of a file whose format complies with
101 * http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd. If {@code null},
102 * defaults to <appDir>/WEB-INF/web.xml
103 * @param appEngineWebXmlLocation The name of the app engine config file. If
104 * {@code null}, defaults to <appDir>/WEB-INF/appengine-web.xml
105 * @param address The address on which to run
106 * @param port The port on which to run
107 * @param useCustomStreamHandler If {@code true} (typical), install {@link StreamHandlerFactory}.
108 * @param containerConfigProperties Additional properties used in the
109 * configuration of the specific container implementation.
111 public DevAppServerImpl(File appDir, File externalResourceDir, File webXmlLocation,
112 File appEngineWebXmlLocation, String address, int port, boolean useCustomStreamHandler,
113 Map<String, Object> containerConfigProperties) {
114 String serverInfo = ContainerUtils.getServerInfo();
115 if (useCustomStreamHandler) {
116 StreamHandlerFactory.install();
118 DevSocketImplFactory.install();
120 backendContainer = BackendServers.getInstance();
121 requestedPort = port;
122 ApplicationConfigurationManager tempManager = null;
123 File schemaFile = new File(SdkInfo.getSdkRoot(), "docs/appengine-application.xsd");
124 try {
125 if (EarHelper.isEar(appDir.getAbsolutePath())) {
126 tempManager = ApplicationConfigurationManager.newEarConfigurationManager(appDir,
127 SdkInfo.getLocalVersion().getRelease(), schemaFile);
128 String contextRootWarning =
129 "Ignoring application.xml context-root element, for details see "
130 + "https://developers.google.com/appengine/docs/java/modules/#config";
131 logger.info(contextRootWarning);
132 } else {
133 tempManager = ApplicationConfigurationManager.newWarConfigurationManager(
134 appDir, appEngineWebXmlLocation, webXmlLocation, externalResourceDir,
135 SdkInfo.getLocalVersion().getRelease());
137 } catch (AppEngineConfigException configurationException) {
138 modules = null;
139 applicationConfigurationManager = null;
140 this.containerConfigProperties = null;
141 this.configurationException = configurationException;
142 return;
144 this.applicationConfigurationManager = tempManager;
145 this.modules = Modules.createModules(applicationConfigurationManager, serverInfo,
146 externalResourceDir, address, this);
147 DelegatingModulesFilterHelper modulesFilterHelper =
148 new DelegatingModulesFilterHelper(backendContainer, modules);
149 this.containerConfigProperties =
150 ImmutableMap.<String, Object>builder()
151 .putAll(containerConfigProperties)
152 .put(MODULES_FILTER_HELPER_PROPERTY, modulesFilterHelper)
153 .put(AbstractContainerService.PORT_MAPPING_PROVIDER_PROP, backendContainer)
154 .build();
155 backendContainer.init(address,
156 applicationConfigurationManager.getPrimaryModuleConfigurationHandle(),
157 externalResourceDir, this.containerConfigProperties, this);
158 configurationException = null;
162 * Sets the properties that will be used by the local services to
163 * configure themselves. This method must be called before the server
164 * has been started.
166 * @param properties a, maybe {@code null}, set of properties.
168 * @throws IllegalStateException if the server has already been started.
170 @Override
171 public void setServiceProperties(Map<String, String> properties) {
172 if (serverState != ServerState.INITIALIZING) {
173 String msg = "Cannot set service properties after the server has been started.";
174 throw new IllegalStateException(msg);
177 if (configurationException == null) {
178 serviceProperties = new ConcurrentHashMap<String, String>(properties);
179 if (requestedPort != 0) {
180 DevAppServerPortPropertyHelper.setPort(modules.getMainModule().getModuleName(),
181 requestedPort, serviceProperties);
183 backendContainer.setServiceProperties(properties);
184 DevAppServerDatastorePropertyHelper.setDefaultProperties(serviceProperties);
188 Map<String, String> getServiceProperties() {
189 return serviceProperties;
193 * Starts the server.
195 * @throws IllegalStateException If the server has already been started or
196 * shutdown.
197 * @throws AppEngineConfigException If no WEB-INF directory can be found or
198 * WEB-INF/appengine-web.xml does not exist.
199 * @return a latch that will be decremented to zero when the server is shutdown.
201 @Override
202 public CountDownLatch start() throws Exception {
203 if (serverState != ServerState.INITIALIZING) {
204 throw new IllegalStateException("Cannot start a server that has already been started.");
207 reportDeferredConfigurationException();
209 initializeLogging();
210 modules.configure(containerConfigProperties);
211 try {
212 modules.createConnections();
213 } catch (BindException ex) {
214 System.err.println();
215 System.err.println("************************************************");
216 System.err.println("Could not open the requested socket: " + ex.getMessage());
217 System.err.println("Try overriding --address and/or --port.");
218 System.exit(2);
221 ApiProxyLocalFactory factory = new ApiProxyLocalFactory();
222 apiProxyLocal = factory.create(modules.getLocalServerEnvironment());
223 setInboundServicesProperty();
224 apiProxyLocal.setProperties(serviceProperties);
225 ApiProxy.setDelegate(apiProxyLocal);
226 LocalModulesService localModulesService =
227 (LocalModulesService) apiProxyLocal.getService(LocalModulesService.PACKAGE);
228 localModulesService.setModulesController(modules);
229 TimeZone currentTimeZone = null;
230 try {
231 currentTimeZone = setServerTimeZone();
232 backendContainer.configureAll(apiProxyLocal);
233 modules.startup();
234 Module mainServer = modules.getMainModule();
235 Map<String, String> portMapping = backendContainer.getPortMapping();
236 AbstractContainerService.installLocalInitializationEnvironment(
237 mainServer.getMainContainer().getAppEngineWebXmlConfig(), LocalEnvironment.MAIN_INSTANCE,
238 getPort(), getPort(), null, -1, portMapping);
239 backendContainer.startupAll(apiProxyLocal);
240 } finally {
241 ApiProxy.clearEnvironmentForCurrentThread();
242 restoreLocalTimeZone(currentTimeZone);
244 shutdownLatch = new CountDownLatch(1);
245 serverState = ServerState.RUNNING;
246 logger.info("Dev App Server is now running");
247 return shutdownLatch;
250 public void setInboundServicesProperty() {
251 ImmutableSet.Builder<String> setBuilder = ImmutableSet.builder();
252 for (ApplicationConfigurationManager.ModuleConfigurationHandle moduleConfigurationHandle :
253 applicationConfigurationManager.getModuleConfigurationHandles()) {
254 setBuilder.addAll(
255 moduleConfigurationHandle.getModule().getAppEngineWebXml().getInboundServices());
258 serviceProperties.put("appengine.dev.inbound-services",
259 Joiner.on(",").useForNull("null").join(setBuilder.build()));
263 * Change the TimeZone for the current thread. By calling this method before
264 * {@link ContainerService#startup()} start}, we set the default TimeZone for the
265 * DevAppServer and all of its related services.
267 * @return the previously installed ThreadLocal TimeZone
269 private TimeZone setServerTimeZone() {
270 String sysTimeZone = serviceProperties.get("appengine.user.timezone.impl");
271 if (sysTimeZone != null && sysTimeZone.trim().length() > 0) {
272 return null;
275 TimeZone utc = TimeZone.getTimeZone("UTC");
276 assert utc.getID().equals("UTC") : "Unable to retrieve the UTC TimeZone";
278 try {
279 Field f = TimeZone.class.getDeclaredField("defaultZoneTL");
280 f.setAccessible(true);
281 ThreadLocal<?> tl = (ThreadLocal<?>) f.get(null);
282 Method getZone = ThreadLocal.class.getMethod("get");
283 TimeZone previousZone = (TimeZone) getZone.invoke(tl);
284 Method setZone = ThreadLocal.class.getMethod("set", Object.class);
285 setZone.invoke(tl, utc);
286 return previousZone;
287 } catch (Exception e) {
288 try {
289 Method getZone = TimeZone.class.getDeclaredMethod("getDefaultInAppContext");
290 getZone.setAccessible(true);
291 TimeZone previousZone = (TimeZone) getZone.invoke(null);
292 Method setZone = TimeZone.class.getDeclaredMethod("setDefaultInAppContext", TimeZone.class);
293 setZone.setAccessible(true);
294 setZone.invoke(null, utc);
295 return previousZone;
296 } catch (Exception ex) {
297 throw new RuntimeException("Unable to set the TimeZone to UTC", ex);
303 * Restores the ThreadLocal TimeZone to {@code timeZone}.
305 private void restoreLocalTimeZone(TimeZone timeZone) {
306 String sysTimeZone = serviceProperties.get("appengine.user.timezone.impl");
307 if (sysTimeZone != null && sysTimeZone.trim().length() > 0) {
308 return;
311 try {
312 Field f = TimeZone.class.getDeclaredField("defaultZoneTL");
313 f.setAccessible(true);
314 ThreadLocal<?> tl = (ThreadLocal<?>) f.get(null);
315 Method setZone = ThreadLocal.class.getMethod("set", Object.class);
316 setZone.invoke(tl, timeZone);
317 } catch (Exception e) {
318 try {
319 Method setZone = TimeZone.class.getDeclaredMethod("setDefaultInAppContext", TimeZone.class);
320 setZone.setAccessible(true);
321 setZone.invoke(null, timeZone);
322 } catch (Exception ex) {
323 throw new RuntimeException("Unable to restore the previous TimeZone", ex);
328 @Override
329 public CountDownLatch restart() throws Exception {
330 if (serverState != ServerState.RUNNING) {
331 throw new IllegalStateException("Cannot restart a server that is not currently running.");
333 modules.shutdown();
334 backendContainer.shutdownAll();
335 shutdownLatch.countDown();
336 modules.createConnections();
337 backendContainer.configureAll(apiProxyLocal);
338 modules.startup();
339 backendContainer.startupAll(apiProxyLocal);
340 shutdownLatch = new CountDownLatch(1);
341 return shutdownLatch;
344 @Override
345 public void shutdown() throws Exception {
346 if (serverState != ServerState.RUNNING) {
347 throw new IllegalStateException("Cannot shutdown a server that is not currently running.");
349 modules.shutdown();
350 backendContainer.shutdownAll();
351 ApiProxy.setDelegate(null);
352 apiProxyLocal = null;
353 serverState = ServerState.SHUTDOWN;
354 shutdownLatch.countDown();
357 @Override
358 public void gracefulShutdown() throws IllegalStateException {
360 AccessController.doPrivileged(new PrivilegedAction<Future<Void>>() {
361 @Override
362 public Future<Void> run() {
363 return shutdownScheduler.schedule(new Callable<Void>() {
364 @Override
365 public Void call() throws Exception {
366 shutdown();
367 return null;
369 }, 1000, TimeUnit.MILLISECONDS);
374 @Override
375 public int getPort() {
376 reportDeferredConfigurationException();
377 return modules.getMainModule().getMainContainer().getPort();
380 protected void reportDeferredConfigurationException() {
381 if (configurationException != null) {
382 throw new AppEngineConfigException("Invalid configuration", configurationException);
386 @Override
387 public AppContext getAppContext() {
388 reportDeferredConfigurationException();
389 return modules.getMainModule().getMainContainer().getAppContext();
392 @Override
393 public AppContext getCurrentAppContext() {
394 AppContext result = null;
395 Environment env = ApiProxy.getCurrentEnvironment();
396 if (env != null && env.getVersionId() != null) {
397 String moduleName = LocalEnvironment.getModuleName(env.getVersionId());
398 result = modules.getModule(moduleName).getMainContainer().getAppContext();
400 return result;
403 @Override
404 public void setThrowOnEnvironmentVariableMismatch(boolean throwOnMismatch) {
405 if (configurationException == null) {
406 applicationConfigurationManager.setEnvironmentVariableMismatchReportingPolicy(
407 throwOnMismatch ? MismatchReportingPolicy.EXCEPTION : MismatchReportingPolicy.LOG);
412 * We're happy with the default logging behavior, which is to
413 * install a {@link ConsoleHandler} at the root level. The only
414 * issue is that we want its level to be FINEST to be consistent
415 * with our runtime environment.
417 * <p>Note that this does not mean that any fine messages will be
418 * logged by default -- each Logger still defaults to INFO.
419 * However, it is sufficient to call {@link Logger#setLevel(Level)}
420 * to adjust the level.
422 private void initializeLogging() {
423 for (Handler handler : Logger.getLogger("").getHandlers()) {
424 if (handler instanceof ConsoleHandler) {
425 handler.setLevel(Level.FINEST);
430 ServerState getServerState() {
431 return serverState;