google-nomulus/java/google/registry/tools/RegistryCli.java
mmuller 18290911a0 Fix multiple invocations of the "shell" command
JCommander doesn't seem to reset objects when it populates them with data from
an argument list during command processing, so recreate the command objects
every time we do a run().

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=191332392
2018-04-02 16:55:15 -04:00

201 lines
7.8 KiB
Java

// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.tools;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.tools.Injector.injectReflectively;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.beust.jcommander.ParametersDelegate;
import com.google.appengine.tools.remoteapi.RemoteApiInstaller;
import com.google.appengine.tools.remoteapi.RemoteApiOptions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import google.registry.model.ofy.ObjectifyService;
import google.registry.tools.Command.RemoteApiCommand;
import google.registry.tools.params.ParameterFactory;
import java.security.Security;
import java.util.Map;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
/** Container class to create and run remote commands against a Datastore instance. */
@Parameters(separators = " =", commandDescription = "Command-line interface to the registry")
final class RegistryCli implements AutoCloseable, CommandRunner {
@Parameter(
names = {"-e", "--environment"},
description = "Sets the default environment to run the command.")
private RegistryToolEnvironment environment = RegistryToolEnvironment.PRODUCTION;
@Parameter(
names = {"-c", "--commands"},
description = "Returns all command names.")
private boolean showAllCommands;
// Do not make this final - compile-time constant inlining may interfere with JCommander.
@ParametersDelegate
private AppEngineConnectionFlags appEngineConnectionFlags =
new AppEngineConnectionFlags();
// Do not make this final - compile-time constant inlining may interfere with JCommander.
@ParametersDelegate
private LoggingParameters loggingParams = new LoggingParameters();
// These are created lazily on first use.
private AppEngineConnection connection;
private RemoteApiInstaller installer;
Map<String, ? extends Class<? extends Command>> commands;
String programName;
RegistryCli(
String programName, ImmutableMap<String, ? extends Class<? extends Command>> commands) {
this.programName = programName;
this.commands = commands;
Security.addProvider(new BouncyCastleProvider());
}
// The <? extends Class<? extends Command>> wildcard looks a little funny, but is needed so that
// we can accept maps with value types that are subtypes of Class<? extends Command> rather than
// literally that type. For more explanation, see:
// http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeArguments.html#FAQ104
@Override
public void run(String[] args) throws Exception {
// Create the JCommander instance.
JCommander jcommander = new JCommander(this);
jcommander.addConverterFactory(new ParameterFactory());
jcommander.setProgramName(programName);
// Create the "help" and "shell" commands (these are special in that they don't have a default
// constructor).
jcommander.addCommand("help", new HelpCommand(jcommander));
jcommander.addCommand("shell", new ShellCommand(System.in, this));
// Create all command instances. It would be preferrable to do this in the constructor, but
// JCommander mutates the command instances and doesn't reset them so we have to do it for every
// run.
try {
for (Map.Entry<String, ? extends Class<? extends Command>> entry : commands.entrySet()) {
Command command = entry.getValue().getDeclaredConstructor().newInstance();
jcommander.addCommand(entry.getKey(), command);
}
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
try {
jcommander.parse(args);
} catch (ParameterException e) {
// If we failed to fully parse the command but at least found a valid command name, show only
// the usage for that command. Otherwise, show full usage. Either way, rethrow the error.
if (jcommander.getParsedCommand() == null) {
jcommander.usage();
} else {
jcommander.usage(jcommander.getParsedCommand());
}
// Don't rethrow if we said: nomulus command --help
if ("Unknown option: --help".equals(e.getMessage())) {
return;
}
throw e;
}
if (showAllCommands) {
for (Map.Entry<String, ? extends Class<? extends Command>> entry : commands.entrySet()) {
System.out.println(entry.getKey());
}
return;
}
checkState(RegistryToolEnvironment.get() == environment,
"RegistryToolEnvironment argument pre-processing kludge failed.");
// JCommander stores sub-commands as nested JCommander objects containing a list of user objects
// to be populated. Extract the subcommand by getting the JCommander wrapper and then
// retrieving the first (and, by virtue of our usage, only) object from it.
JCommander jcCommand = jcommander.getCommands().get(jcommander.getParsedCommand());
Command command = (Command) Iterables.getOnlyElement(jcCommand.getObjects());
loggingParams.configureLogging(); // Must be called after parameters are parsed.
try {
runCommand(command);
} catch (AuthModule.LoginRequiredException ex) {
System.err.println("===================================================================");
System.err.println("You must login using 'nomulus login' prior to running this command.");
System.err.println("===================================================================");
}
}
@Override
public void close() {
if (installer != null) {
installer.uninstall();
installer = null;
}
}
private void runCommand(Command command) throws Exception {
// Create the main component and use it to inject the command class.
RegistryToolComponent component = DaggerRegistryToolComponent.builder()
.flagsModule(new AppEngineConnectionFlags.FlagsModule(appEngineConnectionFlags))
.build();
injectReflectively(RegistryToolComponent.class, component, command);
if (!(command instanceof RemoteApiCommand)) {
// TODO(mmuller): this should be in the try/catch LoginRequiredException in case future
// commands use our credential.
command.run();
return;
}
// Get the App Engine connection, advise the user if they are not currently logged in..
if (connection == null) {
connection = component.appEngineConnection();
}
if (command instanceof ServerSideCommand) {
((ServerSideCommand) command).setConnection(connection);
}
// RemoteApiCommands need to have the remote api installed to work.
if (installer == null) {
installer = new RemoteApiInstaller();
RemoteApiOptions options = new RemoteApiOptions();
options.server(connection.getServer().getHost(), connection.getServer().getPort());
if (connection.isLocalhost()) {
// Use dev credentials for localhost.
options.useDevelopmentServerCredential();
} else {
options.useApplicationDefaultCredential();
}
installer.install(options);
}
// Ensure that all entity classes are loaded before command code runs.
ObjectifyService.initOfy();
command.run();
}
void setEnvironment(RegistryToolEnvironment environment) {
this.environment = environment;
}
}