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
.tools
.development
.EnvironmentVariableChecker
.MismatchReportingPolicy
;
8 import com
.google
.appengine
.tools
.info
.SdkInfo
;
9 import com
.google
.apphosting
.api
.ApiProxy
;
10 import com
.google
.apphosting
.api
.ApiProxy
.Environment
;
11 import com
.google
.apphosting
.utils
.config
.AppEngineConfigException
;
12 import com
.google
.apphosting
.utils
.config
.AppEngineWebXml
;
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
.ImmutableList
;
17 import com
.google
.common
.collect
.ImmutableMap
;
18 import com
.google
.common
.collect
.ImmutableSet
;
21 import java
.lang
.reflect
.Field
;
22 import java
.lang
.reflect
.Method
;
23 import java
.net
.BindException
;
24 import java
.util
.HashMap
;
25 import java
.util
.List
;
27 import java
.util
.TimeZone
;
28 import java
.util
.concurrent
.ConcurrentHashMap
;
29 import java
.util
.logging
.ConsoleHandler
;
30 import java
.util
.logging
.Handler
;
31 import java
.util
.logging
.Level
;
32 import java
.util
.logging
.Logger
;
35 * {@code DevAppServer} launches a local Jetty server (by default) with a single
36 * hosted web application. It can be invoked from the command-line by
37 * providing the path to the directory in which the application resides as the
41 class DevAppServerImpl
implements DevAppServer
{
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
>();
48 enum ServerState
{ INITIALIZING
, RUNNING
, STOPPING
, SHUTDOWN
}
51 * The current state of the server.
53 private ServerState serverState
= ServerState
.INITIALIZING
;
56 * Contains the backend servers configured as part of the "Servers" feature.
57 * Each backend server is started on a separate port and keep their own
58 * internal state. Memcache, datastore, and other API services are shared by
59 * all servers, including the "main" server.
61 private final BackendContainer backendContainer
;
64 * The api proxy we created when we started the web containers. Not initialized until after
65 * {@link #start()} is called.
67 private ApiProxyLocal apiProxyLocal
;
70 * We defer reporting construction time configuration exceptions until
71 * {@link #start()} for compatibility.
73 private final AppEngineConfigException configurationException
;
76 * Constructs a development application server that runs the single
77 * application located in the given directory. The application is configured
78 * via <webXmlLocation> and the {@link AppEngineWebXml}
79 * instance returned by the provided {@link AppEngineWebXmlReader}.
81 * @param appDir The location of the application to run.
82 * @param externalResourceDir If not {@code null}, a resource directory external to the appDir.
83 * This will be searched before appDir when looking for resources.
84 * @param webXmlLocation The location of a file whose format complies with
85 * http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd. If {@code null},
86 * defaults to <appDir>/WEB-INF/web.xml
87 * @param appEngineWebXmlLocation The name of the app engine config file. If
88 * {@code null}, defaults to <appDir>/WEB-INF/appengine-web.xml
89 * @param address The address on which to run
90 * @param port The port on which to run
91 * @param useCustomStreamHandler If {@code true}, install
92 * {@link StreamHandlerFactory}. This is "normal" behavior for the dev app
94 * @param containerConfigProperties Additional properties used in the
95 * configuration of the specific container implementation.
97 public DevAppServerImpl(File appDir
, File externalResourceDir
, File webXmlLocation
,
98 File appEngineWebXmlLocation
, String address
, int port
, boolean useCustomStreamHandler
,
99 Map
<String
, Object
> containerConfigProperties
) {
100 String serverInfo
= ContainerUtils
.getServerInfo();
101 if (useCustomStreamHandler
) {
102 StreamHandlerFactory
.install();
104 DevSocketImplFactory
.install();
106 ApplicationConfigurationManager tempManager
= null;
108 if (EarHelper
.isEar(appDir
.getAbsolutePath())) {
109 tempManager
= ApplicationConfigurationManager
.newEarConfigurationManager(appDir
,
110 SdkInfo
.getLocalVersion().getRelease());
112 tempManager
= ApplicationConfigurationManager
.newWarConfigurationManager(
113 appDir
, appEngineWebXmlLocation
, webXmlLocation
, externalResourceDir
,
114 SdkInfo
.getLocalVersion().getRelease());
116 } catch (AppEngineConfigException configurationException
) {
118 applicationConfigurationManager
= null;
119 backendContainer
= null;
120 this.configurationException
= configurationException
;
123 this.applicationConfigurationManager
= tempManager
;
124 this.servers
= Servers
.createServers(applicationConfigurationManager
, serverInfo
,
125 externalResourceDir
, address
, port
, containerConfigProperties
, this);
126 backendContainer
= BackendServers
.getInstance();
127 backendContainer
.init(address
,
128 applicationConfigurationManager
.getPrimaryServerConfigurationHandle(),
129 externalResourceDir
, containerConfigProperties
, this);
130 configurationException
= null;
134 * Sets the properties that will be used by the local services to
135 * configure themselves. This method must be called before the server
138 * @param properties a, maybe {@code null}, set of properties.
140 * @throws IllegalStateException if the server has already been started.
143 public void setServiceProperties(Map
<String
,String
> properties
) {
144 if (serverState
!= ServerState
.INITIALIZING
) {
145 String msg
= "Cannot set service properties after the server has been started.";
146 throw new IllegalStateException(msg
);
149 if (configurationException
== null) {
150 serviceProperties
= new ConcurrentHashMap
<String
, String
>(properties
);
151 backendContainer
.setServiceProperties(properties
);
155 Map
<String
, String
> getServiceProperties() {
156 return serviceProperties
;
162 * @throws IllegalStateException If the server has already been started or
164 * @throws AppEngineConfigException If no WEB-INF directory can be found or
165 * WEB-INF/appengine-web.xml does not exist.
168 public void start() throws Exception
{
169 if (serverState
!= ServerState
.INITIALIZING
) {
170 throw new IllegalStateException("Cannot start a server that has already been started.");
173 reportDeferredConfigurationException();
177 servers
.createConnections();
179 ApiProxyLocalFactory factory
= new ApiProxyLocalFactory();
180 apiProxyLocal
= factory
.create(servers
.getLocalServerEnvironment());
181 setInboundServicesProperty();
182 apiProxyLocal
.setProperties(serviceProperties
);
183 ApiProxy
.setDelegate(apiProxyLocal
);
185 TimeZone currentTimeZone
= null;
188 currentTimeZone
= setServerTimeZone();
191 Environment env
= ApiProxy
.getCurrentEnvironment();
192 env
.getAttributes().put(DEFAULT_VERSION_HOSTNAME
, "localhost:" + getPort());
194 backendContainer
.startupAll(apiProxyLocal
);
195 } catch (BindException ex
) {
196 System
.err
.println();
197 System
.err
.println("************************************************");
198 System
.err
.println("Could not open the requested socket: " + ex
.getMessage());
199 System
.err
.println("Try overriding --address and/or --port.");
202 ApiProxy
.clearEnvironmentForCurrentThread();
203 restoreLocalTimeZone(currentTimeZone
);
205 serverState
= ServerState
.RUNNING
;
206 logger
.info("Dev App Server is now running");
209 public void setInboundServicesProperty() {
210 ImmutableSet
.Builder
<String
> setBuilder
= ImmutableSet
.builder();
211 for (ApplicationConfigurationManager
.ServerConfigurationHandle serverConfigurationHandle
:
212 applicationConfigurationManager
.getServerConfigurationHandles()) {
214 serverConfigurationHandle
.getModule().getAppEngineWebXml().getInboundServices());
217 serviceProperties
.put("appengine.dev.inbound-services",
218 Joiner
.on(",").useForNull("null").join(setBuilder
.build()));
222 * Change the TimeZone for the current thread. By calling this method before
223 * {@link ContainerService#startup()} start}, we set the default TimeZone for the
224 * DevAppServer and all of its related services.
226 * @return the previously installed ThreadLocal TimeZone
228 private TimeZone
setServerTimeZone() {
229 String sysTimeZone
= serviceProperties
.get("appengine.user.timezone.impl");
230 if (sysTimeZone
!= null && sysTimeZone
.trim().length() > 0) {
234 TimeZone utc
= TimeZone
.getTimeZone("UTC");
235 assert utc
.getID().equals("UTC") : "Unable to retrieve the UTC TimeZone";
238 Field f
= TimeZone
.class.getDeclaredField("defaultZoneTL");
239 f
.setAccessible(true);
240 ThreadLocal
<?
> tl
= (ThreadLocal
<?
>) f
.get(null);
241 Method getZone
= ThreadLocal
.class.getMethod("get");
242 TimeZone previousZone
= (TimeZone
) getZone
.invoke(tl
);
243 Method setZone
= ThreadLocal
.class.getMethod("set", Object
.class);
244 setZone
.invoke(tl
, utc
);
246 } catch (Exception e
) {
248 Method getZone
= TimeZone
.class.getDeclaredMethod("getDefaultInAppContext");
249 getZone
.setAccessible(true);
250 TimeZone previousZone
= (TimeZone
) getZone
.invoke(null);
251 Method setZone
= TimeZone
.class.getDeclaredMethod("setDefaultInAppContext", TimeZone
.class);
252 setZone
.setAccessible(true);
253 setZone
.invoke(null, utc
);
255 } catch (Exception ex
) {
256 throw new RuntimeException("Unable to set the TimeZone to UTC", ex
);
262 * Restores the ThreadLocal TimeZone to {@code timeZone}.
264 private void restoreLocalTimeZone(TimeZone timeZone
) {
265 String sysTimeZone
= serviceProperties
.get("appengine.user.timezone.impl");
266 if (sysTimeZone
!= null && sysTimeZone
.trim().length() > 0) {
271 Field f
= TimeZone
.class.getDeclaredField("defaultZoneTL");
272 f
.setAccessible(true);
273 ThreadLocal
<?
> tl
= (ThreadLocal
<?
>) f
.get(null);
274 Method setZone
= ThreadLocal
.class.getMethod("set", Object
.class);
275 setZone
.invoke(tl
, timeZone
);
276 } catch (Exception e
) {
278 Method setZone
= TimeZone
.class.getDeclaredMethod("setDefaultInAppContext", TimeZone
.class);
279 setZone
.setAccessible(true);
280 setZone
.invoke(null, timeZone
);
281 } catch (Exception ex
) {
282 throw new RuntimeException("Unable to restore the previous TimeZone", ex
);
288 public void restart() throws Exception
{
289 if (serverState
!= ServerState
.RUNNING
) {
290 throw new IllegalStateException("Cannot restart a server that is not currently running.");
293 backendContainer
.shutdownAll();
294 servers
.createConnections();
296 backendContainer
.startupAll(apiProxyLocal
);
300 public void shutdown() throws Exception
{
301 if (serverState
!= ServerState
.RUNNING
) {
302 throw new IllegalStateException("Cannot shutdown a server that is not currently running.");
305 backendContainer
.shutdownAll();
306 ApiProxy
.setDelegate(null);
307 apiProxyLocal
= null;
308 serverState
= ServerState
.SHUTDOWN
;
312 public int getPort() {
313 reportDeferredConfigurationException();
314 return servers
.getMainContainer().getPort();
317 protected void reportDeferredConfigurationException() {
318 if (configurationException
!= null) {
319 throw new AppEngineConfigException("Invalid configuration", configurationException
);
324 public AppContext
getAppContext() {
325 reportDeferredConfigurationException();
326 return servers
.getMainContainer().getAppContext();
330 public AppContext
getCurrentAppContext() {
331 AppContext result
= null;
332 Environment env
= ApiProxy
.getCurrentEnvironment();
333 if (env
!= null && env
.getVersionId() != null) {
334 String serverName
= LocalEnvironment
.getServerName(env
.getVersionId());
335 result
= servers
.getServer(serverName
).getMainContainer().getAppContext();
341 public void setThrowOnEnvironmentVariableMismatch(boolean throwOnMismatch
) {
342 if (configurationException
== null) {
343 applicationConfigurationManager
.setEnvironmentVariableMismatchReportingPolicy(
344 throwOnMismatch ? MismatchReportingPolicy
.EXCEPTION
: MismatchReportingPolicy
.LOG
);
349 * We're happy with the default logging behavior, which is to
350 * install a {@link ConsoleHandler} at the root level. The only
351 * issue is that we want its level to be FINEST to be consistent
352 * with our runtime environment.
354 * <p>Note that this does not mean that any fine messages will be
355 * logged by default -- each Logger still defaults to INFO.
356 * However, it is sufficient to call {@link Logger#setLevel(Level)}
357 * to adjust the level.
359 private void initializeLogging() {
360 for (Handler handler
: Logger
.getLogger("").getHandlers()) {
361 if (handler
instanceof ConsoleHandler
) {
362 handler
.setLevel(Level
.FINEST
);
367 ServerState
getServerState() {
371 static class Servers
{
372 private final List
<Server
> servers
;
373 private final Map
<String
, Server
> serverNameToServerMap
;
375 static Servers
createServers(ApplicationConfigurationManager applicationConfigurationManager
,
376 String serverInfo
, File externalResourceDir
, String address
, int port
, Map
<String
,
377 Object
> containerConfigProperties
, DevAppServerImpl devAppServer
) {
379 ImmutableList
.Builder
<Server
> builder
= ImmutableList
.builder();
380 LocalServerEnvironment mainEnvironment
= null;
381 for (ApplicationConfigurationManager
.ServerConfigurationHandle serverConfigurationHandle
:
382 applicationConfigurationManager
.getServerConfigurationHandles()) {
383 AppEngineWebXml appEngineWebXml
=
384 serverConfigurationHandle
.getModule().getAppEngineWebXml();
385 Server server
= null;
386 if (!appEngineWebXml
.getBasicScaling().isEmpty()) {
387 throw new AppEngineConfigException("Basic scaling servers are currently not supported");
388 } else if (!appEngineWebXml
.getManualScaling().isEmpty()) {
389 server
= new ManualServer(serverConfigurationHandle
, serverInfo
, address
,
390 containerConfigProperties
, devAppServer
, appEngineWebXml
);
392 server
= new AutomaticServer(serverConfigurationHandle
, serverInfo
, externalResourceDir
,
393 address
, port
, containerConfigProperties
, devAppServer
);
398 externalResourceDir
= null;
400 return new Servers(builder
.build());
403 void shutdown() throws Exception
{
404 for (Server server
: servers
) {
409 void configure() throws Exception
{
410 for (Server server
: servers
) {
415 void createConnections() throws Exception
{
416 for (Server server
: servers
) {
417 server
.createConnection();
421 void startup() throws Exception
{
422 for (Server server
: servers
) {
427 ContainerService
getMainContainer() {
428 return servers
.get(0).getMainContainer();
431 private Servers(List
<Server
> servers
) {
432 if (servers
.size() < 1) {
433 throw new IllegalArgumentException("servers must not be empty.");
435 this.servers
= servers
;
437 ImmutableMap
.Builder
<String
, Server
> mapBuilder
= ImmutableMap
.builder();
438 for (Server server
: this.servers
) {
439 mapBuilder
.put(server
.getServerName(), server
);
441 serverNameToServerMap
= mapBuilder
.build();
444 LocalServerEnvironment
getLocalServerEnvironment() {
445 return servers
.get(0).getLocalServerEnvironment();
448 Server
getServer(String serverName
) {
449 return serverNameToServerMap
.get(serverName
);