Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / development / DevAppServerMain.java
blobacf543dff1a2e88049228a0334fcd2adcb6ec235
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;
21 import java.io.File;
22 import java.io.PrintStream;
23 import java.util.Arrays;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.TimeZone;
29 /**
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;
55 /**
56 * An {@code Option} for running {@link DevAppServerMain}.
58 private abstract static class DevAppServerOption extends Option {
60 protected DevAppServerMain main;
62 /**
63 * Constructor.
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);
73 this.main = main;
78 /**
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) {
89 return Arrays.asList(
90 new Option("h", "help", true) {
91 @Override
92 public void apply() {
93 printHelp(System.err);
94 System.exit(0);
96 @Override
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) {
103 @Override
104 public void apply() {
105 this.main.server = getValue();
107 @Override
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) {
115 @Override
116 public void apply() {
117 this.main.address = getValue();
119 @Override
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) {
127 @Override
128 public void apply() {
129 this.main.port = Integer.valueOf(getValue());
131 @Override
132 public List<String> getHelpLines() {
133 return ImmutableList.of(
134 " --port=PORT The port number to bind to on the local machine.",
135 " -p PORT");
138 new DevAppServerOption(main, null, "sdk_root", false) {
139 @Override
140 public void apply() {
141 System.setProperty("appengine.sdk.root", getValue());
143 @Override
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) {
150 @Override
151 public void apply() {
152 this.main.disableUpdateCheck = true;
154 @Override
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) {
161 @Override
162 public void apply() {
163 this.main.generatedDirectory = getValue();
165 @Override
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) {
172 @Override
173 public void apply() {
174 this.main.disableRestrictedCheck = true;
177 new DevAppServerOption(main, null, EXTERNAL_RESOURCE_DIR_ARG, false) {
178 @Override
179 public void apply() {
180 this.main.externalResourceDir = getValue();
183 new DevAppServerOption(main, null, "property", false) {
184 @Override
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);
207 return options;
210 private final List<Option> PARSERS = buildOptions(this);
212 @SuppressWarnings("unchecked")
213 public static void main(String args[]) throws Exception {
214 recordTimeZone();
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);
243 result.applyArgs();
246 public static void printHelp(PrintStream out) {
247 out.println("Usage: <dev-appserver> [options] <app directory>");
248 out.println("");
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 {
260 StartAction() {
261 super("start");
264 @Override
265 public void apply() {
266 List<String> args = getArgs();
267 try {
268 File externalResourceDir = getExternalResourceDir();
269 if (args.size() != 1) {
270 printHelp(System.err);
271 System.exit(1);
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);
305 server.start();
307 try {
308 while (true) {
309 Thread.sleep(1000 * 60 * 60);
311 } catch (InterruptedException e) {
314 System.out.println("Shutting down.");
315 System.exit(0);
316 } catch (Exception ex) {
317 ex.printStackTrace();
318 System.exit(1);
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));
326 } else {
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);
335 String error = null;
336 if (dir.exists()) {
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;
345 if (error != null) {
346 System.err.println(error);
347 System.exit(1);
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) {
365 return;
367 File file = findRdbmsPropertiesFile(externalResourceDir);
368 if (file == null) {
369 file = findRdbmsPropertiesFile(appDir);
371 if (file != null) {
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()) {
386 return candidate;
388 return null;
391 private File getExternalResourceDir() {
392 if (externalResourceDir == null) {
393 return null;
395 externalResourceDir = externalResourceDir.trim();
396 String error = null;
397 File dir = null;
398 if (externalResourceDir.isEmpty()) {
399 error = "The empty string was specified for external_resource_dir";
400 } else {
401 dir = new File(externalResourceDir);
402 if (dir.exists()) {
403 if (!dir.isDirectory()) {
404 error = externalResourceDir + " is not a directory.";
406 } else {
407 error = "No such directory: " + externalResourceDir;
410 if (error != null) {
411 System.err.println(error);
412 System.exit(1);
414 return dir;
419 public static void validateWarPath(File war) {
420 if (!war.exists()) {
421 System.out.println("Unable to find the webapp directory " + war);
422 printHelp(System.err);
423 System.exit(1);
424 } else if (!war.isDirectory()) {
425 System.out.println("dev_appserver only accepts webapp directories, not war files.");
426 printHelp(System.err);
427 System.exit(1);
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:
448 * name=value
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.
456 @VisibleForTesting
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");
466 } else {
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) {
476 if (!condition) {
477 System.err.println(PREFIX + " " + suffix);
478 System.exit(1);