2 // Copyright 2008 Google Inc. All Rights Reserved.
4 package com
.google
.appengine
.tools
.admin
;
6 import com
.google
.api
.client
.auth
.oauth2
.Credential
;
7 import com
.google
.appengine
.tools
.admin
.AppAdmin
.LogSeverity
;
8 import com
.google
.appengine
.tools
.admin
.AppAdminFactory
.ConnectOptions
;
9 import com
.google
.appengine
.tools
.admin
.ClientLoginServerConnection
.ClientAuthFailException
;
10 import com
.google
.appengine
.tools
.admin
.IndexDeleter
.DeleteIndexAction
;
11 import com
.google
.appengine
.tools
.development
.DevAppServerMain
;
12 import com
.google
.appengine
.tools
.info
.SupportInfo
;
13 import com
.google
.appengine
.tools
.info
.UpdateCheck
;
14 import com
.google
.appengine
.tools
.plugins
.ActionsAndOptions
;
15 import com
.google
.appengine
.tools
.plugins
.SDKPluginManager
;
16 import com
.google
.appengine
.tools
.plugins
.SDKRuntimePlugin
;
17 import com
.google
.appengine
.tools
.plugins
.SDKRuntimePlugin
.ApplicationDirectories
;
18 import com
.google
.appengine
.tools
.util
.Action
;
19 import com
.google
.appengine
.tools
.util
.ClientCookieManager
;
20 import com
.google
.appengine
.tools
.util
.Logging
;
21 import com
.google
.appengine
.tools
.util
.Option
;
22 import com
.google
.appengine
.tools
.util
.Parser
;
23 import com
.google
.appengine
.tools
.util
.Parser
.ParseResult
;
24 import com
.google
.appengine
.tools
.wargen
.WarGenerator
;
25 import com
.google
.appengine
.tools
.wargen
.WarGeneratorFactory
;
26 import com
.google
.apphosting
.utils
.config
.AppEngineConfigException
;
27 import com
.google
.apphosting
.utils
.config
.BackendsXml
;
28 import com
.google
.common
.base
.Joiner
;
29 import com
.google
.common
.collect
.ImmutableList
;
31 import java
.io
.BufferedReader
;
32 import java
.io
.ByteArrayInputStream
;
34 import java
.io
.FileWriter
;
35 import java
.io
.IOException
;
36 import java
.io
.InputStreamReader
;
37 import java
.io
.ObjectInputStream
;
38 import java
.io
.PrintWriter
;
39 import java
.io
.Reader
;
40 import java
.util
.ArrayList
;
41 import java
.util
.Arrays
;
42 import java
.util
.HashMap
;
43 import java
.util
.HashSet
;
44 import java
.util
.Iterator
;
45 import java
.util
.LinkedList
;
46 import java
.util
.List
;
49 import java
.util
.TreeSet
;
50 import java
.util
.prefs
.Preferences
;
53 * The command-line SDK tool for administration of App Engine applications.
58 public static final String USE_JAVA7_SYSTEM_PROP
="com.google.apphosting.runtime.use_java7";
60 private static final String EXTERNAL_RESOURCE_DIR_ARG
=
61 DevAppServerMain
.EXTERNAL_RESOURCE_DIR_ARG
;
62 private static final String GENERATE_WAR_ARG
=
63 DevAppServerMain
.GENERATE_WAR_ARG
;
64 private static final String GENERATED_WAR_DIR_ARG
=
65 DevAppServerMain
.GENERATED_WAR_DIR_ARG
;
67 private final ConnectOptions connectOptions
;
68 private String externalResourceDir
;
69 private boolean generateWar
= false;
70 private String generatedWarDir
;
71 private AppCfgAction action
;
72 private String applicationDirectory
;
73 private AppAdmin admin
;
74 private boolean passin
;
75 private boolean doBatch
= true;
76 private boolean doJarSplitting
= false;
77 private Set
<String
> jarSplittingExcludeSuffixes
= null;
78 private boolean disablePrompt
= false;
79 private File logFile
= null;
80 private String compileEncoding
= null;
81 private final String prefsEmail
;
82 private LoginReader loginReader
= null;
83 private String overrideAppId
;
84 private String overrideAppVersion
;
85 private boolean oauth2
= false;
86 private boolean useCookies
= true;
88 public static void main(String
[] args
) {
89 Logging
.initializeLogging();
93 protected AppCfg(String
[] cmdLineArgs
) {
94 this(new AppAdminFactory(), cmdLineArgs
);
97 public AppCfg(AppAdminFactory factory
, String
[] cmdLineArgs
) {
98 connectOptions
= new ConnectOptions();
99 Parser parser
= new Parser();
101 PrintWriter logWriter
;
104 logFile
= File
.createTempFile("appcfg", ".log");
105 logWriter
= new PrintWriter(new FileWriter(logFile
), true);
106 } catch (IOException e
) {
107 throw new RuntimeException("Unable to enable logging.", e
);
110 String prefsEmail
= null;
114 parser
.parseArgs(actionsAndOptions
.actions
, actionsAndOptions
.options
, cmdLineArgs
);
115 action
= (AppCfgAction
) result
.getAction();
118 } catch (IllegalArgumentException e
) {
119 e
.printStackTrace(logWriter
);
120 System
.out
.println("Bad argument: " + e
.getMessage());
121 System
.out
.println(action
.getHelpString());
124 if (System
.getProperty("http.proxyHost") != null &&
125 System
.getProperty("https.proxyHost") == null) {
126 System
.setProperty("https.proxyHost",
127 System
.getProperty("http.proxyHost"));
128 if (System
.getProperty("http.proxyPort") != null &&
129 System
.getProperty("https.proxyPort") == null) {
130 System
.setProperty("https.proxyPort",
131 System
.getProperty("http.proxyPort"));
135 Application app
= null;
136 if (applicationDirectory
!= null) {
137 File appDirectoryFile
= new File(applicationDirectory
);
138 validateWarPath(appDirectoryFile
);
140 UpdateCheck updateCheck
= new UpdateCheck(connectOptions
.getServer(), appDirectoryFile
,
141 connectOptions
.getSecure());
142 updateCheck
.maybePrintNagScreen(System
.out
);
143 updateCheck
.checkJavaVersion(System
.out
);
146 authorizeOauth2(connectOptions
);
148 prefsEmail
= loadCookies(connectOptions
);
151 factory
.setBatchMode(doBatch
);
153 factory
.setJarSplittingEnabled(doJarSplitting
);
154 if (jarSplittingExcludeSuffixes
!= null) {
155 factory
.setJarSplittingExcludes(jarSplittingExcludeSuffixes
);
157 if (compileEncoding
!= null) {
158 factory
.setCompileEncoding(compileEncoding
);
160 System
.out
.println("Reading application configuration data...");
161 app
= Application
.readApplication(applicationDirectory
, overrideAppId
,
163 if (externalResourceDir
!= null) {
164 app
.setExternalResourceDir(externalResourceDir
);
167 app
.setListener(new UpdateListener() {
168 public void onProgress(UpdateProgressEvent event
) {
169 System
.out
.println(event
.getPercentageComplete() + "% " + event
.getMessage());
172 public void onSuccess(UpdateSuccessEvent event
) {
173 System
.out
.println("Operation complete.");
176 public void onFailure(UpdateFailureEvent event
) {
177 System
.out
.println(event
.getFailureMessage());
181 admin
= factory
.createAppAdmin(connectOptions
, app
, logWriter
);
183 System
.out
.println("Beginning server interaction for " + app
.getAppId() + "...");
185 admin
= factory
.createAppAdmin(connectOptions
, null, logWriter
);
190 } catch (AdminException ex
) {
191 System
.out
.println(ex
.getMessage());
192 ex
.printStackTrace(logWriter
);
196 System
.out
.println("Success.");
199 if (!connectOptions
.getRetainUploadDir()) {
200 System
.out
.println("Cleaning up temporary files...");
201 app
.cleanStagingDirectory();
203 File stage
= app
.getStagingDir();
205 System
.out
.println("Temporary staging directory was not needed, and not created");
207 System
.out
.println("Temporary staging directory left in " + stage
.getCanonicalPath());
212 } catch (IllegalArgumentException e
) {
213 e
.printStackTrace(logWriter
);
214 System
.out
.println("Bad argument: " + e
.getMessage());
217 } catch (AppEngineConfigException e
) {
218 e
.printStackTrace(logWriter
);
219 System
.out
.println("Bad configuration: " + e
.getMessage());
220 if (e
.getCause() != null) {
221 System
.out
.println(" Caused by: " + e
.getCause().getMessage());
225 } catch (Exception e
) {
226 System
.out
.println("Encountered a problem: " + e
.getMessage());
227 e
.printStackTrace(logWriter
);
231 this.prefsEmail
= prefsEmail
;
235 * Prints a uniform message to direct the user to the given logfile for
238 private void printLogLocation() {
239 if (logFile
!= null) {
240 System
.out
.println("Please see the logs [" + logFile
.getAbsolutePath() +
241 "] for further information.");
245 private String
loadCookies(final ConnectOptions options
) {
246 Preferences prefs
= Preferences
.userNodeForPackage(ServerConnection
.class);
247 String prefsEmail
= prefs
.get("email", null);
249 if (options
.getUsePersistedCredentials() && prefsEmail
!= null) {
250 ClientCookieManager cookies
= null;
251 byte[] serializedCookies
= prefs
.getByteArray("cookies", null);
252 if (serializedCookies
!= null) {
254 cookies
= (ClientCookieManager
)
255 new ObjectInputStream(
256 new ByteArrayInputStream(serializedCookies
)).readObject();
257 } catch (ClassNotFoundException ex
) {
258 } catch (IOException ex
) {
262 if (options
.getUserId() == null ||
263 prefsEmail
.equals(options
.getUserId())) {
264 options
.setCookies(cookies
);
268 options
.setPasswordPrompt(new AppAdminFactory
.PasswordPrompt() {
269 public String
getPassword() {
271 options
.setUserId(loginReader
.getUsername());
272 return loginReader
.getPassword();
279 * Tries to get an OAuth2 access token and set it in the ConnectOptions.
280 * It exists with exit code 1 in case no token could be obtained.
282 private void authorizeOauth2(final ConnectOptions options
){
283 OAuth2Native client
= new OAuth2Native(useCookies
);
284 Credential credential
= client
.authorize();
285 if (credential
!= null && credential
.getAccessToken() != null) {
286 options
.setOauthToken(credential
.getAccessToken());
288 System
.out
.println("Either the code entered is invalid or the token was revoked.");
294 * Helper function for generating a war directory based on an app.yaml file located in an external
295 * resource directory. First the command line arguments are checked to ensure that they are
296 * appropriate for war generation. If there is a problem then a {@link RuntimeException} is
297 * thrown. Otherwise a war directory is generated and its path is returned, and a success
298 * message is written to standard out.
300 * @return The path of the generated war directory.
302 private String
validateArgsAndGenerateWar() {
303 if (externalResourceDir
== null) {
304 throw new IllegalArgumentException("When generating a war directory --"
305 + EXTERNAL_RESOURCE_DIR_ARG
+ " must also be specified.");
307 File externalResourceDirectory
= new File(externalResourceDir
);
308 if (!externalResourceDirectory
.isDirectory()) {
309 throw new IllegalArgumentException(externalResourceDir
+ " is not an existing directory.");
311 File appYamlFile
= new File(externalResourceDirectory
, WarGenerator
.APP_YAML
);
312 if (!appYamlFile
.isFile()) {
313 throw new IllegalArgumentException(appYamlFile
.getPath() + " not found.");
315 File destination
= (generatedWarDir
== null ?
null : new File(generatedWarDir
));
317 WarGenerator warGen
=
318 WarGeneratorFactory
.newWarGenerator(externalResourceDirectory
, destination
);
319 String warDir
= warGen
.generateWarDirectory().getPath();
320 System
.out
.println("Successfully generated war directory at " + warDir
);
322 } catch (IOException e
) {
323 throw new RuntimeException("Unable to generate a war directory.", e
);
327 private void doPrompt() {
330 System
.out
.println("Your authentication credentials can't be found and may have expired.\n" +
331 "Please run appcfg directly from the command line to re-establish your credentials.");
335 getLoginReader().doPrompt();
339 private LoginReader
getLoginReader() {
340 if (loginReader
== null) {
341 loginReader
= LoginReaderFactory
.createLoginReader(connectOptions
, passin
, prefsEmail
);
346 private static final List
<String
> generalOptionNamesInHelpOrder
=
362 private static final List
<String
> optionNamesInHelpOrder
=
363 ImmutableList
.<String
>builder().addAll(generalOptionNamesInHelpOrder
).add(
364 "enable_jar_splitting",
365 "jar_splitting_excludes",
375 private static final List
<String
> actionNamesInHelpOrder
=
387 "set_default_version",
389 "resource_limits_info",
397 "backends configure");
399 private String helpText
= null;
400 private void printHelp() {
401 if (helpText
== null) {
402 List
<String
> helpLines
= new LinkedList
<String
>();
403 helpLines
.add("usage: AppCfg [options] <action> [<app-dir>] [<argument>]");
405 helpLines
.add("Action must be one of:");
406 for (String actionName
: actionsAndOptions
.actionNames
) {
407 Action action
= actionsAndOptions
.getAction(actionName
);
408 if (action
!= null) {
409 helpLines
.add(" " + actionName
+ ": " + action
.getShortDescription());
412 helpLines
.add("Use 'help <action>' for a detailed description.");
414 helpLines
.add("options:");
415 for (String optionName
: actionsAndOptions
.optionNames
) {
416 Option option
= actionsAndOptions
.getOption(optionName
);
417 helpLines
.addAll(option
.getHelpLines());
419 helpText
= Joiner
.on("\n").join(helpLines
);
421 System
.out
.println(helpText
);
422 System
.out
.println();
425 private final List
<Option
> builtInOptions
= Arrays
.asList(
427 new Option("h", "help", true) {
429 public List
<String
> getHelpLines() {
430 return ImmutableList
.<String
>of(
431 " -h, --help Show the help message and exit.");
434 public void apply() {
440 new Option("s", "server", false) {
442 public List
<String
> getHelpLines() {
443 return ImmutableList
.<String
>of(
444 " -s SERVER, --server=SERVER",
445 " The server to connect to.");
448 public void apply() {
449 connectOptions
.setServer(getValue());
453 new Option("e", "email", false) {
455 public List
<String
> getHelpLines() {
456 return ImmutableList
.<String
>of(
457 " -e EMAIL, --email=EMAIL",
458 " The username to use. Will prompt if omitted.");
461 public void apply() {
462 connectOptions
.setUserId(getValue());
466 new Option("H", "host", false) {
468 public List
<String
> getHelpLines() {
469 return ImmutableList
.<String
>of(
470 " -H HOST, --host=HOST Overrides the Host header sent with all RPCs.");
473 public void apply() {
474 connectOptions
.setHost(getValue());
478 new Option("p", "proxy", false) {
480 public List
<String
> getHelpLines() {
481 return ImmutableList
.<String
>of(
482 " -p PROXYHOST[:PORT], --proxy=PROXYHOST[:PORT]",
483 " Proxies requests through the given proxy server.",
484 " If --proxy_https is also set, only HTTP will be",
485 " proxied here, otherwise both HTTP and HTTPS will.");
488 public void apply() {
489 HostPort hostport
= new HostPort(getValue());
491 System
.setProperty("http.proxyHost", hostport
.getHost());
492 if (hostport
.hasPort()) {
493 System
.setProperty("http.proxyPort", hostport
.getPort());
498 new Option(null, "proxy_https", false) {
500 public List
<String
> getHelpLines() {
501 return ImmutableList
.<String
>of(
502 " --proxy_https=PROXYHOST[:PORT]",
503 " Proxies HTTPS requests through the given proxy server.");
506 public void apply() {
507 HostPort hostport
= new HostPort(getValue());
509 System
.setProperty("https.proxyHost", hostport
.getHost());
510 if (hostport
.hasPort()) {
511 System
.setProperty("https.proxyPort", hostport
.getPort());
516 new Option(null, "insecure", true) {
518 public List
<String
> getHelpLines() {
519 return ImmutableList
.<String
>of(
520 " --insecure Do not use HTTPS to communicate with the Admin Console.");
523 public void apply() {
524 connectOptions
.setSecure(false);
528 new Option(null, "no_cookies", true) {
530 public List
<String
> getHelpLines() {
531 return ImmutableList
.<String
>of(
533 " --no_cookies Do not save/load access credentials to/from disk.");
536 public void apply() {
538 connectOptions
.setUsePersistedCredentials(false);
542 new Option("f", "force", true) {
544 public List
<String
> getHelpLines() {
545 return ImmutableList
.<String
>of(
546 " -f, --force Force deletion of indexes without being prompted.");
550 if (action
instanceof VacuumIndexesAction
){
551 VacuumIndexesAction viAction
= (VacuumIndexesAction
) action
;
552 viAction
.promptUserForEachDelete
= false;
557 new Option("a", "append", true) {
559 public List
<String
> getHelpLines() {
560 return ImmutableList
.<String
>of(
561 " -a, --append Append to existing file.");
564 public void apply() {
565 RequestLogsAction logsAction
= (RequestLogsAction
) action
;
566 logsAction
.append
= true;
570 new Option("n", "num_days", false) {
572 public List
<String
> getHelpLines() {
573 return ImmutableList
.<String
>of(
574 " -n NUM_DAYS, --num_days=NUM_DAYS",
575 " Number of days worth of log data to get. The cut-off",
576 " point is midnight UTC. Use 0 to get all available",
577 " logs. Default is 1.");
580 public void apply() {
581 if (action
instanceof RequestLogsAction
) {
582 RequestLogsAction logsAction
= (RequestLogsAction
) action
;
584 logsAction
.numDays
= Integer
.parseInt(getValue());
585 } catch (NumberFormatException e
) {
586 throw new IllegalArgumentException("num_days must be an integral number.");
589 CronInfoAction croninfoAction
= (CronInfoAction
) action
;
590 croninfoAction
.setNumRuns(getValue());
595 new Option(null, "num_runs", false) {
597 public List
<String
> getHelpLines() {
598 return ImmutableList
.<String
>of(
599 " -n NUM_RUNS, --num_runs=NUM_RUNS",
600 " Number of scheduled execution times to compute");
603 public void apply() {
604 CronInfoAction croninfoAction
= (CronInfoAction
) action
;
605 croninfoAction
.setNumRuns(getValue());
609 new Option(null, "severity", false) {
611 public List
<String
> getHelpLines() {
612 return ImmutableList
.<String
>of(
613 " --severity=SEVERITY Severity of app-level log messages to get. The range",
614 " is 0 (DEBUG) through 4 (CRITICAL). If omitted, only",
615 " request logs are returned.");
618 public void apply() {
619 RequestLogsAction logsAction
= (RequestLogsAction
) action
;
621 int severity
= Integer
.parseInt(getValue());
622 int maxSeverity
= LogSeverity
.values().length
;
623 if (severity
< 0 || severity
> maxSeverity
) {
624 throw new IllegalArgumentException("severity must be between 0 and " + maxSeverity
);
626 logsAction
.severity
= severity
;
627 } catch (NumberFormatException e
) {
628 for (Enum severity
: LogSeverity
.values()) {
629 if (getValue().equalsIgnoreCase(severity
.toString())) {
630 logsAction
.severity
= severity
.ordinal();
634 throw new IllegalArgumentException("severity must be an integral "
635 + "number 0-4, or one of DEBUG, INFO, WARN, ERROR, CRITICAL");
640 new Option(null, "sdk_root", false) {
642 public List
<String
> getHelpLines() {
643 return ImmutableList
.<String
>of(
644 " --sdk_root=root Overrides where the SDK is located.");
647 public void apply() {
648 connectOptions
.setSdkRoot(getValue());
652 new Option(null, "enable_jar_splitting", true) {
654 public List
<String
> getHelpLines() {
655 return ImmutableList
.<String
>of(
656 " --enable_jar_splitting",
657 " Split large jar files (> 10M) into smaller fragments.");
660 public void apply() {
661 doJarSplitting
= true;
665 new Option(null, "jar_splitting_excludes", false) {
667 public List
<String
> getHelpLines() {
668 return ImmutableList
.<String
>of(
669 " --jar_splitting_excludes=SUFFIXES",
670 " When --enable-jar-splitting is set, files that match",
671 " the list of comma separated SUFFIXES will be excluded",
675 public void apply() {
676 jarSplittingExcludeSuffixes
= new HashSet
<String
>(Arrays
.asList(getValue().split(",")));
680 new Option(null, "retain_upload_dir", true) {
682 public List
<String
> getHelpLines() {
683 return ImmutableList
.<String
>of(
684 " --retain_upload_dir",
685 " Do not delete temporary (staging) directory used in",
689 public void apply() {
690 connectOptions
.setRetainUploadDir(true);
694 new Option(null, "passin", true) {
696 public List
<String
> getHelpLines() {
697 return ImmutableList
.<String
>of(
698 " --passin Always read the login password from stdin.");
701 public void apply() {
706 new Option(null, "no_batch", true) {
708 public void apply() {
712 new Option(null, "compile_encoding", false) {
714 public List
<String
> getHelpLines() {
715 return ImmutableList
.<String
>of(
716 " --compile_encoding",
717 " The character encoding to use when compiling JSPs.");
720 public void apply() {
721 compileEncoding
= getValue();
725 new Option(null, "disable_prompt", true) {
727 public void apply() {
728 disablePrompt
= true;
732 new Option("A", "application", false) {
734 public List
<String
> getHelpLines() {
735 return ImmutableList
.<String
>of(
736 " -A APP_ID, --application=APP_ID",
737 " Override application id from appengine-web.xml or app.yaml");
740 public void apply() {
741 overrideAppId
= getValue();
745 new Option("V", "version" , false) {
747 public List
<String
> getHelpLines() {
748 return ImmutableList
.<String
>of(
749 " -V VERSION, --version=VERSION",
750 " Override (major) version from appengine-web.xml " +
754 public void apply() {
755 overrideAppVersion
= getValue();
759 new Option(null, "oauth2", true) {
761 public List
<String
> getHelpLines() {
762 return ImmutableList
.<String
>of(
763 " --oauth2 Use OAuth2 instead of password auth.");
766 public void apply() {
771 new Option(null, "use_java7", true) {
773 public List
<String
> getHelpLines() {
774 return ImmutableList
.<String
>of(
775 " --use_java7 Use the App Engine Java 7 runtime for this app.");
778 public void apply() {
779 System
.setProperty(USE_JAVA7_SYSTEM_PROP
, "true");
783 new Option(null, EXTERNAL_RESOURCE_DIR_ARG
, false) {
785 public void apply() {
786 externalResourceDir
= getValue();
790 new Option(null, GENERATE_WAR_ARG
, true) {
792 public void apply() {
797 new Option(null, GENERATED_WAR_DIR_ARG
, false) {
799 public void apply() {
801 generatedWarDir
= getValue();
805 private final List
<Action
> builtInActions
= Arrays
.<Action
>asList(
807 new RequestLogsAction(),
808 new RollbackAction(),
809 new UpdateIndexesAction(),
810 new UpdateCronAction(),
811 new UpdateDosAction(),
812 new UpdateQueueAction(),
813 new CronInfoAction(),
814 new VacuumIndexesAction(),
816 new DownloadAppAction(),
818 new SetDefaultVersionAction(),
819 new ResourceLimitsInfoAction(),
820 new BackendsListAction(),
821 new BackendsRollbackAction(),
822 new BackendsUpdateAction(),
823 new BackendsStartAction(),
824 new BackendsStopAction(),
825 new BackendsDeleteAction(),
826 new BackendsConfigureAction(),
830 private Map
<String
, Option
> builtInOptionMap
;
832 private List
<Option
> builtInOptions(String
... optionNames
) {
833 if (builtInOptionMap
== null) {
834 builtInOptionMap
= new HashMap
<String
, Option
>(builtInOptions
.size());
835 for (Option option
: builtInOptions
){
836 builtInOptionMap
.put(option
.getLongName(), option
);
839 List
<Option
> options
= new LinkedList
<Option
>();
840 for (String name
: optionNames
) {
841 Option option
= builtInOptionMap
.get(name
);
842 if (option
!= null) {
849 private final ActionsAndOptions actionsAndOptions
= buildActionsAndOptions();
851 private ActionsAndOptions
buildActionsAndOptions() {
852 ActionsAndOptions actionsAndOptions
= getBuiltInActionsAndOptions();
853 for (SDKRuntimePlugin runtimePlugin
: SDKPluginManager
.findAllRuntimePlugins()) {
854 runtimePlugin
.customizeAppCfgActionsAndOptions(actionsAndOptions
);
856 return actionsAndOptions
;
860 * Builds the collection of built-in Actions and Options.
862 private ActionsAndOptions
getBuiltInActionsAndOptions() {
863 ActionsAndOptions actionsAndOptions
= new ActionsAndOptions();
864 actionsAndOptions
.actions
= builtInActions
;
865 actionsAndOptions
.actionNames
= actionNamesInHelpOrder
;
866 actionsAndOptions
.options
= builtInOptions
;
867 actionsAndOptions
.optionNames
= optionNamesInHelpOrder
;
868 actionsAndOptions
.generalOptionNames
= generalOptionNamesInHelpOrder
;
869 return actionsAndOptions
;
872 abstract class AppCfgAction
extends Action
{
874 AppCfgAction(String
... names
) {
878 AppCfgAction(List
<Option
> options
, String
... names
) {
879 super(options
, names
);
883 protected void setArgs(List
<String
> args
) {
888 public void apply() {
890 applicationDirectory
= validateArgsAndGenerateWar();
891 List
<String
> args
= getArgs();
892 List
<String
> newArgs
= new ArrayList
<String
>(args
.size() + 1);
893 newArgs
.add(applicationDirectory
);
894 newArgs
.addAll(args
);
897 if (getArgs().size() < 1) {
898 throw new IllegalArgumentException("Expected the application directory"
899 + " as an argument after the action name.");
901 applicationDirectory
= getArgs().get(0);
903 SDKRuntimePlugin runtimePlugin
= SDKPluginManager
.findRuntimePlugin(
904 new File(applicationDirectory
));
905 if (runtimePlugin
!= null) {
907 ApplicationDirectories appDirs
= runtimePlugin
.generateApplicationDirectories(
908 new File(applicationDirectory
));
909 applicationDirectory
= appDirs
.getWarDir().getPath();
910 getArgs().set(0, applicationDirectory
);
911 externalResourceDir
= appDirs
.getExternalResourceDir().getPath();
912 } catch (IOException e
) {
913 throw new RuntimeException("Unable to generate the war directory", e
);
919 public abstract void execute();
922 protected List
<String
> getHelpLines() {
923 List
<String
> helpLines
= new LinkedList
<String
>();
924 helpLines
.addAll(getInitialHelpLines());
926 helpLines
.add("Options:");
927 for (String optionName
: actionsAndOptions
.generalOptionNames
) {
928 Option option
= actionsAndOptions
.getOption(optionName
);
929 if (option
!= null) {
930 helpLines
.addAll(option
.getHelpLines());
933 if (extraOptions
!= null) {
934 for (Option option
: extraOptions
) {
935 helpLines
.addAll(option
.getHelpLines());
942 * Returns a list of Strings to be displayed as the initial lines of a help text. Subclasses
943 * should override this method.
945 * The text returned by this method should describe the base Action without any of its options.
946 * Text describing the options will be added in lines below this text.
948 protected List
<String
> getInitialHelpLines() {
949 return ImmutableList
.of();
954 class UpdateAction
extends AppCfgAction
{
956 super(builtInOptions("enable_jar_splitting", "jar_splitting_excludes", "retain_upload_dir",
957 "compile_encoding"), "update");
958 shortDescription
= "Create or update an app version.";
962 public void execute() {
963 admin
.update(new AppCfgUpdateListener());
967 protected List
<String
> getInitialHelpLines() {
968 return ImmutableList
.of(
969 "AppCfg [options] update <app-dir>",
971 "Installs a new version of the application onto the server, as the",
972 "default version for end users.");
976 class RequestLogsAction
extends AppCfgAction
{
980 boolean append
= false;
982 RequestLogsAction() {
983 super(builtInOptions("num_days", "severity", "append"), "request_logs");
984 shortDescription
= "Write request logs in Apache common log format.";
987 public void apply() {
989 if (getArgs().size() != 2) {
990 throw new IllegalArgumentException("Expected the application directory"
991 + " and log file as arguments after the request_logs action name.");
993 outputFile
= getArgs().get(1);
996 public void execute() {
997 Reader reader
= admin
.requestLogs(numDays
,
998 severity
>= 0 ? LogSeverity
.values()[severity
] : null);
999 if (reader
== null) {
1003 BufferedReader r
= new BufferedReader(reader
);
1004 PrintWriter writer
= null;
1006 if (outputFile
.equals("-")) {
1007 writer
= new PrintWriter(System
.out
);
1009 writer
= new PrintWriter(new FileWriter(outputFile
, append
));
1012 while ((line
= r
.readLine()) != null) {
1013 writer
.println(line
);
1015 } catch (IOException e
) {
1016 throw new RuntimeException("Failed to read logs: " + e
);
1018 if (writer
!= null) {
1023 } catch (IOException e
) {
1028 protected List
<String
> getInitialHelpLines() {
1029 return ImmutableList
.of(
1030 "AppCfg [options] request_logs <app-dir> <output-file>",
1032 "Populates the output-file with recent logs from the application.");
1036 class RollbackAction
extends AppCfgAction
{
1039 shortDescription
= "Rollback an in-progress update.";
1042 public void execute() {
1046 protected List
<String
> getInitialHelpLines() {
1047 return ImmutableList
.of(
1048 "AppCfg [options] rollback <app-dir>",
1050 "The 'update' command requires a server-side transaction.",
1051 "Use 'rollback' if you experience an error during 'update'",
1052 "and want to begin a new update transaction.");
1056 class UpdateIndexesAction
extends AppCfgAction
{
1057 UpdateIndexesAction() {
1058 super("update_indexes");
1059 shortDescription
= "Update application indexes.";
1062 public void execute() {
1063 admin
.updateIndexes();
1066 protected List
<String
> getInitialHelpLines() {
1067 return ImmutableList
.of(
1068 "AppCfg [options] update_indexes <app-dir>",
1070 "Updates the datastore indexes for the server to add any in the current",
1071 "application directory. Does not alter the running application version, nor",
1072 "remove any existing indexes.");
1076 class UpdateCronAction
extends AppCfgAction
{
1077 UpdateCronAction() {
1078 super("update_cron");
1079 shortDescription
= "Update application cron jobs.";
1082 public void execute() {
1084 shortDescription
= "Update application cron jobs.";
1087 protected List
<String
> getInitialHelpLines() {
1088 return ImmutableList
.of(
1089 "AppCfg [options] update_cron <app-dir>",
1091 "Updates the cron jobs for the server. Updates any new, removed or changed",
1092 "cron jobs. Does not otherwise alter the running application version.");
1096 class UpdateDosAction
extends AppCfgAction
{
1098 super("update_dos");
1099 shortDescription
= "Update application DoS protection configuration.";
1102 public void execute() {
1106 protected List
<String
> getInitialHelpLines() {
1107 return ImmutableList
.of(
1108 "AppCfg [options] update_dos <app-dir>",
1110 "Updates the DoS protection configuration for the server.",
1111 "Does not otherwise alter the running application version.");
1115 class UpdateQueueAction
extends AppCfgAction
{
1116 UpdateQueueAction() {
1117 super("update_queues");
1118 shortDescription
= "Update application task queue definitions.";
1121 public void execute() {
1122 admin
.updateQueues();
1125 protected List
<String
> getInitialHelpLines() {
1126 return ImmutableList
.of(
1127 "AppCfg [options] " + getNameString() + " <app-dir>",
1129 "Updates any new, removed or changed task queue definitions.",
1130 "Does not otherwise alter the running application version.");
1134 class CronInfoAction
extends AppCfgAction
{
1138 super(builtInOptions("num_runs"), "cron_info");
1139 shortDescription
= "Displays times for the next several runs of each cron job.";
1142 public void execute() {
1143 List
<CronEntry
> entries
= admin
.cronInfo();
1144 if (entries
.size() == 0) {
1145 System
.out
.println("No cron jobs defined.");
1147 System
.out
.println(entries
.size() + " cron entries defined.\n");
1148 for (CronEntry entry
: entries
) {
1149 System
.out
.println(entry
.toXml());
1150 System
.out
.println("Next " + numRuns
+ " execution times:");
1151 Iterator
<String
> iter
= entry
.getNextTimesIterator();
1152 for (int i
= 0; i
< numRuns
; i
++) {
1153 System
.out
.println(" " + iter
.next());
1155 System
.out
.println("");
1160 protected List
<String
> getInitialHelpLines() {
1161 return ImmutableList
.of(
1162 "AppCfg [options] cron_info <app-dir>",
1164 "Displays times for the next several runs of each cron job.");
1166 public void setNumRuns(String numberString
) {
1168 numRuns
= Integer
.parseInt(numberString
);
1169 } catch (NumberFormatException e
) {
1170 throw new IllegalArgumentException("num_runs must be an integral number.");
1173 throw new IllegalArgumentException("num_runs must be positive.");
1178 class VacuumIndexesAction
extends AppCfgAction
{
1179 public boolean promptUserForEachDelete
= true;
1181 VacuumIndexesAction() {
1182 super(builtInOptions("force"), "vacuum_indexes");
1183 shortDescription
= "Delete unused indexes from application.";
1187 public void execute() {
1188 ConfirmationCallback
<IndexDeleter
.DeleteIndexAction
> callback
= null;
1189 if (promptUserForEachDelete
) {
1190 callback
= new ConfirmationCallback
<IndexDeleter
.DeleteIndexAction
>() {
1192 public Response
confirmAction(DeleteIndexAction action
) {
1194 String prompt
= "\n" + action
.getPrompt() + " (N/y/a): ";
1195 System
.out
.print(prompt
);
1197 BufferedReader in
= new BufferedReader(new InputStreamReader(System
.in
));
1200 response
= in
.readLine();
1201 } catch (IOException ioe
) {
1204 response
= (null == response ?
"" : response
.trim().toLowerCase());
1205 if ("y".equals(response
)) {
1206 return Response
.YES
;
1208 if ("n".equals(response
) || response
.isEmpty()) {
1211 if ("a".equals(response
)) {
1212 return Response
.YES_ALL
;
1218 admin
.vacuumIndexes(callback
, new AppCfgVacuumIndexesListener());
1222 protected List
<String
> getInitialHelpLines() {
1223 return ImmutableList
.of(
1224 "AppCfg [options] vacuum_indexes <app-dir>",
1226 "Deletes indexes on the server that are not present in the local",
1227 "index configuration file. The user is prompted before each delete.");
1231 class HelpAction
extends AppCfgAction
{
1234 shortDescription
= "Print help for a specific action.";
1237 public void apply() {
1238 if (getArgs().isEmpty()) {
1241 Action foundAction
= Parser
.lookupAction(actionsAndOptions
.actions
,
1242 getArgs().toArray(new String
[0]), 0);
1243 if (foundAction
== null) {
1244 System
.out
.println("No such command \"" + getArgs().get(0) + "\"\n\n");
1247 System
.out
.println(foundAction
.getHelpString());
1248 System
.out
.println();
1254 public void execute() {
1257 protected List
<String
> getHelpLines() {
1258 return ImmutableList
.of("AppCfg help <command>",
1260 "Prints help about a specific command.",
1265 class DownloadAppAction
extends AppCfgAction
{
1266 DownloadAppAction() {
1267 super("download_app");
1268 shortDescription
= "Download a previously uploaded app version.";
1271 public void apply() {
1272 if (getArgs().size() != 1) {
1273 throw new IllegalArgumentException("Expected download directory"
1274 + " as an argument after download_app.");
1276 File downloadDir
= new File(getArgs().get(0));
1277 if (overrideAppId
== null) {
1278 throw new IllegalArgumentException("You must specify an app ID via -A or --application");
1282 authorizeOauth2(connectOptions
);
1284 loadCookies(connectOptions
);
1287 AppDownload appDownload
=
1288 new AppDownload(ServerConnectionFactory
.getServerConnection(connectOptions
),
1289 new AppCfgListener("download_app"));
1290 int exitCode
= appDownload
.download(overrideAppId
, overrideAppVersion
, downloadDir
) ?
0 : 1;
1291 System
.exit(exitCode
);
1294 public void execute() {
1297 protected List
<String
> getInitialHelpLines() {
1298 return ImmutableList
.of(
1299 "AppCfg [options] -A app_id [ -V version ] download_app <out-dir>",
1301 "Download a previously-uploaded app to the specified directory. The app",
1302 "ID is specified by the \"-A\" option. The optional version is specified",
1303 "by the \"-V\" option.");
1307 class VersionAction
extends AppCfgAction
{
1310 shortDescription
= "Prints version information.";
1313 public void apply() {
1314 System
.out
.println(SupportInfo
.getVersionString());
1318 public void execute() {
1321 protected List
<String
> getHelpLines() {
1322 return ImmutableList
.of(
1325 "Prints version information.");
1329 class SetDefaultVersionAction
extends AppCfgAction
{
1330 SetDefaultVersionAction() {
1331 super("set_default_version");
1332 shortDescription
= "Set the default serving version.";
1335 public void execute() {
1336 admin
.setDefaultVersion();
1339 protected List
<String
> getInitialHelpLines() {
1340 return ImmutableList
.of(
1341 "AppCfg [options] set_default_version <app-dir>",
1343 "Sets the default (serving) version");
1347 class ResourceLimitsInfoAction
extends AppCfgAction
{
1348 public ResourceLimitsInfoAction() {
1349 super("resource_limits_info");
1350 shortDescription
= "Display resource limits.";
1354 public void execute() {
1355 ResourceLimits resourceLimits
= admin
.getResourceLimits();
1356 for (String key
: new TreeSet
<String
>(resourceLimits
.keySet())) {
1357 System
.out
.println(key
+ ": " + resourceLimits
.get(key
));
1362 protected List
<String
> getInitialHelpLines() {
1363 return ImmutableList
.of(
1364 "AppCfg [options] resource_limits_info <app-dir>",
1366 "Displays the resource limits available to the app. An app will",
1367 "not update if any of the app's resources are larger than the",
1368 "appropriate resource limit.");
1372 class BackendsListAction
extends AppCfgAction
{
1373 BackendsListAction() {
1374 super("backends", "list");
1375 shortDescription
= "List the currently configured backends.";
1379 public void execute() {
1380 for (BackendsXml
.Entry backend
: admin
.listBackends()) {
1381 System
.out
.println(backend
.toString());
1386 protected List
<String
> getInitialHelpLines() {
1387 return ImmutableList
.of(
1388 "AppCfg [options] backends list <app-dir>",
1390 "List the currently configured backends.");
1394 class BackendsRollbackAction
extends AppCfgAction
{
1395 private String backendName
;
1397 BackendsRollbackAction() {
1398 super("backends", "rollback");
1399 shortDescription
= "Roll back a previously in-progress update.";
1403 public void apply() {
1405 if (getArgs().size() < 1 || getArgs().size() > 2) {
1406 throw new IllegalArgumentException("Expected <app-dir> [<backend-name>]");
1407 } else if (getArgs().size() == 2) {
1408 backendName
= getArgs().get(1);
1413 public void execute() {
1414 List
<String
> backends
;
1415 if (backendName
!= null) {
1416 admin
.rollbackBackend(backendName
);
1418 admin
.rollbackAllBackends();
1423 protected List
<String
> getInitialHelpLines() {
1424 return ImmutableList
.of(
1425 "AppCfg [options] backends rollback <app-dir> [<backend-name>]",
1427 "The 'backends update' command requires a server-side transaction.",
1428 "Use 'backends rollback' if you experience an error during 'backends update'",
1429 "and want to begin a new update transaction.");
1433 class BackendsUpdateAction
extends AppCfgAction
{
1434 private String backendName
;
1436 BackendsUpdateAction() {
1437 super("backends", "update");
1438 shortDescription
= "Update the specified backend or all backends.";
1442 public void apply() {
1444 if (getArgs().size() < 1 || getArgs().size() > 2) {
1445 throw new IllegalArgumentException("Expected <app-dir> [<backend-name>]");
1446 } else if (getArgs().size() == 2) {
1447 backendName
= getArgs().get(1);
1452 public void execute() {
1453 List
<String
> backends
;
1454 if (backendName
!= null) {
1455 admin
.updateBackend(backendName
, new AppCfgUpdateListener());
1457 admin
.updateAllBackends(new AppCfgUpdateListener());
1462 protected List
<String
> getInitialHelpLines() {
1463 return ImmutableList
.of(
1464 "AppCfg [options] backends update <app-dir> [<backend-name>]",
1466 "Update the specified backend or all backends.");
1470 class BackendsStartAction
extends AppCfgAction
{
1471 private String backendName
;
1473 BackendsStartAction() {
1474 super("backends", "start");
1475 shortDescription
= "Start the specified backend.";
1479 public void apply() {
1481 if (getArgs().size() != 2) {
1482 throw new IllegalArgumentException("Expected the backend name");
1484 backendName
= getArgs().get(1);
1488 public void execute() {
1489 admin
.setBackendState(backendName
, BackendsXml
.State
.START
);
1493 protected List
<String
> getInitialHelpLines() {
1494 return ImmutableList
.of(
1495 "AppCfg [options] backends start <app-dir> <backend>",
1497 "Starts the backend with the specified name.");
1501 class BackendsStopAction
extends AppCfgAction
{
1502 private String backendName
;
1504 BackendsStopAction() {
1505 super("backends", "stop");
1506 shortDescription
= "Stop the specified backend.";
1510 public void apply() {
1512 if (getArgs().size() != 2) {
1513 throw new IllegalArgumentException("Expected the backend name");
1515 backendName
= getArgs().get(1);
1518 public void execute() {
1519 admin
.setBackendState(backendName
, BackendsXml
.State
.STOP
);
1523 protected List
<String
> getInitialHelpLines() {
1524 return ImmutableList
.of(
1525 "AppCfg [options] backends stop <app-dir> <backend>",
1527 "Stops the backend with the specified name.");
1531 class BackendsDeleteAction
extends AppCfgAction
{
1532 private String backendName
;
1534 BackendsDeleteAction() {
1535 super("backends", "delete");
1536 shortDescription
= "Delete the specified backend.";
1540 public void apply() {
1542 if (getArgs().size() != 2) {
1543 throw new IllegalArgumentException("Expected the backend name");
1545 backendName
= getArgs().get(1);
1548 public void execute() {
1549 admin
.deleteBackend(backendName
);
1553 protected List
<String
> getInitialHelpLines() {
1554 return ImmutableList
.of(
1555 "AppCfg [options] backends delete",
1557 "Deletes the specified backend.");
1561 class BackendsConfigureAction
extends AppCfgAction
{
1562 private String backendName
;
1564 BackendsConfigureAction() {
1565 super("backends", "configure");
1566 shortDescription
= "Configure the specified backend.";
1570 public void apply() {
1572 if (getArgs().size() != 2) {
1573 throw new IllegalArgumentException("Expected the backend name");
1575 backendName
= getArgs().get(1);
1578 public void execute() {
1579 admin
.configureBackend(backendName
);
1583 protected List
<String
> getInitialHelpLines() {
1584 return ImmutableList
.of(
1585 "AppCfg [options] backends configure <app-dir> <backend>",
1587 "Updates the configuration of the backend with the specified name, without",
1588 "stopping instances that are currently running. Only valid for certain",
1589 "settings (instances, options: failfast, options: public).");
1594 * This is a catchall for the case where the user enters "appcfg.sh
1595 * backends app-dir sub-command" rather than "appcfg.sh backends
1596 * sub-command app-dir". It was added to maintain compatibility
1597 * with Python. It simply remaps the arguments and dispatches the
1598 * appropriate action.
1600 class BackendsAction
extends AppCfgAction
{
1601 private AppCfgAction subAction
;
1608 public void apply() {
1610 if (getArgs().size() < 2) {
1611 throw new IllegalArgumentException("Expected backends <app-dir> <sub-command> [...]");
1613 String dir
= getArgs().get(0);
1614 String subCommand
= getArgs().get(1);
1615 subAction
= (AppCfgAction
) Parser
.lookupAction(actionsAndOptions
.actions
,
1616 new String
[] {"backends", subCommand
},
1618 if (subAction
instanceof BackendsAction
) {
1619 throw new IllegalArgumentException("Unknown backends subcommand.");
1621 List
<String
> newArgs
= new ArrayList
<String
>();
1623 newArgs
.addAll(getArgs().subList(2, getArgs().size()));
1624 subAction
.setArgs(newArgs
);
1629 public void execute() {
1630 subAction
.execute();
1634 protected List
<String
> getHelpLines() {
1635 return ImmutableList
.of(
1636 "AppCfg [options] backends list: List the currently configured backends.",
1637 "AppCfg [options] backends update: Update the specified backend or all backends.",
1638 "AppCfg [options] backends rollback: Roll back a previously in-progress update.",
1639 "AppCfg [options] backends start: Start the specified backend.",
1640 "AppCfg [options] backends stop: Stop the specified backend.",
1641 "AppCfg [options] backends delete: Delete the specified backend.",
1642 "AppCfg [options] backends configure: Configure the specified backend.");
1646 private static class AppCfgListener
implements UpdateListener
{
1647 private String operationName
;
1649 AppCfgListener(String opName
){
1650 operationName
= opName
;
1652 public void onProgress(UpdateProgressEvent event
) {
1653 System
.out
.println(event
.getPercentageComplete() + "% " + event
.getMessage());
1656 public void onSuccess(UpdateSuccessEvent event
) {
1657 String details
= event
.getDetails();
1658 if (details
.length() > 0) {
1659 System
.out
.println();
1660 System
.out
.println("Details:");
1661 System
.out
.println(details
);
1664 System
.out
.println();
1665 System
.out
.println(operationName
+ " completed successfully.");
1668 public void onFailure(UpdateFailureEvent event
) {
1669 String details
= event
.getDetails();
1670 if (details
.length() > 0) {
1671 System
.out
.println();
1672 System
.out
.println("Error Details:");
1673 System
.out
.println(details
);
1676 System
.out
.println();
1677 String failMsg
= event
.getFailureMessage();
1678 System
.out
.println(failMsg
);
1679 if (event
.getCause() instanceof ClientAuthFailException
) {
1680 System
.out
.println("Consider using the -e EMAIL option if that"
1681 + " email address is incorrect.");
1686 private static class AppCfgUpdateListener
extends AppCfgListener
{
1687 AppCfgUpdateListener(){
1692 private static class AppCfgVacuumIndexesListener
extends AppCfgListener
{
1693 AppCfgVacuumIndexesListener(){
1694 super("vacuum_indexes");
1698 private static class HostPort
{
1699 private String host
;
1700 private String port
;
1702 public HostPort(String hostport
) {
1703 int colon
= hostport
.indexOf(':');
1704 host
= colon
< 0 ? hostport
: hostport
.substring(0, colon
);
1705 port
= colon
< 0 ?
"" : hostport
.substring(colon
+ 1);
1708 public String
getHost() {
1712 public String
getPort() {
1716 public boolean hasPort() {
1717 return port
.length() > 0;
1721 private void validateWarPath(File war
) {
1722 if (!war
.exists()) {
1723 System
.out
.println("Unable to find the webapp directory " + war
);
1726 } else if (!war
.isDirectory()) {
1727 System
.out
.println("appcfg only accepts webapp directories, not war files.");