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
;
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
;
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
;
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
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
}
60 * The current state of the server.
62 private ServerState serverState
= ServerState
.INITIALIZING
;
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
;
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
;
79 * We defer reporting construction time configuration exceptions until
80 * {@link #start()} for compatibility.
82 private final AppEngineConfigException configurationException
;
85 * Used to schedule the graceful shutdown of the server.
87 private final ScheduledExecutorService shutdownScheduler
= Executors
.newScheduledThreadPool(1);
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;
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");
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
);
135 tempManager
= ApplicationConfigurationManager
.newWarConfigurationManager(
136 appDir
, appEngineWebXmlLocation
, webXmlLocation
, externalResourceDir
,
137 SdkInfo
.getLocalVersion().getRelease());
139 } catch (AppEngineConfigException configurationException
) {
141 applicationConfigurationManager
= null;
142 this.containerConfigProperties
= null;
143 this.configurationException
= configurationException
;
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
)
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
167 * @param properties a, maybe {@code null}, set of properties.
169 * @throws IllegalStateException if the server has already been started.
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
);
190 public Map
<String
, String
> getServiceProperties() {
191 return serviceProperties
;
197 * @throws IllegalStateException If the server has already been started or
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.
204 public CountDownLatch
start() throws Exception
{
206 return AccessController
.doPrivileged(new PrivilegedExceptionAction
<CountDownLatch
>() {
207 @Override public CountDownLatch
run() throws Exception
{
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();
225 modules
.configure(containerConfigProperties
);
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.");
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;
247 currentTimeZone
= setServerTimeZone();
248 backendContainer
.configureAll(apiProxyLocal
);
249 modules
.setApiProxyDelegate(apiProxyLocal
);
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();
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()) {
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) {
305 TimeZone utc
= TimeZone
.getTimeZone("UTC");
306 assert utc
.getID().equals("UTC") : "Unable to retrieve the UTC TimeZone";
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
);
317 } catch (Exception e
) {
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
);
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)");
335 * Restores the ThreadLocal TimeZone to {@code timeZone}.
337 private void restoreLocalTimeZone(TimeZone timeZone
) {
338 if (timeZone
== null) {
342 String sysTimeZone
= serviceProperties
.get("appengine.user.timezone.impl");
343 if (sysTimeZone
!= null && sysTimeZone
.trim().length() > 0) {
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
) {
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
);
365 public CountDownLatch
restart() throws Exception
{
366 if (serverState
!= ServerState
.RUNNING
) {
367 throw new IllegalStateException("Cannot restart a server that is not currently running.");
370 return AccessController
.doPrivileged(new PrivilegedExceptionAction
<CountDownLatch
>() {
371 @Override public CountDownLatch
run() throws Exception
{
373 backendContainer
.shutdownAll();
374 shutdownLatch
.countDown();
375 modules
.createConnections();
376 backendContainer
.configureAll(apiProxyLocal
);
377 modules
.setApiProxyDelegate(apiProxyLocal
);
379 backendContainer
.startupAll();
380 shutdownLatch
= new CountDownLatch(1);
381 return shutdownLatch
;
384 } catch (PrivilegedActionException e
) {
385 throw e
.getException();
390 public void shutdown() throws Exception
{
391 if (serverState
!= ServerState
.RUNNING
) {
392 throw new IllegalStateException("Cannot shutdown a server that is not currently running.");
395 AccessController
.doPrivileged(new PrivilegedExceptionAction
<Void
>() {
396 @Override public Void
run() throws Exception
{
398 backendContainer
.shutdownAll();
399 ApiProxy
.setDelegate(null);
400 apiProxyLocal
= null;
401 serverState
= ServerState
.SHUTDOWN
;
402 shutdownLatch
.countDown();
406 } catch (PrivilegedActionException e
) {
407 throw e
.getException();
412 public void gracefulShutdown() throws IllegalStateException
{
414 AccessController
.doPrivileged(new PrivilegedAction
<Future
<Void
>>() {
416 public Future
<Void
> run() {
417 return shutdownScheduler
.schedule(new Callable
<Void
>() {
419 public Void
call() throws Exception
{
423 }, 1000, TimeUnit
.MILLISECONDS
);
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
);
441 public AppContext
getAppContext() {
442 reportDeferredConfigurationException();
443 return modules
.getMainModule().getMainContainer().getAppContext();
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();
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() {