1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.tools
.development
;
5 import com
.google
.appengine
.api
.log
.dev
.LocalLogService
;
6 import com
.google
.appengine
.api
.utils
.SystemProperty
;
7 import com
.google
.appengine
.tools
.info
.SdkImplInfo
;
8 import com
.google
.appengine
.tools
.info
.SdkInfo
;
9 import com
.google
.appengine
.tools
.plugins
.AppYamlProcessor
;
10 import com
.google
.apphosting
.api
.ApiProxy
;
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
.BackendsXml
;
15 import com
.google
.apphosting
.utils
.config
.BackendsXmlReader
;
16 import com
.google
.apphosting
.utils
.config
.WebXml
;
17 import com
.google
.apphosting
.utils
.config
.WebXmlReader
;
18 import com
.google
.common
.base
.Join
;
19 import com
.google
.common
.collect
.ImmutableMap
;
20 import com
.google
.common
.collect
.Maps
;
22 import java
.io
.BufferedInputStream
;
23 import java
.io
.ByteArrayInputStream
;
24 import java
.io
.ByteArrayOutputStream
;
26 import java
.io
.FileInputStream
;
27 import java
.io
.IOException
;
28 import java
.io
.InputStream
;
29 import java
.net
.InetAddress
;
31 import java
.net
.UnknownHostException
;
32 import java
.security
.Permissions
;
33 import java
.util
.ArrayList
;
34 import java
.util
.Collection
;
35 import java
.util
.List
;
37 import java
.util
.Properties
;
38 import java
.util
.concurrent
.CountDownLatch
;
39 import java
.util
.logging
.Handler
;
40 import java
.util
.logging
.Level
;
41 import java
.util
.logging
.LogManager
;
42 import java
.util
.logging
.Logger
;
45 * Common implementation for the {@link ContainerService} interface.
47 * There should be no reference to any third-party servlet container from here.
50 public abstract class AbstractContainerService
implements ContainerService
{
52 private static final Logger log
= Logger
.getLogger(AbstractContainerService
.class.getName());
54 protected static final String _AH_URL_RELOAD
= "/_ah/reloadwebapp";
56 private static final String LOGGING_CONFIG_FILE
= "java.util.logging.config.file";
58 private static final String USER_CODE_CLASSPATH_MANAGER_PROP
=
59 "devappserver.userCodeClasspathManager";
61 private static final String USER_CODE_CLASSPATH
= USER_CODE_CLASSPATH_MANAGER_PROP
+ ".classpath";
62 private static final String USER_CODE_REQUIRES_WEB_INF
=
63 USER_CODE_CLASSPATH_MANAGER_PROP
+ ".requiresWebInf";
65 protected String devAppServerVersion
;
66 protected File appDir
;
67 protected File externalResourceDir
;
70 * The location of web.xml. If not provided, defaults to
71 * <appDir>/WEB-INF/web.xml
73 protected File webXmlLocation
;
76 * The hostname on which the server is listening for http requests.
78 protected String hostName
;
81 * The location of appengine-web.xml. If not provided, defaults to
82 * <appDir>/WEB-INF/appengine-web.xml
84 protected File appEngineWebXmlLocation
;
87 * The network address on which the server is listening for http requests.
89 protected String address
;
92 * The port on which the server is listening for http requests.
97 * A reference to the parent DevAppServer that configured this container.
99 protected DevAppServer devAppServer
;
101 protected AppEngineWebXml appEngineWebXml
;
103 protected WebXml webXml
;
105 protected BackendsXml backendsXml
;
108 * We modify the system properties when the dev appserver is
109 * launched using key/value pairs defined in appengine-web.xml.
110 * This can make it very easy to leak state across tests that launch
111 * instances of the dev appserver, so we use this member to keep
112 * track of the original values all system properties at start-up.
113 * We then restore the values when we shutdown the server.
115 private static Properties originalSysProps
= null;
118 * The severity of an environment variable mismatch.
120 private static EnvironmentVariableMismatchSeverity envVarMismatchSeverity
=
121 EnvironmentVariableMismatchSeverity
.ERROR
;
124 * Latch that will open once the server is fully initialized.
126 private CountDownLatch serverInitLatch
;
129 * Not initialized until {@link #startup()} has been called.
131 protected ApiProxyLocal apiProxyLocal
;
133 protected UserCodeClasspathManager userCodeClasspathManager
;
135 public final LocalServerEnvironment
configure(String devAppServerVersion
, final File appDir
,
136 final File externalResourceDir
, File webXmlLocation
, File appEngineWebXmlLocation
,
137 final String address
, int port
, Map
<String
, Object
> containerConfigProperties
,
138 DevAppServer devAppServer
) {
139 this.devAppServerVersion
= devAppServerVersion
;
140 this.appDir
= appDir
;
141 this.externalResourceDir
= externalResourceDir
;
142 this.webXmlLocation
= webXmlLocation
;
143 this.appEngineWebXmlLocation
= appEngineWebXmlLocation
;
144 this.address
= address
;
146 this.serverInitLatch
= new CountDownLatch(1);
147 this.hostName
= "localhost";
148 this.devAppServer
= devAppServer
;
149 if ("0.0.0.0".equals(address
)) {
151 InetAddress localhost
= InetAddress
.getLocalHost();
152 this.hostName
= localhost
.getHostName();
153 } catch (UnknownHostException ex
) {
154 log
.log(Level
.WARNING
,
155 "Unable to determine hostname - defaulting to localhost.");
159 this.userCodeClasspathManager
= newUserCodeClasspathProvider(containerConfigProperties
);
160 return new LocalServerEnvironment() {
162 public File
getAppDir() {
167 public String
getAddress() {
172 public String
getHostName() {
177 public int getPort() {
178 return AbstractContainerService
.this.port
;
182 public void waitForServerToStart() throws InterruptedException
{
183 serverInitLatch
.await();
187 public boolean simulateProductionLatencies() {
192 public boolean enforceApiDeadlines() {
193 return !Boolean
.getBoolean("com.google.appengine.disable_api_deadlines");
199 * Constructs a {@link UserCodeClasspathManager} from the given properties.
201 private static UserCodeClasspathManager
newUserCodeClasspathProvider(
202 Map
<String
, Object
> containerConfigProperties
) {
203 if (containerConfigProperties
.containsKey(USER_CODE_CLASSPATH_MANAGER_PROP
)) {
204 final Map
<String
, Object
> userCodeClasspathManagerProps
=
205 (Map
<String
, Object
>) containerConfigProperties
.get(USER_CODE_CLASSPATH_MANAGER_PROP
);
206 return new UserCodeClasspathManager() {
208 public Collection
<URL
> getUserCodeClasspath(File root
) {
209 return (Collection
<URL
>) userCodeClasspathManagerProps
.get(USER_CODE_CLASSPATH
);
213 public boolean requiresWebInf() {
214 return (Boolean
) userCodeClasspathManagerProps
.get(USER_CODE_REQUIRES_WEB_INF
);
218 return new WebAppUserCodeClasspathManager();
222 * This is made final, and detail implementation (that is specific to any
223 * particular servlet container) goes to individual "template" methods.
226 public final void startup() throws Exception
{
227 apiProxyLocal
= (ApiProxyLocal
) ApiProxy
.getDelegate();
228 File webAppDir
= initContext();
229 loadAppEngineWebXml(webAppDir
);
232 startHotDeployScanner();
233 serverInitLatch
.countDown();
237 public final void shutdown() throws Exception
{
238 stopHotDeployScanner();
240 restoreSystemProperties();
245 public Map
<String
, String
> getServiceProperties() {
246 return ImmutableMap
.of("appengine.dev.inbound-services",
247 Join
.join(",", appEngineWebXml
.getInboundServices()));
251 * Set up the webapp context in a container specific way.
253 * @return the effective webapp directory.
255 protected abstract File
initContext() throws IOException
;
258 * Start up the servlet container runtime.
260 protected abstract void startContainer() throws Exception
;
263 * Stop the servlet container runtime.
265 protected abstract void stopContainer() throws Exception
;
268 * Start up the hot-deployment scanner.
270 protected abstract void startHotDeployScanner() throws Exception
;
273 * Stop the hot-deployment scanner.
275 protected abstract void stopHotDeployScanner() throws Exception
;
278 * Re-deploy the current webapp context in a container specific way,
279 * while taking into account possible appengine-web.xml change too,
280 * without restarting the server.
282 protected abstract void reloadWebApp() throws Exception
;
285 public String
getAddress() {
290 public AppEngineWebXml
getAppEngineWebXmlConfig(){
291 return appEngineWebXml
;
295 public BackendsXml
getBackendsXml() {
300 public int getPort() {
305 public String
getHostName() {
310 public void setEnvironmentVariableMismatchSeverity(EnvironmentVariableMismatchSeverity val
) {
311 envVarMismatchSeverity
= val
;
314 protected Permissions
getUserPermissions() {
315 return appEngineWebXml
.getUserPermissions();
318 protected void loadAppEngineWebXml(File webAppDir
) {
320 WebXmlReader webXmlReader
;
321 if (webXmlLocation
== null) {
322 webXmlReader
= new WebXmlReader(webAppDir
.getAbsolutePath());
323 webXmlLocation
= new File(webXmlReader
.getFilename());
325 webXmlReader
= new WebXmlReader(webXmlLocation
.getParent(), webXmlLocation
.getName());
328 AppEngineWebXmlReader appEngineWebXmlReader
;
329 if (appEngineWebXmlLocation
== null) {
330 appEngineWebXmlReader
= new AppEngineWebXmlReader(webAppDir
.getAbsolutePath());
331 appEngineWebXmlLocation
= new File(appEngineWebXmlReader
.getFilename());
333 appEngineWebXmlReader
= new AppEngineWebXmlReader(
334 appEngineWebXmlLocation
.getParent(), appEngineWebXmlLocation
.getName());
337 AppYamlProcessor
.convert(new File(appDir
, "WEB-INF"),
338 appEngineWebXmlReader
.getFilename(), webXmlLocation
.getAbsolutePath());
340 appEngineWebXml
= appEngineWebXmlReader
.readAppEngineWebXml();
341 if (appEngineWebXml
.getAppId() == null || appEngineWebXml
.getAppId().length() == 0) {
342 appEngineWebXml
.setAppId("no_app_id");
344 webXml
= webXmlReader
.readWebXml();
345 staticInitialize(appEngineWebXml
, appDir
, externalResourceDir
);
348 backendsXml
= new BackendsXmlReader(webAppDir
.getAbsolutePath()).readBackendsXml();
350 ApiProxy
.setEnvironmentForCurrentThread(new LocalInitializationEnvironment(
351 appEngineWebXml
.getAppId(), appEngineWebXml
.getMajorVersionId()));
354 private static synchronized void staticInitialize(
355 AppEngineWebXml appEngineWebXml
, File appDir
, File externalResourceDir
) {
356 setSystemProperties(appEngineWebXml
);
357 checkEnvironmentVariables(appEngineWebXml
);
358 updateLoggingConfiguration(
359 originalSysProps
, appEngineWebXml
.getSystemProperties(), appDir
, externalResourceDir
);
362 protected void restoreSystemProperties() {
363 for (String key
: appEngineWebXml
.getSystemProperties().keySet()) {
364 System
.clearProperty(key
);
366 System
.getProperties().putAll(originalSysProps
);
369 /** Returns {@code true} if appengine-web.xml <sessions-enabled> is true. */
370 protected boolean isSessionsEnabled() {
371 return appEngineWebXml
.getSessionsEnabled();
375 * Gets all of the URLs that should be added to the classpath for an
376 * application located at {@code root}.
378 protected URL
[] getClassPathForApp(File root
) {
379 List
<URL
> appUrls
= new ArrayList
<URL
>();
381 appUrls
.addAll(SdkImplInfo
.getAgentRuntimeLibs());
382 appUrls
.addAll(userCodeClasspathManager
.getUserCodeClasspath(root
));
383 for (URL url
: SdkImplInfo
.getUserJspLibs()) {
386 for (URL url
: SdkImplInfo
.getWebApiToolLibs()) {
389 return appUrls
.toArray(new URL
[appUrls
.size()]);
393 * Sets system properties that are defined in {@link AppEngineWebXml}.
395 private static void setSystemProperties(AppEngineWebXml appEngineWebXml
) {
396 SystemProperty
.environment
.set(SystemProperty
.Environment
.Value
.Development
);
397 String release
= SdkInfo
.getLocalVersion().getRelease();
398 if (release
== null) {
401 SystemProperty
.version
.set(release
);
402 SystemProperty
.applicationId
.set(appEngineWebXml
.getAppId());
403 SystemProperty
.applicationVersion
.set(appEngineWebXml
.getMajorVersionId() + ".1");
405 synchronized (AbstractContainerService
.class) {
406 if (null == originalSysProps
){
407 originalSysProps
= new Properties();
408 originalSysProps
.putAll(System
.getProperties());
411 System
.getProperties().putAll(appEngineWebXml
.getSystemProperties());
415 * Updates the JVM's logging configuration to include both the
416 * user's custom logging configuration and the SDK's internal
417 * logging configurations.
419 * @param systemProperties
420 * @param userSystemProperties
422 private static void updateLoggingConfiguration(Properties systemProperties
,
423 Map
<String
, String
> userSystemProperties
, File appDir
, File externalResourceDir
) {
424 String userConfigFile
= userSystemProperties
.get(LOGGING_CONFIG_FILE
);
426 boolean shouldLogWarning
= (externalResourceDir
== null);
427 Properties userProperties
= loadPropertiesFile(userConfigFile
, appDir
, shouldLogWarning
);
428 if (userProperties
== null && externalResourceDir
!= null) {
429 userProperties
= loadPropertiesFile(userConfigFile
, externalResourceDir
, true);
431 String sdkConfigFile
= systemProperties
.getProperty(LOGGING_CONFIG_FILE
);
432 Properties sdkProperties
= loadPropertiesFile(sdkConfigFile
, appDir
, true);
433 Properties allProperties
= new Properties();
434 if (sdkProperties
!= null) {
435 allProperties
.putAll(sdkProperties
);
437 if (userProperties
!= null) {
438 allProperties
.putAll(userProperties
);
441 ByteArrayOutputStream out
= new ByteArrayOutputStream();
444 allProperties
.store(out
, null);
445 LogManager
.getLogManager().readConfiguration(new ByteArrayInputStream(out
.toByteArray()));
447 Logger root
= Logger
.getLogger("");
449 ApiProxyLocal proxy
= (ApiProxyLocal
) ApiProxy
.getDelegate();
450 LocalLogService logService
= (LocalLogService
) proxy
.getService(LocalLogService
.PACKAGE
);
451 root
.addHandler(logService
.getLogHandler());
453 Handler
[] handlers
= root
.getHandlers();
454 if (handlers
!= null) {
455 for (Handler handler
: handlers
) {
456 handler
.setLevel(Level
.FINEST
);
459 } catch (IOException e
) {
460 log
.log(Level
.WARNING
, "Unable to configure logging properties.", e
);
464 private static Properties
loadPropertiesFile(String file
, File appDir
, boolean logWarning
) {
468 file
= file
.replace('/', File
.separatorChar
);
469 File f
= new File(file
);
470 if (!f
.isAbsolute()) {
471 f
= new File(appDir
+ File
.separator
+ f
.getPath());
473 InputStream inputStream
= null;
475 inputStream
= new BufferedInputStream(new FileInputStream(f
));
476 Properties props
= new Properties();
477 props
.load(inputStream
);
479 } catch (IOException e
) {
481 log
.log(Level
.WARNING
, "Unable to load properties file, " + f
.getAbsolutePath(), e
);
485 if (inputStream
!= null) {
488 } catch (IOException e
) {
494 private static void checkEnvironmentVariables(AppEngineWebXml appEngineWebXml
) {
495 Map
<String
, String
> missingEnvEntries
= Maps
.newHashMap();
496 for (Map
.Entry
<String
, String
> entry
: appEngineWebXml
.getEnvironmentVariables().entrySet()) {
497 if (!entry
.getValue().equals(System
.getenv(entry
.getKey()))) {
498 missingEnvEntries
.put(entry
.getKey(), entry
.getValue());
501 if (!missingEnvEntries
.isEmpty()) {
503 "One or more environment variables have been configured in appengine-web.xml that have "
504 + "missing or different values in your local environment. We recommend you use system "
505 + "properties instead, but if you are interacting with legacy code that requires "
506 + "specific environment variables to have specific values, please set these environment "
507 + "variables in your environment before running.\n"
508 + Join
.join("\n", missingEnvEntries
);
510 if (envVarMismatchSeverity
== EnvironmentVariableMismatchSeverity
.WARNING
) {
512 } else if (envVarMismatchSeverity
== EnvironmentVariableMismatchSeverity
.ERROR
) {
513 throw new IncorrectEnvironmentVariableException(msg
, missingEnvEntries
);
518 static class IncorrectEnvironmentVariableException
extends
519 AppEngineConfigException
{
521 private final Map
<String
, String
> missingEnvEntries
;
522 private IncorrectEnvironmentVariableException(String msg
,
523 Map
<String
, String
> missingEnvEntries
) {
525 this.missingEnvEntries
= missingEnvEntries
;
528 public Map
<String
, String
> getMissingEnvEntries() {
529 return missingEnvEntries
;
534 * A fake {@link LocalEnvironment} implementation that is used during the
535 * initialization of the Development AppServer.
537 public static class LocalInitializationEnvironment
extends LocalEnvironment
{
538 public LocalInitializationEnvironment(String appId
, String majorVersionId
) {
539 super(appId
, majorVersionId
);
543 public String
getEmail() {
548 public boolean isLoggedIn() {
553 public boolean isAdmin() {