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
.labs
.servers
.dev
.LocalServersService
;
8 import com
.google
.appengine
.tools
.development
.EnvironmentVariableChecker
.MismatchReportingPolicy
;
9 import com
.google
.appengine
.tools
.info
.SdkInfo
;
10 import com
.google
.apphosting
.api
.ApiProxy
;
11 import com
.google
.apphosting
.api
.ApiProxy
.Environment
;
12 import com
.google
.apphosting
.utils
.config
.AppEngineConfigException
;
13 import com
.google
.apphosting
.utils
.config
.AppEngineWebXmlReader
;
14 import com
.google
.apphosting
.utils
.config
.EarHelper
;
15 import com
.google
.common
.base
.Joiner
;
16 import com
.google
.common
.collect
.ImmutableMap
;
17 import com
.google
.common
.collect
.ImmutableSet
;
20 import java
.lang
.reflect
.Field
;
21 import java
.lang
.reflect
.Method
;
22 import java
.net
.BindException
;
23 import java
.util
.HashMap
;
25 import java
.util
.TimeZone
;
26 import java
.util
.concurrent
.ConcurrentHashMap
;
27 import java
.util
.logging
.ConsoleHandler
;
28 import java
.util
.logging
.Handler
;
29 import java
.util
.logging
.Level
;
30 import java
.util
.logging
.Logger
;
33 * {@code DevAppServer} launches a local Jetty server (by default) with a single
34 * hosted web application. It can be invoked from the command-line by
35 * providing the path to the directory in which the application resides as the
39 class DevAppServerImpl
implements DevAppServer
{
40 public static final String SERVERS_FILTER_HELPER_PROPERTY
=
41 "com.google.appengine.tools.development.servers_filter_helper";
42 private static final Logger logger
= Logger
.getLogger(DevAppServerImpl
.class.getName());
44 private final ApplicationConfigurationManager applicationConfigurationManager
;
45 private final Servers servers
;
46 private Map
<String
, String
> serviceProperties
= new HashMap
<String
, String
>();
47 private final Map
<String
, Object
> containerConfigProperties
;
49 enum ServerState
{ INITIALIZING
, RUNNING
, STOPPING
, SHUTDOWN
}
52 * The current state of the server.
54 private ServerState serverState
= ServerState
.INITIALIZING
;
57 * Contains the backend servers configured as part of the "Servers" feature.
58 * Each backend server is started on a separate port and keep their own
59 * internal state. Memcache, datastore, and other API services are shared by
60 * all servers, including the "main" server.
62 private final BackendServers backendContainer
;
65 * The api proxy we created when we started the web containers. Not initialized until after
66 * {@link #start()} is called.
68 private ApiProxyLocal apiProxyLocal
;
71 * We defer reporting construction time configuration exceptions until
72 * {@link #start()} for compatibility.
74 private final AppEngineConfigException configurationException
;
77 * Constructs a development application server that runs the single
78 * application located in the given directory. The application is configured
79 * via <webXmlLocation> and the {@link com.google.apphosting.utils.config.AppEngineWebXml}
80 * instance returned by the provided {@link AppEngineWebXmlReader}.
82 * @param appDir The location of the application to run.
83 * @param externalResourceDir If not {@code null}, a resource directory external to the appDir.
84 * This will be searched before appDir when looking for resources.
85 * @param webXmlLocation The location of a file whose format complies with
86 * http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd. If {@code null},
87 * defaults to <appDir>/WEB-INF/web.xml
88 * @param appEngineWebXmlLocation The name of the app engine config file. If
89 * {@code null}, defaults to <appDir>/WEB-INF/appengine-web.xml
90 * @param address The address on which to run
91 * @param port The port on which to run
92 * @param useCustomStreamHandler If {@code true}, install
93 * {@link StreamHandlerFactory}. This is "normal" behavior for the dev app
95 * @param containerConfigProperties Additional properties used in the
96 * configuration of the specific container implementation.
98 public DevAppServerImpl(File appDir
, File externalResourceDir
, File webXmlLocation
,
99 File appEngineWebXmlLocation
, String address
, int port
, boolean useCustomStreamHandler
,
100 Map
<String
, Object
> containerConfigProperties
) {
101 String serverInfo
= ContainerUtils
.getServerInfo();
102 if (useCustomStreamHandler
) {
103 StreamHandlerFactory
.install();
105 DevSocketImplFactory
.install();
107 ApplicationConfigurationManager tempManager
= null;
109 if (EarHelper
.isEar(appDir
.getAbsolutePath())) {
110 tempManager
= ApplicationConfigurationManager
.newEarConfigurationManager(appDir
,
111 SdkInfo
.getLocalVersion().getRelease());
113 tempManager
= ApplicationConfigurationManager
.newWarConfigurationManager(
114 appDir
, appEngineWebXmlLocation
, webXmlLocation
, externalResourceDir
,
115 SdkInfo
.getLocalVersion().getRelease());
117 } catch (AppEngineConfigException configurationException
) {
119 applicationConfigurationManager
= null;
120 backendContainer
= null;
121 this.containerConfigProperties
= null;
122 this.configurationException
= configurationException
;
125 this.applicationConfigurationManager
= tempManager
;
126 this.servers
= Servers
.createServers(applicationConfigurationManager
, serverInfo
,
127 externalResourceDir
, address
, port
, this);
128 backendContainer
= BackendServers
.getInstance();
129 DelegatingServersFilterHelper serversFilterHelper
=
130 new DelegatingServersFilterHelper(backendContainer
, servers
);
131 this.containerConfigProperties
=
132 ImmutableMap
.<String
, Object
>builder()
133 .putAll(containerConfigProperties
)
134 .put(SERVERS_FILTER_HELPER_PROPERTY
, serversFilterHelper
)
136 backendContainer
.init(address
,
137 applicationConfigurationManager
.getPrimaryServerConfigurationHandle(),
138 externalResourceDir
, this.containerConfigProperties
, this);
139 configurationException
= null;
143 * Sets the properties that will be used by the local services to
144 * configure themselves. This method must be called before the server
147 * @param properties a, maybe {@code null}, set of properties.
149 * @throws IllegalStateException if the server has already been started.
152 public void setServiceProperties(Map
<String
,String
> properties
) {
153 if (serverState
!= ServerState
.INITIALIZING
) {
154 String msg
= "Cannot set service properties after the server has been started.";
155 throw new IllegalStateException(msg
);
158 if (configurationException
== null) {
159 serviceProperties
= new ConcurrentHashMap
<String
, String
>(properties
);
160 backendContainer
.setServiceProperties(properties
);
164 Map
<String
, String
> getServiceProperties() {
165 return serviceProperties
;
171 * @throws IllegalStateException If the server has already been started or
173 * @throws AppEngineConfigException If no WEB-INF directory can be found or
174 * WEB-INF/appengine-web.xml does not exist.
177 public void start() throws Exception
{
178 if (serverState
!= ServerState
.INITIALIZING
) {
179 throw new IllegalStateException("Cannot start a server that has already been started.");
182 reportDeferredConfigurationException();
185 servers
.configure(containerConfigProperties
);
186 servers
.createConnections();
188 ApiProxyLocalFactory factory
= new ApiProxyLocalFactory();
189 apiProxyLocal
= factory
.create(servers
.getLocalServerEnvironment());
190 setInboundServicesProperty();
191 apiProxyLocal
.setProperties(serviceProperties
);
192 ApiProxy
.setDelegate(apiProxyLocal
);
193 LocalServersService localServersService
=
194 (LocalServersService
) apiProxyLocal
.getService(LocalServersService
.PACKAGE
);
195 localServersService
.setServersControler(servers
);
196 TimeZone currentTimeZone
= null;
199 currentTimeZone
= setServerTimeZone();
202 Environment env
= ApiProxy
.getCurrentEnvironment();
203 env
.getAttributes().put(DEFAULT_VERSION_HOSTNAME
, "localhost:" + getPort());
205 backendContainer
.startupAll(apiProxyLocal
);
206 } catch (BindException ex
) {
207 System
.err
.println();
208 System
.err
.println("************************************************");
209 System
.err
.println("Could not open the requested socket: " + ex
.getMessage());
210 System
.err
.println("Try overriding --address and/or --port.");
213 ApiProxy
.clearEnvironmentForCurrentThread();
214 restoreLocalTimeZone(currentTimeZone
);
216 serverState
= ServerState
.RUNNING
;
217 logger
.info("Dev App Server is now running");
220 public void setInboundServicesProperty() {
221 ImmutableSet
.Builder
<String
> setBuilder
= ImmutableSet
.builder();
222 for (ApplicationConfigurationManager
.ServerConfigurationHandle serverConfigurationHandle
:
223 applicationConfigurationManager
.getServerConfigurationHandles()) {
225 serverConfigurationHandle
.getModule().getAppEngineWebXml().getInboundServices());
228 serviceProperties
.put("appengine.dev.inbound-services",
229 Joiner
.on(",").useForNull("null").join(setBuilder
.build()));
233 * Change the TimeZone for the current thread. By calling this method before
234 * {@link ContainerService#startup()} start}, we set the default TimeZone for the
235 * DevAppServer and all of its related services.
237 * @return the previously installed ThreadLocal TimeZone
239 private TimeZone
setServerTimeZone() {
240 String sysTimeZone
= serviceProperties
.get("appengine.user.timezone.impl");
241 if (sysTimeZone
!= null && sysTimeZone
.trim().length() > 0) {
245 TimeZone utc
= TimeZone
.getTimeZone("UTC");
246 assert utc
.getID().equals("UTC") : "Unable to retrieve the UTC TimeZone";
249 Field f
= TimeZone
.class.getDeclaredField("defaultZoneTL");
250 f
.setAccessible(true);
251 ThreadLocal
<?
> tl
= (ThreadLocal
<?
>) f
.get(null);
252 Method getZone
= ThreadLocal
.class.getMethod("get");
253 TimeZone previousZone
= (TimeZone
) getZone
.invoke(tl
);
254 Method setZone
= ThreadLocal
.class.getMethod("set", Object
.class);
255 setZone
.invoke(tl
, utc
);
257 } catch (Exception e
) {
259 Method getZone
= TimeZone
.class.getDeclaredMethod("getDefaultInAppContext");
260 getZone
.setAccessible(true);
261 TimeZone previousZone
= (TimeZone
) getZone
.invoke(null);
262 Method setZone
= TimeZone
.class.getDeclaredMethod("setDefaultInAppContext", TimeZone
.class);
263 setZone
.setAccessible(true);
264 setZone
.invoke(null, utc
);
266 } catch (Exception ex
) {
267 throw new RuntimeException("Unable to set the TimeZone to UTC", ex
);
273 * Restores the ThreadLocal TimeZone to {@code timeZone}.
275 private void restoreLocalTimeZone(TimeZone timeZone
) {
276 String sysTimeZone
= serviceProperties
.get("appengine.user.timezone.impl");
277 if (sysTimeZone
!= null && sysTimeZone
.trim().length() > 0) {
282 Field f
= TimeZone
.class.getDeclaredField("defaultZoneTL");
283 f
.setAccessible(true);
284 ThreadLocal
<?
> tl
= (ThreadLocal
<?
>) f
.get(null);
285 Method setZone
= ThreadLocal
.class.getMethod("set", Object
.class);
286 setZone
.invoke(tl
, timeZone
);
287 } catch (Exception e
) {
289 Method setZone
= TimeZone
.class.getDeclaredMethod("setDefaultInAppContext", TimeZone
.class);
290 setZone
.setAccessible(true);
291 setZone
.invoke(null, timeZone
);
292 } catch (Exception ex
) {
293 throw new RuntimeException("Unable to restore the previous TimeZone", ex
);
299 public void restart() throws Exception
{
300 if (serverState
!= ServerState
.RUNNING
) {
301 throw new IllegalStateException("Cannot restart a server that is not currently running.");
304 backendContainer
.shutdownAll();
305 servers
.createConnections();
307 backendContainer
.startupAll(apiProxyLocal
);
311 public void shutdown() throws Exception
{
312 if (serverState
!= ServerState
.RUNNING
) {
313 throw new IllegalStateException("Cannot shutdown a server that is not currently running.");
316 backendContainer
.shutdownAll();
317 ApiProxy
.setDelegate(null);
318 apiProxyLocal
= null;
319 serverState
= ServerState
.SHUTDOWN
;
323 public int getPort() {
324 reportDeferredConfigurationException();
325 return servers
.getMainContainer().getPort();
328 protected void reportDeferredConfigurationException() {
329 if (configurationException
!= null) {
330 throw new AppEngineConfigException("Invalid configuration", configurationException
);
335 public AppContext
getAppContext() {
336 reportDeferredConfigurationException();
337 return servers
.getMainContainer().getAppContext();
341 public AppContext
getCurrentAppContext() {
342 AppContext result
= null;
343 Environment env
= ApiProxy
.getCurrentEnvironment();
344 if (env
!= null && env
.getVersionId() != null) {
345 String serverName
= LocalEnvironment
.getServerName(env
.getVersionId());
346 result
= servers
.getServer(serverName
).getMainContainer().getAppContext();
352 public void setThrowOnEnvironmentVariableMismatch(boolean throwOnMismatch
) {
353 if (configurationException
== null) {
354 applicationConfigurationManager
.setEnvironmentVariableMismatchReportingPolicy(
355 throwOnMismatch ? MismatchReportingPolicy
.EXCEPTION
: MismatchReportingPolicy
.LOG
);
360 * We're happy with the default logging behavior, which is to
361 * install a {@link ConsoleHandler} at the root level. The only
362 * issue is that we want its level to be FINEST to be consistent
363 * with our runtime environment.
365 * <p>Note that this does not mean that any fine messages will be
366 * logged by default -- each Logger still defaults to INFO.
367 * However, it is sufficient to call {@link Logger#setLevel(Level)}
368 * to adjust the level.
370 private void initializeLogging() {
371 for (Handler handler
: Logger
.getLogger("").getHandlers()) {
372 if (handler
instanceof ConsoleHandler
) {
373 handler
.setLevel(Level
.FINEST
);
378 ServerState
getServerState() {