Add a "shell" pseudo-command to nomulus tool

Add the "shell" command which lets you run multiple other command in a single
session, sparing you the initialization costs for all but the first of them.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=188712815
This commit is contained in:
mmuller 2018-03-12 06:16:24 -07:00 committed by jianglai
parent 64986442bc
commit f1c29633fb
6 changed files with 256 additions and 35 deletions

View file

@ -0,0 +1,24 @@
// Copyright 2018 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;
/**
* Interface for things that run commands.
*
* <p>This exists only to allow us to test ShellCommand.
*/
interface CommandRunner {
public void run(String[] args) throws Exception;
}

View file

@ -35,7 +35,7 @@ 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 {
final class RegistryCli implements AutoCloseable, CommandRunner {
@Parameter(
names = {"-e", "--environment"},
@ -57,33 +57,54 @@ final class RegistryCli {
@ParametersDelegate
private LoggingParameters loggingParams = new LoggingParameters();
// 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
void run(
String programName,
String[] args,
ImmutableMap<String, ? extends Class<? extends Command>> commands) throws Exception {
// These are created lazily on first use.
private AppEngineConnection connection;
private RemoteApiInstaller installer;
Map<String, Command> commandInstances;
Map<String, ? extends Class<? extends Command>> commands;
JCommander jcommander;
RegistryCli(
String programName, ImmutableMap<String, ? extends Class<? extends Command>> commands) {
this.commands = commands;
Security.addProvider(new BouncyCastleProvider());
JCommander jcommander = new JCommander(this);
jcommander = new JCommander(this);
jcommander.addConverterFactory(new ParameterFactory());
jcommander.setProgramName(programName);
// Store the instances of each Command class here so we can retrieve the same one for the
// called command later on. JCommander could have done this for us, but it doesn't.
Map<String, Command> commandInstances = new HashMap<>();
commandInstances = new HashMap<>();
HelpCommand helpCommand = new HelpCommand(jcommander);
jcommander.addCommand("help", helpCommand);
commandInstances.put("help", helpCommand);
for (Map.Entry<String, ? extends Class<? extends Command>> entry : commands.entrySet()) {
Command command = entry.getValue().getDeclaredConstructor().newInstance();
jcommander.addCommand(entry.getKey(), command);
commandInstances.put(entry.getKey(), command);
}
// Add the shell command.
ShellCommand shellCommand = new ShellCommand(System.in, this);
jcommander.addCommand("shell", shellCommand);
commandInstances.put("shell", shellCommand);
try {
for (Map.Entry<String, ? extends Class<? extends Command>> entry : commands.entrySet()) {
Command command = entry.getValue().getDeclaredConstructor().newInstance();
jcommander.addCommand(entry.getKey(), command);
commandInstances.put(entry.getKey(), command);
}
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
// 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 {
try {
jcommander.parse(args);
} catch (ParameterException e) {
@ -127,6 +148,14 @@ final class RegistryCli {
}
}
@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()
@ -142,31 +171,35 @@ final class RegistryCli {
}
// Get the App Engine connection, advise the user if they are not currently logged in..
AppEngineConnection connection = component.appEngineConnection();
if (connection == null) {
connection = component.appEngineConnection();
}
if (command instanceof ServerSideCommand) {
((ServerSideCommand) command).setConnection(connection);
}
// RemoteApiCommands need to have the remote api installed to work.
RemoteApiInstaller 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();
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);
}
installer.install(options);
// Ensure that all entity classes are loaded before command code runs.
ObjectifyService.initOfy();
try {
command.run();
} finally {
installer.uninstall();
}
command.run();
}
void setEnvironment(RegistryToolEnvironment environment) {
this.environment = environment;
}
}

View file

@ -132,6 +132,8 @@ public final class RegistryTool {
public static void main(String[] args) throws Exception {
RegistryToolEnvironment.parseFromArgs(args).setup();
new RegistryCli().run("nomulus", args, COMMAND_MAP);
try (RegistryCli cli = new RegistryCli("nomulus", COMMAND_MAP)) {
cli.run(args);
}
}
}

View file

@ -0,0 +1,93 @@
// Copyright 2018 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 java.nio.charset.StandardCharsets.US_ASCII;
import com.beust.jcommander.Parameters;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.io.StringReader;
/**
* Implements a tiny shell interpreter for the nomulus tool.
*
* <p>Parses a very simple command grammar. Tokens are either whitespace delimited words or
* double-quoted strings.
*/
@Parameters(commandDescription = "Run an interactive shell")
public class ShellCommand implements Command {
private final CommandRunner runner;
private final BufferedReader lineReader;
ShellCommand(InputStream in, CommandRunner runner) {
this.runner = runner;
this.lineReader = new BufferedReader(new InputStreamReader(in, US_ASCII));
}
/** Run the shell until the user presses "Ctrl-D". */
@Override
public void run() {
String line;
while ((line = getLine()) != null) {
String[] lineArgs = parseCommand(line);
try {
runner.run(lineArgs);
} catch (Exception e) {
System.err.println("Got an exception:\n" + e);
}
}
}
private String getLine() {
if (System.console() != null) {
System.err.print("nom> ");
}
try {
return lineReader.readLine();
} catch (IOException e) {
return null;
}
}
@VisibleForTesting
static String[] parseCommand(String line) {
ImmutableList.Builder<String> resultBuilder = new ImmutableList.Builder<>();
// Create a tokenizer, make everything word characters except quoted strings and unprintable
// ascii chars and space (just treat them all as whitespace).
StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(line));
tokenizer.resetSyntax();
tokenizer.whitespaceChars(0, ' ');
tokenizer.wordChars('!', '~');
tokenizer.quoteChar('"');
try {
while (tokenizer.nextToken() != StreamTokenizer.TT_EOF) {
resultBuilder.add(tokenizer.sval);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return resultBuilder.build().toArray(new String[0]);
}
}

View file

@ -90,8 +90,8 @@ public class RegistryToolTest {
/**
* Gets the set of all non-abstract classes implementing the {@link Command} interface (abstract
* class and interface subtypes of Command aren't expected to have cli commands). Note that this
* also filters out HelpCommand, which has special handling in {@link RegistryCli} and isn't in
* the command map.
* also filters out HelpCommand and ShellCommand, which have special handling in {@link
* RegistryCli} and aren't in the command map.
*
* @throws IOException if reading the classpath resources fails.
*/
@ -105,7 +105,8 @@ public class RegistryToolTest {
if (Command.class.isAssignableFrom(clazz)
&& !Modifier.isAbstract(clazz.getModifiers())
&& !Modifier.isInterface(clazz.getModifiers())
&& !clazz.equals(HelpCommand.class)) {
&& !clazz.equals(HelpCommand.class)
&& !clazz.equals(ShellCommand.class)) {
builder.add((Class<? extends Command>) clazz);
}
}

View file

@ -0,0 +1,68 @@
// Copyright 2018 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.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.mockito.Mockito.mock;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class ShellCommandTest {
CommandRunner cli = mock(CommandRunner.class);
public ShellCommandTest() {}
@Test
public void testParsing() {
assertThat(ShellCommand.parseCommand("foo bar 123 baz+ // comment \"string data\""))
.isEqualTo(new String[] {"foo", "bar", "123", "baz+", "//", "comment", "string data"});
assertThat(ShellCommand.parseCommand("\"got \\\" escapes?\""))
.isEqualTo(new String[] {"got \" escapes?"});
assertThat(ShellCommand.parseCommand("")).isEqualTo(new String[0]);
}
@Test
public void testCommandProcessing() {
String testData = "test1 foo bar\ntest2 foo bar\n";
ImmutableMap<String, Class<? extends Command>> commandMap = ImmutableMap.of();
MockCli cli = new MockCli();
ShellCommand shellCommand =
new ShellCommand(new ByteArrayInputStream(testData.getBytes(US_ASCII)), cli);
shellCommand.run();
assertThat(cli.calls)
.containsExactly(
ImmutableList.of("test1", "foo", "bar"), ImmutableList.of("test2", "foo", "bar"))
.inOrder();
}
static class MockCli implements CommandRunner {
public ArrayList<ImmutableList<String>> calls = new ArrayList<>();
@Override
public void run(String[] args)
throws Exception {
calls.add(ImmutableList.copyOf(args));
}
}
}