1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.tools
.development
;
5 import static com
.google
.appengine
.tools
.development
.ContainerService
.EnvironmentVariableMismatchSeverity
.ERROR
;
6 import static com
.google
.appengine
.tools
.development
.ContainerService
.EnvironmentVariableMismatchSeverity
.WARNING
;
7 import static com
.google
.appengine
.tools
.development
.LocalEnvironment
.DEFAULT_VERSION_HOSTNAME
;
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
;
16 import java
.lang
.reflect
.Field
;
17 import java
.lang
.reflect
.Method
;
18 import java
.net
.BindException
;
19 import java
.util
.HashMap
;
21 import java
.util
.TimeZone
;
22 import java
.util
.logging
.ConsoleHandler
;
23 import java
.util
.logging
.Handler
;
24 import java
.util
.logging
.Level
;
25 import java
.util
.logging
.Logger
;
28 * {@code DevAppServer} launches a local Jetty server (by default) with a single
29 * hosted web application. It can be invoked from the command-line by
30 * providing the path to the directory in which the application resides as the
34 class DevAppServerImpl
implements DevAppServer
{
36 private final LocalServerEnvironment environment
;
38 private Map
<String
, String
> serviceProperties
= new HashMap
<String
, String
>();
40 private Logger logger
= Logger
.getLogger(DevAppServerImpl
.class.getName());
42 enum ServerState
{ INITIALIZING
, RUNNING
, STOPPING
, SHUTDOWN
}
45 * The current state of the server.
47 private ServerState serverState
= ServerState
.INITIALIZING
;
50 * This is the main container containing the main (default) server
52 private ContainerService mainContainer
= null;
55 * Contains the backend servers configured as part of the "Servers" feature.
56 * Each backend server is started on a separate port and keep their own
57 * internal state. Memcache, datastore, and other API services are shared by
58 * all servers, including the "main" server.
60 private final BackendContainer backendContainer
;
63 * The api proxy we created when we started the web containers. Not initialized until after
64 * {@link #start()} is called.
66 private ApiProxyLocal apiProxyLocal
;
69 * Constructs a development application server that runs the single
70 * application located in the given directory. The application is configured
71 * via <webXmlLocation> and the {@link AppEngineWebXml}
72 * instance returned by the provided {@link AppEngineWebXmlReader}.
74 * @param appDir The location of the application to run.
75 * @param externalResourceDir If not {@code null}, a resource directory external to the appDir.
76 * This will be searched before appDir when looking for resources.
77 * @param webXmlLocation The location of a file whose format complies with
78 * http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd. If {@code null},
79 * defaults to <appDir>/WEB-INF/web.xml
80 * @param appEngineWebXmlLocation The name of the app engine config file. If
81 * {@code null}, defaults to <appDir>/WEB-INF/appengine-web.xml
82 * @param address The address on which to run
83 * @param port The port on which to run
84 * @param useCustomStreamHandler If {@code true}, install
85 * {@link StreamHandlerFactory}. This is "normal" behavior for the dev app
87 * @param containerConfigProperties Additional properties used in the
88 * configuration of the specific container implementation.
90 public DevAppServerImpl(File appDir
, File externalResourceDir
, File webXmlLocation
,
91 File appEngineWebXmlLocation
, String address
, int port
, boolean useCustomStreamHandler
,
92 Map
<String
, Object
> containerConfigProperties
) {
93 String serverInfo
= ContainerUtils
.getServerInfo();
94 if (useCustomStreamHandler
) {
95 StreamHandlerFactory
.install();
97 DevSocketImplFactory
.install();
98 mainContainer
= ContainerUtils
.loadContainer();
99 environment
= mainContainer
.configure(serverInfo
, appDir
, externalResourceDir
,
100 webXmlLocation
, appEngineWebXmlLocation
, address
, port
,
101 containerConfigProperties
, this);
102 backendContainer
= BackendServers
.getInstance();
103 backendContainer
.init(appDir
, externalResourceDir
, webXmlLocation
, appEngineWebXmlLocation
,
104 address
, containerConfigProperties
, this);
108 * Sets the properties that will be used by the local services to
109 * configure themselves. This method must be called before the server
112 * @param properties a, maybe {@code null}, set of properties.
114 * @throws IllegalStateException if the server has already been started.
117 public void setServiceProperties(Map
<String
,String
> properties
) {
118 if (serverState
!= ServerState
.INITIALIZING
) {
119 String msg
= "Cannot set service properties after the server has been started.";
120 throw new IllegalStateException(msg
);
122 serviceProperties
= new HashMap
<String
, String
>(properties
);
123 backendContainer
.setServiceProperties(properties
);
129 * @throws IllegalStateException If the server has already been started or
131 * @throws AppEngineConfigException If no WEB-INF directory can be found or
132 * WEB-INF/appengine-web.xml does not exist.
135 public void start() throws Exception
{
136 if (serverState
!= ServerState
.INITIALIZING
) {
137 throw new IllegalStateException("Cannot start a server that has already been started.");
142 ApiProxyLocalFactory factory
= new ApiProxyLocalFactory();
143 apiProxyLocal
= factory
.create(environment
);
144 apiProxyLocal
.setProperties(serviceProperties
);
145 ApiProxy
.setDelegate(apiProxyLocal
);
147 TimeZone currentTimeZone
= null;
150 currentTimeZone
= setServerTimeZone();
151 mainContainer
.startup();
153 Environment env
= ApiProxy
.getCurrentEnvironment();
154 env
.getAttributes().put(DEFAULT_VERSION_HOSTNAME
, "localhost:" + getPort());
156 this.serviceProperties
.putAll(mainContainer
.getServiceProperties());
157 apiProxyLocal
.appendProperties(mainContainer
.getServiceProperties());
159 backendContainer
.startupAll(mainContainer
.getBackendsXml(), apiProxyLocal
);
160 } catch (BindException ex
) {
161 System
.err
.println();
162 System
.err
.println("************************************************");
163 System
.err
.println("Could not open the requested socket: " + ex
.getMessage());
164 System
.err
.println("Try overriding --address and/or --port.");
167 ApiProxy
.clearEnvironmentForCurrentThread();
168 restoreLocalTimeZone(currentTimeZone
);
170 serverState
= ServerState
.RUNNING
;
172 String prettyAddress
= mainContainer
.getAddress();
173 if (prettyAddress
.equals("0.0.0.0") || prettyAddress
.equals("127.0.0.1")) {
174 prettyAddress
= "localhost";
177 String listeningHostAndPort
= prettyAddress
+ ":" + mainContainer
.getPort();
178 logger
.info("The server is running at http://" + listeningHostAndPort
+ "/");
179 logger
.info("The admin console is running at http://" + listeningHostAndPort
+ "/_ah/admin");
183 * Change the TimeZone for the current thread. By calling this method before
184 * {@link ContainerService#startup()} start}, we set the default TimeZone for the
185 * DevAppServer and all of its related services.
187 * @return the previously installed ThreadLocal TimeZone
189 private TimeZone
setServerTimeZone() {
190 String sysTimeZone
= serviceProperties
.get("appengine.user.timezone.impl");
191 if (sysTimeZone
!= null && sysTimeZone
.trim().length() > 0) {
195 TimeZone utc
= TimeZone
.getTimeZone("UTC");
196 assert utc
.getID().equals("UTC") : "Unable to retrieve the UTC TimeZone";
199 Field f
= TimeZone
.class.getDeclaredField("defaultZoneTL");
200 f
.setAccessible(true);
201 ThreadLocal tl
= (ThreadLocal
) f
.get(null);
202 Method getZone
= ThreadLocal
.class.getMethod("get");
203 TimeZone previousZone
= (TimeZone
) getZone
.invoke(tl
);
204 Method setZone
= ThreadLocal
.class.getMethod("set", Object
.class);
205 setZone
.invoke(tl
, utc
);
207 } catch (Exception e
) {
209 Method getZone
= TimeZone
.class.getDeclaredMethod("getDefaultInAppContext");
210 getZone
.setAccessible(true);
211 TimeZone previousZone
= (TimeZone
) getZone
.invoke(null);
212 Method setZone
= TimeZone
.class.getDeclaredMethod("setDefaultInAppContext", TimeZone
.class);
213 setZone
.setAccessible(true);
214 setZone
.invoke(null, utc
);
216 } catch (Exception ex
) {
217 throw new RuntimeException("Unable to set the TimeZone to UTC", ex
);
223 * Restores the ThreadLocal TimeZone to {@code timeZone}.
225 private void restoreLocalTimeZone(TimeZone timeZone
) {
226 String sysTimeZone
= serviceProperties
.get("appengine.user.timezone.impl");
227 if (sysTimeZone
!= null && sysTimeZone
.trim().length() > 0) {
232 Field f
= TimeZone
.class.getDeclaredField("defaultZoneTL");
233 f
.setAccessible(true);
234 ThreadLocal tl
= (ThreadLocal
) f
.get(null);
235 Method setZone
= ThreadLocal
.class.getMethod("set", Object
.class);
236 setZone
.invoke(tl
, timeZone
);
237 } catch (Exception e
) {
239 Method setZone
= TimeZone
.class.getDeclaredMethod("setDefaultInAppContext", TimeZone
.class);
240 setZone
.setAccessible(true);
241 setZone
.invoke(null, timeZone
);
242 } catch (Exception ex
) {
243 throw new RuntimeException("Unable to restore the previous TimeZone", ex
);
249 public void restart() throws Exception
{
250 if (serverState
!= ServerState
.RUNNING
) {
251 throw new IllegalStateException("Cannot restart a server that is not currently running.");
253 mainContainer
.shutdown();
254 backendContainer
.shutdownAll();
255 mainContainer
.startup();
256 backendContainer
.startupAll(mainContainer
.getBackendsXml(), apiProxyLocal
);
260 * Shut down the server.
262 * @throws IllegalStateException If the server has not been started or has
263 * already been shutdown.
266 public void shutdown() throws Exception
{
267 if (serverState
!= ServerState
.RUNNING
) {
268 throw new IllegalStateException("Cannot shutdown a server that is not currently running.");
270 mainContainer
.shutdown();
271 backendContainer
.shutdownAll();
272 ApiProxy
.setDelegate(null);
273 apiProxyLocal
= null;
274 serverState
= ServerState
.SHUTDOWN
;
278 * @return the servlet container listener port number.
281 public int getPort() {
282 return mainContainer
.getPort();
286 * Returns the web app context. Useful in embedding scenarios to allow the
287 * embedder to install servlets, etc. Any such modification should be done
288 * before calling {@link #start()}.
290 * @see ContainerService#getAppContext
293 public AppContext
getAppContext() {
294 return mainContainer
.getAppContext();
298 * Reset the container EnvironmentVariableMismatchSeverity.
301 public void setThrowOnEnvironmentVariableMismatch(boolean throwOnMismatch
) {
302 mainContainer
.setEnvironmentVariableMismatchSeverity(throwOnMismatch ? ERROR
: WARNING
);
306 * We're happy with the default logging behavior, which is to
307 * install a {@link ConsoleHandler} at the root level. The only
308 * issue is that we want its level to be FINEST to be consistent
309 * with our runtime environment.
311 * <p>Note that this does not mean that any fine messages will be
312 * logged by default -- each Logger still defaults to INFO.
313 * However, it is sufficient to call {@link Logger#setLevel(Level)}
314 * to adjust the level.
316 private void initializeLogging() {
317 for (Handler handler
: Logger
.getLogger("").getHandlers()) {
318 if (handler
instanceof ConsoleHandler
) {
319 handler
.setLevel(Level
.FINEST
);
324 ServerState
getServerState() {