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
;
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
;
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
;
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
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
}
58 * The current state of the server.
60 private ServerState serverState
= ServerState
.INITIALIZING
;
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
;
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
;
77 * We defer reporting construction time configuration exceptions until
78 * {@link #start()} for compatibility.
80 private final AppEngineConfigException configurationException
;
83 * Used to schedule the graceful shutdown of the server.
85 private final ScheduledExecutorService shutdownScheduler
= Executors
.newScheduledThreadPool(1);
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;
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");
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
);
133 tempManager
= ApplicationConfigurationManager
.newWarConfigurationManager(
134 appDir
, appEngineWebXmlLocation
, webXmlLocation
, externalResourceDir
,
135 SdkInfo
.getLocalVersion().getRelease());
137 } catch (AppEngineConfigException configurationException
) {
139 applicationConfigurationManager
= null;
140 this.containerConfigProperties
= null;
141 this.configurationException
= configurationException
;
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
)
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
166 * @param properties a, maybe {@code null}, set of properties.
168 * @throws IllegalStateException if the server has already been started.
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
;
195 * @throws IllegalStateException If the server has already been started or
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.
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();
210 modules
.configure(containerConfigProperties
);
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.");
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;
231 currentTimeZone
= setServerTimeZone();
232 backendContainer
.configureAll(apiProxyLocal
);
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
);
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()) {
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) {
275 TimeZone utc
= TimeZone
.getTimeZone("UTC");
276 assert utc
.getID().equals("UTC") : "Unable to retrieve the UTC TimeZone";
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
);
287 } catch (Exception e
) {
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
);
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) {
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
) {
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
);
329 public CountDownLatch
restart() throws Exception
{
330 if (serverState
!= ServerState
.RUNNING
) {
331 throw new IllegalStateException("Cannot restart a server that is not currently running.");
334 backendContainer
.shutdownAll();
335 shutdownLatch
.countDown();
336 modules
.createConnections();
337 backendContainer
.configureAll(apiProxyLocal
);
339 backendContainer
.startupAll(apiProxyLocal
);
340 shutdownLatch
= new CountDownLatch(1);
341 return shutdownLatch
;
345 public void shutdown() throws Exception
{
346 if (serverState
!= ServerState
.RUNNING
) {
347 throw new IllegalStateException("Cannot shutdown a server that is not currently running.");
350 backendContainer
.shutdownAll();
351 ApiProxy
.setDelegate(null);
352 apiProxyLocal
= null;
353 serverState
= ServerState
.SHUTDOWN
;
354 shutdownLatch
.countDown();
358 public void gracefulShutdown() throws IllegalStateException
{
360 AccessController
.doPrivileged(new PrivilegedAction
<Future
<Void
>>() {
362 public Future
<Void
> run() {
363 return shutdownScheduler
.schedule(new Callable
<Void
>() {
365 public Void
call() throws Exception
{
369 }, 1000, TimeUnit
.MILLISECONDS
);
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
);
387 public AppContext
getAppContext() {
388 reportDeferredConfigurationException();
389 return modules
.getMainModule().getMainContainer().getAppContext();
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();
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() {