1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.tools
.development
;
5 import com
.google
.appengine
.tools
.info
.SdkInfo
;
6 import com
.google
.appengine
.tools
.info
.UpdateCheck
;
7 import com
.google
.appengine
.tools
.plugins
.SDKPluginManager
;
8 import com
.google
.appengine
.tools
.plugins
.SDKRuntimePlugin
;
9 import com
.google
.appengine
.tools
.plugins
.SDKRuntimePlugin
.ApplicationDirectories
;
10 import com
.google
.appengine
.tools
.util
.Action
;
11 import com
.google
.appengine
.tools
.util
.Logging
;
12 import com
.google
.appengine
.tools
.util
.Option
;
13 import com
.google
.appengine
.tools
.util
.Parser
;
14 import com
.google
.appengine
.tools
.util
.Parser
.ParseResult
;
15 import com
.google
.appengine
.tools
.wargen
.WarGenerator
;
16 import com
.google
.apphosting
.utils
.config
.GenerationDirectory
;
17 import com
.google
.common
.annotations
.VisibleForTesting
;
18 import com
.google
.common
.collect
.ImmutableList
;
20 import java
.awt
.Toolkit
;
22 import java
.io
.PrintStream
;
23 import java
.util
.Arrays
;
24 import java
.util
.HashMap
;
25 import java
.util
.List
;
27 import java
.util
.TimeZone
;
30 * The command-line entry point for DevAppServer.
33 public class DevAppServerMain
{
35 public static final String EXTERNAL_RESOURCE_DIR_ARG
= "external_resource_dir";
36 public static final String GENERATE_WAR_ARG
= "generate_war";
37 public static final String GENERATED_WAR_DIR_ARG
= "generated_war_dir";
38 private static final String DEFAULT_RDBMS_PROPERTIES_FILE
= ".local.rdbms.properties";
39 private static final String RDBMS_PROPERTIES_FILE_SYSTEM_PROPERTY
= "rdbms.properties.file";
41 private static String originalTimeZone
;
43 private final Action ACTION
= new StartAction();
45 private String server
= SdkInfo
.getDefaultServer();
47 private String address
= DevAppServer
.DEFAULT_HTTP_ADDRESS
;
48 private int port
= DevAppServer
.DEFAULT_HTTP_PORT
;
49 private boolean disableUpdateCheck
;
50 private String generatedDirectory
= null;
51 private boolean disableRestrictedCheck
= false;
52 private String externalResourceDir
= null;
53 private List
<String
> propertyOptions
= null;
56 * An {@code Option} for running {@link DevAppServerMain}.
58 private abstract static class DevAppServerOption
extends Option
{
60 protected DevAppServerMain main
;
65 * @param main the instance of DevAppServerMain for which this is an option. This may be
66 * {@code null} if {@link Option#Apply()} will never be invoked.
67 * @param shortName The short name to support. May be {@code null}.
68 * @param longName The long name to support. May be {@code null}.
69 * @param isFlag true to indicate that the Option represents a boolean value.
71 DevAppServerOption(DevAppServerMain main
, String shortName
, String longName
, boolean isFlag
) {
72 super(shortName
, longName
, isFlag
);
79 * Returns the list of built-in {@link Option Options} for the given instance of
80 * {@link DevAppServerMain}. The built-in options are those that are independent of any
81 * {@link SDKRuntimePlugin SDKRuntimePlugins} that may be installed.
83 * @param main The instance of {@code DevAppServerMain} for which the built-in options are being
84 * requested. This may be {@code null} if {@link Option#apply()} will never be invoked on
85 * any of the returned {@code Options}.
86 * @return The list of built-in options
88 private static List
<Option
> getBuiltInOptions(DevAppServerMain main
) {
90 new Option("h", "help", true) {
93 printHelp(System
.err
);
97 public List
<String
> getHelpLines() {
98 return ImmutableList
.of(
99 " --help, -h Show this help message and exit.");
102 new DevAppServerOption(main
, "s", "server", false) {
104 public void apply() {
105 this.main
.server
= getValue();
108 public List
<String
> getHelpLines() {
109 return ImmutableList
.of(
110 " --server=SERVER The server to use to determine the latest",
111 " -s SERVER SDK version.");
114 new DevAppServerOption(main
, "a", "address", false) {
116 public void apply() {
117 this.main
.address
= getValue();
120 public List
<String
> getHelpLines() {
121 return ImmutableList
.of(
122 " --address=ADDRESS The address of the interface on the local machine",
123 " -a ADDRESS to bind to (or 0.0.0.0 for all interfaces).");
126 new DevAppServerOption(main
, "p", "port", false) {
128 public void apply() {
129 this.main
.port
= Integer
.valueOf(getValue());
132 public List
<String
> getHelpLines() {
133 return ImmutableList
.of(
134 " --port=PORT The port number to bind to on the local machine.",
138 new DevAppServerOption(main
, null, "sdk_root", false) {
140 public void apply() {
141 System
.setProperty("appengine.sdk.root", getValue());
144 public List
<String
> getHelpLines() {
145 return ImmutableList
.of(
146 " --sdk_root=DIR Overrides where the SDK is located.");
149 new DevAppServerOption(main
, null, "disable_update_check", true) {
151 public void apply() {
152 this.main
.disableUpdateCheck
= true;
155 public List
<String
> getHelpLines() {
156 return ImmutableList
.of(
157 " --disable_update_check Disable the check for newer SDK versions.");
160 new DevAppServerOption(main
, null, "generated_dir", false) {
162 public void apply() {
163 this.main
.generatedDirectory
= getValue();
166 public List
<String
> getHelpLines() {
167 return ImmutableList
.of(
168 " --generated_dir=DIR Set the directory where generated files are created.");
171 new DevAppServerOption(main
, null, "disable_restricted_check", true) {
173 public void apply() {
174 this.main
.disableRestrictedCheck
= true;
177 new DevAppServerOption(main
, null, EXTERNAL_RESOURCE_DIR_ARG
, false) {
179 public void apply() {
180 this.main
.externalResourceDir
= getValue();
183 new DevAppServerOption(main
, null, "property", false) {
185 public void apply() {
186 this.main
.propertyOptions
= getValues();
193 * Builds the complete list of {@link Option Options} for the given instance of
194 * {@link DevAppServerMain}. The list consists of the built-in options, possibly modified and
195 * extended by any {@link SDKRuntimePlugin SDKRuntimePlugins} that may be installed.
197 * @param main The instance of {@code DevAppServerMain} for which the options are being requested.
198 * This may be {@code null} if {@link Option#apply()} will never be invoked on any of the
199 * returned {@code Options}.
200 * @return The list of all options
202 private static List
<Option
> buildOptions(DevAppServerMain main
) {
203 List
<Option
> options
= getBuiltInOptions(main
);
204 for (SDKRuntimePlugin runtimePlugin
: SDKPluginManager
.findAllRuntimePlugins()) {
205 options
= runtimePlugin
.customizeDevAppServerOptions(options
);
210 private final List
<Option
> PARSERS
= buildOptions(this);
212 @SuppressWarnings("unchecked")
213 public static void main(String args
[]) throws Exception
{
215 Logging
.initializeLogging();
216 if (System
.getProperty("os.name").equalsIgnoreCase("Mac OS X")) {
217 Toolkit
.getDefaultToolkit();
219 new DevAppServerMain(args
);
223 * We attempt to record user.timezone before the JVM alters its value.
224 * This can happen just by asking for
225 * {@link java.util.TimeZone#getDefault()}.
227 * We need this information later, so that we can know if the user
228 * actually requested an override of the timezone. We can still be wrong
229 * about this, for example, if someone directly or indirectly calls
230 * {@code TimeZone.getDefault} before the main method to this class.
231 * This doesn't happen in the App Engine tools themselves, but might
232 * theoretically happen in some third-party tool that wraps the App Engine
233 * tools. In that case, they can set {@code appengine.user.timezone}
234 * to override what we're inferring for user.timezone.
236 private static void recordTimeZone() {
237 originalTimeZone
= System
.getProperty("user.timezone");
240 public DevAppServerMain(String
[] args
) throws Exception
{
241 Parser parser
= new Parser();
242 ParseResult result
= parser
.parseArgs(ACTION
, PARSERS
, args
);
246 public static void printHelp(PrintStream out
) {
247 out
.println("Usage: <dev-appserver> [options] <app directory>");
249 out
.println("Options:");
250 for (Option option
: buildOptions(null)) {
251 for (String helpString
: option
.getHelpLines()) {
252 out
.println(helpString
);
255 out
.println(" --jvm_flag=FLAG Pass FLAG as a JVM argument. May be repeated to");
256 out
.println(" supply multiple flags.");
259 class StartAction
extends Action
{
265 public void apply() {
266 List
<String
> args
= getArgs();
268 File externalResourceDir
= getExternalResourceDir();
269 if (args
.size() != 1) {
270 printHelp(System
.err
);
273 File appDir
= new File(args
.get(0)).getCanonicalFile();
274 validateWarPath(appDir
);
276 SDKRuntimePlugin runtimePlugin
= SDKPluginManager
.findRuntimePlugin(appDir
);
277 if (runtimePlugin
!= null) {
278 ApplicationDirectories appDirs
= runtimePlugin
.generateApplicationDirectories(appDir
);
279 appDir
= appDirs
.getWarDir();
280 externalResourceDir
= appDirs
.getExternalResourceDir();
283 UpdateCheck updateCheck
= new UpdateCheck(server
, appDir
, true);
284 if (updateCheck
.allowedToCheckForUpdates() && !disableUpdateCheck
) {
285 updateCheck
.maybePrintNagScreen(System
.err
);
287 updateCheck
.checkJavaVersion(System
.err
);
289 DevAppServer server
= new DevAppServerFactory().createDevAppServer(appDir
,
290 externalResourceDir
, address
, port
);
292 @SuppressWarnings("rawtypes")
293 Map properties
= System
.getProperties();
294 @SuppressWarnings("unchecked")
295 Map
<String
, String
> stringProperties
= properties
;
296 setTimeZone(stringProperties
);
297 setGeneratedDirectory(stringProperties
);
298 if (disableRestrictedCheck
) {
299 stringProperties
.put("appengine.disableRestrictedCheck", "");
301 setRdbmsPropertiesFile(stringProperties
, appDir
, externalResourceDir
);
302 stringProperties
.putAll(parsePropertiesList(propertyOptions
));
303 server
.setServiceProperties(stringProperties
);
309 Thread
.sleep(1000 * 60 * 60);
311 } catch (InterruptedException e
) {
314 System
.out
.println("Shutting down.");
316 } catch (Exception ex
) {
317 ex
.printStackTrace();
322 private void setTimeZone(Map
<String
,String
> serviceProperties
) {
323 String timeZone
= serviceProperties
.get("appengine.user.timezone");
324 if (timeZone
!= null) {
325 TimeZone
.setDefault(TimeZone
.getTimeZone(timeZone
));
327 timeZone
= originalTimeZone
;
329 serviceProperties
.put("appengine.user.timezone.impl", timeZone
);
332 private void setGeneratedDirectory(Map
<String
, String
> stringProperties
) {
333 if (generatedDirectory
!= null) {
334 File dir
= new File(generatedDirectory
);
337 if (!dir
.isDirectory()) {
338 error
= generatedDirectory
+ " is not a directory.";
339 } else if (!dir
.canWrite()) {
340 error
= generatedDirectory
+ " is not writable.";
342 } else if (!dir
.mkdirs()) {
343 error
= "Could not make " + generatedDirectory
;
346 System
.err
.println(error
);
349 stringProperties
.put(GenerationDirectory
.GENERATED_DIR_PROPERTY
, generatedDirectory
);
354 * Sets the property named {@link #RDBMS_PROPERTIES_FILE_SYSTEM_PROPERTY} to the default value
355 * {@link #DEFAULT_RDBMS_PROPERTIES_FILE} if the property is not already set and if there is a
356 * file by that name in either {@code appDir} or {@code externalResourceDir}.
358 * @param stringProperties The map in which the value will be set
359 * @param appDir The appDir, aka the WAR dir
360 * @param externalResourceDir the external resource dir, or {@code null} if there is not one.
362 private void setRdbmsPropertiesFile(
363 Map
<String
, String
> stringProperties
, File appDir
, File externalResourceDir
) {
364 if (stringProperties
.get(RDBMS_PROPERTIES_FILE_SYSTEM_PROPERTY
) != null) {
367 File file
= findRdbmsPropertiesFile(externalResourceDir
);
369 file
= findRdbmsPropertiesFile(appDir
);
372 String path
= file
.getPath();
373 System
.out
.println("Reading local rdbms properties from " + path
);
374 stringProperties
.put(RDBMS_PROPERTIES_FILE_SYSTEM_PROPERTY
, path
);
379 * Returns the default rdbms properties file in the given dir if it exists.
380 * @param dir The directory in which to look
381 * @return The default rdbs properties file, or {@code null}.
383 private File
findRdbmsPropertiesFile(File dir
) {
384 File candidate
= new File(dir
, DEFAULT_RDBMS_PROPERTIES_FILE
);
385 if (candidate
.isFile() && candidate
.canRead()) {
391 private File
getExternalResourceDir() {
392 if (externalResourceDir
== null) {
395 externalResourceDir
= externalResourceDir
.trim();
398 if (externalResourceDir
.isEmpty()) {
399 error
= "The empty string was specified for external_resource_dir";
401 dir
= new File(externalResourceDir
);
403 if (!dir
.isDirectory()) {
404 error
= externalResourceDir
+ " is not a directory.";
407 error
= "No such directory: " + externalResourceDir
;
411 System
.err
.println(error
);
419 public static void validateWarPath(File war
) {
421 System
.out
.println("Unable to find the webapp directory " + war
);
422 printHelp(System
.err
);
424 } else if (!war
.isDirectory()) {
425 System
.out
.println("dev_appserver only accepts webapp directories, not war files.");
426 printHelp(System
.err
);
432 * Checks preconditions for using the --generate_war feature. Exits with
433 * an error message if any of the conditions are not met
434 * @param args The non-option arguments on the command line.
436 private void validateArgsForWarGeneration(List
<String
> args
) {
437 ifNotWarGenConditionExit(externalResourceDir
!= null,
438 "--" + EXTERNAL_RESOURCE_DIR_ARG
+ " must also be specified.");
439 File appYamlFile
= new File(externalResourceDir
, WarGenerator
.APP_YAML
);
440 ifNotWarGenConditionExit(appYamlFile
.isFile(),
441 "the external resource directory must contain a file named " + WarGenerator
.APP_YAML
+ ".");
442 ifNotWarGenConditionExit(args
.size() == 0,
443 "the command line should not include a war directory argument.");
447 * Parse the properties list. Each string in the last may take the the form:
449 * name shorthand for name=true
450 * noname shorthand for name=false
451 * name= required syntax to specify an empty value
453 * @param properties A list of unparsed properties (may be null).
454 * @returns A map from property names to values.
457 static Map
<String
, String
> parsePropertiesList(List
<String
> properties
) {
458 Map
<String
, String
> parsedProperties
= new HashMap
<String
, String
>();
459 if (properties
!= null) {
460 for (String property
: properties
) {
461 String
[] propertyKeyValue
= property
.split("=", 2);
462 if (propertyKeyValue
.length
== 2) {
463 parsedProperties
.put(propertyKeyValue
[0], propertyKeyValue
[1]);
464 } else if (propertyKeyValue
[0].startsWith("no")) {
465 parsedProperties
.put(propertyKeyValue
[0].substring(2), "false");
467 parsedProperties
.put(propertyKeyValue
[0], "true");
471 return parsedProperties
;
474 private static final String PREFIX
= "When generating a war directory,";
475 private static void ifNotWarGenConditionExit(boolean condition
, String suffix
) {
477 System
.err
.println(PREFIX
+ " " + suffix
);